security: hacher l'identifiant machine avant de l'envoyer au serveur de licences #350

Closed
opened 2026-09-25 00:49:35 +00:00 by maximus · 5 comments
Owner

Corps réécrit par /analyze le 2026-10-01. Le corps d'origine du 2026-09-24 est archivé dans le premier commentaire.

Contexte

Le client envoie au serveur de licences l'identifiant machine brut : activate_machine_impl passe machine_uid::get() tel quel dans le payload de POST /v1/licenses/activate, avec le nom d'hôte. Sous Linux c'est le contenu de /var/lib/dbus/machine-id ou de /etc/machine-id (32 caractères hexadécimaux), sous Windows la valeur MachineGuid du registre. maximus-api le stocke avec le courriel de la licence et le nom d'hôte (license_activations.machine_id), le renvoie dans chaque liste de machines (verify, corps du 409) et le signe dans le jeton d'activation (claim machine_id, 2 ans).

machine-id(5) demande de ne jamais exposer cet identifiant sur le réseau et d'utiliser à sa place un hachage à clé fixe, propre à l'application. spec-monetisation.md:317 prévoyait d'ailleurs un « hash unique de la machine ». Trouvé par la revue /review-spec du 2026-09-24.

La vente est ouverte depuis le 2026-09-29 (maximus/la-compagnie-maximus#141) : chaque activation faite avec une version sans ce correctif laisse un identifiant brut de plus sur le serveur.

État du code (vérifié le 2026-10-01, main c101730, maximus-api ac91da4)

Dans src-tauri/src/commands/license_commands.rs, se repérer aux noms de fonctions :

  • machine_id_internal() appelle machine_uid::get(). Quatre fonctions s'en servent, en plus de la commande get_machine_id : activate_machine_impl (payload), persist_activation_token (le jeton reçu doit porter l'identifiant local), get_activation_status (état « Activée » et identifiant renvoyé au front), deactivate_machine_impl (suppression du jeton local quand la machine désactivée est celle-ci).
  • validate_activation_with_key compare le claim machine_id du jeton à l'identifiant local. activation_status_in en dépend.
  • L'édition ne dépend plus du jeton depuis #347 (D12) : current_entitlements lit la clé seule. Un jeton refusé coûte l'état « Activée » de la carte et les cours du Bilan, jamais l'édition.
  • get_machine_id (commande Tauri) renvoie l'identifiant brut au webview et n'a aucun appelant : getMachineId de src/services/licenseService.ts est du code mort.
  • LicenseCard.tsx marque « Cette machine » par machines.activation?.machine_id === m.machine_id : il compare l'identifiant local aux identifiants de la liste serveur.
  • fetch_price (balance_commands.rs) envoie activation.token en Bearer sans le relire.

Côté serveur, rien à changer pour ce correctif : machine_id est une chaîne opaque comparée par égalité (licenseService.ts), le schéma accepte 64 caractères (z.string().min(1), borné à 255 sur main), et /v1/prices (licenseAuth.ts) ne lit jamais le claim machine_id.

Aucune dépendance à ajouter : hmac = "0.12" et sha2 = "0.10" sont déjà déclarés (patron HMAC dans account_cache.rs, encodage hexadécimal comme dans profile_commands.rs). Cargo.lock ne doit pas bouger.

Décisions (2026-10-01)

  • D1 — une machine déjà activée garde son activation. Un jeton dont le claim vaut l'identifiant brut de cette machine reste accepté. Écartées : la coupure nette (machine fantôme dans la liste, une place occupée, cours perdus jusqu'à la réactivation) et la réactivation automatique (appels réseau non sollicités, limite de 5 requêtes par minute).
  • D2 — ce que D1 laisse en place. L'identifiant brut reste dans un jeton antérieur (envoyé en Bearer aux cours, donc Premium seulement) et dans sa ligne serveur, jusqu'à une réactivation. Le critère « aucun identifiant brut ne quitte le poste » est donc borné aux activations faites avec cette version. Les identifiants déjà stockés relèvent de maximus/maximus-api#82.
  • D3 — le client ne suppose rien du serveur. Une ligne de la liste est « cette machine » si son identifiant vaut le dérivé ou le brut : le correctif reste juste si le serveur rehache un jour ses lignes en place.

Tâches

Rust — license_commands.rs

  • derive_machine_id(raw) : fonction pure, HMAC-SHA256(clé, raw) rendu en 64 caractères hexadécimaux minuscules. Clé figée : com.simpl.resultat/license-machine-id/v1 (octets ASCII). La clé n'est pas un secret, le dépôt est public : elle rend l'identifiant propre à l'application. Elle ne change plus jamais, la changer rendrait orphelines toutes les activations.
  • Un identifiant brut vide (après trim) est une erreur (LicenseError::Internal), jamais haché : toutes les machines sans machine-id partageraient sinon le même identifiant dérivé.
  • Activation : le payload porte l'identifiant dérivé. Le jeton reçu doit porter ce même identifiant dérivé, sinon InvalidResponse.
  • État d'activation : un jeton valide dont le claim vaut le dérivé ou le brut de cette machine se lit « Activée ». Expiré, d'une autre machine (sous l'une ou l'autre forme) ou signé par une autre clé : refusé, comme aujourd'hui.
  • Liste des machines : chaque ligne reçoit is_this_machine, calculé ici, vrai quand son identifiant vaut le dérivé ou le brut de cette machine.
  • Désactivation : le jeton local est supprimé si l'identifiant désactivé est le dérivé de cette machine, ou celui que porte le jeton stocké. Désactiver l'ancienne ligne brute de cette machine alors qu'un jeton haché est en place garde ce jeton.
  • ActivationStatus ne porte plus d'identifiant. Retirer get_machine_id (commande, enregistrement dans lib.rs).

Front

  • licenseService.ts : retirer getMachineId, ajouter is_this_machine à MachineInfo, retirer machine_id d'ActivationStatus.
  • LicenseCard.tsx : le marqueur « Cette machine » lit m.is_this_machine.
  • Fixtures de useLicenseMachines.test.ts, LicenseCard.render.test.tsx, licenseService.test.ts.

Documentation

  • ADR 0020 : dérivation et clé figées, transition (D1 à D3), condition de retrait de la compatibilité (voir plus bas).
  • docs/architecture.md : section license_commands.rs (une commande de moins, dérivation, transition) et total des commandes.
  • docs/guide-utilisateur.md (carte Licence) et docs.editions.tips en FR et EN : une phrase sur ce que l'activation envoie (la clé, le nom de l'ordinateur, un identifiant dérivé propre à Simpl'Résultat à la place de l'identifiant du système).
  • CHANGELOG.md et CHANGELOG.fr.md, sous Security / Sécurité. Ne pas promettre l'effacement des identifiants déjà reçus : c'est l'objet de maximus/maximus-api#82.

Tests

Les tests n'appellent jamais machine_uid::get() : fonctions pures à identifiant injecté, comme aujourd'hui (précaution : un conteneur de CI n'a pas forcément de machine-id).

  • Vecteurs de référence de derive_machine_id (les mêmes serviront au serveur) :
    • 0123456789abcdef0123456789abcdef donne 4f8817b37072a3a1e6ef014f34c46835ba715b6f2f97afee1fc680d7fc170db1
    • a1b2c3d4-e5f6-7890-abcd-ef1234567890 donne 3b9579ec36b3daf37a2aaab0abe2315fa4a9f07200ae568c5aaa614663037d9d
  • Le payload d'activation porte le dérivé et jamais le brut, testé au point de décision (serveur mockito) : remplacer le dérivé par le brut dans le code doit faire rougir ce test.
  • Identifiant brut vide : erreur, aucun appel.
  • Jeton : claim dérivé accepté ; claim brut de cette machine accepté ; claim brut d'une autre machine refusé ; claim dérivé d'une autre machine refusé ; jeton brut expiré refusé.
  • is_this_machine : vrai pour une ligne dérivée, vrai pour une ligne brute, faux pour une ligne étrangère.
  • Suppression du jeton à la désactivation, quatre cas :
Jeton stocké Identifiant désactivé Jeton après
brut brut de cette machine supprimé
haché haché de cette machine supprimé
haché brut de cette machine (ancienne ligne) gardé
brut haché de cette machine (ligne rehachée par le serveur) supprimé
  • Rendu de LicenseCard : « Cette machine » suit is_this_machine.

Tests existants touchés : activation_token_*, activation_status_*, activate_posts_the_trimmed_key_and_product_to_v1, les tests de désactivation, et les trois fichiers de test front cités plus haut.

Critères d'acceptation

  • Aucune activation faite avec cette version n'envoie l'identifiant brut.
  • Une machine activée avant le changement reste « Activée », est marquée « Cette machine » dans la liste et garde les cours.
  • get_machine_id n'existe plus et ActivationStatus ne porte aucun identifiant. La liste renvoyée par le serveur garde les identifiants de ses lignes, bruts pour les lignes antérieures.
  • Cargo.lock inchangé.
  • cargo test, npm test et la CI (check-rust, check-frontend) verts.
  • Vérification manuelle sur un poste activé avant le changement : état conservé après la mise à jour ; puis « Désactiver » et « Activer » donnent dans la liste une ligne dont l'identifiant fait 64 caractères.

Condition de retrait de la compatibilité

L'acceptation de l'identifiant brut (jeton et liste) se retire quand deux conditions sont réunies : deux ans se sont écoulés depuis la dernière version qui envoyait l'identifiant brut (le serveur signe les jetons pour deux ans, plus aucun jeton brut n'est alors valide), et le serveur ne renvoie plus de ligne brute (maximus/maximus-api#82). À consigner dans l'ADR 0020.

Hors périmètre

  • Identifiants bruts déjà stockés par le serveur, lignes désactivées comprises : maximus/maximus-api#82.
  • Politique de confidentialité du site, qui ne déclare pas les données d'activation : maximus/la-compagnie-maximus#184.
  • store_activation_token n'a pas d'appelant côté front non plus : nettoyage à part.
  • Le nom d'hôte continue d'être envoyé en clair : il sert à reconnaître ses machines dans la liste.

Dépendances

  • #270 et #347, mergées.
  • #365 réécrit le même fichier : la traiter après celle-ci.

Complexité

Moyenne : un PR, Rust et front, sans migration de base ni changement serveur.

> Corps réécrit par `/analyze` le 2026-10-01. Le corps d'origine du 2026-09-24 est archivé dans le premier commentaire. ## Contexte Le client envoie au serveur de licences l'identifiant machine **brut** : `activate_machine_impl` passe `machine_uid::get()` tel quel dans le payload de `POST /v1/licenses/activate`, avec le nom d'hôte. Sous Linux c'est le contenu de `/var/lib/dbus/machine-id` ou de `/etc/machine-id` (32 caractères hexadécimaux), sous Windows la valeur `MachineGuid` du registre. maximus-api le stocke avec le courriel de la licence et le nom d'hôte (`license_activations.machine_id`), le renvoie dans chaque liste de machines (`verify`, corps du 409) et le signe dans le jeton d'activation (claim `machine_id`, 2 ans). machine-id(5) demande de ne jamais exposer cet identifiant sur le réseau et d'utiliser à sa place un hachage à clé fixe, propre à l'application. `spec-monetisation.md:317` prévoyait d'ailleurs un « hash unique de la machine ». Trouvé par la revue `/review-spec` du 2026-09-24. La vente est ouverte depuis le 2026-09-29 (maximus/la-compagnie-maximus#141) : chaque activation faite avec une version sans ce correctif laisse un identifiant brut de plus sur le serveur. ## État du code (vérifié le 2026-10-01, `main` `c101730`, maximus-api `ac91da4`) Dans `src-tauri/src/commands/license_commands.rs`, se repérer aux noms de fonctions : - `machine_id_internal()` appelle `machine_uid::get()`. Quatre fonctions s'en servent, en plus de la commande `get_machine_id` : `activate_machine_impl` (payload), `persist_activation_token` (le jeton reçu doit porter l'identifiant local), `get_activation_status` (état « Activée » et identifiant renvoyé au front), `deactivate_machine_impl` (suppression du jeton local quand la machine désactivée est celle-ci). - `validate_activation_with_key` compare le claim `machine_id` du jeton à l'identifiant local. `activation_status_in` en dépend. - L'édition **ne dépend plus** du jeton depuis #347 (D12) : `current_entitlements` lit la clé seule. Un jeton refusé coûte l'état « Activée » de la carte et les cours du Bilan, jamais l'édition. - `get_machine_id` (commande Tauri) renvoie l'identifiant brut au webview et n'a aucun appelant : `getMachineId` de `src/services/licenseService.ts` est du code mort. - `LicenseCard.tsx` marque « Cette machine » par `machines.activation?.machine_id === m.machine_id` : il compare l'identifiant local aux identifiants de la liste serveur. - `fetch_price` (`balance_commands.rs`) envoie `activation.token` en Bearer sans le relire. Côté serveur, rien à changer pour ce correctif : `machine_id` est une chaîne opaque comparée par égalité (`licenseService.ts`), le schéma accepte 64 caractères (`z.string().min(1)`, borné à 255 sur `main`), et `/v1/prices` (`licenseAuth.ts`) ne lit jamais le claim `machine_id`. Aucune dépendance à ajouter : `hmac = "0.12"` et `sha2 = "0.10"` sont déjà déclarés (patron HMAC dans `account_cache.rs`, encodage hexadécimal comme dans `profile_commands.rs`). `Cargo.lock` ne doit pas bouger. ## Décisions (2026-10-01) - **D1 — une machine déjà activée garde son activation.** Un jeton dont le claim vaut l'identifiant brut de cette machine reste accepté. Écartées : la coupure nette (machine fantôme dans la liste, une place occupée, cours perdus jusqu'à la réactivation) et la réactivation automatique (appels réseau non sollicités, limite de 5 requêtes par minute). - **D2 — ce que D1 laisse en place.** L'identifiant brut reste dans un jeton antérieur (envoyé en Bearer aux cours, donc Premium seulement) et dans sa ligne serveur, jusqu'à une réactivation. Le critère « aucun identifiant brut ne quitte le poste » est donc borné aux activations faites avec cette version. Les identifiants déjà stockés relèvent de maximus/maximus-api#82. - **D3 — le client ne suppose rien du serveur.** Une ligne de la liste est « cette machine » si son identifiant vaut le dérivé **ou** le brut : le correctif reste juste si le serveur rehache un jour ses lignes en place. ## Tâches ### Rust — `license_commands.rs` - [ ] `derive_machine_id(raw)` : fonction pure, `HMAC-SHA256(clé, raw)` rendu en 64 caractères hexadécimaux minuscules. Clé figée : `com.simpl.resultat/license-machine-id/v1` (octets ASCII). La clé n'est pas un secret, le dépôt est public : elle rend l'identifiant propre à l'application. Elle ne change plus jamais, la changer rendrait orphelines toutes les activations. - [ ] Un identifiant brut vide (après `trim`) est une erreur (`LicenseError::Internal`), jamais haché : toutes les machines sans `machine-id` partageraient sinon le même identifiant dérivé. - [ ] Activation : le payload porte l'identifiant dérivé. Le jeton reçu doit porter ce même identifiant dérivé, sinon `InvalidResponse`. - [ ] État d'activation : un jeton valide dont le claim vaut le dérivé ou le brut de cette machine se lit « Activée ». Expiré, d'une autre machine (sous l'une ou l'autre forme) ou signé par une autre clé : refusé, comme aujourd'hui. - [ ] Liste des machines : chaque ligne reçoit `is_this_machine`, calculé ici, vrai quand son identifiant vaut le dérivé ou le brut de cette machine. - [ ] Désactivation : le jeton local est supprimé si l'identifiant désactivé est le dérivé de cette machine, ou celui que porte le jeton stocké. Désactiver l'ancienne ligne brute de cette machine alors qu'un jeton haché est en place garde ce jeton. - [ ] `ActivationStatus` ne porte plus d'identifiant. Retirer `get_machine_id` (commande, enregistrement dans `lib.rs`). ### Front - [ ] `licenseService.ts` : retirer `getMachineId`, ajouter `is_this_machine` à `MachineInfo`, retirer `machine_id` d'`ActivationStatus`. - [ ] `LicenseCard.tsx` : le marqueur « Cette machine » lit `m.is_this_machine`. - [ ] Fixtures de `useLicenseMachines.test.ts`, `LicenseCard.render.test.tsx`, `licenseService.test.ts`. ### Documentation - [ ] ADR 0020 : dérivation et clé figées, transition (D1 à D3), condition de retrait de la compatibilité (voir plus bas). - [ ] `docs/architecture.md` : section `license_commands.rs` (une commande de moins, dérivation, transition) et total des commandes. - [ ] `docs/guide-utilisateur.md` (carte Licence) et `docs.editions.tips` en FR et EN : une phrase sur ce que l'activation envoie (la clé, le nom de l'ordinateur, un identifiant dérivé propre à Simpl'Résultat à la place de l'identifiant du système). - [ ] `CHANGELOG.md` et `CHANGELOG.fr.md`, sous `Security` / `Sécurité`. Ne pas promettre l'effacement des identifiants déjà reçus : c'est l'objet de maximus/maximus-api#82. ## Tests Les tests n'appellent jamais `machine_uid::get()` : fonctions pures à identifiant injecté, comme aujourd'hui (précaution : un conteneur de CI n'a pas forcément de `machine-id`). - [ ] Vecteurs de référence de `derive_machine_id` (les mêmes serviront au serveur) : - `0123456789abcdef0123456789abcdef` donne `4f8817b37072a3a1e6ef014f34c46835ba715b6f2f97afee1fc680d7fc170db1` - `a1b2c3d4-e5f6-7890-abcd-ef1234567890` donne `3b9579ec36b3daf37a2aaab0abe2315fa4a9f07200ae568c5aaa614663037d9d` - [ ] Le payload d'activation porte le dérivé et jamais le brut, testé au point de décision (serveur mockito) : remplacer le dérivé par le brut dans le code doit faire rougir ce test. - [ ] Identifiant brut vide : erreur, aucun appel. - [ ] Jeton : claim dérivé accepté ; claim brut de cette machine accepté ; claim brut d'une autre machine refusé ; claim dérivé d'une autre machine refusé ; jeton brut expiré refusé. - [ ] `is_this_machine` : vrai pour une ligne dérivée, vrai pour une ligne brute, faux pour une ligne étrangère. - [ ] Suppression du jeton à la désactivation, quatre cas : | Jeton stocké | Identifiant désactivé | Jeton après | |---|---|---| | brut | brut de cette machine | supprimé | | haché | haché de cette machine | supprimé | | haché | brut de cette machine (ancienne ligne) | gardé | | brut | haché de cette machine (ligne rehachée par le serveur) | supprimé | - [ ] Rendu de `LicenseCard` : « Cette machine » suit `is_this_machine`. Tests existants touchés : `activation_token_*`, `activation_status_*`, `activate_posts_the_trimmed_key_and_product_to_v1`, les tests de désactivation, et les trois fichiers de test front cités plus haut. ## Critères d'acceptation - [ ] Aucune activation faite avec cette version n'envoie l'identifiant brut. - [ ] Une machine activée avant le changement reste « Activée », est marquée « Cette machine » dans la liste et garde les cours. - [ ] `get_machine_id` n'existe plus et `ActivationStatus` ne porte aucun identifiant. La liste renvoyée par le serveur garde les identifiants de ses lignes, bruts pour les lignes antérieures. - [ ] `Cargo.lock` inchangé. - [ ] `cargo test`, `npm test` et la CI (`check-rust`, `check-frontend`) verts. - [ ] Vérification manuelle sur un poste activé avant le changement : état conservé après la mise à jour ; puis « Désactiver » et « Activer » donnent dans la liste une ligne dont l'identifiant fait 64 caractères. ## Condition de retrait de la compatibilité L'acceptation de l'identifiant brut (jeton et liste) se retire quand deux conditions sont réunies : deux ans se sont écoulés depuis la dernière version qui envoyait l'identifiant brut (le serveur signe les jetons pour deux ans, plus aucun jeton brut n'est alors valide), et le serveur ne renvoie plus de ligne brute (maximus/maximus-api#82). À consigner dans l'ADR 0020. ## Hors périmètre - Identifiants bruts déjà stockés par le serveur, lignes désactivées comprises : maximus/maximus-api#82. - Politique de confidentialité du site, qui ne déclare pas les données d'activation : maximus/la-compagnie-maximus#184. - `store_activation_token` n'a pas d'appelant côté front non plus : nettoyage à part. - Le nom d'hôte continue d'être envoyé en clair : il sert à reconnaître ses machines dans la liste. ## Dépendances - #270 et #347, mergées. - #365 réécrit le même fichier : la traiter après celle-ci. ## Complexité Moyenne : un PR, Rust et front, sans migration de base ni changement serveur.
maximus added this to the spec-paiements milestone 2026-09-25 00:49:35 +00:00
maximus added the
status:ready
type:security
source:human
labels 2026-09-25 00:49:35 +00:00
Author
Owner

Analyse du 2026-10-01 (/analyze) : le corps est réécrit, le texte d'origine du 2026-09-24 est conservé plus bas.

Ce que l'analyse change, vérifié sur main (c101730) et sur maximus-api (ac91da4) :

  • La prémisse tient. L'identifiant brut part dans le payload d'activation ; le serveur le stocke, le renvoie dans les listes de machines et le signe dans le jeton.
  • « Avant le go-live » n'a plus cours. La vente est ouverte depuis le 2026-09-29 : chaque activation faite sans le correctif laisse un identifiant brut de plus côté serveur.
  • « Les machines déjà activées retomberaient en Gratuite » est faux depuis #347 (D12). L'édition suit la clé seule. Un jeton refusé coûte l'état « Activée » et les cours du Bilan, pas l'édition.
  • current_entitlements ne compare plus rien au jeton, et get_machine_id n'a aucun appelant côté front.
  • Trois sites manquaient : la suppression du jeton local dans deactivate_machine_impl, le marqueur « Cette machine » de LicenseCard.tsx, et le Bearer des cours, qui envoie un jeton antérieur avec son claim brut.
  • Le critère « aucun identifiant machine brut ne quitte le poste » ne peut pas tenir tel quel avec la transition retenue : il est borné aux activations faites avec la version corrigée.

Décisions de Max (2026-10-01) : une machine déjà activée garde son activation ; les identifiants déjà stockés et la politique de confidentialité partent dans leurs dépôts, maximus/maximus-api#82 et maximus/la-compagnie-maximus#184.

Corps d'origine (2026-09-24)

Le client envoie au serveur de licences l'identifiant machine brut (machine_uid::get() : /etc/machine-id sous Linux, MachineGuid du registre sous Windows), que maximus-api stocke avec le courriel et le nom d'hôte (license_activations). machine-id(5) demande de ne jamais exposer cet identifiant sur le réseau et d'utiliser à la place un hachage à clé propre à l'application. spec-monetisation.md:317 prévoyait d'ailleurs un « hash unique de la machine ». Trouvé par la revue /review-spec du 2026-09-24 (séance /plan-run paiements).

À traiter avant le go-live (maximus/la-compagnie-maximus#141) : changer la dérivation après les premières ventes invaliderait les activations de tous les acheteurs.

Contexte vérifié (2026-09-24)

  • src-tauri/src/commands/license_commands.rs : machine_id_internal() (l.360-362) appelle machine_uid::get(). Il sert au payload d'activation (l.396) et aux comparaisons locales avec le claim machine_id du jeton d'activation : validate_activation_with_key (l.141-155), get_activation_status (l.535-558), current_entitlements (l.315).
  • get_machine_id (commande Tauri, l.356) expose aussi l'identifiant au front.
  • Le serveur n'interprète pas l'identifiant : c'est une chaîne opaque (license_activations.machine_id), comparée par égalité.
  • Numéros de ligne relevés sur main le 2026-09-24. #270 et #347 réécrivent ce fichier : se repérer aux noms de fonctions.

Tâches

  • Dériver HMAC-SHA256(clé fixe propre à l'app, machine_uid) en hexadécimal, dans une seule fonction utilisée pour le payload et pour toutes les comparaisons locales. La clé n'est pas un secret (le dépôt est public) : elle rend l'identifiant propre à l'application, comme le demande machine-id(5).
  • Transition : un jeton d'activation dont le machine_id vaut l'identifiant brut de cette machine reste accepté, sinon les machines déjà activées retomberaient en Gratuite. Les nouvelles activations envoient le hachage. L'ancienne place côté serveur reste libérable depuis la liste des machines.
  • Tests Rust : dérivation stable ; jeton au format brut accepté pour cette machine ; jeton d'une autre machine refusé, qu'il soit brut ou haché.
  • CHANGELOG FR/EN ; docs/architecture.md (dérivation de l'identifiant).

Critères d'acceptation

  • Aucun identifiant machine brut ne quitte le poste.
  • Une machine activée avant le changement garde son édition.
  • cargo test vert.

Depends on

**Analyse du 2026-10-01 (`/analyze`)** : le corps est réécrit, le texte d'origine du 2026-09-24 est conservé plus bas. Ce que l'analyse change, vérifié sur `main` (`c101730`) et sur maximus-api (`ac91da4`) : - **La prémisse tient.** L'identifiant brut part dans le payload d'activation ; le serveur le stocke, le renvoie dans les listes de machines et le signe dans le jeton. - **« Avant le go-live » n'a plus cours.** La vente est ouverte depuis le 2026-09-29 : chaque activation faite sans le correctif laisse un identifiant brut de plus côté serveur. - **« Les machines déjà activées retomberaient en Gratuite » est faux depuis #347 (D12).** L'édition suit la clé seule. Un jeton refusé coûte l'état « Activée » et les cours du Bilan, pas l'édition. - **`current_entitlements` ne compare plus rien au jeton**, et `get_machine_id` n'a aucun appelant côté front. - **Trois sites manquaient** : la suppression du jeton local dans `deactivate_machine_impl`, le marqueur « Cette machine » de `LicenseCard.tsx`, et le Bearer des cours, qui envoie un jeton antérieur avec son claim brut. - **Le critère « aucun identifiant machine brut ne quitte le poste »** ne peut pas tenir tel quel avec la transition retenue : il est borné aux activations faites avec la version corrigée. Décisions de Max (2026-10-01) : une machine déjà activée garde son activation ; les identifiants déjà stockés et la politique de confidentialité partent dans leurs dépôts, maximus/maximus-api#82 et maximus/la-compagnie-maximus#184. <details> <summary>Corps d'origine (2026-09-24)</summary> Le client envoie au serveur de licences l'identifiant machine **brut** (`machine_uid::get()` : `/etc/machine-id` sous Linux, `MachineGuid` du registre sous Windows), que maximus-api stocke avec le courriel et le nom d'hôte (`license_activations`). machine-id(5) demande de ne jamais exposer cet identifiant sur le réseau et d'utiliser à la place un hachage à clé propre à l'application. `spec-monetisation.md:317` prévoyait d'ailleurs un « hash unique de la machine ». Trouvé par la revue `/review-spec` du 2026-09-24 (séance `/plan-run` paiements). À traiter **avant le go-live** (maximus/la-compagnie-maximus#141) : changer la dérivation après les premières ventes invaliderait les activations de tous les acheteurs. ## Contexte vérifié (2026-09-24) - `src-tauri/src/commands/license_commands.rs` : `machine_id_internal()` (l.360-362) appelle `machine_uid::get()`. Il sert au payload d'activation (l.396) et aux comparaisons locales avec le claim `machine_id` du jeton d'activation : `validate_activation_with_key` (l.141-155), `get_activation_status` (l.535-558), `current_entitlements` (l.315). - `get_machine_id` (commande Tauri, l.356) expose aussi l'identifiant au front. - Le serveur n'interprète pas l'identifiant : c'est une chaîne opaque (`license_activations.machine_id`), comparée par égalité. - Numéros de ligne relevés sur `main` le 2026-09-24. #270 et #347 réécrivent ce fichier : se repérer aux noms de fonctions. ## Tâches - [ ] Dériver `HMAC-SHA256(clé fixe propre à l'app, machine_uid)` en hexadécimal, dans une seule fonction utilisée pour le payload et pour toutes les comparaisons locales. La clé n'est pas un secret (le dépôt est public) : elle rend l'identifiant propre à l'application, comme le demande machine-id(5). - [ ] Transition : un jeton d'activation dont le `machine_id` vaut l'identifiant **brut** de cette machine reste accepté, sinon les machines déjà activées retomberaient en Gratuite. Les nouvelles activations envoient le hachage. L'ancienne place côté serveur reste libérable depuis la liste des machines. - [ ] Tests Rust : dérivation stable ; jeton au format brut accepté pour cette machine ; jeton d'une autre machine refusé, qu'il soit brut ou haché. - [ ] CHANGELOG FR/EN ; `docs/architecture.md` (dérivation de l'identifiant). ## Critères d'acceptation - [ ] Aucun identifiant machine brut ne quitte le poste. - [ ] Une machine activée avant le changement garde son édition. - [ ] `cargo test` vert. ## Depends on - #270 - #347 </details>
maximus changed title from security: hacher l'identifiant machine avant de l'envoyer au serveur de licences (avant go-live) to security: hacher l'identifiant machine avant de l'envoyer au serveur de licences 2026-10-01 23:22:24 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-10-01 23:29:09 +00:00
maximus added
status:review
and removed
status:in-progress
labels 2026-10-01 23:58:35 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-02 00:54:54 +00:00
maximus added
status:review
and removed
status:approved
labels 2026-10-02 01:13:41 +00:00
Author
Owner

PR #366 mergée le 2026-10-01 (main 0a9c9f7, fast-forward). L'issue reste ouverte : la PR portait Refs, parce qu'un critère d'acceptation ne se vérifie pas dans les tests. Elle passe en status:blocked jusqu'à cette vérification.

Ce qui reste : la vérification manuelle, avant le tag de la release

La dérivation devient définitive à la première activation en production, et ce contrôle est le seul qui tienne le câblage de l'activation à l'identifiant réel du poste. Il demande un poste activé par la v0.15.0 et le serveur de licences de production, avec un build de main (npm run tauri dev, même dossier de données) ou le build de la release avant sa publication.

  • 1. Lancer cette version sur un poste activé par la v0.15.0 : la carte Licence affiche toujours « Activée », la liste marque ce poste « Cette machine », et les cours du Bilan répondent (Premium).
  • 2. Lancer la commande ci-dessous. Attendu : 32 brut: True dérivé: False.
  • 3. « Désactiver » ce poste puis « Activer » : la carte revient à « Activée », la nouvelle ligne est marquée « Cette machine », et la commande rend 64 brut: False dérivé: True.
  • 4. Fermer cette issue à la main.
python3 - <<'EOF'
import base64, hashlib, hmac, json, os
raw = next(open(p).read().strip() for p in ("/var/lib/dbus/machine-id", "/etc/machine-id") if os.path.exists(p))
p = open(os.path.expanduser("~/.local/share/com.simpl.resultat/activation.token")).read().split(".")[1]
claim = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["machine_id"]
want = hmac.new(b"com.simpl.resultat/license-machine-id/v1", raw.encode(), hashlib.sha256).hexdigest()
print(len(claim), "brut:", claim == raw, "dérivé:", claim == want)
EOF

La commande est pour Linux et n'affiche qu'une longueur et deux booléens. Sous Windows, le jeton est dans %APPDATA%\com.simpl.resultat\activation.token et l'identifiant du système dans HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid.

Deux corrections au corps de cette issue

  • Condition de retrait de la compatibilité. Le corps compte deux ans depuis la dernière version qui envoyait l'identifiant brut. C'est faux : une v0.15.0 en édition Gratuite ne se met plus à jour, et son utilisateur peut acheter une clé puis activer en brut des mois plus tard. La condition livrée, dans l'ADR 0020 : deux ans depuis la dernière émission d'un jeton à claim brut, que seul le serveur connaît, et plus aucune ligne brute renvoyée par le serveur. Trouvé par la revue de la PR.
  • Règle de suppression du jeton à la désactivation. Elle est plus large que la phrase du corps : toute ligne de ce poste emporte le jeton, sauf l'ancienne ligne brute quand le jeton vaut pour la ligne dérivée. Les quatre cas du tableau sont tenus ; le détail est dans la PR et dans l'ADR 0020.

Suites

  • maximus/maximus-api#82 : identifiants bruts déjà stockés. Deux précisions y sont reportées : rehacher les lignes actives avant l'expiration des premiers jetons bruts, sinon une réactivation crée une seconde ligne ; et consigner la date de la dernière activation brute.
  • maximus/la-compagnie-maximus#184 : données d'activation absentes de la politique de confidentialité.
**PR #366 mergée le 2026-10-01** (`main` `0a9c9f7`, fast-forward). L'issue reste ouverte : la PR portait `Refs`, parce qu'un critère d'acceptation ne se vérifie pas dans les tests. Elle passe en `status:blocked` jusqu'à cette vérification. ## Ce qui reste : la vérification manuelle, avant le tag de la release La dérivation devient définitive à la première activation en production, et ce contrôle est le seul qui tienne le câblage de l'activation à l'identifiant réel du poste. Il demande un poste activé par la v0.15.0 et le serveur de licences de production, avec un build de `main` (`npm run tauri dev`, même dossier de données) ou le build de la release avant sa publication. - [ ] 1. Lancer cette version sur un poste activé par la v0.15.0 : la carte Licence affiche toujours « Activée », la liste marque ce poste « Cette machine », et les cours du Bilan répondent (Premium). - [ ] 2. Lancer la commande ci-dessous. Attendu : `32 brut: True dérivé: False`. - [ ] 3. « Désactiver » ce poste puis « Activer » : la carte revient à « Activée », la nouvelle ligne est marquée « Cette machine », et la commande rend `64 brut: False dérivé: True`. - [ ] 4. Fermer cette issue à la main. ```bash python3 - <<'EOF' import base64, hashlib, hmac, json, os raw = next(open(p).read().strip() for p in ("/var/lib/dbus/machine-id", "/etc/machine-id") if os.path.exists(p)) p = open(os.path.expanduser("~/.local/share/com.simpl.resultat/activation.token")).read().split(".")[1] claim = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["machine_id"] want = hmac.new(b"com.simpl.resultat/license-machine-id/v1", raw.encode(), hashlib.sha256).hexdigest() print(len(claim), "brut:", claim == raw, "dérivé:", claim == want) EOF ``` La commande est pour Linux et n'affiche qu'une longueur et deux booléens. Sous Windows, le jeton est dans `%APPDATA%\com.simpl.resultat\activation.token` et l'identifiant du système dans `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid`. ## Deux corrections au corps de cette issue - **Condition de retrait de la compatibilité.** Le corps compte deux ans depuis la dernière version qui envoyait l'identifiant brut. C'est faux : une v0.15.0 en édition Gratuite ne se met plus à jour, et son utilisateur peut acheter une clé puis activer en brut des mois plus tard. La condition livrée, dans l'ADR 0020 : deux ans depuis la dernière émission d'un jeton à claim brut, que seul le serveur connaît, et plus aucune ligne brute renvoyée par le serveur. Trouvé par la revue de la PR. - **Règle de suppression du jeton à la désactivation.** Elle est plus large que la phrase du corps : toute ligne de ce poste emporte le jeton, sauf l'ancienne ligne brute quand le jeton vaut pour la ligne dérivée. Les quatre cas du tableau sont tenus ; le détail est dans la PR et dans l'ADR 0020. ## Suites - maximus/maximus-api#82 : identifiants bruts déjà stockés. Deux précisions y sont reportées : rehacher les lignes actives avant l'expiration des premiers jetons bruts, sinon une réactivation crée une seconde ligne ; et consigner la date de la dernière activation brute. - maximus/la-compagnie-maximus#184 : données d'activation absentes de la politique de confidentialité.
maximus added
status:blocked
and removed
status:review
labels 2026-10-02 01:25:43 +00:00
Author
Owner

Vérification du 2026-10-04 sur le poste Linux de Max (v0.16.0) : les étapes 1 et 2 ne sont plus faisables sur ce poste.

Lancée avec un script local en lecture seule, qui reprend la commande ci-dessus (même clé HMAC, même ordre de lecture du machine-id que le crate machine-uid) et n'affiche ni identifiant ni clé. Heures locales :

license.key      : présent, modifié le 2026-10-03 10:45
activation.token : absent
license.check    : présent, modifié le 2026-10-03 10:31
Clé enregistrée : édition Base
Dernier contrôle de révocation de cette clé : 2026-10-03 10:31, révoquée
Jeton d'activation : aucun.

Lecture. La clé enregistrée est la clé Base révoquée de l'achat de contrôle, saisie le 2026-10-03 pour vérifier #365. Enregistrer une autre clé efface le jeton d'activation (#347), si bien que le jeton émis par la v0.15.0, celui qui portait l'identifiant brut, n'existe plus. Les étapes 1 et 2, qui demandaient ce jeton intact, ne se vérifient donc plus ici.

Reste à faire sur ce poste : l'étape 3. Tant que la clé révoquée est enregistrée, « Activer » est refusé. Il faut donc ressaisir la clé testeur Premium, cliquer « Activer », constater « Activée » et « Cette machine » dans la carte Licence, puis relancer la vérification. Attendu : 64 brut: False dérivé: True.

Étapes 1 et 2, à trancher avant de fermer. Seul un autre poste activé par la v0.15.0, dont la clé n'a pas changé depuis, peut encore les tenir : par exemple le poste Windows de l'autre testeur, s'il avait activé. Après sa mise à jour en v0.16.0, la carte doit y afficher « Activée » et « Cette machine », et les cours du Bilan doivent répondre. Sinon, l'acceptation des jetons bruts (D1) n'est gardée que par les tests activation_token_from_before_the_derivation_still_matches et activation_status_accepts_a_token_from_before_the_derivation.

**Vérification du 2026-10-04 sur le poste Linux de Max (v0.16.0) : les étapes 1 et 2 ne sont plus faisables sur ce poste.** Lancée avec un script local en lecture seule, qui reprend la commande ci-dessus (même clé HMAC, même ordre de lecture du `machine-id` que le crate `machine-uid`) et n'affiche ni identifiant ni clé. Heures locales : ``` license.key : présent, modifié le 2026-10-03 10:45 activation.token : absent license.check : présent, modifié le 2026-10-03 10:31 Clé enregistrée : édition Base Dernier contrôle de révocation de cette clé : 2026-10-03 10:31, révoquée Jeton d'activation : aucun. ``` **Lecture.** La clé enregistrée est la clé Base révoquée de l'achat de contrôle, saisie le 2026-10-03 pour vérifier #365. Enregistrer une autre clé efface le jeton d'activation (#347), si bien que le jeton émis par la v0.15.0, celui qui portait l'identifiant brut, n'existe plus. Les étapes 1 et 2, qui demandaient ce jeton intact, ne se vérifient donc plus ici. **Reste à faire sur ce poste : l'étape 3.** Tant que la clé révoquée est enregistrée, « Activer » est refusé. Il faut donc ressaisir la clé testeur Premium, cliquer « Activer », constater « Activée » et « Cette machine » dans la carte Licence, puis relancer la vérification. Attendu : `64 brut: False dérivé: True`. **Étapes 1 et 2, à trancher avant de fermer.** Seul un autre poste activé par la v0.15.0, dont la clé n'a pas changé depuis, peut encore les tenir : par exemple le poste Windows de l'autre testeur, s'il avait activé. Après sa mise à jour en v0.16.0, la carte doit y afficher « Activée » et « Cette machine », et les cours du Bilan doivent répondre. Sinon, l'acceptation des jetons bruts (D1) n'est gardée que par les tests `activation_token_from_before_the_derivation_still_matches` et `activation_status_accepts_a_token_from_before_the_derivation`.
Author
Owner

Étape 3 validée côté jeton, le 2026-10-04 sur le poste Linux de Max (v0.16.0). Clé testeur Premium ressaisie à 15:37, « Activer » à 15:38. Sortie de la vérification, en heures locales :

license.key      : présent, modifié le 2026-10-04 15:37
activation.token : présent, modifié le 2026-10-04 15:38
Clé enregistrée : édition Premium
Jeton d'activation : émis le 2026-10-04 15:38, expire le 2028-10-03
Résultat : 64 brut: False dérivé: True

Une activation faite en production par la v0.16.0 envoie donc l'identifiant dérivé, et le jeton reçu le porte : le câblage de l'activation à l'identifiant réel du poste, que les tests ne gardent pas, est tenu.

Reste à constater avant de fermer :

  • dans la carte Licence : « Activée », la nouvelle ligne marquée « Cette machine », et les cours du Bilan qui répondent ;
  • la ligne brute de l'activation faite par la v0.15.0 n'a été désactivée par personne, donc la liste devrait montrer deux lignes « Cette machine » pour ce poste. Si c'est le cas, la reconnaissance d'une ligne brute (is_this_machine, moitié « liste » de l'étape 1) est vérifiée en production. Désactiver ensuite cette ligne brute, la plus ancienne, doit laisser la carte « Activée » et le jeton en place, une nouvelle vérification rendant toujours 64 brut: False dérivé: True : c'est l'exception de la règle de suppression (ADR 0020), et cela libère la place ;
  • l'acceptation d'un jeton brut (« Activée » et cours avec le jeton de la v0.15.0) : poste Windows de l'autre testeur s'il avait activé en v0.15.0, sinon fermeture sur les deux tests nommés dans le commentaire précédent.
**Étape 3 validée côté jeton, le 2026-10-04 sur le poste Linux de Max (v0.16.0).** Clé testeur Premium ressaisie à 15:37, « Activer » à 15:38. Sortie de la vérification, en heures locales : ``` license.key : présent, modifié le 2026-10-04 15:37 activation.token : présent, modifié le 2026-10-04 15:38 Clé enregistrée : édition Premium Jeton d'activation : émis le 2026-10-04 15:38, expire le 2028-10-03 Résultat : 64 brut: False dérivé: True ``` Une activation faite en production par la v0.16.0 envoie donc l'identifiant dérivé, et le jeton reçu le porte : le câblage de l'activation à l'identifiant réel du poste, que les tests ne gardent pas, est tenu. **Reste à constater avant de fermer :** - dans la carte Licence : « Activée », la nouvelle ligne marquée « Cette machine », et les cours du Bilan qui répondent ; - la ligne brute de l'activation faite par la v0.15.0 n'a été désactivée par personne, donc la liste devrait montrer deux lignes « Cette machine » pour ce poste. Si c'est le cas, la reconnaissance d'une ligne brute (`is_this_machine`, moitié « liste » de l'étape 1) est vérifiée en production. Désactiver ensuite cette ligne brute, la plus ancienne, doit laisser la carte « Activée » et le jeton en place, une nouvelle vérification rendant toujours `64 brut: False dérivé: True` : c'est l'exception de la règle de suppression (ADR 0020), et cela libère la place ; - l'acceptation d'un jeton brut (« Activée » et cours avec le jeton de la v0.15.0) : poste Windows de l'autre testeur s'il avait activé en v0.15.0, sinon fermeture sur les deux tests nommés dans le commentaire précédent.
Author
Owner

Fermée le 2026-10-04, sur décision de Max.

  • Étape 3 tenue. Sur le poste Linux (v0.16.0), « Activer » avec la clé testeur Premium a donné un jeton à identifiant dérivé (64 brut: False dérivé: True, commentaire précédent), et la carte Licence affiche le poste activé, marqué « Cette machine ». Le câblage de l'activation à l'identifiant réel du poste, que les tests ne gardent pas, est vérifié en production.
  • Étapes 1 et 2 non vérifiées en production, et elles ne le seront plus sur ce poste : le jeton de la v0.15.0 a été effacé par le changement de clé fait pour vérifier #365, et la liste ne montre qu'une machine, donc aucune ligne à identifiant brut n'est encore active pour cette licence. L'acceptation d'un jeton brut (ADR 0020, D1) reste gardée par les tests activation_token_from_before_the_derivation_still_matches et activation_status_accepts_a_token_from_before_the_derivation. Un défaut éventuel ne toucherait que les installations activées par la v0.15.0, soit les testeurs : la carte n'afficherait plus « Activée » et les cours seraient refusés jusqu'à un nouveau clic sur « Activer », sans effet sur l'édition, qui ne dépend que de la clé (#347).
  • Suites ouvertes ailleurs : maximus/maximus-api#82 (identifiants bruts déjà stockés) et maximus/la-compagnie-maximus#184 (politique de confidentialité).
**Fermée le 2026-10-04, sur décision de Max.** - **Étape 3 tenue.** Sur le poste Linux (v0.16.0), « Activer » avec la clé testeur Premium a donné un jeton à identifiant dérivé (`64 brut: False dérivé: True`, commentaire précédent), et la carte Licence affiche le poste activé, marqué « Cette machine ». Le câblage de l'activation à l'identifiant réel du poste, que les tests ne gardent pas, est vérifié en production. - **Étapes 1 et 2 non vérifiées en production**, et elles ne le seront plus sur ce poste : le jeton de la v0.15.0 a été effacé par le changement de clé fait pour vérifier #365, et la liste ne montre qu'une machine, donc aucune ligne à identifiant brut n'est encore active pour cette licence. L'acceptation d'un jeton brut (ADR 0020, D1) reste gardée par les tests `activation_token_from_before_the_derivation_still_matches` et `activation_status_accepts_a_token_from_before_the_derivation`. Un défaut éventuel ne toucherait que les installations activées par la v0.15.0, soit les testeurs : la carte n'afficherait plus « Activée » et les cours seraient refusés jusqu'à un nouveau clic sur « Activer », sans effet sur l'édition, qui ne dépend que de la clé (#347). - Suites ouvertes ailleurs : maximus/maximus-api#82 (identifiants bruts déjà stockés) et maximus/la-compagnie-maximus#184 (politique de confidentialité).
maximus added
status:approved
and removed
status:blocked
labels 2026-10-04 19:47:00 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#350
No description provided.