# Sessions scellées de bout en bout (sealed sessions)

> HTML: https://remote.ivjn.us/docs/fr/sealed-sessions/

Android Console ouvre le chiffrement de bout en bout depuis les paramètres, avec choix de l’appareil sur la page. Les paramètres du compte Android et Web permettent de définir ou modifier le mot de passe et d’associer ou remplacer l’e-mail ; l’ancienne adresse reste active jusqu’à vérification. Console sauvegarde par défaut la clé racine dans Exchange avec une phrase secrète vide, sans écraser une sauvegarde protégée existante. La suppression désactive l’enregistrement automatique sur ce client. Une phrase vide permet à Exchange ou au détenteur de la base de déchiffrer la clé. La restauration manuelle vérifie la racine de l’espace avant de la conserver et de synchroniser les autorisations.

Objectif : même si Exchange est compromis ou son exploitant curieux, il **ne peut lire** commandes, sorties, octets PTY, fichiers, captures d’écran et données ubus, ni **décider ou falsifier** qui peut consulter ou administrer un appareil. Exchange devient un relais de texte chiffré et de métadonnées de routage.

Définitions : `crates/proto/src/seal.rs` (types réseau, autorisation locale), `crates/proto/src/seal/crypto.rs` (cryptographie des sessions, ring uniquement), `crates/proto/src/seal/backup.rs` (sauvegarde par phrase secrète, Argon2id + ChaCha20-Poly1305, non lié à l’agent), `crates/agent/src/seal.rs` (appareil), `crates/cli/src/seal.rs` (CLI), `shared/seal-session.js` (sessions communes aux consoles Web et Android, @noble/curves · @noble/hashes · @noble/ciphers), `shared/seal-backup.js` (sauvegarde Argon2id sur les mêmes bibliothèques). Vecteurs interlangages : `shared/seal-vectors.json` (générés par JS ; Rust `javascript_vectors_interoperate` vérifie signatures/HKDF/texte chiffré octet par octet), `shared/seal-rust-vectors.json` (réponse d’appareil Rust ; JS `shared/seal-session.test.js` termine la négociation et déchiffre).

`seal approve` publie toujours automatiquement les autorisations. Un appel suivant à `seal grant --publish` (ou la publication par défaut après connexion) ignore une autorisation publiée non expirée ayant les mêmes clés émettrice et destinataire, appareil et level, indique qu’elle est déjà publiée et n’émet aucun nouveau jeton. Utilisez `--renew-before DAYS` pour renouveler les autorisations proches de l’expiration ; `--no-publish` conserve la signature hors ligne. Ce contrôle évite les doublons lors de tentatives successives, sans transaction entre clients concurrents ni suppression des anciens doublons. `seal show` crée une identité locale et enregistre sa clé publique si vous êtes connecté ; utilisez un profile séparé pour les essais.


## Modèle de menace

| Élément | Protection et limites des sessions scellées |
|---|---|
| Commande, stdout/stderr, code de sortie | **Invisibles** ; audit limité à `sealed:true`, clé publique opérateur, niveau d’autorisation, expiration et résultat final |
| Saisie PTY et sortie à l’écran | **Invisibles**, les trames chiffrées ne sont pas auditées |
| Fichiers, captures, arbre d’interface, paramètres et résultats ubus | **Invisibles** |
| Permissions (superadministrateur/administrateur/opérateur/lecture seule, consultation/gestion) | **Décidées par l’appareil** : seules les autorisations signées par sa racine locale épinglée sont reconnues ; Exchange ne peut les délivrer, étendre ou falsifier |
| Usurpation d’appareil | Impossible : signature d’identité native et clé publique de l’appareil épinglée par la CLI ; l’extension vérifie la preuve de chaîne racine et la signature de réponse |
| Injection de touches ou de commandes | Impossible : authentification AEAD et compteurs strictement croissants ; l’agent rejette tout contenu en clair adressé à un identifiant de session scellée |
| Rejeu, réordonnancement, perte, troncature | Détectés, avec fermeture de la session en cas d’échec |
| Métadonnées encore visibles | Identifiant d’appareil, compte opérateur, identifiant de session, horaires, taille et nombre de paquets ; la distinction stdout/stderr reste invisible (type dans le contenu chiffré) |
| Actions encore possibles | Déni de service : ne pas transmettre, déconnecter ou fermer une session (`sealed_close`). Cela concerne la disponibilité, pas les permissions |

Hors protection : appareil ou ordinateur de l’opérateur compromis ; usurpation par Exchange dès la première connexion TOFU, évitable par comparaison d’empreintes sur un canal indépendant. Toutes les opérations sur les appareils exigent des sessions scellées ; les limites du catalogue de commandes et de l’autorisation par chaîne racine du navigateur figurent ci-dessous.

## Couverture des chemins et conception

| Chemin | Comportement des sessions scellées | Autorisation sur l’appareil et limites |
|---|---|---|
| HTTP `POST /api/devices/{id}/exec` | **Refus explicite**, HTTP 409, indication d’utiliser une session WebSocket `open_sealed` | L’appareil refuse toujours `exec_req` en clair même si Exchange ignore la politique |
| HTTP de commande prédéfinie `/commands/{cmd}/run` | **Refus explicite**, même indication d’utiliser un WS scellé | Une commande de la base serveur ne constitue pas une autorisation sur l’appareil |
| Commandes prédéfinies Web Console / CLI | Modèle extrait du catalogue et envoyé par l’opérateur comme `exec_req` dans un WS scellé ; sortie également scellée | L’appareil vérifie hello, grant signé par la racine, révocation, validité et niveau `manage` ; `view` ne permet pas d’exécuter un modèle |
| LAN `/ws/operator` | Sessions scellées uniquement, grant et chaîne de certificats inclus dans hello ; aucun jeton Bearer | Même chaîne racine et mêmes règles view/manage qu’Exchange. La couche externe ne vérifie que l’adresse privée et Origin. `/api/devices` expose seulement l’identifiant de l’appareil et la clé/le mode de scellement |
| Agent navigateur / routes scellées natives | `open_sealed` / `sealed_input` / `sealed_event`, ancien canal navigateur refusé | L’extension vérifie le grant et la chaîne opérateur signés par la racine ; CLI/Console vérifient la chaîne de `SealAccept.peer` et exigent que sa clé publique soit celle qui signe la réponse |
| CLI `exec-attach` / `exec-hold-tail` / `exec-hold-cat` | Requêtes natives, trames de sortie et événements de fin scellés | `manage` requis ; les tâches scellées ne peuvent être lues ni rejointes en clair |
| CLI `sensors --live` | Une session scellée indépendante par échantillon (une opération par session), Ctrl-C arrête le rafraîchissement | `view` suffit ; plusieurs échantillons ne sont pas regroupés dans un tunnel à opération unique |
| CLI `net` | `net_req` et `net_resp` scellés, y compris URL, en-têtes, corps et résultats | `manage` requis |
| CLI `device update` | `update_req`, toute la progression et l’état final sont scellés | `manage` requis ; la commande CLI `update` au premier niveau met à jour le client local, ce n’est pas une session distante |
| exec / PTY / file / ubus / Android / sensors ponctuel / hold list, kill | Implémentation scellée native existante conservée | Même `required_level` et même répartiteur côté appareil |
| Liste des appareils, métriques, état en ligne, liste Exchange sessions | Métadonnées de routage et de supervision, pas des opérations scellées sur l’appareil | Visibles par Exchange ; ne constituent pas une autorisation sur l’appareil |

### HTTP et commandes prédéfinies

Les opérations chiffrées natives acceptent désormais des clients utilisant uniquement HTTP. Utilisez `remote-cli --transport http` ou `REMOTE_TRANSPORT=http`. `POST /api/sealed/connections` crée une connexion ; `POST /input` transmet la négociation signée ou le texte chiffré, `GET /events` récupère les résultats et `DELETE` ferme la connexion. Le client conserve la cryptographie et la vérification des autorisations. Les anciens points d’action renvoient toujours 409 ; la connexion de l’appareil reste identique.

https://remote.ivjn.us/sealed-http.md


Création, édition et liste des prédéfinis restent l’administration de la base Exchange. **Modèles, noms et descriptions du catalogue ne sont ni scellés ni une liste de commandes autorisées signée par la racine.** Console et CLI détenteurs de clés exécutent le modèle comme exec scellé ordinaire (sans hold). L’appareil fait confiance à sa politique et au grant, pas au catalogue, au rôle serveur ou à une étiquette « prédéfini ». `view` ne suffit pas, la racine doit accorder `manage`. Altérer le catalogue n’étend pas le grant, mais `manage` permet déjà tout exec : vérifiez les modèles, sans mots de passe/jetons dans le catalogue. Sorties déchiffrées et affichées localement seulement ; audit limité aux métadonnées sealed, sans commande ni sortie.

### LAN et sélection côté client

Les sessions d’appareil de la CLI utilisent exclusivement le scellement de bout en bout, sans dépendre de `seal_key`, des capacités ou des indications de politique annoncés par Exchange. Des informations de capacité absentes ou supprimées ne provoquent aucun passage en clair. Une erreur de lecture de la base locale d’épinglage est signalée immédiatement, sans ignorer les enregistrements de confiance endommagés.

Exec scellés avec/sans hold et tâches fichiers sont marqués avant publication dans le répertoire global, empêchant une fermeture avant le premier événement sortant de révéler un aperçu en clair. Les journaux appareil ne conservent ni cible/méthode de sonde HTTP, ni objet/méthode ubus.

LAN réutilise enveloppes natives, transcript signé, compteurs, AEAD, épinglage et révocation, sans nouvelle cryptographie. Les sorties passent par `Tunnels::seal_outbound` avant traduction `OperatorDown` ; le dispatcher rejette l’injection en clair vers un ID scellé. La fermeture détache les hold exec, annule le reste, et les ID isolés rejettent les sorties tardives. Aucun jeton LAN : grant et chaîne hors ligne sont joints à hello ; l’appareil utilise le même `authorize` qu’Exchange pour view/manage. Seules adresse privée et Origin sont vérifiées extérieurement. Le clair LAN est toujours refusé, quelle que soit l’ancienne valeur de politique. La console Web hébergée reste connectée à Exchange.

Les informations facultatives d’appareil `info.seal_mode` (`allow` / `require`), `seal_key` et la capacité `sealed` restent compatibles. Elles ne constituent ni une autorisation ni une clé publique d’appareil fiable ; l’appareil applique la politique en dernier ressort. CLI, Web Console et Android Console utilisent des sessions scellées natives pour toutes les commandes, terminaux, fichiers, ubus, capteurs, tâches, sondes réseau, opérations de bureau, mises à jour d’appareil, commandes prédéfinies et opérations du navigateur, sans chemin en clair. La valeur de politique `allow` refuse également les opérations en clair. Tout échec de négociation, d’épinglage d’appareil, d’autorisation ou de déchiffrement arrête l’opération, sans nouvelle tentative en clair. Après une modification de politique en ligne, les informations LAN sont actualisées immédiatement, celles du cloud lors de l’enregistrement après reconnexion.

### Authentification racine de l’agent navigateur

L’extension annonce `sealed`, sa clé Ed25519 `seal_key` et `seal_mode: require`. Après appairage ou configuration, elle appelle `POST /api/seal/device` avec les identifiants de l’appareil et une preuve de possession signée. Exchange déduit l’espace de l’appareil et lie sa clé publique. Un client détenant la clé privée racine de confiance signe un grant view limité à cet appareil navigateur. Enregistrement et bearer permettent l’accès au relais et la récupération des autorisations signées ; les sessions exigent une authentification par certificats.

L’extension vérifie signature, chaîne racine, appareil, niveau, validité et révocations locales, puis signe `SealAccept` avec son propre `SealHello` signé dans `peer`. CLI `finish_workspace_operator` et `finishBrowser` vérifient la preuve, imposent `peer.operator_key == accept.device_key` et vérifient la signature de réponse. Une preuve absente ou invalide ferme la session. Les sessions valides démarrent automatiquement. Raw CDP exige aussi un consentement local par requête ou une autorisation sans surveillance limitée à la clé opérateur, au serveur et à l’appareil ; view ne permet pas CDP.

Les autorisations sont isolées par origine du serveur, espace et appareil. La graine d’identité locale est accessible uniquement aux contextes d’extension de confiance. La première racine est épinglée localement et tout changement est refusé. CLI et Console utilisent leur identité seal et chaîne racine habituelles, sans éditeur de pin propre au navigateur. Signature racine, droits du certificat et identifiants d’accès de l’appareil sont des contrôles distincts ; le TOFU des appareils natifs reste séparé.

**Limite de sécurité :** le premier TOFU ne détecte pas la fausse racine d’un relais malveillant ; vérification hors bande nécessaire. PoP prouve la possession, pas l’approbation. L’approbation automatique dépend de l’annuaire ; examiner les nouvelles clés en déploiement sensible. Protégez profil/graine et chaîne de publication Web ; endpoint compromis ou full CDP offert à un opérateur malveillant hors protection. Supprimer l’annuaire ne révoque pas les justificatifs signés en cache ; révocations et expiration locales restent importantes. Exchange voit les métadonnées et peut retenir les justificatifs ou refuser le service.

**Identité indisponible :** si l’identité de l’appareil ne peut être chargée ou écrite, les sessions scellées sont indisponibles et toutes les opérations en clair restent refusées. L’absence de fichier de politique, une politique endommagée ou une ancienne valeur n’ouvre aucun chemin en clair ; le stockage des clés doit être réparé localement sur l’appareil.

Les requêtes HTTP doivent atteindre l’instance Exchange qui a créé la connexion. Celle-ci expire après 90 secondes sans activité. Sérialisez les entrées et réessayez avec le même numéro et le même texte chiffré. Confirmez les événements traités avec `after`. Web Console, mobile et LAN conservent leur transport. Les commandes prédéfinies exigent toujours `manage`.

### Vérification des régressions

Rust couvre négociation réelle des grants, résultats scellés exec/attach/tail/cat/sensors/net/update, refus du clair pour toutes les valeurs de politique, négociation/trames/politique LAN WebSocket, indication HTTP 409, fermeture browser require et invariants imposant le scellement côté clients et informations d’appareil. JavaScript couvre vecteurs Rust/JS bidirectionnels, enveloppes, sortie scellée des prédéfinis sans repli après refus, configuration/enregistrement/refus navigateur, changement de politique et erreurs d’épinglage. Les singletons seal des tests sont initialisés dans des sous-processus isolés pour éviter les interférences concurrentes.

```sh
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo test --workspace
cargo fmt --all --check
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo clippy --workspace --all-targets -- -D warnings
npm test --prefix shared
npm test --prefix website/console
npm test --prefix browser-agent
node --test scripts/test-pages-worker.mjs
```

JavaScript nécessite Node 24+. Les régressions LAN utilisent des flux duplex mémoire pour réaliser un véritable HTTP Upgrade / WebSocket et vérifier la négociation, les trames et l’obligation de scellement pour toutes les valeurs de politique même sans sockets en sandbox. Les tests loopback WS/TLS/HTTP existants nécessitent la liaison de ports ; sinon `Operation not permitted` ne prouve pas la réussite de la suite. Les tests réseau/mise à jour vérifient les résultats scellés de succès/erreur, sans télécharger ni installer.

**Risque Web Console :** le code vient du site Pages ; un site/CDN compromis peut fournir un script lisant le clair ou signant avec les clés du navigateur (sans extraire la clé privée non exportable). La confidentialité vis-à-vis d’Exchange suppose une chaîne de publication statique fiable ; sinon utilisez CLI ou Android Console.

## Identités et clés

| Clé | Type | Stockage | Visibilité publique |
|---|---|---|---|
| Appareil | Ed25519 | agent : `agent.seal.key` (0600). État en tmpfs (`/var/run`, `/run`, `/tmp`) : stockage `/etc/remote-agent/`, remplaçable par `REMOTE_SEAL_DIR` ; conffiles OpenWrt | `info.seal_key` à l’enregistrement (affichage seulement, copie non fiable pour le CLI) |
| Opérateur | Ed25519 | CLI : `seal-identity.pk8` (0600) près de `config.toml` ; Web : `CryptoKeyPair` WebCrypto Ed25519 **non exportable** dans IndexedDB `remote-seal` (sinon graine aléatoire IndexedDB si non pris en charge) ; Android : graine `expo-secure-store` (chiffrement Android Keystore) | Négociation |
| Racine d’espace | Ed25519 | Clé opérateur du superadministrateur. Web exige une graine exportable (remplace la clé WebCrypto non exportable à la création de racine) ; export `rsk1.`, sauvegarde Exchange automatique par Console avec une phrase vide | `rsr1.` publié par Exchange, épinglé sur appareil |
| Session | X25519 éphémère → HKDF-SHA256 → une ChaCha20-Poly1305 par direction | Mémoire, détruite à la fin (confidentialité persistante) | Aucune |

Les clés privées opérateur restent locales. Console envoie par défaut une sauvegarde de la racine avec une phrase secrète vide ; Exchange ou les détenteurs de la base peuvent la déchiffrer. Une phrase peut être définie manuellement. Supprimer la sauvegarde désactive son enregistrement automatique sur ce client.

## Autorisations (Exchange n’est pas l’autorité)

L’appareil fait confiance à **une seule clé publique racine**. Politique `agent.seal.json` :

```json
{"mode":"require","root_key":"<base64 Ed25519>","revoked_keys":[]}
```

L’agent décide du niveau de chaque `sealed_open` :

1. Aucune racine épinglée → refus (`no workspace root pinned on this device`).
2. Clé opérateur dans `revoked_keys` → refus.
3. Clé opérateur égale à `root_key` → `owner` (gestion et politique locale).
4. `grant` joint : signature valide, `operator_key` identique à la négociation, `device_id` local ou `*`, validité avec ±5 minutes d’horloge. Signataire racine épinglée, ou chaîne `rsc1.` jointe à hello (`hello.chain`, hors signature de négociation), du certificat racine au signataire du grant ; chaque certificat autorise la délivrance, sans dépassement de profondeur, niveau, portée ou expiration du précédent. Niveau `view` ou `manage`, jamais `owner`.
5. Sinon → refus.

## Contraintes des chaînes de certificats

L’appareil reconnaît une chaîne `rsc1.` remontant à la racine épinglée, pas un intermédiaire isolé (`hello.chain`, certificat racine en premier). **Aucune limite métier de profondeur** (plus de 10 niveaux possibles), seulement une protection DoS généreuse : `MAX_CERT_CHAIN` = **64** certificats, somme des longueurs canoniques des jetons `rsc1.` `MAX_CERT_CHAIN_BYTES` ≤ **64 KiB** (`check_chain_limits` / `checkChainLimits` dans `crates/proto/src/seal.rs` / `shared/seal-cert.js`, mêmes règles appareil/Exchange/CLI/Web/Android). Dépassement refusé.

Chaque certificat a `max_path_len` (0–255), contrainte **facultative** de profondeur descendante de l’émetteur. Sans `--max-path`, `seal cert issue` conserve toute la profondeur restante (255 pratiquement illimité). Les certificats suivants ne doivent pas dépasser `max_path_len`. L’intermédiaire administrateur racine a `max_path_len` de 0 : il signe des grants, pas d’autres certificats. `can_issue` faux interdit aussi toute poursuite.

Les enfants doivent rester dans les limites du parent (`cert_within_parent` / `verify_chain`) :

- **Niveau** au plus égal au parent. Racine `owner` ; attribution `view` ou `manage` seulement, sans dépasser le parent. Aucun `owner` dans certificat/grant.
- **Portée** incluse dans le parent. Tous les appareils seulement si parent `*` ; sinon chaque appareil figure dans la liste parent. Enfant `*` exige parent `*`.
- **Expiration** au plus tard celle du parent ; racine sans borne. Le grant respecte aussi expiration et portée de son certificat signataire.

**Révoquer un intermédiaire invalide tous ses descendants.** L’appareil compare chaque `subject_key` et `serial` aux `revoked_keys` de la politique. Les entrées `revoked_keys` sont vérifiées sur toute la chaîne. Toute chaîne contenant l’intermédiaire révoqué échoue, donc certificats et grants descendants aussi. Supprimer sur Exchange arrête seulement la diffusion ; les copies restent valides jusqu’à expiration. Pour invalider immédiatement la sous-chaîne, ajouter clé du sujet (ou numéro de série) aux `revoked_keys` de l’appareil.

Niveaux/opérations (`proto::seal::required_level`) : `view` autorise capteurs, liste hold et actions Android en lecture seule (status/tree/screenshot/find_text) ; tout le reste (exec, PTY, fichiers, ubus, sonde réseau, automise à jour, saisie Android) exige `manage`.

Le grant est du JSON signé par la racine ; modifier un champ (view→manage, appareil, durée, bénéficiaire) invalide la signature ; celle du hello le lie à la session. Les rôles Exchange décident seulement du routage vers l’appareil (visibilité locataire), sans modifier les permissions scellées. Aucun propriétaire/copropriétaire ou revendication d’appareil : l’unique `owner` est la racine.

**Épinglage de la racine :**

- **TOFU :** Exchange envoie `seal_root {token, rotation?}` (`rsr1.` autosigné) à l’appareil compatible connecté. L’appareil vérifie l’autosignature et, sans racine locale, l’épingle dans `agent.seal.json`.
- **Rotation :** après épinglage, nouvelle racine acceptée seulement avec `rsx1.` **signé par la racine épinglée** (`old_root` ancien, `new_root` nouveau, même espace) ; sinon ignorée et journalisée. La racine peut aussi changer via `seal_policy_set {root_key}` scellé (seule la racine actuelle est `owner`).
- **Local :** `remote-agent seal root <key>` écrit la racine ; `remote-agent seal forget-root` l’efface, nouveau TOFU à la connexion suivante (redémarrage nécessaire).

**Risque :** Exchange compromis dès le premier TOFU peut fournir une fausse racine. Avant mise en ligne, `remote-agent seal root <key>` fixe la clé (empreinte via `remote-cli seal root`). Ensuite Exchange ne peut plus la remplacer.

**Révocation :** racine `remote-cli seal policy <设备> --revoke <key>`, ou local `remote-agent seal revoke <key>`. Racine non révocable. Grants expirants (90 jours par défaut). Supprimer un grant Exchange arrête sa diffusion seulement ; les copies importées nécessitent la politique appareil pour révocation avant expiration.

## Exchange comme relais d’autorisations (non fiable)

Plus besoin de copier `rsg1.` ou clés publiques dans une discussion : Exchange fournit **annuaire de clés opérateur** et **relais de grants** par espace. Modèle inchangé : pas de clair, ni délivrance, extension ou falsification. Il stocke seulement :

1. Clés Ed25519 dont l’opérateur prouve la possession (`POST /api/seal/keys`).
2. Texte original des `rsg1.` signés (`POST /api/seal/grants`).
3. Certificats `rsc1.` signés (`POST /api/seal/certs`). Exchange vérifie la signature Ed25519 sans pouvoir falsifier. `GET /api/seal/certs/chain?subject_key=` récupère la filiation, racine en premier, sous les mêmes limites 64 / 64 KiB.

L’appareil n’autorise que par racine épinglée ou chaîne `rsc1.` y remontant. Exchange analyse pour indexer `owner_key` / `operator_key` / expiration, **sans jamais assimiler téléversement et autorisation**. Au pire il retient un jeton (`no grant`), sans transformer `view` en `manage` ni usurper la racine.

Tout membre peut publier un grant autosuffisant signé ; publier n’autorise rien, seuls les grants signés racine sont valides. CLI connecté : `--publish` par défaut ; Web/Android : même API pour « Délivrer et publier ».

La preuve de possession lie :

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

Signature Ed25519 par cette clé, `issued_at_ms` à ±10 minutes du serveur. CLI tente l’enregistrement à `login` et `seal show` ; consoles après connexion.

Sans grant local correspondant avant une opération, tentative `seal sync` ; après refus `no grant`, une synchronisation et un nouvel essai uniques.

Le client émetteur renouvelle automatiquement ses grants publiés proches d’expiration à sa connexion. Signature locale ; Exchange conserve le nouveau `rsg1.` et supprime l’ancien, sans modifier niveau ou bénéficiaire.

- **Quand :** CLI après connexion et au début de toute commande authentifiée (`seal renew` reste manuel sans couche automatique). Web à la connexion/entrée d’espace, y compris session préexistante au chargement. Android après connexion et retour au premier plan. Rien sans connexion ou sans grants publiés par cette clé.
- **Règles :** `GET /api/seal/grants?owner_key=<本机公钥>`. Expiration sous 30 jours : mêmes `device_id`, `operator_key`, `level`, nouvelle durée 90 jours ; `DELETE` ancien après publication réussie. Si `GET /api/seal/keys` réussit, ignorer les `operator_key` disparus ; sinon aucun filtrage d’annuaire.
- **Limitation :** Web/Android: une tentative par 12 heures et clé. CLI : `seal-renew-state.json` du répertoire de configuration ; Web `localStorage`, Android SecureStore. Échec sans effet sur l’opération : une ligne stderr CLI, `console.warn` console ; succès CLI brièvement signalé sur stderr. Web racine délivre d’abord à l’entrée d’espace, Android à l’ouverture « Chiffrement de bout en bout », sans limitation ; seuls les rafraîchissements suivants de la visite sont limités.
- **Renouvellement CLI :** les succès sont enregistrés par serveur et clé émettrice. Après un succès, les requêtes de renouvellement sont évitées pendant 12 heures ; un échec ou une interruption impose seulement 5 minutes avant une nouvelle tentative. Le processus complet dispose de 30 secondes. Les erreurs indiquent l’étape et la durée ; un échec de la commande principale annule immédiatement le renouvellement. Les révocations en attente contournent cette limitation et conservent un budget distinct de 4 secondes. Renouvellement explicite : `remote-cli seal renew`. Une connexion réussie force une actualisation.
- **Racine :** le superadministrateur utilise la délivrance par rôle ci-dessous au lieu du renouvellement générique pour ne pas recréer un grant supprimé.
- **Manuel :** `remote-cli seal renew`, défaut `--before 30 --days 90`, mêmes règles.

```bash
# 按工作区用户名授权对方每一把已登记的钥匙，并发布到 Exchange（默认 90 天）
remote-cli seal grant --device lab-router --to alice --level view
# 只打印 token，不上传
remote-cli seal grant --device lab-router --operator-key <KEY> --no-publish
remote-cli seal keys          # 工作区公钥目录
remote-cli seal sync          # 拉取签给自己钥匙的 grant 并导入本地
remote-cli seal renew         # 手动续签，默认 --before 30 --days 90
```

## Le superadministrateur est la racine de confiance

La racine est la clé Ed25519 du client superadministrateur. Seules cette clé et les autorisations `rsg1.` remontant par `rsc1.` sont reconnues ; le client délivre/renouvelle selon les rôles. Exchange conserve `rsr1.` autosignés, `rsx1.` et invitations `rsi1.`, vérifie format/signature mais **ne décide pas des approbations**. Il peut retenir ou ajouter de faux membres, pas falsifier la racine. CLI, Web et Android approuvent automatiquement par défaut : ces racines signent directement les nouvelles clés de l’annuaire. Seule une désactivation explicite laisse en attente les clés jamais approuvées/publiées par cette racine.

### Modèle de confiance et compromis

- **Épinglage TOFU de racine.** Premier enregistrement vérifié épinglé localement. CLI : `seal-root.json` (0600) près de `config.toml`, index `服务器|工作区 id`. Web `localStorage` (`remote.seal.root.<工作区 id>`). Android SecureStore (`remote.seal.rootstate`, JSON par serveur/espace). Autosignature/empreinte ensuite contrôlées. Changement : alerte forte, **aucune confiance automatique côté client** (l’appareil suit `rsx1.`, voir plus haut). Après contrôle, `remote-cli seal root --trust <key>` ou « Faire confiance à la nouvelle racine ».
- **Invitation = enregistrement seulement.** Racine signe `rsi1.` (ID espace, clé racine, nonce 16 octets, expiration). Clients administrateur/opérateur/lecture seule récupèrent aux connexions, commandes, chargements ou retours au premier plan, vérifient par racine **épinglée**, puis `POST /api/seal/keys`. Invitation non autorisante.
- **Délivrance par rôle et approbation locale.** Client racine lit `GET /api/seal/roster` (superadministrateur seulement, membres désactivés, rôles, `operator_device_grants`, clés) et ses grants publiés. Chaque clé enregistrée suit :
  - Superadministrateur / administrateur non restreint : `manage` `*`.
  - Administrateur restreint : `manage` si `can_manage`, sinon `view`, selon délégations.
  - Opérateur non restreint : `view` `*`, plus `manage` pour chaque appareil `can_manage`.
  - Opérateur restreint : `manage` ou `view` par appareil délégué.
  - Lecture seule non restreinte : `view` `*`.
  - Lecture seule restreinte : `view` par appareil ; jamais `manage`, même `can_manage`, car ce compte ne gère pas l’appareil.
  - Désactivé : aucun grant.
  - 90 jours par défaut, renouvellement sous 30 jours restants.
- **Intermédiaire administrateur.** Racine délivre aux administrateurs actifs `rsc1.` (`manage`, `can_issue`, `max_path_len` 0, 90 jours). Portée `*` sans restriction, sinon appareils délégués `can_manage`. Renouvelé sous 30 jours, jamais pour clé non approuvée. L’administrateur avec certificat valide signe pour ses membres avec sa clé et chaîne, intersection certificat/délégations. Pas de nouvelle émission si grant racine frais couvre déjà opérateur/appareil/niveau (`*` couvre appareil, `manage` couvre `view`). Supprimer Exchange arrête seulement la diffusion ; copies valides jusqu’à expiration, révocation ferme via `revoked_keys` appareil.
- **Approbation automatique activée par défaut.** Web `localStorage` et Android SecureStore interprètent absent comme activé (absent = on), seul `false` explicite garde la désactivation, jamais annulée par défaut. Signature directe de l’annuaire **moins sûre** : Exchange compromis peut obtenir immédiatement la signature de faux membres. Libellé « Approuver automatiquement les nouvelles clés publiques ». CLI est activé par défaut, comme Web/Android Console : si le fichier `seal-approved.json` ou le champ `auto_approve` est absent, l’approbation est activée ; `"auto_approve": false` explicite reste désactivé. Utilisez `remote-cli seal approve --no-auto-approve` pour désactiver ou `remote-cli seal approve --auto-approve` pour réactiver ; le choix est enregistré. Désactivé : clés jamais approuvées/publiées par cette racine en file locale (CLI `seal-approved.json`, Web `localStorage`, Android SecureStore). `remote-cli seal pending` affiche nom/empreinte ; `seal approve --all` ou `seal approve <key>…` délivre immédiatement. Clés déjà approuvées/publiées suivent renouvellements/rôles automatiquement ; clé du client émetteur sans nouvelle approbation.
- **Émission à l’ouverture.** Web racine à l’entrée d’espace, Android à l’ouverture « Chiffrement de bout en bout », sans limite 12 heures ; rafraîchissements suivants limités. Modifier le commutateur ou « Approuver » force un passage.
- **Dialogue Compte, badge, échecs.** Web : commutateur et attente dans « Compte » de la page appareil (barre « Compte »). Badge « N clés publiques en attente » ouvre le dialogue. Erreur « Échec de délivrance par la racine : … » dans la barre, sans jetons ni longues données de clés. Android : mêmes éléments sur « Chiffrement de bout en bout ».
- **Rétrograder ne révoque pas les anciens grants appareil.** Membre retiré/désactivé ou clé disparue : supprimer grants Exchange, arrêter renouvellement, tenter `revoked_keys` sur appareils connectés compatibles. Rétrogradation : nouveau grant seulement, sans révoquer la clé. Anciens grants importés valides jusqu’à expiration. Racine jamais révoquée. Révocations au-delà du budget 4 secondes conservées localement pour la commande suivante, sans limite 12 heures. Une clé redevenue souhaitée n’est rétablie que sur les appareils où la révocation avait réussi.
- **Création automatique.** À l’inscription superadministrateur (`register`/premier compte) ou toute connexion ultérieure, sans racine Exchange, son client CLI/Web/Android publie sa clé locale et une invitation de 90 jours. Invitation absente ou expirant sous 30 jours rafraîchie selon la limitation. `remote-cli seal root --publish` reste manuel.

### Configuration initiale

1. Inscription/connexion du superadministrateur sur un client : racine créée/publiée automatiquement (stderr CLI, notification console).
2. Conservez la racine (une ou les deux options) :
   - Sans téléversement : `remote-cli seal root --export root.rsk` (0600 ; `-` vers stdout), ou « Exporter la clé privée racine ».
   - Configuration manuelle du cloud : `remote-cli seal backup` ou « Sauvegarde cloud racine ». Phrase vide permise, mais **vide/faible elle permet à Exchange de déchiffrer la racine et contrôler tous les appareils**.
3. Appareil compatible connecté : épinglage automatique TOFU ; plus sûr avant mise en ligne : `remote-agent seal root <根公钥>`.
4. Nouveau membre enregistre sa clé à la connexion. CLI/Web/Android approuvent automatiquement par défaut ; si cette option est désactivée `remote-cli seal pending`, puis `remote-cli seal approve --all` ou « Approuver » pour délivrer.

```bash
remote-cli seal root                       # 显示已固定的根公钥和指纹
remote-cli seal root --export root.rsk     # 导出根私钥（rsk1. 文本）
remote-cli seal root --import root.rsk     # 在另一台机器装上根私钥（或 seal restore）
remote-cli seal root --rotate              # 生成新根，旧根签 rsx1. 轮换，自动按新根重签成员授权
remote-cli seal invite                     # 单独刷新邀请（默认 90 天，--days N）
remote-cli seal pending
remote-cli seal approve --all              # 或 seal approve <key>…；--no-auto-approve 关闭自动批准（保持开启较弱）
```

### Autre machine / changement de racine

- **Racine ailleurs :** après connexion du superadministrateur, sans racine publiée locale et avec sauvegarde cloud `rsb1.` Exchange, CLI/Web/Android **tentent automatiquement la restauration** (voir ci-dessous). Sinon import manuel proposé : `seal root --import <文件|->`, coller `rsk1.` puis « Importer la clé privée racine », ou `seal restore` / « Restaurer depuis Exchange ». Importer la racine actuelle installe localement sans modifier les enregistrements.
- **Rotation :** `seal root --rotate` (« Générer et effectuer la rotation »), ou import `rsk1.` **différent de l’actuel**. Avec racine actuelle locale, signer `rsx1.` par l’ancienne, publier avec nouvelle `rsr1.` via `PUT /api/seal/root`. Exchange pousse aux appareils connectés, qui vérifient avant adoption ; hors ligne à la reconnexion. Membres approuvés conservés localement et tous grants resignés 90 jours. Ancienne clé : `seal-identity.<毫秒>.previous.pk8`. Réexporter/sauvegarder ensuite.
- **Ancienne racine perdue :** impossible de signer la rotation. Nouvelle racine publiée mais refusée par les appareils épinglés ; exécuter `remote-agent seal root <新根公钥>` sur chacun et redémarrer.
- **Transfert :** voir « Transfert du superadministrateur = transfert de la racine ». Le rôle seul ne suffit pas, l’appareil reconnaît la clé racine.

## Restauration automatique à la connexion

Après connexion du superadministrateur, **avant création d’identité locale**, CLI/Web/Android suivent la même décision pure (Rust `decide_auto_restore`, JS `decideAutoRestore`) pour restaurer la racine publiée depuis `rsb1.` Exchange :

1. **Ignorer :** non-superadministrateur, clé déjà identique, cloud désactivé (CLI `REMOTE_SEAL_CLOUD_BACKUP=0`, Web `localStorage.seal.cloudBackup=off`, Android SecureStore `remote.seal.cloudBackup=off`), ou racine non publiée.
2. **Récupération unique :** `GET /api/seal/backup`, limitée par heure. Absence signalée comme racine ailleurs, sans bloquer.
3. **Essai silencieux vide :** déchiffrer avec chaîne vide ; si clé publique identique à la racine et aucune autre clé locale différente, installer/épingler.
4. **Sinon phrase :** CLI essaie `REMOTE_SEAL_BACKUP_PASSPHRASE`, puis saisie masquée TTY ; Web/Android avis **non bloquant** invitant à saisir sur page seal et « Restaurer depuis Exchange ». Aucun dialogue bloquant, connexion normale.
5. **Sécurité :** jamais écraser silencieusement une autre clé (`seal restore --force` ou confirmation console) ; clé publique obligatoirement identique à la racine ; échec averti sans bloquer ; jamais afficher phrase, graine ou ciphertext `rsb1.`.
6. **Avis non bloquant :** plusieurs copies de racine augmentent le risque ; si phrase vide réussie, Exchange peut aussi la déchiffrer.

## Transfert du superadministrateur = transfert de la racine

Rôle de compte et racine de confiance sont distincts. Transférer le rôle sans clé ne change pas la confiance appareil. Remettre la **clé privée racine**, puis tourner immédiatement pour invalider la copie remise.

1. Titulaire détenant la racine publiée : `POST /api/superadmin/transfer`, corps `{operator_id, root_handover?}`. `root_handover` : `rsb1.` chiffré, même sauvegarde (Argon2id 64 MiB, t=3, p=1, ChaCha20-Poly1305), phrase vide permise. CLI `remote-cli seal handover <账号>`. Web/Android demandent une phrase facultative et joignent le paquet si la clé est locale.
2. Exchange **stocke sans déchiffrer** ; vérifie préfixe `rsb1.` ou `rsk1.`, maximum 16384, ASCII URL sûr seulement. Sans `root_handover`, supprime le paquet résiduel de l’espace.
3. Successeur : `GET /api/superadmin/handover` → `{blob, from_operator, created_at}` (404 si absent), déchiffre/installe, puis rotation normale : ancienne racine signe `rsx1.`, nouvelle publiée ; enfin `DELETE /api/superadmin/handover`. CLI `remote-cli seal accept-handover`. Console : « Accepter le transfert de clé privée racine » si paquet présent.
4. **Rotation invalide les copies.** Appareils changent seulement sur signature de l’ancienne racine fiable. L’ancien titulaire ne peut autoriser au nom de la nouvelle ni changer la racine adoptée. Avant rotation, `rsb1.` à phrase faible/vide déchiffrable ; transmettre la phrase séparément.

**Hors bande :** `remote-cli seal handover <账号> --out-of-band` évite Exchange ; stdout contient une ligne `rsb1.`, requête sans `root_handover`. Transmettre ligne/phrase autrement, destinataire `remote-cli seal accept-handover <那一行>`. Annuler la phrase console (« Transférer seulement le compte » Android) transfère le rôle sans clé téléversée ; remettre la clé séparément (`rsk1.` exporté ou `rsb1.` hors bande).

## Négociation et format réseau

```text
CLI                         Exchange                       agent
 │ open_sealed{session_id,    │                               │
 │   device_id, hello}        │──sealed_open{session_id,hello}▶│ 验证 hello 签名、授权 → 级别
 │                            │◀─sealed_accept{session_id,     │
 │◀─sealed_accept (原样转发) ──│        accept}                │
 │ 验证设备签名 + pin          │                               │
 │ sealed_input{packet} ─────▶│──sealed_input────────────────▶│ 解密、校验级别、执行
 │ 二进制帧 kind=4 (PTY 键入)─▶│── 只改写帧头编号 ─────────────▶│
 │◀── sealed_event{packet,last}│◀── sealed_event ──────────────│ 所有输出在出站队列加密
 │◀── 二进制帧 kind=4 ─────────│◀── 二进制帧 kind=4 ───────────│
```

- `hello` = `{v, operator_key, operator_ephemeral, nonce, grant?, sig}`, signe `["remote-seal-v1/hello", v, device_id, operator_key, operator_ephemeral, nonce, grant.sig|""]` (tableau JSON, octets canoniques).
- Racine `rsr1.` signe `["remote-seal-v1/workspace-root", v, workspace_id, root_key, issued_at_ms]` ; rotation `rsx1.` = `{v, workspace_id, old_root, new_root, issued_at_ms, sig}`, signée par `old_root` sur `["remote-seal-v1/root-rotation", v, workspace_id, old_root, new_root, issued_at_ms]`. Exchange → agent : `{"type":"seal_root","token":"rsr1.…","rotation":"rsx1.…"?}` (contrôle en clair uniquement depuis Exchange, invalide en tunnel).
- `accept` = `{v, device_key, device_ephemeral, nonce, level, sig}`, signe `["remote-seal-v1/accept", v, device_id, operator_key, operator_ephemeral, hello.nonce, hello.sig, device_key, device_ephemeral, nonce, level]`. Modifier `level` invalide la signature.
- Clés : `salt = SHA256(accept 签名字节)`, `PRK = HKDF-Extract(salt, X25519(o, b))`, `K_od = Expand("remote-seal-v1/operator_to_device")`, `K_do = Expand("remote-seal-v1/device_to_operator")`.
- AEAD ChaCha20-Poly1305, AAD `remote-seal-v1`, nonce = octet canal (0 texte / 1 binaire) ‖ 3 zéros ‖ compteur `u64` big-endian. Compteurs par direction/canal, de 0 par incréments stricts de 1 ; divergence ferme immédiatement.
- Texte `{"counter":n,"ciphertext":"<base64>"}`. Clair JSON : opérateur → appareil `SealedRequest` (`DeviceDown` existant ou administration `seal_policy_get` / `seal_policy_set {mode?, root_key?, revoke?, unrevoke?}`) ; appareil → opérateur `SealedResponse` (`DeviceUp` ou `seal_policy`). `session_id` interne réécrit par l’agent avec l’ID tunnel.
- Binaire `StreamKind::Sealed = 4`, charge `counter(u64 BE) ‖ AEAD(inner_kind ‖ bytes)`, `inner_kind` 1/2/3 (stdout/stderr/PTY).
- **Une** opération par session (exec, PTY, transfert…), modèle 1:1 conservé. Fin chiffrée par agent, enveloppe `last:true` ; Exchange libère route/complète audit sans lire le contenu.
- `close_session` → `sealed_close{detach:false}` (terminer) ; déconnexion opérateur → `sealed_close{detach:true}` (agent : hold exec détaché, reste terminé).
- Limite : base64 chiffré environ 2,8 MiB (capture Android 2 MiB), enveloppe `MAX_SEALED_WIRE`.

Défense agent : clair sortant d’un ID scellé seulement chiffré ou rejeté ; ID définitivement isolé après fermeture (sorties hold tardives rejetées, jamais en clair). Hold démarré/joint scellé absent des anciennes listes, inaccessible à attach/tail/cat/kill anciens.

## Export et sauvegarde de la clé privée racine

La racine contrôle tous les appareils. Conservation au choix du superadministrateur ; **Console sauvegarde par défaut avec une phrase vide ; supprimer la copie désactive cet enregistrement** :

- **Export sans téléversement :** `rsk1.` + graine 32 octets base64url sans remplissage. CLI `seal root --export FILE|-` (0600), console « Exporter la clé privée racine » (champ Web, presse-papiers Android). Son détenteur contrôle tout.
- **Sauvegarde chiffrée (automatique dans Console, manuelle dans CLI) :** superadministrateur avec clé locale égale à la racine publiée uniquement. `rsb1.` versionné (JSON base64url, maximum 4096 caractères). Argon2id : production 64 MiB, 3 itérations, parallélisme 1, paramètres inclus. Sel 16 octets et nonce ChaCha20-Poly1305 12 octets aléatoires par sauvegarde. AAD `remote-seal-v1/key-backup:{version}:{public_key}` ; changer version/clé/ciphertext invalide. Vérifier aussi la clé publique dérivée de la graine. Avant Argon2, refus des paramètres faibles/excessifs (mémoire 8 KiB–256 MiB, production minimum 64 MiB ; itérations 1–8, production minimum 3 ; parallélisme 1 seulement).
- **Phrase facultative, force non imposée.** Vide utilise aussi Argon2id avec chaîne vide et même format. Avertissement seulement si vide/faible (moins de 12 caractères, catégories insuffisantes, répétitions, phrase courante) : **Exchange ou un lecteur de base peut déchiffrer la racine et contrôler tous les appareils.** Rust/JS partagent règles et vecteurs, vide compris.

`PUT` / `GET` / `DELETE /api/seal/backup` pour compte connecté seulement ; `PUT`/`GET` superadministrateur uniquement. Corps `{blob}`. Exchange vérifie préfixe `rsb1.`/longueur, **jamais de déchiffrement**. `GET` maximum 5 par compte/processus/heure, puis 429 ; ralentit seulement essais en ligne, pas l’attaque hors ligne d’une base copiée. Lecture/écriture/suppression auditées avec nombre d’octets seulement.

- CLI `remote-cli seal backup` (phrase masquée deux fois, Entrée vide, ou `--passphrase-stdin`) ; `remote-cli seal restore` (`--force` si autre clé, ancienne `seal-identity.<毫秒>.previous.pk8`), puis épinglage comme racine locale. Essai automatique aussi à la connexion. Aucun affichage de clé, phrase ou ciphertext.
- Web : racine exportable obligatoire ; création remplace WebCrypto non exportable par graine IndexedDB. Bootstrap restaure avant création d’identité ; `localStorage.seal.cloudBackup=off` désactive.
- Android : graine expo-secure-store ; `bootstrapSeal` restaure aussi d’abord ; `remote.seal.cloudBackup=off` désactive.

## Utilisation

```bash
remote-cli seal show                       # 生成/显示本机操作员公钥与指纹
remote-cli exec lab-router -- uname -a
remote-cli shell lab-router
remote-cli seal policy lab-router          # 根：查看设备授权与吊销策略

# 根给同事手动授权（通常不需要：根客户端按角色自动签发）（默认 90 天；对方已登录的客户端会自动登记公钥）
remote-cli seal grant --device lab-router --to alice --level view
# 或仍用公钥；--no-publish 只打印 token
remote-cli seal grant --device lab-router --operator-key <KEY> --level view --no-publish
# 手动续签即将到期的已发布授权（默认 --before 30 --days 90；上线时也会自动续签）
remote-cli seal renew
# 同事：拉取 Exchange 上签给自己的授权（登录后也会自动 sync）
remote-cli seal sync
# 仍可手动导入
remote-cli seal import-grant rsg1.xxxxx

# 根私钥：导出（不上传）或可选的加密云端备份
remote-cli seal root --export root.rsk
remote-cli seal backup
remote-cli seal restore                    # 已有不同钥匙时加 --force

# 带外固定设备指纹（在设备上 `remote-agent seal show`）
remote-cli seal trust-device lab-router <DEVICE_KEY>
```

**Web Console / Android Console:** toutes les opérations sur les appareils utilisent exclusivement des sessions scellées natives. Commandes, hold list/kill/tail/cat, terminal, fichiers, captures et saisie Android/bureau, arbre d’interface, capteurs, ubus, journaux système et mises à jour automatiques passent dans le tunnel scellé ; la page affiche « 🔒 Chiffré de bout en bout ». Dans les détails d’appareil Web ou les paramètres Android, le panneau « Chiffrement de bout en bout » équivaut à la CLI : clé et empreinte locales, racine d’espace de travail (superadministrateur : exportation, importation, rotation et transfert de clé privée racine, approbation des nouvelles clés publiques), consultation et modification des clés d’appareil et de la politique de révocation (racine requise), délivrance d’autorisations (jeton `rsg1.`, importable dans la CLI avec `seal import-grant`), importation d’autorisation, approbation de la clé publique d’appareil et sauvegarde cloud automatique de la clé privée racine (phrase vide permise, avertissement explicite qu’Exchange peut déchiffrer une sauvegarde à phrase faible). La première connexion épingle l’empreinte de l’appareil (TOFU) ; tout changement ultérieur est refusé avec demande de vérification. Un membre ordinaire utilise des clés distinctes sur CLI, navigateur et téléphone, chacune enregistrée puis autorisée par la racine selon le rôle. Pour utiliser la racine sur plusieurs appareils, le superadministrateur peut téléverser une sauvegarde à phrase secrète, restaurée automatiquement à la connexion sur le nouvel appareil, ou importer la clé privée sur chaque appareil.

Localement sur l’appareil : `remote-agent seal show | root <key> | forget-root | revoke <key>` (redémarrage de l’agent nécessaire ; les modifications via `seal policy` dans une session scellée sont immédiates).

## Comportement par défaut

| Portée | Comportement actuel |
|---|---|
| CLI, Web Console, Android Console | Toutes les opérations sur les appareils utilisent des sessions scellées natives, sans interrupteur de désactivation ni repli en clair |
| Politique locale de l’appareil | Toutes les valeurs, y compris `allow`, refusent les opérations en clair |

- Les nouveaux appareils épinglent la racine d’espace de travail à leur connexion à Exchange (TOFU) ; la racine et ses autorisations signées permettent les opérations. En cas de doute sur Exchange à la première connexion, exécutez d’abord localement `remote-agent seal root <key>`.
- Une clé d’identité indisponible ou une politique endommagée entraîne un refus. Réparez les clés et la politique localement sur l’appareil avant de le reconnecter.
