Activation sent `machine_uid::get()` as is: /etc/machine-id on Linux, MachineGuid on Windows. machine-id(5) calls that id confidential and asks applications to send a keyed hash under a fixed, application-specific key instead. The license server stores what it receives next to the license email and the host name, lists it back and signs it into the activation token. - derive_machine_id: HMAC-SHA256 under a frozen key, 64 lowercase hex characters, pinned by two reference vectors. An empty OS id is refused, never hashed. - Activation sends the derived identifier and requires the issued token to carry it. Nothing else originates a machine identifier. - A machine activated before this change stays activated: a valid token whose claim is the raw id of this machine still reads "Activated" (D1). - The machine list flags `is_this_machine` in Rust, for a row under either identifier (D3). `ActivationStatus` no longer carries an identifier and the unused `get_machine_id` command is removed: no command hands the OS id to the webview any more. - After a deactivation the local token is dropped when it stood for the freed slot: any row of this machine takes it, except the old raw row while the token stands for the derived one. - ADR 0020 records the derivation, the transition and the condition for removing the raw-id compatibility. Behaviour changes beyond the issue text: the machine list and a deactivation no longer fail on an unreadable OS id (best effort), the activation status then reads "not activated" instead of failing, and the unused `store_activation_token` command refuses a token issued for the raw id. No new dependency (hmac and sha2 were already declared); Cargo.lock is untouched. Rust 151 -> 168 tests, vitest 1311 -> 1314. Refs #350 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 KiB
ADR 0020 — Identifiant machine dérivé : un hachage à clé fixe remplace l'identifiant du système auprès du serveur de licences
- Status: Accepted
- Date: 2026-10-01
- Issues: #350 (cette décision), maximus/maximus-api#82 (identifiants bruts déjà stockés par le serveur), maximus/la-compagnie-maximus#184 (données d'activation absentes de la politique de confidentialité)
- S'appuie sur l'ADR 0017, amendement #347 (l'édition suit la clé seule), et sur l'ADR 0009 (cours du marché via maximus-api)
Contexte
Activer une machine la lie à une clé de licence auprès du serveur (POST /v1/licenses/activate). Le serveur compte les machines contre la limite de la clé et émet un jeton d'activation, que la route des cours exige. Pour reconnaître une machine, le client envoyait jusqu'à la v0.15.0 l'identifiant du système tel quel : machine_uid::get(), soit le contenu de /var/lib/dbus/machine-id ou de /etc/machine-id sous Linux (32 caractères hexadécimaux), et la valeur MachineGuid du registre sous Windows (36 caractères).
Trois constats, relevés par la revue du 2026-09-24 et revérifiés le 2026-10-01 :
- machine-id(5) tient cet identifiant pour confidentiel. Il ne doit pas être exposé sur le réseau, et une application qui a besoin d'un identifiant stable lié à la machine doit le hacher avec une fonction à clé, sous une clé fixe qui lui est propre. D'autres logiciels lisent le même identifiant : l'envoyer tel quel permet de recouper une machine d'un service à l'autre.
- Le serveur le garde et le rediffuse. maximus-api le stocke avec le courriel de la licence et le nom de l'ordinateur (
license_activations), le renvoie dans chaque liste de machines et le signe dans le jeton d'activation, valable deux ans. - La vente est ouverte depuis le 2026-09-29. Chaque activation faite par une version qui envoie l'identifiant brut en laisse un de plus sur le serveur. La spec d'origine prévoyait pourtant un « hash unique de la machine ».
Un fait encadre la transition : depuis #347, l'édition ne dépend plus du jeton d'activation. Un jeton que le client refuse coûte l'état « Activée » de la carte Licence et les cours du Bilan, jamais l'édition.
Décision
Le client n'envoie plus au serveur que hex(HMAC-SHA256(clé, identifiant du système)), et reste juste pour une machine activée avant ce changement. Cinq règles la tiennent.
1. Une dérivation figée
derive_machine_id (src-tauri/src/commands/license_commands.rs) calcule HMAC-SHA256 de l'identifiant du système, rogné de ses blancs, sous la clé ASCII com.simpl.resultat/license-machine-id/v1, et rend le résultat en 64 caractères hexadécimaux minuscules.
- La clé n'est pas un secret (le dépôt est public). Elle rend l'identifiant propre à l'application : le même poste donne un autre identifiant à tout autre logiciel qui suivrait la même recette, et l'identifiant du système ne se retrouve pas depuis son dérivé.
- Ni la clé ni la formule ne changent, jamais. Chaque activation est enregistrée par le serveur sous un identifiant dérivé de cette clé : en changer rendrait orphelines toutes les activations. Deux vecteurs de référence les épinglent, côté client aujourd'hui et côté serveur le jour où il en aura besoin (maximus/maximus-api#82) :
| Identifiant du système | Identifiant dérivé |
|---|---|
0123456789abcdef0123456789abcdef |
4f8817b37072a3a1e6ef014f34c46835ba715b6f2f97afee1fc680d7fc170db1 |
a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
3b9579ec36b3daf37a2aaab0abe2315fa4a9f07200ae568c5aaa614663037d9d |
- Un identifiant vide est refusé, pas haché. Toutes les machines sans
machine-idpartageraient sinon le même dérivé, donc la même place sur une licence. - Les deux formes ne se confondent pas : 64 caractères pour un dérivé, 32 ou 36 pour un identifiant brut. Une base peut donc être auditée à la longueur.
2. Seule l'activation envoie un identifiant de cette machine
activate_machineenvoie le dérivé. Le jeton que le serveur vient d'émettre doit porter ce même dérivé, sinon la réponse est refusée (invalid_response) et rien n'est écrit.deactivate_machinerenvoie au serveur l'identifiant de la ligne choisie dans la liste, c'est-à-dire une valeur que le serveur a lui-même fournie. Le client ne calcule rien pour cet appel.- Plus aucune commande ne remet l'identifiant du système au webview :
get_machine_id, qui n'avait pas d'appelant, est retirée, etActivationStatusne porte plus d'identifiant.
3. Une machine déjà activée garde son activation (D1)
Un jeton valide dont le claim machine_id vaut l'identifiant brut de cette machine se lit « Activée », comme un jeton qui porte le dérivé. La carte, la liste et les cours d'une machine activée avant ce changement restent donc tels quels, sans appel réseau ajouté. Un jeton expiré, signé par une autre clé ou émis pour une autre machine, sous l'une ou l'autre forme, reste refusé.
4. Le client ne suppose rien du serveur (D3)
- « Cette machine » se décide côté Rust.
list_activated_machinesposeis_this_machinesur chaque ligne dont l'identifiant vaut le dérivé ou le brut de cette machine. Le client reste juste que le serveur garde ses anciennes lignes telles quelles ou les rehache un jour en place. - La suppression du jeton local suit la place qu'il représente. Après une désactivation réussie,
drop_token_of_deactivated_machine_insupprimeactivation.tokendans deux cas. La ligne désactivée est une ligne de cette machine, sous l'une ou l'autre forme : le jeton part avec elle, sauf quand c'est l'ancienne ligne brute et que le jeton vaut pour la ligne dérivée, restée active. La ligne désactivée est celle d'une autre machine : le jeton ne part que s'il a été émis pour cette ligne-là (un jeton repris d'un autre poste).
| Jeton stocké | Identifiant désactivé | Jeton après |
|---|---|---|
| brut | brut de cette machine | supprimé |
| dérivé | dérivé de cette machine | supprimé |
| dérivé | brut de cette machine (ancienne ligne restée active) | gardé |
| brut | dérivé de cette machine (ligne rehachée par le serveur) | supprimé |
| expiré, illisible ou signé par une autre clé | une ligne de cette machine | supprimé |
| de cette machine | ligne d'une autre machine | gardé |
- Un identifiant du système illisible ne bloque ni la liste ni la désactivation. Aucune ligne n'est alors marquée, et le jeton ne part que s'il a été émis pour la ligne désactivée. L'état d'activation se lit « Non activée », et l'activation échoue avant tout appel.
5. Ce que la transition laisse en place (D2)
- Un jeton antérieur garde l'identifiant brut dans son claim. Il est envoyé en
Bearerà chaque appel de cours, donc pour une licence Premium seulement, jusqu'à ce que la machine soit réactivée (« Désactiver » puis « Activer »). - Les lignes que le serveur a déjà reçues gardent leur identifiant brut, y compris après une désactivation, qui marque la ligne inactive sans la supprimer. Le client ne peut rien y faire : c'est l'objet de maximus/maximus-api#82.
- Le critère « aucun identifiant brut ne quitte le poste » est donc borné aux activations faites avec une version qui dérive.
Condition de retrait de la compatibilité
L'acceptation de l'identifiant brut, dans le jeton comme dans la 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 (v0.15.0) : le serveur signe les jetons pour deux ans, plus aucun jeton brut n'est alors valide ;
- le serveur ne renvoie plus de ligne brute (maximus/maximus-api#82).
Tant que l'une des deux manque, retirer la compatibilité ferait perdre à une machine son état « Activée » ou son marqueur « Cette machine ».
Alternatives considérées
- Coupure nette : refuser les jetons bruts. Rejeté : la machine s'afficherait « Non activée », sa réactivation créerait une seconde ligne, et l'ancienne resterait dans la liste comme une machine fantôme qui occupe une place sur la licence. Les cours tomberaient jusqu'à la réactivation.
- Réactivation automatique au démarrage. Rejeté : deux appels réseau que l'utilisateur n'a pas demandés, à faire tenir dans la limite de cinq requêtes par minute que partagent les routes de licence, et un état bancal si le second échoue.
- Hacher côté serveur seulement. Rejeté comme réponse unique : l'identifiant brut continuerait de quitter le poste, ce que machine-id(5) demande d'éviter. C'est en revanche le complément attendu du serveur pour les versions antérieures (maximus/maximus-api#82).
sd_id128_get_machine_app_specific()de systemd. Rejeté : même principe, mais l'appel n'existe que sous Linux et l'application est aussi livrée sous Windows. Une seule dérivation, écrite dans le client, vaut pour les deux.- Un identifiant d'installation tiré au hasard. Rejeté : il ne serait plus lié à la machine. Copier le dossier de données copierait l'activation, et une réinstallation consommerait une place de plus.
Conséquences
- Positif. Une activation faite par une version qui dérive ne montre plus au serveur qu'un identifiant propre à Simpl'Résultat. Le serveur n'a rien à changer : l'identifiant reste pour lui une chaîne opaque comparée par égalité, et 64 caractères passent son schéma.
- Positif. La dérivation et sa clé ont un seul point de vérité, tenu par deux vecteurs de référence que le serveur peut reprendre tels quels.
- Coût. Une branche de compatibilité vit dans le client jusqu'à la condition de retrait ci-dessus.
- Coût. Les identifiants bruts déjà reçus restent sur le serveur tant que maximus/maximus-api#82 n'est pas traitée, et un jeton antérieur continue de porter le sien.
- À surveiller. Le dérivé dépend de ce que
machine_uid::get()renvoie. Les vecteurs épinglent la dérivation, pas la lecture : une version de la cratemachine-uidqui changerait la forme renvoyée (casse, séparateurs) changerait le dérivé de chaque machine. C'était déjà vrai de l'identifiant brut ; relire ce point avant de monter la crate. - Portée. Le nom de l'ordinateur continue d'être envoyé en clair : il sert à reconnaître ses machines dans la liste. La déclaration de ces données relève de maximus/la-compagnie-maximus#184.
Liens
src-tauri/src/commands/license_commands.rs—derive_machine_id,MachineIds, les règles de jeton, de liste et de désactivation, et leurs testssrc/components/settings/LicenseCard.tsx— marqueur « Cette machine » lu depuisis_this_machine- ADR 0017 — amendement #347 : l'édition suit la clé seule, le jeton ne sert qu'à l'état « Activée » et aux cours
docs/architecture.md— sectionlicense_commands.rs- Issues #350, maximus/maximus-api#82, maximus/la-compagnie-maximus#184