# エンドツーエンドの密封セッション（sealed sessions）

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

Android Console のエンドツーエンド暗号化は設定から開き、ページ内で対象デバイスを選択します。Android と Web Console のアカウント設定ではパスワードの設定・変更とメールの登録・変更ができ、新アドレス確認まで旧アドレスは有効です。Console は既定で空のパスフレーズでルート秘密鍵を Exchange に保存し、既存のパスフレーズ付きバックアップは上書きしません。削除するとそのクライアントの自動保存が停止します。空のパスフレーズでは Exchange やデータベース保有者が鍵を復号できます。手動復元ではワークスペースのルートを照合し、保存後に権限を同期します。

目的：Exchange ホストが侵害された場合や運用者が内容を知りたがった場合でも、コマンド、出力、PTY バイト列、ファイル内容、スクリーンショット、ubus データを**読み取れず**、誰がデバイスを閲覧・管理できるかを**決定したり偽造したりできない**ようにします。Exchange は「暗号文とルーティングのメタデータだけを転送する」中継になります。

定義：`crates/proto/src/seal.rs`（通信型、ローカル認可規則）、`crates/proto/src/seal/crypto.rs`（セッション暗号、ring のみ）、`crates/proto/src/seal/backup.rs`（パスフレーズバックアップ：Argon2id + ChaCha20-Poly1305、agent にはリンクしない）、`crates/agent/src/seal.rs`（デバイス側）、`crates/cli/src/seal.rs`（CLI 側）、`shared/seal-session.js`（Web Console と Android Console が共有するセッション実装、@noble/curves · @noble/hashes · @noble/ciphers）、`shared/seal-backup.js`（同じライブラリ群による Argon2id パスフレーズバックアップ）。言語間テストベクトル：`shared/seal-vectors.json`（JS が生成し、Rust の `javascript_vectors_interoperate` が署名/HKDF/暗号文のバイト単位の一致を検証）、`shared/seal-rust-vectors.json`（Rust デバイス応答を使い、JS の `shared/seal-session.test.js` がハンドシェイクを完了して復号）。

`seal approve` は引き続き権限を自動公開します。その後の `seal grant --publish`（またはログイン時の既定公開）は、発行者鍵、受信者鍵、デバイス、level が同じ有効期限内の公開済み権限を検出するとスキップし、公開済みと表示して新しいトークンを出力しません。期限間近の更新には `--renew-before DAYS`、オフライン発行には `--no-publish` を使用します。逐次再試行の重複を防ぎますが、複数クライアントの同時公開をトランザクション化せず、過去の重複も削除しません。`seal show` はローカル ID を作成し、ログイン済みなら公開鍵を登録します。実験用 ID には別の profile を使ってください。


## 脅威モデル

| 項目 | 密封セッションの保護と境界 |
|---|---|
| コマンドライン、stdout/stderr、終了コード | **見えない**。監査には `sealed:true`、オペレーター公開鍵、認可レベルと有効期限、終了結果だけを記録 |
| PTY 入力と画面出力 | **見えない**。暗号文フレームは監査に入らない |
| ファイル内容、スクリーンショット、UI ツリー、ubus 引数と結果 | **見えない** |
| 誰が何をできるか（スーパー管理者/管理者/運用担当/読み取り専用、閲覧のみ/管理可能） | **デバイスが判断**。ローカルで固定したワークスペースルート公開鍵が署名した認可だけを認める。Exchange は発行、拡大、偽造できない |
| デバイスへのなりすまし | 不可能：ネイティブデバイス ID 鍵による署名と CLI によるデバイス公開鍵の固定（pin）。ブラウザー拡張はワークスペースルートのチェーン証明と応答署名を検証 |
| セッションへのキー入力/コマンド注入 | 不可能：AEAD 認証と厳密に増加するカウンター。agent は密封セッション番号宛ての平文をすべて破棄 |
| リプレイ、並べ替え、パケット欠落、切り詰め | 検出してセッションを閉じる（fail closed） |
| 引き続き見えるメタデータ | デバイス ID、オペレーターアカウント、セッション番号、時刻、パケットサイズと数。stdout/stderr の区別は見えない（型は暗号文内） |
| 引き続き可能なこと | サービス拒否：転送しない、切断する、セッションを閉じる（`sealed_close`）。これは可用性の問題であり、権限ではない |

保護対象外：デバイス自体の侵害、オペレーターのコンピューターの侵害、初回接続（TOFU）時に Exchange がすでにデバイスになりすましている場合。最後のケースは帯域外で指紋を照合して回避できます。すべてのデバイス操作は密封セッションのみを受け付けます。固定コマンドカタログの定義とブラウザーのルートチェーン認可の境界は次の表を参照してください。

## 対象経路と設計

| 経路 | 密封セッションの動作 | デバイス側認可と境界 |
|---|---|---|
| HTTP `POST /api/devices/{id}/exec` | **明示的に拒否**。HTTP 409 で `open_sealed` WebSocket セッションの使用を案内 | Exchange がポリシーを無視してもデバイスは平文の `exec_req` を拒否 |
| HTTP 固定コマンド `/commands/{cmd}/run` | **明示的に拒否**。同様に密封 WS を案内 | サーバー DB 内のコマンドはデバイス側の認可ではない |
| Web Console / CLI 固定コマンド | カタログからテンプレートを取得し、オペレーターが密封 WS 内で `exec_req` を送信。出力も密封 | デバイスは hello、ルート署名 grant、失効、有効期限、`manage` レベルを検証。`view` ではテンプレートを実行できない |
| LAN `/ws/operator` | 密封セッションのみを受け付け、hello に grant と証明書チェーンをインラインで含める。Bearer トークンは使用しない | Exchange と同じルートチェーン検証と view/manage。外側はプライベートアドレスと Origin のみ。`/api/devices` はデバイス ID と seal 公開鍵/モードだけを返す |
| ブラウザー Agent / ネイティブ密封 routes | `open_sealed` / `sealed_input` / `sealed_event` を使用し、旧 browser channel を拒否 | 拡張がオペレーターのルート署名 grant / 証明書チェーンを検証。CLI / Console は拡張の `SealAccept.peer` のルートチェーンを検証し、その公開鍵と応答署名公開鍵の一致を要求 |
| CLI `exec-attach` / `exec-hold-tail` / `exec-hold-cat` | ネイティブの密封リクエスト、出力フレーム、終了イベント | `manage` が必要。密封ジョブを平文で読み取り/接続することはできない |
| CLI `sensors --live` | サンプルごとに独立した密封セッション（1 操作/セッション）を使用。Ctrl-C で更新終了 | `view` で利用可能。複数サンプルを単一操作トンネルへ詰め込まない |
| CLI `net` | URL、headers、body、結果を含む `net_req` と `net_resp` を密封 | `manage` が必要 |
| CLI `device update` | `update_req`、すべての進捗、最終状態を密封 | `manage` が必要。CLI 最上位の `update` はローカルクライアントを更新し、リモートセッションではない |
| exec / PTY / file / ubus / Android / sensors 単発 / hold list、kill | 既存のネイティブ密封実装を維持 | 同じ `required_level` とデバイス側 dispatcher を使用 |
| デバイス一覧、メトリクス、オンライン状態、Exchange sessions 一覧 | ルーティング/監視メタデータであり、密封デバイス操作ではない | Exchange に見える。デバイス操作の認可には使わない |

### HTTP と固定コマンド

ネイティブ暗号化操作は HTTP のみのクライアントにも対応します。`remote-cli --transport http` または `REMOTE_TRANSPORT=http` を使用します。`POST /api/sealed/connections` で接続を作成し、`POST /input` で署名付きハンドシェイクや暗号文を送信、`GET /events` で結果を取得、`DELETE` で接続を閉じます。暗号化とデバイス認証は引き続きクライアントが行います。従来のデバイス操作 API は 409 を返し、デバイス側接続は変わりません。

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


固定コマンドの作成、編集、一覧は引き続き Exchange DB の管理面です。**カタログ内のテンプレート本文、名前、説明は密封データでもルート署名付きコマンド allow-list でもありません**。鍵を持つ Console と CLI は取得したテンプレートを通常の密封 exec（非 hold）として実行します。デバイスが信頼するのはローカルポリシーとオペレーター grant だけで、カタログ ID、サーバー上のロール、「固定コマンドである」という主張は信頼しません。`view` grant しか持たないメンバーは固定コマンドを実行できず、ルートからの `manage` 付与が必要です。カタログの改ざんで grant は拡大できませんが、`manage` はもともと任意の exec を許可するため、テンプレートを確認し、パスワード/トークンをサーバーカタログに書かないでください。出力はオペレーターのローカルでのみ復号・表示され、この実行経路の監査には sealed メタデータだけが記録され、コマンドと出力は記録されません。

### LAN とクライアントの選択

CLI のデバイスセッションはエンドツーエンドの密封のみを使用し、Exchange が報告する `seal_key`、capability、ポリシーヒントには依存しません。機能情報が欠落または削除されても、平文へ切り替えません。ローカル固定データベースの読み取りに失敗した場合は、破損した信頼記録を無視せず、そのままエラーを返します。

密封 held / 非 held exec とファイルタスクは、グローバルタスクカタログへ公開する前に密封とマークされます。「最初の送信イベントを処理する前にセッションを閉じる」競合状態で平文プレビューを読むことはできません。デバイスログには HTTP 検査の宛先やメソッド、ubus オブジェクトやメソッドを記録しません。

LAN はネイティブ envelope、署名 transcript、カウンター、AEAD、デバイス固定、失効規則を再利用し、新しい暗号方式を作りません。LAN の送信はまず `Tunnels::seal_outbound` を通り、その後 `OperatorDown` に変換されます。密封セッション番号への平文注入は device dispatcher が破棄します。接続終了時は held exec を分離し、他の操作をキャンセルします。遅れて届いた出力は fenced id により破棄されます。LAN トークンはありません。クライアントはオフラインキャッシュの grant と証明書チェーンを hello に入れ、デバイスは Exchange と同じ `authorize` で view/manage を判断します。外側ではプライベートアドレスと Origin だけを確認します。LAN の平文セッションは過去のポリシー値に関係なく、常に拒否します。ホストされた Web Console は引き続き Exchange に接続します。

任意のデバイス情報 `info.seal_mode`（`allow` / `require`）、`seal_key`、`sealed` capability は互換性を維持します。これらは認可でも信頼できるデバイス公開鍵でもなく、最終的にはデバイスがポリシーを実施します。CLI、Web Console、Android Console のコマンド、端末、ファイル、ubus、センサー、ジョブ、ネットワーク診断、デスクトップ、デバイス更新、固定コマンド、ブラウザー操作はすべてネイティブの密封セッションを使用し、平文経路は提供しません。ポリシー値 `allow` も平文操作を拒否します。ハンドシェイク、デバイス固定、grant、復号に失敗した時点で停止し、平文を再試行しません。オンラインでのポリシー変更後、LAN 情報は即座に更新され、クラウド情報は再接続登録時に更新されます。

### ブラウザー Agent のワークスペースルートチェーン認証

拡張は `sealed`、Ed25519 の `seal_key`、`seal_mode: require` を通知します。ペアリングまたはデバイス設定後、デバイス認証情報と署名付き鍵保有証明で `POST /api/seal/device` を呼び出します。Exchange はデバイスからワークスペースを特定し、公開鍵を関連付けます。信頼するワークスペースルート秘密鍵を持つクライアントが、このブラウザーデバイスだけを対象とする view grant を署名します。登録とデバイス bearer はリレー接続と署名済み認可の取得に使い、セッションでは証明書認証が必要です。

拡張は接続元の署名、ルートチェーン、デバイス範囲、権限、有効期限、ローカル失効を検証し、自身の署名付き `SealHello` を `peer` に付けて `SealAccept` を署名します。CLI の `finish_workspace_operator` と共通の `finishBrowser` は証明を検証し、`peer.operator_key == accept.device_key` を要求して応答署名を確認します。証明がないか無効なら切断し、有効なら自動的にセッションを開始します。Raw CDP にはリクエストごとのローカル確認、または操作員の鍵・サーバー・デバイスに限定した無人操作許可も必要です。view は CDP を利用できません。

拡張の認可記録はサーバー origin、ワークスペース、デバイスごとに分離されます。ローカル ID のシードには信頼された拡張コンテキストだけがアクセスできます。最初のルートをローカルに固定し、変更は拒否します。CLI と Console は通常の seal ID とルートチェーンを使い、ブラウザー専用の pin 編集は不要です。ルート鍵の署名、証明書の権限、デバイス接続認証情報は別々に検証します。ネイティブデバイス鍵の TOFU は独立しています。

**セキュリティ境界：** 初回ルート TOFU は悪意のある中継が提供する偽ルートを識別できないため、帯域外で照合してください。PoP は鍵の保有を証明するだけで、メンバー承認を証明しません。ルートクライアントの自動承認は名簿に依存するため、機密性の高い環境では新しい鍵を審査してください。拡張 profile / シードと Web Console のリリース経路を保護してください。エンドポイントの侵害や悪意のあるオペレーターへの full CDP 開放は密封の保護範囲外です。カタログ記録の削除はキャッシュ済み署名資格情報を失効させません。デバイスのローカル失効と期限の規則が引き続き重要です。Exchange はルーティングメタデータの観察、資格情報の差し止め、サービス拒否を行えます。

**ID を利用できない場合：** デバイス ID を読み込みまたは書き込みできない場合、密封セッションは利用できず、すべての平文操作も引き続き拒否します（失敗時は閉じる）。ポリシーファイルの不在、破損、旧ポリシー値のいずれも平文経路を有効にしません。デバイス上で鍵ストレージを修復する必要があります。

HTTP 接続のリクエストは作成元の Exchange インスタンスへ送信してください。90 秒間操作がなければ接続は失効します。入力は順番に送信し、再試行では同じ番号と暗号文を使用します。処理済みイベントは `after` で確認します。Web Console、モバイル、LAN の通信方式は従来どおりです。プリセット実行には引き続き `manage` が必要です。

### 回帰検証

Rust はデバイス grant の実ハンドシェイク、exec/attach/tail/cat/sensors/net/update の密封結果、すべてのポリシー値での平文拒否、LAN WebSocket の密封ハンドシェイク/フレーム/ポリシー変更、HTTP 409 案内、browser require でのルート終了、クライアントとデバイス情報に関する密封強制の不変条件を検証します。JavaScript は従来の Rust/JS 双方向ベクトル、操作 envelope、固定コマンドの密封出力と拒否後のフォールバック禁止、ブラウザー設定/登録/デバイス拒否/ローカルポリシー変更、クライアント固定エラーを検証します。テストのプロセス単位 seal singleton は分離した子プロセスで初期化し、並列テストが互いのデバイスポリシーを変更しないようにします。

```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 は Node 24+ を使用します。追加した密封 LAN 回帰テストは、メモリー内 duplex ストリームで実際の HTTP Upgrade / WebSocket を実行し、ネットワーク socket を禁止する sandbox でもハンドシェイク、フレーム、すべてのポリシー値での密封強制動作を検証できます。既存の loopback WS/TLS/HTTP テストにはポートのバインドを許可する環境が必要です。そうでなければ `Operation not permitted` で失敗するため、全テスト合格の証拠にはできません。ネットワーク検査と更新経路のテストは密封の成功/エラー結果を検証し、更新をダウンロード・インストールしません。

**Web Console 固有のリスク：** Web コード自体は Pages サイトから配信されるため、侵害されたサイト/CDN は悪意あるスクリプトを配信し、このページの平文を読み取ったりブラウザーの鍵で署名したりできます（ただしエクスポート不可の秘密鍵は取り出せません）。Exchange 運用者に対する機密性は、静的サイトのリリース経路を信頼することが前提です。不安がある場合は CLI または Android Console を使ってください。

## ID と鍵

| 鍵 | 型 | 保存先 | 公開鍵を見られる相手 |
|---|---|---|---|
| デバイス ID | Ed25519 | agent：`agent.seal.key`（0600）。状態ファイルが tmpfs（`/var/run`、`/run`、`/tmp`）にある場合は `/etc/remote-agent/` に保存し、`REMOTE_SEAL_DIR` で上書き可能。OpenWrt では conffiles に登録済み | 登録時に `info.seal_key` で報告（表示専用であり、CLI はこのコピーを信頼しない） |
| オペレーター ID | Ed25519 | CLI：`config.toml` と同じディレクトリの `seal-identity.pk8`（0600）。Web Console：IndexedDB `remote-seal` 内の**エクスポート不可**な WebCrypto Ed25519 `CryptoKeyPair`（ブラウザーが WebCrypto Ed25519 非対応なら IndexedDB 内のランダムシードへフォールバック）。Android Console：`expo-secure-store`（Android Keystore 暗号化）内のシード | ハンドシェイク時に送信 |
| ワークスペースルート | Ed25519 | スーパー管理者クライアントのオペレーター鍵そのもの。Web Console ではエクスポート可能なシードが必須（スーパー管理者がルートを生成すると、エクスポート不可の WebCrypto 鍵をローカルシードへ置換）。`rsk1.` テキストとしてエクスポートでき、Console が既定で空のパスフレーズで Exchange にバックアップ | `rsr1.` ルート記録（Exchange が公開、デバイスが固定） |
| セッション鍵 | X25519 一時鍵 → HKDF-SHA256 → 方向ごとに ChaCha20-Poly1305 鍵 1 個 | メモリーのみ。セッション終了時に破棄（前方秘匿性） | なし |

オペレーター秘密鍵はローカルに保存します。Console は既定で空のパスフレーズによるルートバックアップをアップロードし、Exchange やデータベースの保有者は復号できます。手動でパスフレーズを設定するか、バックアップを削除してこのクライアントの自動保存を停止できます。

## 認可（Exchange は権威ではない）

デバイスは**1 本のワークスペースルート公開鍵**だけを信頼します。ローカルポリシー `agent.seal.json`：

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

agent は `sealed_open` ごとにレベルを独自に判定します。

1. デバイスにまだルートが固定されていない → 拒否（`no workspace root pinned on this device`）。
2. オペレーター公開鍵が `revoked_keys` にある → 拒否。
3. オペレーター公開鍵が `root_key` と同じ → `owner`（管理とローカルポリシー変更が可能）。
4. 認可 `grant` を持つ：署名が有効、`operator_key` がハンドシェイクと一致、`device_id` が自デバイスまたは `*`、有効期限内（時計のずれ ±5 分）。署名者は固定済みルート、または hello が `rsc1.` 証明書チェーン（`hello.chain`、ハンドシェイク署名には含めない）を持つ。ルートが発行した証明書から grant 署名者の順に並び、各証明書は継続発行が可能で、パス長、レベル、デバイス範囲、有効期限は前の証明書を超えない。認可レベルは `view` または `manage` であり、`owner` には絶対にならない。
5. その他 → 拒否。

## 証明書チェーンの制約

デバイスが認めるのは単独の中間証明書ではなく、固定したワークスペースルートまで検証できる `rsc1.` チェーン（`hello.chain`、ルート発行の証明書から grant 署名者までの順）です。チェーンの階層には**業務上の上限はありません**（組織は 10 階層以上でも構いません）。DoS 防止の緩いガードとして、`MAX_CERT_CHAIN` = **64** 枚、チェーン全体のエンコードサイズ（各証明書の正規 `rsc1.` トークン長の合計）`MAX_CERT_CHAIN_BYTES` ≤ **64 KiB** だけがあります（`crates/proto/src/seal.rs` / `shared/seal-cert.js` の `check_chain_limits` / `checkChainLimits`。デバイス、Exchange、CLI、Web、Android で同じ規則）。超過時は直ちに拒否します。

各証明書には `max_path_len`（0–255）もあります。発行者による**任意**の制約で、その下に追加できる証明書数を制限します。`seal cert issue` で `--max-path` を指定しない場合、既定は無制限です（発行者に残る深さすべてを使い、255 は実質的に無制限）。検証時、その証明書より後の枚数が `max_path_len` を超えてはいけません。ルートが管理者に発行する中間証明書の `max_path_len` は 0 です。管理者は grant を署名できますが、次の証明書は発行できません。`can_issue` が false の場合も継続発行できません。

子証明書は親証明書の範囲内である必要があります（デバイスの `cert_within_parent` / `verify_chain`）。

- **レベル**は親を超えません。ルートは `owner` ですが、付与できるレベルは `view` または `manage` のみで、親証明書を超えられません。`owner` は証明書や grant に含められません。
- **デバイス範囲**は親の部分集合です。親が `*` を含む場合のみ子が全デバイスを対象にできます。それ以外では子の各デバイスが親の一覧に必要です。子を `*` にできるのは親も `*` の場合だけです。
- **有効期限**は親証明書より後にできません。ルートに有効期限の上限はありません。grant も発行元の証明書より後にできず、デバイスもその証明書の範囲内である必要があります。

**中間証明書を 1 枚失効させると、その配下すべてに波及します。** デバイスはチェーン上の各 `subject_key` と `serial` をポリシーの `revoked_keys` と照合します。中間証明書が失効すると、それを含む長いチェーンはすべて失敗し、その下位証明書と、それらが署名した認可も無効になります。Exchange からの証明書削除は配布停止にすぎず、デバイスにあるコピーは期限まで有効です。配下チェーン全体を即時無効にするには、その証明書の主体公開鍵（またはシリアル番号）をデバイスポリシーの `revoked_keys` に追加します。

レベルと操作の対応（`proto::seal::required_level`）：`view`（閲覧のみ）はセンサー、hold ジョブ一覧、Android 読み取り専用アクション（status/tree/screenshot/find_text）を許可します。他のすべて（exec、PTY、ファイル、ubus、ネットワーク検査、自己更新、Android 入力）は `manage` が必要です。

認可はルート署名付き JSON です。Exchange が任意のフィールドを改ざんすると（view→manage、デバイス変更、有効期限延長、受領者変更など）、署名が無効になります。ハンドシェイク署名は認可を今回のセッションにも結び付けます。Exchange 側のアカウントロールは「このデバイスへメッセージをルーティングできるか」（テナントの可視性）だけを決定し、密封セッションの権限を拡大・縮小しません。デバイス単位の「取得」「所有者」「共同所有者」はなく、唯一の `owner` はルートです。

**デバイスによるルート固定：**

- **TOFU：** 密封対応デバイスが Exchange に接続すると、Exchange が `seal_root {token, rotation?}`（`rsr1.` 自己署名ルート記録）を送信します。デバイスは自己署名を検証し、まだローカルにルートがなければ固定して `agent.seal.json` に書き込みます。
- **ローテーション：** ルート固定後に Exchange が公開するルートが変わった場合、デバイスが新ルートを信頼するのは、**固定済みルートが署名した** `rsx1.` ローテーション記録（`old_root` = 固定済みルート、`new_root` = 新ルート、同じワークスペース）も受信した場合だけです。それ以外は無視してログに記録します。密封セッションの `seal_policy_set {root_key}` でもルートを変更できます（現在のルートだけが `owner` レベルを持つ）。
- **ローカル設定：** `remote-agent seal root <key>` で直接ルートを書き込みます。`remote-agent seal forget-root` で消去すると、次の Exchange 接続時に再度 TOFU を行います（agent の再起動で反映）。

**リスク：** TOFU ではデバイスの初回接続時に Exchange が侵害済みだと、偽ルートを渡される可能性があります。懸念がある場合は、デバイスの接続前に `remote-agent seal root <key>` でルート公開鍵を固定してください（指紋は `remote-cli seal root`）。固定後は Exchange がルートを交換できません。

**失効：** ルートが `remote-cli seal policy <设备> --revoke <key>` を使うか、デバイス上で `remote-agent seal revoke <key>` を使います。ルート自身は失効できません。認可には有効期限があります（既定 90 日）。Exchange から公開済み grant を削除しても配布が停止するだけで、ローカルにインポート済みのコピーは期限まで有効です。デバイスポリシーで失効させる必要があります。

## 認可中継としての Exchange（信頼しない）

同僚が `rsg1.` トークンや公開鍵をチャットへ転記する必要はありません。Exchange はワークスペース単位の**オペレーター公開鍵ディレクトリ**と **grant 中継**を提供します。脅威モデルは変わらず、Exchange は平文を見られず、認可の発行、拡大、偽造もできません。保存するのは次のものだけです。

1. オペレーターがローカル秘密鍵で保有を証明した Ed25519 公開鍵（`POST /api/seal/keys`）
2. 署名済み `rsg1.` トークン原文（`POST /api/seal/grants`）
3. 署名済み `rsc1.` 中間証明書原文（`POST /api/seal/certs`）。Exchange は発行者の Ed25519 署名を検証しますが証明書を偽造できません。`GET /api/seal/certs/chain?subject_key=` は発行関係に沿ってトークンを取得し（同じ 64 枚 / 64 KiB ガードが適用）、ルート発行のものを先頭にします。

デバイス認可は、固定済みルート、またはそのルートまで検証できる `rsc1.` 証明書チェーンだけを認めます。Exchange が token を解析するのは `owner_key` / `operator_key` / 有効期限でインデックスを作るためだけで、**アップロードを認可として扱うことはありません**。最悪でも運用者がトークンを差し止めるだけです（相手は grant を取得できず `no grant` になる）。`view` を `manage` に変更したり、ルートになりすましたりできません。

ワークスペースのどのメンバーも grant をアップロードできます。トークン自身が署名を持ち、アップロードは認可行為ではありません。ルート署名の grant だけがデバイスに有効です。ログイン済み CLI は既定で `--publish` を使用し、Web / Android Console の「発行して公開」も同じ API を使います。

鍵登録の possession proof は次に結び付けられます。

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

この公開鍵で Ed25519 署名を行い、`issued_at_ms` はサーバー時刻の ±10 分以内である必要があります。CLI は `login` と `seal show` で可能な範囲で登録します。Console はログイン後に登録します。

密封操作前に一致する grant がローカルになければ、オペレーターは可能な範囲で `seal sync` を実行します。ハンドシェイクが `no grant` で拒否された場合は再同期し、1 回だけ再試行します。

発行者クライアントはオンラインになると、自分が発行した期限間近の公開済み認可を自動更新します。署名はローカルで行い、Exchange は新しい `rsg1.` トークンを保存して古い記録を削除するだけです。レベルや受領者を変更できません。

- **実行時点：** CLI はログイン成功後と、ログイン済みの任意コマンド開始時（`seal renew` は手動経路であり、自動更新を重ねない）。Web Console はログインしてワークスペースに入るとき（ページ読み込み時にセッションが既存でも同様）。Android Console はログイン後とアプリがフォアグラウンドへ戻るとき。未ログイン、またはその公開鍵が公開した grant がない場合は更新しません。
- **更新規則：** `GET /api/seal/grants?owner_key=<本机公钥>`。30 日以内に期限が切れる認可を、元の `device_id`、`operator_key`、`level` で 90 日分再発行し、公開成功後に旧 grant を `DELETE` します。`GET /api/seal/keys` が成功した場合、公開鍵ディレクトリから消えた `operator_key` はスキップします。ディレクトリ要求が失敗した場合はフィルタリングしません。
- **頻度制限：** Web/Android: 同じ発行公開鍵につき最大 12 時間に 1 回試行します。CLI は設定ディレクトリの `seal-renew-state.json`、Web Console は `localStorage`、Android Console は SecureStore に記録します。失敗は現在の操作に影響しません。CLI は stderr に警告を 1 行、Console は `console.warn` を出します。更新成功時は CLI が stderr に短い説明を 1 行出します。ルートを持つ Web Console はワークスペースへ入ると最初に一度発行し、Android は「エンドツーエンド暗号化」を開くと最初に一度発行します。この 2 回には 12 時間の制限を適用せず、同じ画面滞在中の後続更新だけに適用します。
- **CLI の更新：** 成功時刻はサーバーと発行鍵ごとに記録します。成功後 12 時間は更新の通信を省略し、失敗や中断後の待機は 5 分です。更新全体の上限は 30 秒です。エラーには処理段階と経過時間を表示し、主コマンドが失敗した場合は直ちに更新を中止します。未完了のデバイス失効処理は成功後の頻度制限を受けず、独立した 4 秒の予算を使います。明示的な更新には `remote-cli seal renew` を実行します。 ログイン成功後は一度強制的に更新します。
- **ルートクライアント：** ワークスペースルートを持つスーパー管理者クライアントは、削除直後の認可を再発行しないよう、次節のロール別発行でこの汎用更新を置き換えます。
- **手動：** `remote-cli seal renew`（既定 `--before 30 --days 90`）も引き続き使用でき、自動更新と同じ規則です。

```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
```

## ワークスペースのスーパー管理者が信頼ルート

ワークスペースルートは、スーパー管理者クライアントの Ed25519 鍵です。デバイスはそのルート公開鍵と、`rsc1.` 証明書チェーン経由でそのルートまで検証できる `rsg1.` 認可だけを信頼します。ルートクライアントはアカウントロールに従ってメンバー認可を自動発行・更新します。Exchange は引き続き信頼しません。自己署名 `rsr1.` ルート記録、`rsx1.` ローテーション記録、`rsi1.` 招待を保存し、形式と Ed25519 署名を確認するだけで、**誰を承認するかは決定しません**。悪意のある Exchange は記録を差し止めたり名簿に偽メンバーを追加したりできますが、ルート署名を偽造できません。CLI、Web、Android の自動承認は既定で有効なため、名簿の新しい公開鍵はこれらのルートクライアントが直接署名します。自動承認を明示的に無効にした場合のみ、未承認で、そのルートが公開したこともない新公開鍵が承認待ち一覧に残ります。

### 信頼モデルとトレードオフ

- **ルート公開鍵は初回利用時に固定（TOFU）。** クライアントが検証済みルート記録を初めて見ると、公開鍵をローカルに固定します。CLI は `config.toml` の隣の `seal-root.json`（0600）へ書き込み、キーは `服务器|工作区 id` です。Web Console は `localStorage`（`remote.seal.root.<工作区 id>`）、Android Console は SecureStore（`remote.seal.rootstate`、サーバーとワークスペース別に同じ JSON へ保存）を使います。その後毎回、自己署名と指紋を照合します。固定済みルート公開鍵が変わると強く警告し、**クライアントは自動で信頼を切り替えません**（デバイスは `rsx1.` ローテーション記録で自動切り替え。前述参照）。指紋確認後、明示的に `remote-cli seal root --trust <key>` を実行するか、Console の「新しいルート公開鍵を信頼」を選択します。
- **招待は登録だけを開放。** ルート公開鍵が `rsi1.`（ワークスペース id、ルート公開鍵、16 バイト nonce、有効期限）を発行します。管理者、オペレーター、読み取り専用アカウントのクライアントは、ログイン、後続コマンド、ページ読み込み、アプリのフォアグラウンド復帰時に招待を取得し、**固定済み**ルート公開鍵で署名を検証して、従来の `POST /api/seal/keys` で自身のオペレーター公開鍵を登録します。招待は認可ではありません。
- **ロール別発行と新しい鍵のローカル承認。** ルート公開鍵を持つスーパー管理者クライアントは `GET /api/seal/roster`（スーパー管理者のみ。無効化済みメンバー、ロール、`operator_device_grants`、登録公開鍵を含む）と自身の公開済み grant を読み、必要な認可を算出します。同じ登録公開鍵は次の範囲を共有します。
  - スーパー管理者と制限のない管理者：`manage` `*`
  - 制限付き管理者：デバイス認可に従い、`can_manage` なら `manage`、それ以外は `view`
  - 制限のないオペレーター：`view` `*` と、各 `can_manage` デバイスへの `manage`
  - 制限付きオペレーター：デバイス認可に従う `manage` または `view`
  - 制限のない読み取り専用：`view` `*`
  - 制限付き読み取り専用：認可された各デバイスの `view`。読み取り専用アカウントはデバイスを操作できないため、`can_manage` でも `manage` は発行しない
  - 無効化済みメンバー：発行しない
  - 既定 90 日、残り 30 日未満で更新
- **管理者の中間証明書。** ルートクライアントは有効な管理者へ `rsc1.`（`manage`、`can_issue`、`max_path_len` は 0、90 日）を発行します。制限のない管理者のデバイス範囲は `*`、制限付き管理者は委任された `can_manage` デバイスです。残り 30 日で更新し、未承認の新公開鍵には発行しません。管理者クライアントが有効な中間証明書を持つ場合、自身の鍵とこのチェーンで、管理下メンバーへ証明書とデバイス委任の共通範囲内の grant を発行します。ルートの十分新しい grant が同じオペレーター、デバイス、レベルを覆う場合（ルートの `*` は個別デバイスを、`manage` は `view` を覆う）は再発行しません。Exchange からの証明書失効は配布停止にすぎず、デバイスの既存コピーは期限まで有効です。強制失効はデバイスポリシーの `revoked_keys` で行います。
- **自動承認は既定で有効。** Web Console（`localStorage`）と Android Console（SecureStore）では記録にこのフィールドがなければ有効（absent = on）とし、明示的な `false` のみ無効を維持します。すでに無効にした設定を既定ロジックで再有効化しません。有効時はルートクライアントが名簿に従って直接署名するため、**保護が弱くなります**。侵害された Exchange は偽メンバーを追加して直ちに署名を得られます。スイッチの表示は「新しい公開鍵を自動承認」です。CLI も Web/Android Console と同じく既定で有効です。`seal-approved.json` ファイルまたは `auto_approve` フィールドがない場合は有効とし、明示的な `"auto_approve": false` は無効のまま保持します。`remote-cli seal approve --no-auto-approve` で無効、`remote-cli seal approve --auto-approve` で再度有効にでき、設定は保存されます。無効にすると、未承認で、そのルート公開鍵が公開したこともない鍵がローカル承認待ち一覧に入ります（CLI は `seal-approved.json`、Web は `localStorage`、Android は SecureStore）。`remote-cli seal pending` はユーザー名と指紋を表示します。`seal approve --all` または `seal approve <key>…` で承認すると即時発行します。承認済みの鍵と、そのルート公開鍵が公開済みの鍵は更新とロール変更を自動処理します。発行クライアント自身の公開鍵に再承認は不要です。
- **開いたら発行。** ルートを持つクライアントは Web Console のワークスペース進入時と Android の「エンドツーエンド暗号化」表示時に即時発行し、この回には 12 時間制限を適用しません。同じ画面滞在中の後続自動更新には制限を適用します。スイッチ変更や「承認」で再発行を強制します。
- **アカウントダイアログ、バッジ、失敗行。** Web Console のスイッチと承認待ち一覧はデバイスページの「アカウント」ダイアログ（ツールバーの「アカウント」）にあります。承認待ちの鍵がある場合、同じツールバーに「N 個の公開鍵が承認待ち」バッジを表示し、クリックでダイアログを開きます。発行失敗時はツールバーに「ワークスペースルートの発行に失敗：…」という状態行を表示します。この行から密封トークンと長い鍵素材を除去します。Android は同じスイッチ、バッジ、失敗説明を「エンドツーエンド暗号化」ページに表示します。
- **降格はデバイス上の旧認可を失効しない。** メンバー削除、無効化、公開鍵のディレクトリからの消失時は、Exchange の grant を削除して更新を停止し、オンラインかつ密封対応のデバイスの `revoked_keys` へ可能な範囲で書き込みます。ロール降格は新 grant への置換だけで、鍵を `revoked_keys` に入れません。デバイスにインポート済みの旧 grant は期限まで有効です。ルート公開鍵自身を失効一覧へ入れることはありません。4 秒の予算内に完了しなかった失効はローカルに記録し、次のコマンドで続行します。12 時間の発行制限は適用しません。鍵が再び必要な認可対象になった場合、失効に成功したデバイスだけで失効を解除します。
- **ルート記録の自動生成。** スーパー管理者アカウントの作成（`register`/ワークスペースの最初のアカウント）時、または後続のスーパー管理者ログイン時に Exchange にルート記録がなければ、スーパー管理者クライアント（CLI / Web / Android）がローカルで自身の鍵をルートとして公開し、直ちに 90 日の招待を発行します。その後、招待がないか 30 日以内に期限が切れる場合は発行の頻度制限に従って更新します。`remote-cli seal root --publish` は引き続き手動実行できます。

### 初回設定

1. スーパー管理者が任意のクライアントで登録またはログインすると、ルートを自動生成・公開します（CLI は stderr に案内し、Console は通知を表示）。
2. ルート秘密鍵を保管します（片方または両方）。
   - 自分だけで保管し、何もアップロードしない：`remote-cli seal root --export root.rsk`（0600 ファイル、`-` は stdout）または Console の「ルート秘密鍵をエクスポート」。
   - クラウドバックアップの手動設定：`remote-cli seal backup` または Console の「ルート秘密鍵のクラウドバックアップ」。パスフレーズは空でも構いませんが、**空または弱い場合、Exchange はルート秘密鍵を復号して全デバイスを制御できます**。
3. 密封対応デバイスは Exchange 接続時に自動でルートを固定します（TOFU）。より確実にするには、接続前にデバイスで `remote-agent seal root <根公钥>` を実行してください。
4. 新メンバーはログイン後に公開鍵を登録します。CLI、Web、Android は自動承認が既定で有効なため、ルートクライアントが直接署名します。自動承認を無効にしている場合、`remote-cli seal pending` で確認し、`remote-cli seal approve --all`（または Console の「承認」）で承認すると直ちに発行します。

```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 关闭自动批准（保持开启较弱）
```

### 別のマシンへの移行 / ルート変更

- **別のデバイスでルートを使う：** スーパー管理者ログイン後、ローカルに公開済みルートがなく Exchange に `rsb1.` クラウドバックアップがある場合、CLI / Web / Android は**自動復元を試みます**（「ログイン時の自動復元」参照）。それ以外は手動インポートを案内します：`seal root --import <文件|->`、Console へ `rsk1.` を貼り付けて「ルート秘密鍵をインポート」、または Exchange の暗号化バックアップから復元（`seal restore` / Console の「Exchange から復元」）。現在のルートをインポートする場合はローカルに導入するだけで記録を変更しません。
- **ローテーション：** `seal root --rotate`（Console の「新ルートを生成してローテーション」）または**現在のルートと異なる** `rsk1.` 鍵のインポート。ローカルが現在のルートを持つ場合、旧ルートで `rsx1.` ローテーション記録に署名し、新しい `rsr1.` とともに `PUT /api/seal/root` します。Exchange がオンラインデバイスへ配信し、デバイスは固定済みルートの署名を検証して新ルートへ切り替えます。オフラインデバイスは次回接続時に受信します。旧ルートが承認したメンバーはローカルでは承認済みを維持し、続いて全メンバー認可を新ルートで再署名します（90 日）。旧ローカル鍵は `seal-identity.<毫秒>.previous.pk8` として保存します。ローテーション後は新ルートを再エクスポートまたはバックアップしてください。
- **旧ルートを紛失済み：** ローテーション記録を署名できません。クライアントは新ルートを公開しますが、旧ルートを固定したデバイスは受理しません。各デバイスで `remote-agent seal root <新根公钥>` を実行し、agent を再起動する必要があります。
- **スーパー管理者の引き継ぎ：** 「スーパー管理者の引き継ぎ = ルート証明書の引き継ぎ」を参照。アカウントロールの変更だけでは不十分で、デバイスが信頼するのはルート秘密鍵です。

## ログイン時のルート秘密鍵の自動復元

スーパー管理者のログイン成功後、ローカル密封 ID を作成する前に、CLI / Web Console / Android Console は共通の純粋な決定表（Rust の `decide_auto_restore`、JS の `decideAutoRestore`）に従い、Exchange の `rsb1.` バックアップから公開済みワークスペースルートの復元を試みます。

1. **スキップ：** スーパー管理者でない、公開済みルートと同じ鍵をローカルに保持済み、クラウドバックアップが無効（CLI：`REMOTE_SEAL_CLOUD_BACKUP=0`、Web：`localStorage.seal.cloudBackup=off`、Android：SecureStore の `remote.seal.cloudBackup=off`）、Exchange に公開済みルートがない。
2. **バックアップを 1 回取得：** `GET /api/seal/backup`（時間あたりの回数制限あり）。バックアップがなければルートが別の場所にあると案内し、ログインは妨げません。
3. **空パスフレーズで静かに試行：** まず空文字列で復号します。成功し、復号した公開鍵が公開済みルートと一致し、ローカルに別の鍵がなければ導入してワークスペースルートとして固定します。
4. **それ以外はパスフレーズを案内：** CLI は `REMOTE_SEAL_BACKUP_PASSPHRASE` も試し、TTY では入力を隠します。Web / Android は**非ブロッキング**通知のみを出し、密封ページでパスフレーズを入力して「Exchange から復元」を選ぶよう案内します。ブロックするダイアログは出さず、ログインは通常どおり完了します。
5. **セキュリティ境界：** ローカルにある別の密封鍵を黙って上書きしません（`seal restore --force` または Console の復元確認が必要）。復号した公開鍵は公開済みルートと一致しなければなりません。失敗は警告のみでログインを失敗させません。パスフレーズ、シード、`rsb1.` 暗号文を出力しません。
6. **リスク通知（非ブロッキング）：** 導入後は「複数マシンでルートを保持すると漏えいリスクが増える」と案内します。空パスフレーズで復号できた場合、Exchange もルート秘密鍵を復号できることを追加で案内します。

## スーパー管理者の引き継ぎ = ルート証明書の引き継ぎ

スーパー管理者というアカウントロールと、デバイスが信頼するワークスペースルートは別物です。ロールだけを渡しても新スーパー管理者はルート秘密鍵を持たず、デバイスの信頼は切り替わりません。引き継ぎでは**ルート秘密鍵**を渡してから直ちにローテーションし、渡したコピーを無効にします。

1. 現スーパー管理者が公開済みルートをローカルに持つ状態で譲渡します：`POST /api/superadmin/transfer`、本文 `{operator_id, root_handover?}`。`root_handover` はパスフレーズ暗号化した `rsb1.` で、クラウドバックアップと同じアルゴリズム（Argon2id 64 MiB、t=3、p=1、ChaCha20-Poly1305）です。パスフレーズは空でも構いません。CLI：`remote-cli seal handover <账号>`。Web / Android Console は「スーパー管理者を譲渡」でローカルにルート秘密鍵がある場合、任意のパスフレーズを案内してこのパッケージを添付します。
2. Exchange は**保存するだけで復号しません**。`rsb1.` または `rsk1.` 接頭辞、16384 以下の長さ、ASCII URL 安全文字のみであることを確認します。要求に `root_handover` がない場合、そのワークスペースに残る引き継ぎパッケージを削除します。
3. 新スーパー管理者は `GET /api/superadmin/handover` で `{blob, from_operator, created_at}` を取得し（なければ 404）、パスフレーズで復号してローカルへ導入し、「新ルートを生成してローテーション」と同じ処理で旧ルートによる `rsx1.` を発行して新ルートを公開します。最後に `DELETE /api/superadmin/handover`。CLI：`remote-cli seal accept-handover`。Console の密封ページはパッケージがある場合「ルート秘密鍵の引き継ぎを受領」を表示します。
4. **ローテーションで旧コピーを無効化。** 渡したのはローテーション前のルートです。オンラインデバイスは、すでに信頼する旧ルートがローテーション記録に署名した場合だけ新ルートを信頼します。前任者の旧秘密鍵では新ルートの認可を署名できず、すでに切り替えたデバイスのルートも変更できません。完了前は空または弱いパスフレーズの `rsb1.` を復号できるため、パスフレーズは別経路で渡してください。

**帯域外：** 暗号文を Exchange に置きたくない場合、CLI で `remote-cli seal handover <账号> --out-of-band` を使います。標準出力は `rsb1.` の 1 行のみで、譲渡要求に `root_handover` は含めません。この行とパスフレーズを別経路で相手へ渡し、相手が `remote-cli seal accept-handover <那一行>` を実行します。Console のパスフレーズ入力でキャンセル（Android は「アカウントのみ譲渡」）した場合もロールだけを譲渡し、ルート秘密鍵はアップロードしません。秘密鍵は別途渡す必要があります（`rsk1.` エクスポート、または帯域外の `rsb1.`）。

## ハンドシェイクと通信形式

```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}`。署名対象は `["remote-seal-v1/hello", v, device_id, operator_key, operator_ephemeral, nonce, grant.sig|""]`（JSON 配列、正規バイト列）。
- ルート記録 `rsr1.` の署名対象は `["remote-seal-v1/workspace-root", v, workspace_id, root_key, issued_at_ms]`。ローテーション `rsx1.` = `{v, workspace_id, old_root, new_root, issued_at_ms, sig}` は `old_root` により `["remote-seal-v1/root-rotation", v, workspace_id, old_root, new_root, issued_at_ms]` を署名します。Exchange → agent：`{"type":"seal_root","token":"rsr1.…","rotation":"rsx1.…"?}`（Exchange からの平文制御メッセージだけを受理し、トンネル内では無効）。
- `accept` = `{v, device_key, device_ephemeral, nonce, level, sig}`。署名対象は `["remote-seal-v1/accept", v, device_id, operator_key, operator_ephemeral, hello.nonce, hello.sig, device_key, device_ephemeral, nonce, level]`。Exchange が `level` を変更すると署名が無効になります。
- セッション鍵：`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 = チャネルバイト（0 テキスト / 1 バイナリー）‖ 0 を 3 個 ‖ `u64` カウンター（ビッグエンディアン）。方向・チャネルごとのカウンターは 0 から厳密に 1 ずつ増加し、不一致があれば直ちにセッションを閉じます。
- テキスト：`{"counter":n,"ciphertext":"<base64>"}`。平文は JSON で、オペレーター → デバイスは `SealedRequest`（既存の `DeviceDown` メッセージ、またはルート管理メッセージ `seal_policy_get` / `seal_policy_set {mode?, root_key?, revoke?, unrevoke?}`）、デバイス → オペレーターは `SealedResponse`（既存の `DeviceUp` メッセージまたは `seal_policy`）。内部の `session_id` は agent がトンネル番号へ書き換えます。
- バイナリーフレーム：`StreamKind::Sealed = 4`、ペイロード = `counter(u64 BE) ‖ AEAD(inner_kind ‖ bytes)`、`inner_kind` は 1/2/3（stdout/stderr/PTY）。
- 各密封セッションは**1 つ**の操作（1 回の exec、1 個の PTY、1 回のファイル転送など）を運び、1:1 セッションモデルを維持します。終了メッセージは agent が暗号化し、外側に `last:true` を付けます。Exchange は内容を理解せず、それを使ってルート解放と監査補完を行います。
- `close_session` → `sealed_close{detach:false}`（終了）。運用担当の切断 → `sealed_close{detach:true}`（agent が判断し、hold exec は detach のみ、その他は終了）。
- サイズ上限：暗号文 base64 は約 2.8 MiB（2 MiB の Android スクリーンショットを収容）、外側ラッパーは `MAX_SEALED_WIRE`。

agent 側の防御：送信キューで密封セッション番号に属する平文は、暗号化または破棄のみとなります。終了後はその番号を恒久的に隔離します（hold プロセスの遅延出力は破棄し、平文送信しない）。密封セッションで起動/接続した hold ジョブは旧経路の一覧に現れず、旧経路で attach/tail/cat/kill もできません。

## ルート秘密鍵のエクスポートとバックアップ

ルート秘密鍵はワークスペース内の全デバイスを制御できます。保管方法はスーパー管理者が決定し、**Console は既定で空のパスフレーズで保存し、削除すると自動保存を停止**です。

- **エクスポート（アップロードしない）：** `rsk1.` + base64url（パディングなし）の 32 バイトシード。CLI は `seal root --export FILE|-`（ファイル 0600）、Console は「ルート秘密鍵をエクスポート」（Web は入力欄に表示、Android はクリップボードへコピー）。取得した誰もが全デバイスを制御できます。
- **Exchange への暗号化バックアップ（Console は自動、CLI は手動）：** スーパー管理者で、ローカル鍵が公開済みルートである場合だけアップロード可能です。暗号文はバージョン付き `rsb1.` トークン（base64url エンコードの JSON、最大 4096 文字）。Argon2id でパスフレーズから鍵を導出し、本番パラメーターはメモリー 64 MiB、反復 3、並列度 1 に固定してトークンに記録します。ソルト 16 バイト、ChaCha20-Poly1305 の nonce 12 バイトをバックアップごとにランダム生成します。追加認証データは `remote-seal-v1/key-backup:{version}:{public_key}` で、バージョン、公開鍵、暗号文を変更すると検証に失敗します。復号後はシード由来の公開鍵とトークン内公開鍵の一致も確認します。デコーダーは Argon2 実行前に弱すぎる/大きすぎるパラメーターを拒否します（メモリー 8 KiB–256 MiB、本番復号は最低 64 MiB。反復 1–8、本番は最低 3。並列度は 1 のみ）。
- **パスフレーズは任意で、強度を強制しません。** 空でも Argon2id（空文字列）を使い、形式は同じです。クライアントは通知のみを行います。空または弱い場合（12 文字未満、文字種が少ない、反復、よくあるパスフレーズ）、CLI / Web / Android はいずれも**Exchange（または DB を読める人）がルート秘密鍵を復号して全デバイスを制御できる**と明確に警告します。Rust と JavaScript は同じ通知規則とテストベクトル（空パスフレーズを含む）を使います。

`PUT` / `GET` / `DELETE /api/seal/backup` は現在ログイン中のアカウントのみを対象とし、`PUT` と `GET` はスーパー管理者のみ利用できます。リクエスト本文は `{blob}`。Exchange は `rsb1.` 接頭辞と長さを確認し、**決して復号しません**。`GET` はアカウント・プロセスごとに 1 時間最大 5 回で、超過すると 429 を返します。これはオンライン推測を遅くするだけで、DB をコピーされた場合は弱いパスフレーズをオフラインで総当たりできます。読み取り、書き込み、削除は監査記録に残り、詳細はバイト数だけです。

- CLI：`remote-cli seal backup`（パスフレーズを非表示で 2 回入力。Enter で空、または `--passphrase-stdin`）。`remote-cli seal restore`（別の既存鍵がある場合は `--force`。旧鍵は `seal-identity.<毫秒>.previous.pk8` として保存）。復元後はローカルのワークスペースルートとして固定します。ログイン時にも自動復元を試みます（前節参照）。秘密鍵、パスフレーズ、暗号文は出力しません。
- Web Console：ルート秘密鍵はエクスポート可能である必要があります。スーパー管理者のルート生成時にブラウザーがエクスポート不可の WebCrypto 鍵を持っている場合、IndexedDB のローカルシードをルートに使います。ログイン bootstrap は ID 作成前に自動復元し、`localStorage.seal.cloudBackup=off` で無効化できます。
- Android Console：シードは expo-secure-store に保存します。`bootstrapSeal` も先に自動復元し、`remote.seal.cloudBackup=off` で無効化できます。

## 使用方法

```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：** すべてのデバイス操作はネイティブの密封セッションのみを使用し、コマンド、hold 一覧/kill/tail/cat、ターミナル、ファイル、スクリーンショットと入力（Android/デスクトップ）、UI ツリー、センサー、ubus、システムログ、自己更新はすべて密封トンネルを使い、ページに「🔒 エンドツーエンド暗号化」と表示します。Web のデバイス詳細または Android の設定 →「エンドツーエンド暗号化」パネルは CLI と同等で、ローカル公開鍵/指紋、ワークスペースルート（スーパー管理者：ルート秘密鍵のエクスポート/インポート/ローテーション、引き継ぎ受領、新公開鍵承認）の表示、デバイス鍵と失効ポリシーの表示/変更（ルートが必要）、認可発行（`rsg1.` トークンを出力し CLI で直接 `seal import-grant` 可能）、認可インポート、デバイス公開鍵の信頼、既定のルート秘密鍵クラウドバックアップ（パスフレーズは空でも可。弱い場合は Exchange が復号できると明確に警告）を提供します。初回接続でデバイス指紋を固定（TOFU）し、その後の変更は直ちに拒否して確認を案内します。通常メンバーの CLI、ブラウザー、スマートフォンは別々の 3 本の鍵で、それぞれ登録後にルートクライアントがロール別に発行します。スーパー管理者が複数デバイスでルートを使う場合、パスフレーズバックアップをアップロードして新デバイスのログイン時に自動復元するか、各デバイスへルート秘密鍵をインポートします。

デバイスのローカル操作：`remote-agent seal show | root <key> | forget-root | revoke <key>`（変更後は agent 再起動で反映。密封セッション経由の `seal policy` 変更は即時反映）。

## 既定の動作

| 範囲 | 現在の動作 |
|---|---|
| CLI、Web Console、Android Console | すべてのデバイス操作にネイティブの密封セッションを使用し、無効化スイッチや平文へのフォールバックはない |
| デバイスのローカルポリシー | `allow` を含むすべてのポリシー値で平文操作を拒否 |

- 新デバイスは Exchange に接続するとワークスペースルートを固定します（TOFU）。ルートとルート発行の認可で操作できます。初回接続で Exchange を信頼できない場合は、先にデバイス上で `remote-agent seal root <key>` を実行してください。
- ID 鍵を利用できない場合やポリシーが破損している場合は、失敗時に閉じます。デバイス上で鍵とポリシーを修復してから再接続してください。
