docs(10): correct F-10a's own overstatement — x3dh sites are identifiers, not key material

The F-10a scope correction committed hours earlier asserted semantics its
evidence did not support. The KEY-05 planner caught it against the code:

- mesh/x3dh.rs:100/:114 are u32 prekey IDENTIFIERS (spk_id, otk_id), not key
  agreement material. The X25519 secrets come from
  crypto::generate_x25519_ephemeral() at :99/:113 and were never in scope.
- session.rs's 16 raw matches read as 16 production token sites; #[cfg(test)]
  begins at :470, so it is 4 production + 12 test.
- wallet/bdhke.rs is 2 production of 4 (#[cfg(test)] at :143) — and those two
  ARE genuine key material: generate_secret() :133 and
  random_blinding_factor() :139.

The Medium rating still holds, on narrower grounds: bdhke's two production
sites plus storage_crypto.rs:39's AEAD nonce. It no longer rests on x3dh.

Struck rather than silently rewritten. F-10 was corrected on the grounds that
understatement misleads the next reader; overstatement does the same, and this
table managed both within a day.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-02 07:30:43 -04:00
co-authored by Claude Opus 5
parent dba3a30af9
commit 90ce4bcc46
3 changed files with 32 additions and 12 deletions
+1 -1
View File
@@ -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 18). **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
+3 -2
View File
@@ -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;
+28 -9
View File
@@ -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