Files

12 KiB

phase, plan, subsystem, status, tags, requires, provides, affects, tech-stack, key-files, decisions, metrics
phase plan subsystem status tags requires provides affects tech-stack key-files decisions metrics
quick-260731-upz 01 security complete
security
entropy
bip39
seed
psbt
audit
bitcoin
lnd
docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md
docs/security/PSBT-SIGNING-ARCHITECTURE.md
injectable-RNG seam in core/archipelago/src/seed.rs
core/archipelago/src/seed.rs
docs/UNIFIED-TASK-TRACKER.md
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
created modified
docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md
docs/security/PSBT-SIGNING-ARCHITECTURE.md
core/archipelago/src/seed.rs
docs/UNIFIED-TASK-TRACKER.md
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
duration completed
~75min 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:

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:92bip39-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 execs 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.rsgenerate_mnemonic_with<R: CryptoRng + RngCore> 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 exact bytes the test RNG emitted>) — 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 runcargo-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.