fix(license): send a derived machine identifier, never the OS machine id (#350) #366
4 changed files with 13 additions and 5 deletions
|
|
@ -25,7 +25,7 @@ Un fait encadre la transition : depuis #347, l'édition ne dépend plus du jeton
|
||||||
|
|
||||||
`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.
|
`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é.
|
- **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é. Elle ne protège pas du sens inverse : qui détient déjà l'identifiant du système d'un poste peut recalculer son dérivé, puisque la clé est publique. La protection porte sur la remontée du dérivé vers l'identifiant du système, pas sur ce rapprochement.
|
||||||
- **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) :
|
- **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é |
|
| Identifiant du système | Identifiant dérivé |
|
||||||
|
|
@ -40,7 +40,7 @@ Un fait encadre la transition : depuis #347, l'édition ne dépend plus du jeton
|
||||||
|
|
||||||
- `activate_machine` envoie 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.
|
- `activate_machine` envoie 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_machine` renvoie 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.
|
- `deactivate_machine` renvoie 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, et `ActivationStatus` ne porte plus d'identifiant.
|
- Plus aucune commande ne lit l'identifiant du système pour le remettre au webview : `get_machine_id`, qui n'avait pas d'appelant, est retirée, et `ActivationStatus` ne porte plus d'identifiant. La liste que rend le serveur porte encore celui d'une ligne antérieure à la dérivation, marquée `is_this_machine` quand c'est cette machine, jusqu'à sa réactivation.
|
||||||
|
|
||||||
### 3. Une machine déjà activée garde son activation (D1)
|
### 3. Une machine déjà activée garde son activation (D1)
|
||||||
|
|
||||||
|
|
@ -58,6 +58,7 @@ Un jeton valide dont le claim `machine_id` vaut l'identifiant **brut** de cette
|
||||||
| dérivé | brut de cette machine (ancienne ligne restée active) | gardé |
|
| 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é |
|
| 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é |
|
| expiré, illisible ou signé par une autre clé | une ligne de cette machine | supprimé |
|
||||||
|
| valide, émis pour une autre machine | une ligne de cette machine | supprimé |
|
||||||
| de cette machine | ligne d'une autre machine | gardé |
|
| 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.
|
- **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.
|
||||||
|
|
@ -66,13 +67,14 @@ Un jeton valide dont le claim `machine_id` vaut l'identifiant **brut** de cette
|
||||||
|
|
||||||
- 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 »).
|
- 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.
|
- 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.
|
||||||
|
- **Une seconde ligne à la réactivation.** Quand une ligne brute de cette machine est encore active et que le jeton local manque ou n'est plus valide (expiration au bout de deux ans, dossier de données réinitialisé, autre clé puis retour à la première), « Activer » envoie le dérivé. Le serveur n'a aucune ligne active sous cet identifiant : c'est pour lui une activation nouvelle, là où la v0.15.0 obtenait un rafraîchissement. Elle prend une place de plus, ou rend `machine_limit_reached` si la licence est pleine. Les deux lignes sont alors marquées « Cette machine », et désactiver l'ancienne garde le jeton (règle 4). Ce reste s'éteint si le serveur rehache en place ses lignes actives avant l'expiration des premiers jetons bruts, deux ans après leur émission (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**.
|
- 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é
|
## 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 :
|
L'acceptation de l'identifiant brut, dans le jeton comme dans la liste, se retire quand deux conditions sont réunies :
|
||||||
|
|
||||||
1. **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 ;
|
1. **deux ans se sont écoulés depuis la dernière émission d'un jeton à claim brut.** Le serveur signe les jetons pour deux ans : passé ce délai, plus aucun jeton brut n'est valide. Cette date ne se déduit pas d'un numéro de version. Une v0.15.0 en édition Gratuite ne se met plus à jour ; son utilisateur peut acheter une clé des mois plus tard, la saisir et cliquer « Activer » avant toute mise à jour, et le serveur signe alors un jeton brut neuf. Seul le serveur connaît la date : aujourd'hui le `last_seen_at` le plus récent de ses lignes brutes, que seuls l'activation et son rafraîchissement écrivent. S'il hache un jour les identifiants à l'arrivée, il devra la consigner à part (maximus/maximus-api#82) ;
|
||||||
2. **le serveur ne renvoie plus de ligne brute** (maximus/maximus-api#82).
|
2. **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 ».
|
Tant que l'une des deux manque, retirer la compatibilité ferait perdre à une machine son état « Activée » ou son marqueur « Cette machine ».
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@ Contrat d'erreur des commandes réseau (#270) : `LicenseError`, sérialisé en J
|
||||||
|
|
||||||
Résolution de l'édition (#347, D12) : `current_entitlements` lit la seule `license.key` du dossier de données (`entitlements_from_dir`) ; une clé valide et non expirée donne son édition et ses `features[]`, quel que soit `activation.token`. Le jeton lie la clé à la machine **côté serveur** seulement : il sert à l'état « Activée » de la carte et aux cours du Bilan, que `/v1/prices` vérifie. L'état « Activée » est décidé par `activation_status_in` (signature, `machine_id`, `exp`), testée dans un dossier temporaire. Voir l'amendement #347 de l'[ADR 0017](adr/0017-feature-gating-par-tier.md).
|
Résolution de l'édition (#347, D12) : `current_entitlements` lit la seule `license.key` du dossier de données (`entitlements_from_dir`) ; une clé valide et non expirée donne son édition et ses `features[]`, quel que soit `activation.token`. Le jeton lie la clé à la machine **côté serveur** seulement : il sert à l'état « Activée » de la carte et aux cours du Bilan, que `/v1/prices` vérifie. L'état « Activée » est décidé par `activation_status_in` (signature, `machine_id`, `exp`), testée dans un dossier temporaire. Voir l'amendement #347 de l'[ADR 0017](adr/0017-feature-gating-par-tier.md).
|
||||||
|
|
||||||
Identifiant machine (#350, [ADR 0020](adr/0020-identifiant-machine-derive.md)) : le serveur de licences ne reçoit plus l'identifiant du système (`machine_uid::get()` : `/etc/machine-id`, `MachineGuid`), que machine-id(5) tient pour confidentiel, mais `derive_machine_id`, soit `HMAC-SHA256` sous la clé fixe `com.simpl.resultat/license-machine-id/v1`, rendu en 64 caractères hexadécimaux. La clé et la formule sont figées, épinglées par deux vecteurs de référence ; un identifiant vide est refusé, jamais haché. `MachineIds` porte les deux formes de l'identifiant de cette machine, la dérivée et la brute, et la brute ne sert plus qu'à reconnaître ce qui date d'avant la dérivation : un jeton dont le claim la porte se lit toujours « Activée », et une ligne de la liste qui la porte est toujours marquée `is_this_machine`. Seule l'activation envoie un identifiant de cette machine (`activate_and_store_in`, qui dérive juste avant l'appel et exige que le jeton reçu porte le dérivé) ; la désactivation renvoie au serveur l'identifiant de la ligne choisie. `drop_token_of_deactivated_machine_in` supprime ensuite le jeton local s'il représentait la place libérée : toute ligne de cette machine l'emporte, sauf l'ancienne ligne brute quand le jeton vaut pour la ligne dérivée, restée active. Un identifiant du système illisible ne bloque ni la liste ni la désactivation, et se lit « non activée ». Plus aucune commande ne remet l'identifiant du système au webview. La compatibilité avec l'identifiant brut a une condition de retrait, consignée dans l'ADR.
|
Identifiant machine (#350, [ADR 0020](adr/0020-identifiant-machine-derive.md)) : le serveur de licences ne reçoit plus l'identifiant du système (`machine_uid::get()` : `/etc/machine-id`, `MachineGuid`), que machine-id(5) tient pour confidentiel, mais `derive_machine_id`, soit `HMAC-SHA256` sous la clé fixe `com.simpl.resultat/license-machine-id/v1`, rendu en 64 caractères hexadécimaux. La clé et la formule sont figées, épinglées par deux vecteurs de référence ; un identifiant vide est refusé, jamais haché. `MachineIds` porte les deux formes de l'identifiant de cette machine, la dérivée et la brute, et la brute ne sert plus qu'à reconnaître ce qui date d'avant la dérivation : un jeton dont le claim la porte se lit toujours « Activée », et une ligne de la liste qui la porte est toujours marquée `is_this_machine`. Seule l'activation envoie un identifiant de cette machine (`activate_and_store_in`, qui dérive juste avant l'appel et exige que le jeton reçu porte le dérivé) ; la désactivation renvoie au serveur l'identifiant de la ligne choisie. `drop_token_of_deactivated_machine_in` supprime ensuite le jeton local s'il représentait la place libérée : toute ligne de cette machine l'emporte, sauf l'ancienne ligne brute quand le jeton vaut pour la ligne dérivée, restée active. Un identifiant du système illisible ne bloque ni la liste ni la désactivation, et se lit « non activée ». 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. La compatibilité avec l'identifiant brut a une condition de retrait, consignée dans l'ADR.
|
||||||
|
|
||||||
Le lien d'achat, `/{fr|en}/produits/simpl-resultat` (`src/shared/purchaseUrl.ts`), utilisé par la carte Licence, `PurchaseButton` et `DownloadLatestButton`, est un contrat avec le site : il est gravé dans des binaires qui ne se mettent plus à jour en édition gratuite (voir maximus/la-compagnie-maximus#153).
|
Le lien d'achat, `/{fr|en}/produits/simpl-resultat` (`src/shared/purchaseUrl.ts`), utilisé par la carte Licence, `PurchaseButton` et `DownloadLatestButton`, est un contrat avec le site : il est gravé dans des binaires qui ne se mettent plus à jour en édition gratuite (voir maximus/la-compagnie-maximus#153).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1718,6 +1718,10 @@ mod tests {
|
||||||
(THIS_DERIVED, OTHER_RAW, true, "derived token, raw row of another machine"),
|
(THIS_DERIVED, OTHER_RAW, true, "derived token, raw row of another machine"),
|
||||||
(OTHER_RAW, OTHER_RAW, false, "token of another machine, that machine's row"),
|
(OTHER_RAW, OTHER_RAW, false, "token of another machine, that machine's row"),
|
||||||
(OTHER_RAW, OTHER_DERIVED, true, "token of another machine, a different row"),
|
(OTHER_RAW, OTHER_DERIVED, true, "token of another machine, a different row"),
|
||||||
|
// Data restored from another computer (#360): the foreign token goes with any
|
||||||
|
// row of this machine, as it did before the derivation.
|
||||||
|
(OTHER_RAW, THIS_RAW, false, "token of another machine, raw row of this machine"),
|
||||||
|
(OTHER_DERIVED, THIS_DERIVED, false, "token of another machine, derived row of this machine"),
|
||||||
];
|
];
|
||||||
for (token_id, deactivated, kept, case) in cases {
|
for (token_id, deactivated, kept, case) in cases {
|
||||||
let dir = TestDir::new("deactivation_token");
|
let dir = TestDir::new("deactivation_token");
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ export interface MachineInfo {
|
||||||
last_seen_at: string;
|
last_seen_at: string;
|
||||||
/**
|
/**
|
||||||
* Whether the row names this machine. Decided in Rust, which knows both forms of the
|
* Whether the row names this machine. Decided in Rust, which knows both forms of the
|
||||||
* identifier: the webview never receives the local one (ADR 0020).
|
* identifier: no command reads the local one for the webview (ADR 0020). A row from
|
||||||
|
* before the derivation still carries it in `machine_id`, until the machine is
|
||||||
|
* activated again.
|
||||||
*/
|
*/
|
||||||
is_this_machine: boolean;
|
is_this_machine: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue