security: hacher l'identifiant machine avant de l'envoyer au serveur de licences #350
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#350
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Contexte
Le client envoie au serveur de licences l'identifiant machine brut :
activate_machine_implpassemachine_uid::get()tel quel dans le payload dePOST /v1/licenses/activate, avec le nom d'hôte. Sous Linux c'est le contenu de/var/lib/dbus/machine-idou de/etc/machine-id(32 caractères hexadécimaux), sous Windows la valeurMachineGuiddu 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 (claimmachine_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:317prévoyait d'ailleurs un « hash unique de la machine ». Trouvé par la revue/review-specdu 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,
mainc101730, maximus-apiac91da4)Dans
src-tauri/src/commands/license_commands.rs, se repérer aux noms de fonctions :machine_id_internal()appellemachine_uid::get(). Quatre fonctions s'en servent, en plus de la commandeget_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_keycompare le claimmachine_iddu jeton à l'identifiant local.activation_status_inen dépend.current_entitlementslit 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 :getMachineIddesrc/services/licenseService.tsest du code mort.LicenseCard.tsxmarque « Cette machine » parmachines.activation?.machine_id === m.machine_id: il compare l'identifiant local aux identifiants de la liste serveur.fetch_price(balance_commands.rs) envoieactivation.tokenen Bearer sans le relire.Côté serveur, rien à changer pour ce correctif :
machine_idest une chaîne opaque comparée par égalité (licenseService.ts), le schéma accepte 64 caractères (z.string().min(1), borné à 255 surmain), et/v1/prices(licenseAuth.ts) ne lit jamais le claimmachine_id.Aucune dépendance à ajouter :
hmac = "0.12"etsha2 = "0.10"sont déjà déclarés (patron HMAC dansaccount_cache.rs, encodage hexadécimal comme dansprofile_commands.rs).Cargo.lockne doit pas bouger.Décisions (2026-10-01)
Tâches
Rust —
license_commands.rsderive_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.trim) est une erreur (LicenseError::Internal), jamais haché : toutes les machines sansmachine-idpartageraient sinon le même identifiant dérivé.InvalidResponse.is_this_machine, calculé ici, vrai quand son identifiant vaut le dérivé ou le brut de cette machine.ActivationStatusne porte plus d'identifiant. Retirerget_machine_id(commande, enregistrement danslib.rs).Front
licenseService.ts: retirergetMachineId, ajouteris_this_machineàMachineInfo, retirermachine_idd'ActivationStatus.LicenseCard.tsx: le marqueur « Cette machine » litm.is_this_machine.useLicenseMachines.test.ts,LicenseCard.render.test.tsx,licenseService.test.ts.Documentation
docs/architecture.md: sectionlicense_commands.rs(une commande de moins, dérivation, transition) et total des commandes.docs/guide-utilisateur.md(carte Licence) etdocs.editions.tipsen 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.mdetCHANGELOG.fr.md, sousSecurity/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 demachine-id).derive_machine_id(les mêmes serviront au serveur) :0123456789abcdef0123456789abcdefdonne4f8817b37072a3a1e6ef014f34c46835ba715b6f2f97afee1fc680d7fc170db1a1b2c3d4-e5f6-7890-abcd-ef1234567890donne3b9579ec36b3daf37a2aaab0abe2315fa4a9f07200ae568c5aaa614663037d9dis_this_machine: vrai pour une ligne dérivée, vrai pour une ligne brute, faux pour une ligne étrangère.LicenseCard: « Cette machine » suitis_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
get_machine_idn'existe plus etActivationStatusne porte aucun identifiant. La liste renvoyée par le serveur garde les identifiants de ses lignes, bruts pour les lignes antérieures.Cargo.lockinchangé.cargo test,npm testet la CI (check-rust,check-frontend) verts.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
store_activation_tokenn'a pas d'appelant côté front non plus : nettoyage à part.Dépendances
Complexité
Moyenne : un PR, Rust et front, sans migration de base ni changement serveur.
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) :current_entitlementsne compare plus rien au jeton, etget_machine_idn'a aucun appelant côté front.deactivate_machine_impl, le marqueur « Cette machine » deLicenseCard.tsx, et le Bearer des cours, qui envoie un jeton antérieur avec son claim brut.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-idsous Linux,MachineGuiddu 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:317prévoyait d'ailleurs un « hash unique de la machine ». Trouvé par la revue/review-specdu 2026-09-24 (séance/plan-runpaiements).À 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) appellemachine_uid::get(). Il sert au payload d'activation (l.396) et aux comparaisons locales avec le claimmachine_iddu 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.license_activations.machine_id), comparée par égalité.mainle 2026-09-24. #270 et #347 réécrivent ce fichier : se repérer aux noms de fonctions.Tâches
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).machine_idvaut 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.docs/architecture.md(dérivation de l'identifiant).Critères d'acceptation
cargo testvert.Depends on
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 licencesPR #366 mergée le 2026-10-01 (
main0a9c9f7, fast-forward). L'issue reste ouverte : la PR portaitRefs, parce qu'un critère d'acceptation ne se vérifie pas dans les tests. Elle passe enstatus:blockedjusqu'à 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.32 brut: True dérivé: False.64 brut: False dérivé: True.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.tokenet l'identifiant du système dansHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid.Deux corrections au corps de cette issue
Suites
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-idque le cratemachine-uid) et n'affiche ni identifiant ni clé. Heures locales :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_matchesetactivation_status_accepts_a_token_from_before_the_derivation.É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 :
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 :
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 toujours64 brut: False dérivé: True: c'est l'exception de la règle de suppression (ADR 0020), et cela libère la place ;Fermée le 2026-10-04, sur décision de Max.
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.activation_token_from_before_the_derivation_still_matchesetactivation_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).