diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index af85ab2b..d20a6c76 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -53,12 +53,12 @@ signed/decentralized registry and a user installs it on their node. 13. Genuinely slow opens show loader states (UIFIX-06, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-missing-loader-states-on-slow-opens.md`; 02-08's flagged timing regressions are the starting inventory) NOTE for criteria 7–13: all were added after phase 1's 10 plans were written — before phase 1 execution completes, create gap plan(s) covering FED-07 + UIFIX-01..06 (existing desktop visuals must remain untouched per the standing visual-invisibility rule; UIFIX items themselves are user-approved visual changes) -**Plans**: 8/20 plans executed +**Plans**: 11/20 plans executed Plans: -- [ ] 01-20-PLAN.md — URGENT wave 1: doctor stops restarting Tor every 5min (mesh Tor fallback) (FED-09) -- [ ] 01-19-PLAN.md — URGENT wave 1: wallet invoices embed route hints so private-channel nodes can receive (FED-08) +- [x] 01-20-PLAN.md — URGENT wave 1: doctor stops restarting Tor every 5min (mesh Tor fallback) (FED-09) +- [x] 01-19-PLAN.md — URGENT wave 1: wallet invoices embed route hints so private-channel nodes can receive (FED-08) - [x] 01-01-PLAN.md — Serialize the federation node store and make removal stick (FED-01) - [x] 01-02-PLAN.md — Demo mesh/federation RPC parity + automated parity harness (FED-04) - [x] 01-03-PLAN.md — On-brand paid tick: ScreensaverRing badge variant on both success surfaces (FED-06) @@ -80,7 +80,7 @@ Plans: **Wave 8** *(blocked on Wave 7 completion)* -- [ ] 01-16-PLAN.md — Migrate existing installs off the default gateway credential, data preserved (FED-07) +- [x] 01-16-PLAN.md — Migrate existing installs off the default gateway credential, data preserved (FED-07) - [ ] 01-17-PLAN.md — FIPS/Tor pills pinned against removal and readable at phone widths (UIFIX-01) **Wave 9** *(blocked on Wave 8 completion)* @@ -235,7 +235,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Federation & Mesh Hardening | 8/20 | In Progress| | +| 1. Federation & Mesh Hardening | 11/20 | In Progress| | | 2. UI Performance | 11/12 | Complete | 2026-07-31 | | 3. Multinode Verification Pass | 0/TBD | Not started | - | | 4. Lifecycle Perfection & Quadlet Default | 0/TBD | Not started | - | @@ -267,9 +267,18 @@ 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) +**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 **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:** 5 plans +**Plans:** 5 plans + KEY-05 unplanned (needs a 6th plan) + +> **EXECUTION GATE (user instruction, 2026-08-02):** do **not** begin executing this phase until +> (a) the concurrent agent working Phase 1 has finished, and (b) their changes are synced and +> accounted for. Rationale: Phase 10 edits `middleware.rs`, `identity.rs`, `seed_rpc.rs`, +> `bitcoin.rs` and — under KEY-05 — ~15 further files across the same crate that agent is +> actively committing to. Verify a clean tree and a fetched `gitea-ai/main` before starting. +> +> **KEY-05 is not yet planned.** The 5 plans below predate it; a 6th plan (or a re-plan) is +> required before this phase can be considered fully covered. Plans: diff --git a/docs/UNIFIED-TASK-TRACKER.md b/docs/UNIFIED-TASK-TRACKER.md index b673be70..05193c67 100644 --- a/docs/UNIFIED-TASK-TRACKER.md +++ b/docs/UNIFIED-TASK-TRACKER.md @@ -100,12 +100,26 @@ those are marked ✅ below with the commit that did it, so we stop re-litigating `Math.random()` picks a UX challenge, not key material, so the next auditor does not re-derive that it is benign. -- [ ] **Swap container `generated_secrets` to explicit `OsRng`** (entropy audit R-13, - finding F-10) — `random_hex`/`random_base64` in - `core/archipelago/src/container/secrets.rs:90-102` use `thread_rng()`. Correct on - `rand 0.8.5`, but the same implicit-source pattern as [ARCHY-1]. Two-line change. - **Currently blocked only on tree hygiene** — that file had another agent's - uncommitted work when the audit ran. +- [ ] ~~**Swap container `generated_secrets` to explicit `OsRng`** (entropy audit R-13, + finding F-10) — two-line change in `container/secrets.rs:90-102`~~ + **SUPERSEDED 2026-08-02 by R-16 / KEY-05.** The audit scoped this at 2 call sites; the + real surface is **41 across 15 files** — see the audit's new §F-10a. `secrets.rs` is 2 + of them, and a two-line fix there while 39 other sites inherit the same dependency + default is not a fix. + +- [ ] **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. + 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; + `clippy.toml` `disallowed-methods` ban (compile-time, CI-enforced — no `clippy.toml` + exists yet); `cargo-deny` on duplicate `rand` majors (absorbs R-05); degenerate-entropy + runtime check; persist the CSPRNG-readiness verdict (absorbs R-09). Also retires the + `impl rand::CryptoRng for CountingRng` false promise at `seed.rs:656`. + **Gated: do not start until the concurrent Phase 1 agent is done and synced.** ## Tier 1 — Medium effort, unblocked diff --git a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md index 43e323d1..8499c8bb 100644 --- a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md +++ b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md @@ -156,7 +156,8 @@ action of the "your seed may be predictable, migrate now" kind is warranted — | F-07 | **Medium** | No `cargo audit`/`cargo deny` in CI; two `rand` majors coexist in the graph | `core/archipelago/Cargo.toml:68` | | 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** | 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-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-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` | @@ -488,6 +489,62 @@ One-line change each; batched into the same backlog item. --- +### F-10a — Scope correction: the defaulted-RNG surface is crate-wide — **Medium** + +> **Added 2026-08-02, after the original audit.** F-10 above reported this defect as two call +> sites in one file. That was **understated**. This section records the true scope with evidence. +> F-10's own text and remediation are left unedited above so the correction is auditable rather +> than retroactive. + +**Evidence.** `grep -rn "rand::random\|thread_rng()" core/archipelago/src --include=*.rs` returns +**43 matches across 16 files**. Two of those (`seed.rs:87`, `:671`) are comments in the +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/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/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** | +| `core/archipelago/src/credentials/store.rs` | 1 | credential store material | +| `core/archipelago/src/device_tokens.rs` | 1 | device tokens | +| `core/archipelago/src/federation/invites.rs` | 1 | federation invites | +| `core/archipelago/src/bitcoin_rpc.rs` | 1 | Bitcoin RPC password | +| `core/archipelago/src/totp.rs` | 1 | TOTP backup codes (also F-09) | +| `core/archipelago/src/transport/chunking.rs` | 1 | chunk identifiers | +| `core/archipelago/src/fips/dial.rs` | 1 | dial jitter/identifiers | +| `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. + +**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**. + +**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 +the traced paths — so files outside those traces were never in scope to be looked at. Recorded +here because the same blind spot would recur in the next audit run under the same method. + +**Remediation → tracked as KEY-05 in Phase 10** (`.planning/ROADMAP.md`), which supersedes R-13: +a sealed allowlist trait so only approved RNGs can be passed at key-generation seams; a +`clippy.toml` `disallowed-methods` ban on `rand::thread_rng` / `rand::random` crate-wide so the +default cannot be inherited by *new* code either; `cargo-deny` failing on duplicate `rand` +majors (R-05, the mechanism by which a bump could silently rebind); a degenerate-entropy runtime +check before key generation; and persisting the CSPRNG-readiness verdict (R-09) that +`seed.rs:59` already computes but discards. + +--- + ### F-11 — `Math.random()` inside a seed-handling view — **Informational (benign)** **Evidence.** `neode-ui/src/views/OnboardingSeedVerify.vue:157-163`, `pickRandomIndices` uses @@ -899,7 +956,8 @@ own `/gsd-plan-phase`, not an opportunistic edit. | **R-10** | [ARCHY-2] hardening | Add a test asserting the `getrandom` crate uses the **blocking** syscall, so the invariant is mechanical rather than a comment | `core/archipelago/src/seed.rs` | S | no | | **R-11** | F-08 (Low) | Clear `_seed_words` on route-leave from onboarding, not only on successful verify; add a wall-clock expiry mirroring `MNEMONIC_TTL` | `neode-ui/src/views/OnboardingSeedGenerate.vue`, `OnboardingSeedVerify.vue` | S | no | | **R-12** | F-09 (Low) | Replace `% charset.len()` with `SliceRandom::choose(&mut OsRng)` and pin the uniformity property with a test | `core/archipelago/src/totp.rs` | S | no | -| **R-13** | F-10 (Low) | Swap `random_hex` / `random_base64` from `thread_rng()` to explicit `OsRng` | `core/archipelago/src/container/secrets.rs` | S | no — **blocked**: file has another agent's uncommitted work | +| **R-13** | F-10 (Low) | ~~Swap `random_hex` / `random_base64` from `thread_rng()` to explicit `OsRng`~~ — **SUPERSEDED 2026-08-02 by R-16**; this file is 2 of 41 sites | `core/archipelago/src/container/secrets.rs` | S | no | +| **R-16** | **F-10a (Medium)** | Crate-wide enforcement so a defaulted RNG cannot be inherited anywhere: sealed allowlist trait at key-generation seams; `clippy.toml` `disallowed-methods` ban on `rand::thread_rng`/`rand::random` (compile-time, CI-enforced); `cargo-deny` on duplicate `rand` majors; degenerate-entropy runtime check; persist the CSPRNG-readiness verdict (absorbs R-05, R-09, R-13) | 15 files — see §F-10a | **PHASE** — tracked as **KEY-05**, Phase 10 | no | | **R-14** | F-11 (Informational) | One-line comment at `pickRandomIndices` recording that the `Math.random()` is a UX challenge selector, not key material | `neode-ui/src/views/OnboardingSeedVerify.vue` | S | no | | **R-15** | §6 checklist | Run the on-node verification checklist — especially C-3 (per-node SSH/TLS keys) and C-5 (cross-node collision test) | — | M | **yes** (2+ nodes from one ISO) |