fix(license): send a derived machine identifier, never the OS machine id (#350) #366
No reviewers
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#366
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-350-hash-machine-id"
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?
Refs #350
Ce que la PR change
Rust (
license_commands.rs)derive_machine_idcalculeHMAC-SHA256de l'identifiant du système sous la clé figéecom.simpl.resultat/license-machine-id/v1, rendu en 64 caractères hexadécimaux minuscules. Un identifiant vide est refusé (Internal), jamais haché.MachineIdsporte les deux formes de l'identifiant de ce poste, la dérivée et la brute.activate_and_store_indérive juste avant l'appel, envoie le dérivé et exige que le jeton reçu le porte ; sinonInvalidResponse, et rien n'est écrit.list_activated_machinesposeis_this_machinesur chaque ligne, pour le dérivé comme pour le brut. La valeur n'est jamais lue dans la réponse du serveur.drop_token_of_deactivated_machine_insupprime le jeton local quand il représentait la place libérée.ActivationStatusne porte plus queis_activated.get_machine_idest retirée (commande et enregistrement danslib.rs).Front
licenseService.ts:getMachineIdretirée,MachineInfo.is_this_machineajouté,ActivationStatussans identifiant.LicenseCard.tsx: le marqueur « Cette machine » litm.is_this_machine.Docs : ADR 0020 (dérivation figée, D1 à D3, condition de retrait de la compatibilité),
architecture.md, guide (« Ce que l'activation envoie ») et docs intégrées FR/EN, CHANGELOG FR/EN sous Sécurité.Aucune dépendance ajoutée :
hmacetsha2étaient déjà déclarés,Cargo.tomletCargo.locksont intacts.Écarts assumés par rapport au corps de l'issue
activate_and_store_inreçoit l'identifiant brut, pas unMachineIds. La dérivation et la garde sur l'identifiant vide sont ainsi dans la fonction testée, et « aucun appel » est observé contre un mock, pas déduit. Relevé par le plan checker.Internalaprès que le serveur avait déjà libéré la place.invalid_input.store_activation_token, sans appelant côté front et laissée hors périmètre, refuse désormais un jeton émis pour l'identifiant brut : elle passe par la même fonction que l'activation.Preuves
Suites sur
0a9c9f7:cargo test151 → 168 ; vitest 1311 → 1314 (68 fichiers) ;npm run build(tsc + vite) vert ;cargo checksans avertissement, avec et sans les tests.Vecteurs de référence recalculés hors du code (Python
hmac, puisopensslpar le plan checker et par la revue) : identiques à ceux de l'issue et de maximus/maximus-api#82.Mutations de l'auteur, 17 sur 17 rouges, mesurées sur
e5662ad. Chaque mutation a été vérifiée comme appliquée (git diffnon vide), puis le fichier restauré depuisHEADet l'arbre vérifié propre. Rust : 71 tests du module ; front : 21 tests du fichier de rendu.MachineIdsgarde le brut à la place du dérivéis_this_machinenon sérialisé vers le webviewLa première ligne est le point de décision demandé par l'issue : le seul mock déclaré exige le dérivé dans
machine_idet refuse tout corps où l'identifiant du système apparaît, dans quelque champ que ce soit.Mutation de la revue restée verte, maintenant rouge : « un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste ». Remesurée sur le fichier de
e5662ad: 0 rouge sur 71. Sur0a9c9f7, avec les deux lignes ajoutées au tableau de test : 1 rouge sur 71. Fichier restauré, 71 verts, arbre propre.Ce que les tests ne gardent pas. Le câblage des commandes à l'identifiant réel du poste (
local_machine_ids()etraw_machine_id(), qui appellentmachine_uid::get()) : les tests n'appellent jamais cette lecture, comme le demande l'issue. PasserNoneà la liste ou à la désactivation laisse la suite verte, ce que la revue a confirmé. La vérification manuelle ci-dessous couvre l'état, la liste et l'activation ; rien ne couvre la suppression du jeton à la désactivation au-delà de son point 3.Relevé en route, hors périmètre.
architecture.mdannonce 36 commandes (35 après cette PR) ;generate_handler!en enregistre 42 (41 après). L'écart est antérieur : six commandes ne sont pas listées (fetch_price,send_feedback,get_feedback_user_agent,ensure_backup_dir,get_file_size,file_exists).Vérification manuelle (d'où
Refs, sans mot-clé de fermeture)À faire 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 l'application réelle, un poste activé par la v0.15.0 et le serveur de licences de production. Il se fait avec un build de
mainaprès le merge (npm run tauri dev, même dossier de données) ou avec le build de la release, avant de la publier.32 brut: True dérivé: False. C'est aussi la preuve, sur un vrai jeton, que la v0.15.0 envoyait bien l'identifiant du système tel quel.64 brut: False dérivé: True. Une longueur de 64 ne suffit pas : une double dérivation ou une autre clé la donnent aussi, avecdérivé: False.Commande des points 2 et 3, sous Linux. Elle compare le claim du jeton à l'identifiant du système et à son dérivé, recalculé hors de l'application, et n'affiche qu'une longueur et deux booléens. Essayée sur trois jetons synthétiques (brut, dérivé, double dérivation) par la revue puis par l'auteur.
Sous Windows, le jeton est dans
%APPDATA%\com.simpl.resultat\activation.tokenet l'identifiant du système dansHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid; l'équivalent de la commande reste à écrire si le point 3 se fait sous Windows.🤖 Generated with Claude Code
CI verte sur
e5662ad, logs lus et pas seulement le statut :check-frontend(run 441) :tsc && vite buildvert, 68 fichiers, 1314 tests.check-rust(run 442) :Suppression guard: 2 checks, exit 0,cargo test168 verts.cargo auditrelève 666 crates et 12 warnings autorisés, comme surmain:Cargo.lockn'a pas bougé.Les deux runs ont bien extrait
e5662ad(pull/366/head). La tête n'a pas encore été relue.Revue adversariale — APPROVE
Verdict : APPROVE. Aucun point bloquant. Cinq suggestions ; la première porte sur la vérification manuelle qui reste à faire avant la fermeture à la main de #350.
Résumé. La PR fait ce que dit son titre : l'activation envoie
HMAC-SHA256de l'identifiant du système et plus cet identifiant, une machine activée par la v0.15.0 reste reconnue dans le jeton comme dans la liste, et la suppression du jeton à la désactivation est une règle écrite et testée cas par cas. Les écarts au corps de l'issue sont déclarés, et celui qui change une règle est gardé par un test (mutation 4 du tableau).Ce qui a tourné
Sur une copie hors dépôt de
e5662ad(git archive, dossiertargetneuf). Le dépôt n'a pas été touché.cargo test --all-targets --locked: 168 verts, sans avertissement.--lockedpasse :Cargo.lockest intact et cohérent avecCargo.toml.npm run buildvert ; vitest 1314 verts sur 68 fichiers.e5662ad; 1314 et 168 verts,Suppression guard: 2 checks, exit 0.hmac, puisopenssl dgst -hmac) : identiques aux deux littéraux.local_machine_ids()rend le contenu rogné du fichiermachine-idet le dérivé que donne la recette hors Rust. Comparaison booléenne, aucun identifiant affiché.Mutations du relecteur
22 mutations Rust (71 tests du module) et 5 mutations front (21 tests du fichier de rendu). Chacune a été vérifiée comme appliquée (
git diff --stat), puis le fichier restauré et l'arbre vérifié propre ; la suite est verte à la fin. Celles marquées « PR » rejouent une mutation de la description et rendent le même chiffre.machine_idporte le dérivé, mais le brut part dans un autre champ du corpsMachineIdsgarde le brut sans le rognerskip_deserializingremplacé pardefault, la réécriture du drapeau conservéelist_activated_machines_implpasseNonedeactivate_machine_implpasseNoneLes quatre vertes :
flag_this_machineréécrit le drapeau de chaque ligne, donc l'attribut est une seconde barrière ; lever les deux (mutation 7) rougit.local_machine_ids()n'est gardé par aucun test. Le point 1 de la vérification manuelle couvre la liste et l'état. Rien à changer ici.La mutation 5 isole le
match_requestdu test de décision : le corps satisfait lePartialJson, seul le refus de l'identifiant du système « dans quelque champ que ce soit » le fait tomber. La mutation 4 montre que l'écart déclaré sur la règle de suppression est bien gardé.Vérifié à la source, hors de ce dépôt
3673d09.machine_idest validé parz.string().min(1)(borné à 255 surmain), stocké dans une colonnetext, et le claim du jeton reprendinput.machine_idtel que reçu./v1/pricesne lit pas ce claim. 64 caractères passent, et l'exigence « le jeton reçu porte le dérivé » tient contre la production.machine-uid0.5.4, la version duCargo.lock. La valeur rendue est déjà rognée sous Linux (read_file) et sous Windows (id.trim()). LetrimdeMachineIds::from_rawne change donc rien à ce que la v0.15.0 envoyait : la comparaison de D1 tient à l'octet près.Fermeture de l'issue
La timeline de #350 porte un
pull_refvenant de cette PR avecref_action: "none", et le message du commit ditRefs. Le merge laissera #350 ouverte, ce qui est voulu : la vérification manuelle reste due.Suggestions
1. Vérification manuelle : comparer la valeur, pas la longueur. Le point 3 attend « 64 ». Une double dérivation, une autre clé ou un identifiant lu ailleurs donnent aussi 64. Or le câblage de
activate_machine_implà la lecture réelle de l'identifiant (raw_machine_id()) n'a que ce contrôle manuel pour lui, et la dérivation devient définitive à la première activation en production. La commande ci-dessous affiche une longueur et deux booléens, aucun identifiant. Essayée sur trois jetons synthétiques : brut32 brut: True dérivé: False, dérivé64 brut: False dérivé: True, double dérivation64 brut: False dérivé: False.Attendu : au point 2,
32 brut: True dérivé: False; au point 3,64 brut: False dérivé: True. La première ligne, sur un vrai jeton de la v0.15.0, est aussi la preuve de la prémisse de D1 que les tests ne peuvent pas donner. L'équivalent Windows reste à écrire si le point 3 se fait sous Windows.2. « Plus aucune commande ne remet l'identifiant du système au webview » est plus large que le code. Pour une activation antérieure à la dérivation,
list_activated_machinesrend la ligne de ce poste avecmachine_idégal à l'identifiant du système etis_this_machine: true. C'est le jeu de données même dethe_webview_gets_the_flag_and_no_identifier_of_its_own(license_commands.rs:1685), et le commentaire du champmachine_id, dans la même interface delicenseService.ts, le dit. La phrase est vraie d'un poste activé sous le dérivé. Trois endroits à borner, comme D2 borne le critère réseau : ADR 0020 §2 (ligne 43),docs/architecture.md(paragraphe « Identifiant machine »),src/services/licenseService.ts:44-46. Formulation possible : « Plus aucune commande ne lit l'identifiant du système pour le remettre au webview. La liste du serveur porte encore celui d'une ligne antérieure à la dérivation, jusqu'à la réactivation. »3. Deux lignes à ajouter au tableau de
deactivation_drops_the_token_that_stood_for_the_freed_slot. La règle livrée dit que toute ligne de ce poste emporte le jeton. Pour un jeton valide émis pour un autre poste (données restaurées d'un autre ordinateur, le cas de #360), rien ne le tient : la mutation 22 reste verte.(OTHER_RAW, THIS_RAW, false, …)et(OTHER_DERIVED, THIS_DERIVED, false, …)épinglent le comportement actuel, qui est aussi celui d'avant la PR. La ligne correspondante manque au tableau du §4 de l'ADR.4. ADR 0020, condition de retrait : compter depuis la dernière activation brute, pas depuis la dernière version. « Deux ans depuis la v0.15.0, plus aucun jeton brut n'est alors valide » ne tient pas. Une v0.15.0 en édition Gratuite ne se met plus à jour ; son utilisateur peut acheter une clé dans un an, la saisir et cliquer « Activer » avant toute mise à jour. Le serveur signe alors un jeton brut neuf, valable deux ans. La borne exacte est la dernière émission d'un jeton à claim brut, que seul le serveur connaît : aujourd'hui le
last_seen_atdes lignes de 32 ou 36 caractères, que seules l'activation et son rafraîchissement écrivent. Après la piste 2 de maximus/maximus-api#82 (hachage à l'arrivée, claim gardé tel qu'envoyé), les lignes ne le diront plus : la date devra être consignée à l'arrivée. La phrase vient du corps de l'issue, l'ADR la reprend fidèlement.5. ADR 0020 §5 : deux précisions.
machine_limit_reachedsi la licence est pleine. Le code s'en sort bien : les deux lignes sont marquées « Cette machine », et désactiver l'ancienne garde le jeton. Le §5 ne nomme pas ce reste, alors que le rejet de la « coupure nette » s'appuie sur la même machine fantôme. À consigner, avec ce qui l'éteint : le rehachage en place des lignes actives (piste 1 de maximus/maximus-api#82), fait avant l'expiration des premiers jetons. Les points d'attention de cette issue décrivent le cas miroir, une ancienne version après le rehachage, pas celui-ci.Confirmé au passage, hors périmètre
generate_handler!enregistre 41 commandes sur cette tête,architecture.mden annonce 35 : le décompte de la description est exact, et l'écart est antérieur à la PR.Les cinq suggestions de la revue sont appliquées dans
0a9c9f7. Cette tête n'a pas été relue : l'approbation portait sure5662ad, et #350 repasse enstatus:review. Max a demandé d'appliquer puis de merger sans nouvelle revue ; le commit ne change aucun comportement (docs, commentaire, deux lignes de test).32 brut: True dérivé: Falseavant,64 brut: False dérivé: Trueaprès. Je l'ai rejouée sur trois jetons synthétiques (brut, dérivé, double dérivation) : mêmes sorties que celles annoncées. La vérification est maintenant datée : avant le tag de la release.docs/architecture.mdet le commentaire delicenseService.ts.(OTHER_RAW, THIS_RAW)et(OTHER_DERIVED, THIS_DERIVED)ajoutées àdeactivation_drops_the_token_that_stood_for_the_freed_slot, et la ligne correspondante au tableau du §4 de l'ADR.Preuve du rouge, mutation 22 de la revue (« un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste ») :
e5662ad, mutation appliquée0a9c9f7, mutation appliquéedeactivation_drops_the_token_that_stood_for_the_freed_slot)0a9c9f7, fichier restauréSuites sur
0a9c9f7:cargo test168, vitest 1314,npm run buildvert. Les mutations 20 et 21 de la revue (câblage de la liste et de la désactivation à l'identifiant réel) restent vertes, comme annoncé : la description le dit maintenant pour les deux commandes.