diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index bd48625d..cad83120 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -267,7 +267,7 @@ Plans: ### Phase 10: Key-Material Hardening **Goal:** Every path that creates, restores, or persists node key material proves the caller is authorized and the material is per-node — closing the three exploitable findings from `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`. A node that is already onboarded must refuse to have its identity replaced; a node flashed from the shared rootfs must never share another node's host keys; and the wallet spending key must not exist in cleartext outside the encrypted envelope. -**Requirements**: KEY-01 (F-01, **Critical**) `seed.generate`/`seed.restore` are unauthenticated (`api/rpc/middleware.rs:25`) and `NodeIdentity::from_seed` (`identity.rs:79`) overwrites `node_key`/`nostr_secret`/FIPS key unconditionally — one unauthenticated POST with an attacker-chosen mnemonic hijacks a live node; gate on onboarding-incomplete (the unused `identity.rs:117` `key_exists` guard) + rate-limit; KEY-02 (F-03, **High**) first-boot per-device secret regeneration is fail-open and its completion marker is set even on failure (`image-recipe/_archived/build-auto-installer-iso.sh:1647,:1659,:1663`), over a fleet-shared cached rootfs that bakes SSH host keys + the TLS key — make it fail-closed and retried; KEY-03 (F-13, **High**) the BIP-84 account **private** key is imported into Bitcoin Core's wallet (`api/rpc/bitcoin.rs:203,:229-231`), duplicating the spending key outside the encrypted envelope — move to watch-only descriptors per `docs/security/PSBT-SIGNING-ARCHITECTURE.md`; KEY-04 on-node verification of C-3/C-4/C-6 from the audit's UNVERIFIED checklist (host-key uniqueness across two real nodes, rootfs tar contents on the build host, unauthenticated LAN reachability of the RPC endpoint); KEY-05 (F-10a, **Medium**, added 2026-08-02) **a defaulted RNG cannot be inherited anywhere in the crate**. The audit's F-10 recorded this as 2 call sites; it is **41 across 15 files** (`session.rs` 16, `pine_ha.rs` 6, `wallet/bdhke.rs` 4 — *ecash key material*, `mesh/x3dh.rs` 2 — *key-agreement material*, `storage_crypto.rs` 1 — *AEAD nonce*, +10 more; full table in the audit's §F-10a). Nothing is broken today — `rand::random()`/`thread_rng()` are ChaCha12 seeded from `getrandom(2)` — but this is the exact T1 structural shape that produced the 2026-07-30 COLDCARD defect, now with key material in its blast radius. Five layers, all required: (a) **sealed allowlist trait** at key-generation seams (private supertrait, so no other module *or crate* can implement it; exactly one production impl, `OsRng`) — this also retires the `impl rand::CryptoRng for CountingRng` false promise at `seed.rs:656`; (b) **`clippy.toml` `disallowed-methods`** banning `rand::thread_rng`/`rand::random` crate-wide, so enforcement is a compile failure in CI rather than a review convention (no `clippy.toml` exists today; CI already runs clippy); (c) **`cargo-deny`** failing on duplicate `rand` majors — two coexist today, which is the mechanism by which a bump could silently rebind (absorbs R-05); (d) **degenerate-entropy runtime check** before key generation (rejects all-zero / counter-like draws — the one layer that would catch the Coldcard failure *on the device* rather than in review); (e) **persist the CSPRNG-readiness verdict** that `seed.rs:59` already computes and discards, so a node can answer after the fact "was the pool seeded when this key was born?" (absorbs R-09). Supersedes R-13 +**Requirements**: KEY-01 (F-01, **Critical**) `seed.generate`/`seed.restore` are unauthenticated (`api/rpc/middleware.rs:25`) and `NodeIdentity::from_seed` (`identity.rs:79`) overwrites `node_key`/`nostr_secret`/FIPS key unconditionally — one unauthenticated POST with an attacker-chosen mnemonic hijacks a live node; gate on onboarding-incomplete (the unused `identity.rs:117` `key_exists` guard) + rate-limit; KEY-02 (F-03, **High**) first-boot per-device secret regeneration is fail-open and its completion marker is set even on failure (`image-recipe/_archived/build-auto-installer-iso.sh:1647,:1659,:1663`), over a fleet-shared cached rootfs that bakes SSH host keys + the TLS key — make it fail-closed and retried; KEY-03 (F-13, **High**) the BIP-84 account **private** key is imported into Bitcoin Core's wallet (`api/rpc/bitcoin.rs:203,:229-231`), duplicating the spending key outside the encrypted envelope — move to watch-only descriptors per `docs/security/PSBT-SIGNING-ARCHITECTURE.md`; KEY-04 on-node verification of C-3/C-4/C-6 from the audit's UNVERIFIED checklist (host-key uniqueness across two real nodes, rootfs tar contents on the build host, unauthenticated LAN reachability of the RPC endpoint); KEY-05 (F-10a, **Medium**, added 2026-08-02) **a defaulted RNG cannot be inherited anywhere in the crate**. The audit's F-10 recorded this as 2 call sites; it is **41 raw matches across 15 files** (`session.rs` 16 → 4 prod + 12 test, `pine_ha.rs` 6, `wallet/bdhke.rs` 4 → 2 prod — *Cashu proof secret + blinding factor, genuine key material*, `storage_crypto.rs` 1 — *AEAD nonce*, `mesh/x3dh.rs` 2 — *prekey identifiers, **not** key material, corrected 2026-08-02*, +10 more; full table in the audit's §F-10a. Per-site prod/test classification is KEY-05's Task 1, not an assumption). Nothing is broken today — `rand::random()`/`thread_rng()` are ChaCha12 seeded from `getrandom(2)` — but this is the exact T1 structural shape that produced the 2026-07-30 COLDCARD defect, now with key material in its blast radius. Five layers, all required: (a) **sealed allowlist trait** at key-generation seams (private supertrait, so no other module *or crate* can implement it; exactly one production impl, `OsRng`) — this also retires the `impl rand::CryptoRng for CountingRng` false promise at `seed.rs:656`; (b) **`clippy.toml` `disallowed-methods`** banning `rand::thread_rng`/`rand::random` crate-wide, so enforcement is a compile failure in CI rather than a review convention (no `clippy.toml` exists today; CI already runs clippy); (c) **`cargo-deny`** failing on duplicate `rand` majors — two coexist today, which is the mechanism by which a bump could silently rebind (absorbs R-05); (d) **degenerate-entropy runtime check** before key generation (rejects all-zero / counter-like draws — the one layer that would catch the Coldcard failure *on the device* rather than in review); (e) **persist the CSPRNG-readiness verdict** that `seed.rs:59` already computes and discards, so a node can answer after the fact "was the pool seeded when this key was born?" (absorbs R-09). Supersedes R-13 **Depends on:** Nothing (independent security work; parallelizable with Phases 1–8). **Priority override: F-01 is Critical and live on every fleet node — this phase should be planned and executed ahead of its numeric position, which reflects append order in a shared roadmap, not sequencing.** **Plans:** 6 plans diff --git a/docs/UNIFIED-TASK-TRACKER.md b/docs/UNIFIED-TASK-TRACKER.md index 05193c67..d969aeca 100644 --- a/docs/UNIFIED-TASK-TRACKER.md +++ b/docs/UNIFIED-TASK-TRACKER.md @@ -110,8 +110,9 @@ those are marked ✅ below with the commit that did it, so we stop re-litigating - [ ] **Crate-wide CSPRNG enforcement — a defaulted RNG cannot be inherited anywhere** (entropy audit **R-16 / F-10a**, Medium) — tracked as **KEY-05 in Phase 10**, so plan and execute it there rather than as a standalone item. `session.rs` (16 sites), - `pine_ha.rs` (6), `wallet/bdhke.rs` (4 — **ecash key material**), `mesh/x3dh.rs` (2 — - **key-agreement material**), `storage_crypto.rs` (1 — **AEAD nonce**), +10 more files. + `pine_ha.rs` (6), `wallet/bdhke.rs` (2 prod — **Cashu proof secret + blinding factor, + genuine key material**), `storage_crypto.rs` (1 — **AEAD nonce**), `mesh/x3dh.rs` (2 — + prekey *identifiers*, **not** key material — corrected 2026-08-02), +10 more files. Nothing is broken today (`rand::random()`/`thread_rng()` are ChaCha12 from `getrandom(2)`), but it is the T1 shape that produced the COLDCARD defect, now with key material in the blast radius. Five layers: sealed allowlist trait at key-gen seams; diff --git a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md index 8499c8bb..41b6e55b 100644 --- a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md +++ b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md @@ -157,7 +157,7 @@ action of the "your seed may be predictable, migrate now" kind is warranted — | F-08 | **Low** | 24-word master mnemonic persisted in browser `sessionStorage` during onboarding | `neode-ui/src/views/OnboardingSeedGenerate.vue:330` | | F-09 | **Low** | Modulo bias in TOTP backup-code generation | `core/archipelago/src/totp.rs:305` | | F-10 | **Low** → **see F-10a** | Container `generated_secrets` use `thread_rng()` rather than an explicit `OsRng` (same T1 shape as F-02, smaller blast radius) | `core/archipelago/src/container/secrets.rs:92`, `:101` | -| F-10a | **Medium** | **Scope correction to F-10 (2026-08-02):** the defaulted-RNG surface is crate-wide — **41 call sites across 15 files**, not 2 — and includes ecash and X3DH key material | `core/archipelago/src/session.rs` (16), `wallet/bdhke.rs` (4), `mesh/x3dh.rs` (2), `storage_crypto.rs`, +11 more — full table in §F-10a | +| F-10a | **Medium** | **Scope correction to F-10 (2026-08-02):** the defaulted-RNG surface is crate-wide — **41 raw matches across 15 files**, not 2 — and includes ecash key material and an AEAD nonce | `core/archipelago/src/wallet/bdhke.rs:133`,`:139` (Cashu secret + blinding factor), `storage_crypto.rs:39` (nonce), `session.rs` (4 prod), +12 more files — full table in §F-10a | | F-11 | **Informational** | `Math.random()` inside a seed-handling view (benign — UX challenge selection only) | `neode-ui/src/views/OnboardingSeedVerify.vue:159` | | F-12 | **Informational** | Identical default OS credentials on every flashed node | `image-recipe/archipelago-scripts/install-to-disk.sh:205` | | F-13 | **High** | BIP-84 account **private** key is imported into Bitcoin Core's wallet, duplicating the spending key outside the encrypted envelope | `core/archipelago/src/api/rpc/bitcoin.rs:203`, `:229-231` | @@ -502,10 +502,10 @@ already-remediated F-02 file, leaving **41 matches across 15 files**: | File | Matches | Generates | |---|---|---| -| `core/archipelago/src/session.rs` | 16 | session tokens | +| `core/archipelago/src/session.rs` | 16 → **4 prod + 12 test** | session tokens (`#[cfg(test)]` begins `:470`) | | `core/archipelago/src/api/rpc/package/pine_ha.rs` | 6 | app credentials | -| `core/archipelago/src/wallet/bdhke.rs` | 4 | **Cashu blinded-key-exchange values — key material** | -| `core/archipelago/src/mesh/x3dh.rs` | 2 | **X3DH key agreement — key material** | +| `core/archipelago/src/wallet/bdhke.rs` | 4 → **2 prod + 2 test** | **Cashu proof secret (`generate_secret`, `:133`) and blinding factor (`random_blinding_factor`, `:139`) — genuine key material** (`#[cfg(test)]` begins `:143`) | +| `core/archipelago/src/mesh/x3dh.rs` | 2 | ~~X3DH key agreement — key material~~ **CORRECTED: `u32` prekey *identifiers*** (`spk_id` `:100`, `otk_id` `:114`). The X25519 secrets come from `crypto::generate_x25519_ephemeral()` at `:99`/`:113` and are **not** in this table | | `core/archipelago/src/container/secrets.rs` | 2 | `generated_secrets` (the original F-10) | | `core/archipelago/src/api/rpc/package/install.rs` | 2 | install-time secrets | | `core/archipelago/src/storage_crypto.rs` | 1 | **ChaCha20-Poly1305 nonce — reuse breaks the AEAD** | @@ -519,16 +519,35 @@ already-remediated F-02 file, leaving **41 matches across 15 files**: | `core/archipelago/src/api/rpc/auth.rs` | 1 | auth-path material | **Per-site production-vs-test classification is deliberately NOT asserted here** — it is the -first task of the remediation, not an assumption of this correction. The counts above are raw -matches. +first task of the remediation (KEY-05 Task 1), not an assumption of this correction. The counts +above are raw matches. + +> **Self-correction, 2026-08-02 (same day, before any KEY-05 work began).** The first version of +> this table asserted semantics in its right-hand column that two entries did not support, and +> the KEY-05 planner caught it against the code. Both are fixed inline above, struck rather than +> silently rewritten: +> - **`mesh/x3dh.rs` was wrong.** Called "X3DH key agreement — key material"; the two sites are +> `u32` prekey *identifiers*. The X25519 secrets are drawn elsewhere and were never in scope. +> - **`session.rs` was misleading.** 16 raw matches read as 16 production token sites; it is 4 +> production and 12 test. +> +> **The Medium rating still holds, on narrower grounds.** It now rests on `wallet/bdhke.rs`'s two +> production sites (the Cashu proof secret and the blinding factor — a predictable blinding +> factor breaks the ecash unlinkability guarantee and is genuinely key material) and on +> `storage_crypto.rs:39`'s AEAD nonce. It does **not** rest on x3dh. Recorded because the +> correction to F-10 was made on the grounds that understatement misleads the next reader — +> overstatement does exactly the same, and this table did both within one day. **Assessment.** Unchanged from F-10 in kind: `rand::random()` and `thread_rng()` are backed by ChaCha12 seeded from `getrandom(2)` on `rand 0.8.5`, so **nothing in this table is broken today**. What changes is the *blast radius* of the T1 structural objection. F-10 rated this Low on the basis of "per-app credentials rather than the master key hierarchy". That justification -does not survive the true scope: `wallet/bdhke.rs` and `mesh/x3dh.rs` generate key material, and -`storage_crypto.rs:39` draws an AEAD nonce, where a silent rebinding to a non-cryptographic PRNG -would be catastrophic rather than merely undesirable. Re-rated **Medium**. +does not survive the true scope: `wallet/bdhke.rs:133`/`:139` generate the Cashu proof secret and +blinding factor — genuine key material, where a predictable blinding factor breaks ecash +unlinkability — and `storage_crypto.rs:39` draws an AEAD nonce, where a silent rebinding to a +non-cryptographic PRNG would be catastrophic rather than merely undesirable. Re-rated **Medium** +on those two grounds. (An earlier version of this sentence also cited `mesh/x3dh.rs`; that was +incorrect and is struck in the table above — those sites are prekey identifiers.) **Why the original audit missed it.** F-10 was reached by tracing the *manifest secrets* path (secret class 4). No step enumerated defaulted-RNG use across the whole crate independently of