--- phase: quick-260731-upz plan: 01 subsystem: security status: complete tags: [security, entropy, bip39, seed, psbt, audit, bitcoin, lnd] requires: [] provides: - docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md - docs/security/PSBT-SIGNING-ARCHITECTURE.md - injectable-RNG seam in core/archipelago/src/seed.rs affects: - core/archipelago/src/seed.rs - docs/UNIFIED-TASK-TRACKER.md tech-stack: added: [] patterns: - "Key-generation entropy source is passed as an argument, never inherited from a dependency default" - "Injection seam + deterministic test RNG as the regression guard for entropy-source rebinding" key-files: created: - docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md - docs/security/PSBT-SIGNING-ARCHITECTURE.md modified: - core/archipelago/src/seed.rs - docs/UNIFIED-TASK-TRACKER.md decisions: - "image-recipe/_archived/ is NOT dead code — build-debian-iso.sh execs it; it is the live ISO builder and therefore in audit scope" - "ARCHY-1 fix applied as an injectable-RNG seam with a known-answer test; no derivation, word-count, passphrase or at-rest-encryption behaviour changed" - "ARCHY-5 refuted as a present defect (32 divides 256, so no modulo bias today) but retained as a latent one" - "PSBT spec ships wsh(sortedmulti) and defers taproot/MuSig2 as UNVERIFIED; BC-UR v2 chosen over BBQr on graceful-degradation grounds" - "Migration section deliberately does NOT tell Archipelago users to rotate seeds — the audit found no entropy defect, and over-alarming has real cost" metrics: duration: ~75min completed: 2026-08-01 --- # Quick Task 260731-upz: Entropy/Seed Audit + PSBT Signing Architecture — Summary Turned the confirmed 2026-07-30 Coinkite COLDCARD low-entropy incident into an evidence-backed audit of Archipelago's own entropy paths, a plannable PSBT-first signing spec, a prioritised remediation backlog wired into the tracker, and one small, test-proven hardening fix to master-seed generation. ## ⚠️ Push deliberately withheld **Nothing was pushed.** Per explicit instruction, the `core/archipelago/src/seed.rs` diff is held for human review before it leaves this machine — it is master-seed generation code for every new node. **Review commands:** ```bash git show 8b51b7e2 # the full seed.rs diff (122 insertions, 2 deletions) git log --oneline -4 # this plan's four commits git show --stat 8b51b7e2 ``` **Commits awaiting review, all on `main`, none pushed:** | Commit | Type | Contents | |---|---|---| | `f11db4ea` | docs | `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` (the audit) | | `5faf1a3c` | docs | `docs/security/PSBT-SIGNING-ARCHITECTURE.md` (the spec) | | `5ba80e49` | docs | Remediation backlog + F-13 + tracker items | | `8b51b7e2` | **fix** | **`core/archipelago/src/seed.rs` — the diff to review** | **What to check in `8b51b7e2`:** that the change is limited to (a) routing mnemonic generation through a helper that takes its RNG as a parameter, with `OsRng` passed at the production call site, and (b) two new tests — and that **no** derivation path, word count, BIP-39 passphrase decision, or at-rest encryption behaviour changed. It does not, but that is the thing worth confirming with your own eyes. ## Headline result **No Coldcard-class entropy defect exists in this codebase.** Every first-party key-generation call site draws from a genuine CSPRNG. There is no Mersenne Twister, no clock-seeded key, no `SmallRng`, no `seed_from_u64`, and no `Math.random()` in any browser key path. The code also does several things better than most implementations (audit §5). **But the audit found something more urgent than anything entropy-related.** ## The Critical finding (F-01) — not what we went looking for `seed.generate` and `seed.restore` are in `UNAUTHENTICATED_METHODS` (`core/archipelago/src/api/rpc/middleware.rs:24-28`), which skips session, RBAC **and** CSRF. Neither handler checks whether onboarding is already complete, and `NodeIdentity::from_seed` (`core/archipelago/src/identity.rs:79-114`) overwrites `node_key`, `nostr_secret` and the FIPS mesh key **unconditionally**. There is no rate limit. The endpoint is proxied to the LAN over plaintext HTTP (`image-recipe/configs/nginx-archipelago.conf:11`, `:165`, `:192`) and mesh peers can reach it too (`core/archipelago/src/server.rs:2080`). **One unauthenticated POST can take over or destroy a live node's identity**, and `seed.restore` lets the attacker choose the mnemonic. The guard already exists and is simply never called — `NodeIdentity::key_exists` (`identity.rs:117`). Surfaced by tracing secret classes (3) and (4) end-to-end rather than only checking where their bits come from. Queued as backlog **R-01** and as a Tier 2 tracker item; it changes an authentication boundary on a live fleet and needs its own phase. ## ARCHY findings — adjudicated | Tag | Verdict | Note | |---|---|---| | **[ARCHY-1]** | **CONFIRMED** | `seed.rs:92` → `bip39-2.1.0/src/lib.rs:311-313` → `:296-298` (`&mut rand::thread_rng()`) → `:267-283`. **FIXED.** | | **[ARCHY-2]** | **CONFIRMED (positive)** | The `GRND_NONBLOCK` probe is used as a probe only; its byte is discarded; no key material comes from it. Better than most. | | **[ARCHY-3]** | **PARTIALLY CONFIRMED** | The feared version does not exist. Three of four sub-questions answered from the tree; the rest is an UNVERIFIED on-node checklist. | | **[ARCHY-4]** | **CONFIRMED, and worse** | Every claim checks out, plus it is an integrity/availability exposure too — that is F-01. | | **[ARCHY-5]** | **REFUTED as a present defect** | `totp.rs:305`'s charset is 32 chars and 32 divides 256, so bias is **zero** today. Latent, not live. Stated plainly rather than dropped. | | Open Q9 | **DIVERGENCE CONFIRMED** | `Argon2::default()` = 19 MiB / t=2 / p=1; ADR-005 says 64 MB / 3. | ## Two findings the research did not predict - **F-03 (High)** — the installed rootfs is a **cached container export shared by every node**, baking SSH host keys and a TLS keypair. Per-device regeneration exists and is correct in intent, but both branches are **fail-open** and `touch "$MARKER"` runs **unconditionally** (`image-recipe/_archived/build-auto-installer-iso.sh:1647`, `:1659`, `:1663`), so one transient failure permanently leaves that node on the image-wide shared keys, visible only in a log file. - **F-13 (High)** — `bitcoin.rs:203` passes `disable_private_keys=false` and `:229-231` imports `wpkh(xprv/...)`, so the BIP-84 account **private** key is persisted in Bitcoin Core's `wallet.dat` (with an empty wallet passphrase) in addition to the Argon2 envelope. The descriptors also carry no key-origin annotation, so no hardware signer could use them. ## Scoping correction worth carrying forward `image-recipe/_archived/` is **not dead code**. `image-recipe/build-debian-iso.sh:19-40` copies `_archived/build-auto-installer-iso.sh` to a temp path, rewrites its relative paths, and `exec`s it. **The "archived" auto-installer is the live ISO builder.** The plan scoped it out; treating it as dead would have made [ARCHY-3] unanswerable and hidden F-03 entirely. ## Deliverables **`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`** — 13 findings, each with severity, `file:line` evidence, exploitability, blast radius and concrete remediation; all five ARCHY tags adjudicated; all six mandated secret classes traced; a 13-item "What we do right" section; a 7-item UNVERIFIED on-node checklist with paste-ready commands; and an R-00…R-15 remediation backlog. **103 `file:line` evidence references** (gate required ≥20). **`docs/security/PSBT-SIGNING-ARCHITECTURE.md`** — watch-only descriptor wallets, the full Core RPC loop with wallet- vs node-scoped RPCs, `analyzepsbt`-driven UI state, Tier 1 single-sig and Tier 2 `wsh(sortedmulti)` on BIP-48, BC-UR v2 vs BBQr vs file transport, the honest LND capability matrix, the hot wallet as an explicitly-secondary tier, migration guidance, and a 7-phase rollout with dependencies and candidate requirements. Cross-links and answers two open items in `docs/hardware-signer-design.md`. **`docs/UNIFIED-TASK-TRACKER.md`** — 9 new items in the file's existing tier/checkbox format: 4 in Tier 0, 3 in Tier 1, 4 in Tier 2 (including the Critical F-01 item and PSBT Phase 1). ## The one code change `core/archipelago/src/seed.rs` — `generate_mnemonic_with` calls bip39's **injectable** `generate_in_with`; `MasterSeed::generate()` passes `OsRng` explicitly. `mnemonic_generation_uses_injected_rng` asserts the result equals `bip39::Mnemonic::from_entropy()` — the direct proof that the **injected** RNG, not bip39's transitive default, is the one consumed — plus a known-answer pin and a determinism check. **This test cannot be written against the previous code**, because `Mnemonic::generate(24)` exposes no seam. **Verified:** `CARGO_INCREMENTAL=0 cargo test -p archipelago seed::` → **25 passed, 0 failed** (23 pre-existing + 2 new). **Honest limitation, recorded in the audit:** this removes a *future* failure mode. It does not retroactively change seeds generated before it, which came from `rand::thread_rng()` — a genuine CSPRNG, so nothing is weakened, but their guarantee rests on `rand 0.8.5`'s behaviour rather than on this call site. ## Deviations from plan 1. **`image-recipe/_archived/` brought into scope** (plan said excluded). Justified above; documented in the audit's §1 so the next auditor does not re-derive it. 2. **F-13 added to the audit during Task 3.** Discovered while reading `bitcoin.rs` for the PSBT spec. It belongs to secret class (1), which Task 1 was required to trace, so it was written up rather than left in the spec alone. 3. **R-12 (`totp.rs` modulo bias) NOT applied**, though the plan permitted it. `[ARCHY-5]` was refuted as a present defect — 32 divides 256, so there is no bias today. Changing working crypto code for a latent-only issue did not meet the plan's "small and obviously correct" bar during a security-sensitive pass. Queued as R-12. 4. **`cargo audit` not run** — `cargo-audit` is not installed. Recorded as gap F-07 with CI remediation R-05 rather than silently skipped. ## Not done, deliberately - **No push, no tag, no deploy** (see the banner above). - **No PSBT/watch-only/multisig implementation** — the spec is a spec. - **`core/archipelago/src/container/secrets.rs` untouched** (backlog R-13) — it carried another agent's uncommitted work. Read-only for the audit, as required. ## Concurrent-agent hygiene All four commits verified against the forbidden-path list: **no commit authored by this plan contains any of the other agents' files.** Every commit staged by explicit path; no `git add -A`, no `git add .`, no `git commit -a`. The submodule guard (`indeedhub`) ran before each commit and passed. Their uncommitted work (`ScreensaverRing.vue`, `SendBitcoinModal.vue`, `WalletScanModal.vue`, and the earlier set) is intact. ## Self-Check: PASSED - `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` — FOUND - `docs/security/PSBT-SIGNING-ARCHITECTURE.md` — FOUND - `core/archipelago/src/seed.rs` — modified, tests green - Commits `f11db4ea`, `5faf1a3c`, `5ba80e49`, `8b51b7e2` — all FOUND in `git log` - Task 1 verify gate — OK (103 evidence refs, all required tokens present, no secret-shaped strings) - Task 2 verify gate — OK (all 12 required tokens present, no secret-shaped strings) - Task 3 verify gate — OK (backlog present, both tracker links present, no forbidden paths in any of the four commits) - No real secret value appears in any produced document — verified by pattern scan on both.