From 0c63a8518cf22eef424d4f8365bb0ef3d167a95f Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 31 Jul 2026 22:29:23 -0400 Subject: [PATCH] docs(quick-260731-upz): plan entropy/seed audit + PSBT signing architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following the confirmed 2026-07-30 Coinkite COLDCARD low-entropy incident, plan three deliverables as a single 3-task quick plan: 1. docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md — evidence-backed audit of every key-material path (Rust/TS/shell), adjudicating research findings [ARCHY-1]..[ARCHY-4] with file:line evidence, incl. the one-ISO-many-nodes correlation risk and an explicit UNVERIFIED on-node checklist. 2. docs/security/PSBT-SIGNING-ARCHITECTURE.md — descriptor watch-only, wsh(sortedmulti) multisig, air-gap transport, honest LND limits (channel/revocation/HTLC keys cannot be air-gapped), hot wallet as explicitly secondary, migration path, phased rollout. 3. Remediation backlog into docs/UNIFIED-TASK-TRACKER.md + one gated hardening fix (explicit-OsRng injection at the mnemonic call site) proven by a known-answer test that cannot exist before the change. Audit-and-spec only — no wallet/signing implementation. Verify gates enforce file:line evidence density and a secret-shaped-string check on both docs, and assert no commit authored by this plan touches the concurrent agent's files. Co-Authored-By: Claude Opus 5 (1M context) --- .../260731-upz-PLAN.md | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 .planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-PLAN.md diff --git a/.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-PLAN.md b/.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-PLAN.md new file mode 100644 index 00000000..16ce4002 --- /dev/null +++ b/.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-PLAN.md @@ -0,0 +1,440 @@ +--- +phase: quick-260731-upz +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md + - docs/security/PSBT-SIGNING-ARCHITECTURE.md + - docs/UNIFIED-TASK-TRACKER.md + - core/archipelago/src/seed.rs +autonomous: false +requirements: [QUICK-UPZ-01, QUICK-UPZ-02, QUICK-UPZ-03] + +must_haves: + truths: + - "docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md exists and every single finding carries file:line evidence from the real tree — no finding is asserted from the research file alone (QUICK-UPZ-01)" + - "Each of [ARCHY-1] [ARCHY-2] [ARCHY-3] [ARCHY-4] is explicitly CONFIRMED, REFUTED, or UNVERIFIED against real code, and a refuted finding says so plainly rather than being quietly dropped (QUICK-UPZ-01)" + - "The audit covers all six mandated secret classes: master BIP-39 seed, LND aezeed, fleet release-root + catalog/manifest signing keys, node identity keys (nostr/FIPS/Reticulum), container::secrets generated_secrets, and session tokens/CSRF (QUICK-UPZ-01)" + - "[ARCHY-3] (one-ISO-many-nodes correlation) is answered from image-recipe/ evidence where the tree can answer it, and everything the tree cannot answer is listed in a separate, explicitly-labelled on-node verification checklist marked UNVERIFIED — never asserted as verified (QUICK-UPZ-01)" + - "Every finding has Severity (Critical/High/Medium/Low/Informational) + evidence + exploitability + blast radius + concrete remediation, and there is a 'What we do right' section (QUICK-UPZ-01)" + - "docs/security/PSBT-SIGNING-ARCHITECTURE.md exists and covers: descriptor-only watch-only wallet, the create->export->sign->import->finalize->broadcast loop with named Core RPCs, single-sig-HW and wsh(sortedmulti) multisig tiers, air-gap transport choice, LND's honest limits, hot wallet as explicitly-secondary, migration for existing hot-seed users, and a phased rollout that a future /gsd-plan-phase can consume (QUICK-UPZ-02)" + - "The PSBT spec states plainly that a routing node's channel/revocation/HTLC keys cannot be air-gapped, and splits the design into on-chain (PSBT-protectable) vs lightning (necessarily hot) (QUICK-UPZ-02)" + - "No produced document contains any real secret value — no mnemonic words, no private keys, no tokens, no passwords; secrets are referenced by path/variable name only (QUICK-UPZ-01, QUICK-UPZ-02, QUICK-UPZ-03)" + - "A prioritised remediation backlog exists in the audit doc and the resulting open items appear in docs/UNIFIED-TASK-TRACKER.md in that file's existing tier/checkbox format (QUICK-UPZ-03)" + - "If [ARCHY-1] is confirmed, the mnemonic-generation call site takes its RNG as an injected parameter (OsRng in production) and a test proves the injected RNG is the one actually used — a test that cannot exist before the change; if it is not applied, the audit records why in a greppable line (QUICK-UPZ-03)" + - "The other agent's work is untouched: container/secrets.rs, federation/storage.rs, pip.ts, Cloud.vue, OnboardingSeedGenerate.vue and the new composables/ files are read-only in this plan, and no commit authored by this plan contains any of those paths" + artifacts: + - "docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md (evidence-backed audit + prioritised remediation backlog + on-node verification checklist)" + - "docs/security/PSBT-SIGNING-ARCHITECTURE.md (watch-only/multisig/air-gap/LND spec + phased rollout)" + - "docs/UNIFIED-TASK-TRACKER.md (updated with the resulting open items)" + - "core/archipelago/src/seed.rs + its test module (ONLY if [ARCHY-1] is confirmed and the fix is small/obviously-correct/testable)" + key_links: + - "core/archipelago/src/seed.rs is the fan-out root — every other key class (node Ed25519 did:key, nostr, FIPS, release-root signing, per-identity keys, BIP-84 bitcoin, LND aezeed via HKDF) descends from the one mnemonic, so a defect there has strictly larger blast radius than a hardware wallet's" + - "bip39 2.1.0's Mnemonic::generate -> generate_in -> generate_in_with(&mut rand::thread_rng(), ...) is the transitive-default hop that makes the entropy source implicit at Archipelago's call site — this is the exact structural shape of the COLDCARD defect (T1)" + - "docs/hardware-signer-design.md already exists (TROPIC01 air-gapped signer, exploratory stub) — the PSBT spec must cross-link it as the future first-party signer, not duplicate or contradict it" + - "image-recipe/build-debian-iso.sh + image-recipe/archipelago-scripts/install-to-disk.sh + image-recipe/configs/*.service are the only places that can bake a random-seed or order key generation against crng init — they are the whole [ARCHY-3] evidence surface in-tree" +--- + + +Turn the confirmed 2026-07-30 Coinkite COLDCARD low-entropy incident into three concrete +Archipelago artifacts: + +1. **A real entropy & seed-generation security audit** of this codebase (`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`) — + performed against actual code with `file:line` evidence, not a restatement of the research. +2. **A PSBT-first signing architecture spec** (`docs/security/PSBT-SIGNING-ARCHITECTURE.md`) — + watch-only descriptors, multisig, air-gap transport, honest LND limits, hot wallet as an + explicitly-secondary tier, and a phased rollout a future `/gsd-plan-phase` can consume. +3. **A prioritised remediation backlog** wired into `docs/UNIFIED-TASK-TRACKER.md`, plus — only + if the audit proves it necessary — one small, obviously-correct, independently-tested + hardening fix. + +Purpose: Archipelago derives its **entire key hierarchy from one 24-word BIP-39 mnemonic**, +including the **fleet release-root signing key**. A Coldcard-class entropy defect here would not +just drain wallets, it would let an attacker forge signed release manifests for the whole fleet. +The research found no such defect today — but it did find the *structural shape* that produced +T1, and it flagged the ISO/first-boot entropy story as the single most plausible real exposure. + +Output: 3 commits on `main` (audit doc, spec doc, backlog + optional fix), pushed via `gitea-ai`. + +**This is an audit-and-spec task, not a feature build.** Do NOT implement PSBT, watch-only, +multisig, or any wallet/signing behaviour. Do NOT refactor beyond the single gated fix in Task 3. + +**Tracer-first decomposition deliberately does not apply here** — there are no layers to slice +through; this is a deliverable set. Task 1 is the load-bearing evidence pass and Tasks 2 and 3 +strictly depend on its findings. Execute the three tasks **in order**. + + + +@/home/archipelago/Projects/archy/CLAUDE.md +@/home/archipelago/Projects/archy/.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-RESEARCH.md + +**Read the RESEARCH.md above IN FULL before starting.** It contains the confirmed incident +analysis, the historical low-entropy catalogue (T1–T7), the greppable audit checklist with +exact dangerous/correct API names, findings [ARCHY-1]…[ARCHY-5], and the LND capability matrix. +Use it to aim the audit and to justify the spec's technical choices — do **not** re-derive it, +and do **not** copy it wholesale into the deliverables. + +## Facts already established — do not re-derive + +- **The RNG-touching surface in this tree is bounded and already enumerated.** 33 Rust files + under `core/*/src` and 7 TypeScript/Vue files under `neode-ui/src` match the RNG API set. + Task 1's grep pipeline reproduces exactly this set. There is no need to read the whole repo. +- **Dependency versions** (`core/archipelago/Cargo.toml`): `rand = "0.8.5"` (still has fork + protection; 0.9.0 removed it), `bip39 = { version = "=2.1.0", features = ["rand"] }` + (2.2.2 is current — do not bump in this task), `argon2 = "0.5.3"`, `zeroize = "1.8.2"`. +- **`docs/security/` does not exist yet** — create it. +- **`docs/hardware-signer-design.md` already exists** (171 lines, 2026-06-24, exploratory + TROPIC01 air-gapped-signer stub). Task 2 must cross-link and stay consistent with it. +- **`docs/UNIFIED-TASK-TRACKER.md`** is 268 lines, organised as `## Tier 0 — Quick / mechanical, + no blockers`, `## Tier 1 — Medium effort, unblocked`, etc., with `- [ ]` / `- [x] ~~struck~~` + items, ordered fastest/simplest first. Match that format exactly. +- **`image-recipe/_archived/` is out of audit scope** (dead auto-installer path). Note it as + explicitly excluded in the audit doc so the next auditor does not re-derive that. +- **`core/archipelago/src/seed.rs` is CLEAN** in git (safe to edit in Task 3). Its test module + starts at line 479. + +## CONCURRENT-AGENT HAZARD — read carefully + +Another agent has uncommitted work in this shared tree. These files are **dirty**: + +``` +core/archipelago/src/container/secrets.rs <- IN AUDIT SCOPE, read-only +core/archipelago/src/federation/storage.rs +neode-ui/src/utils/pip.ts +neode-ui/src/views/Cloud.vue +neode-ui/src/views/OnboardingSeedGenerate.vue <- IN AUDIT SCOPE, read-only +neode-ui/src/composables/usePaidItemViewer.ts (untracked) +neode-ui/src/composables/usePipSession.ts (untracked) +neode-ui/src/composables/__tests__/*.test.ts (untracked) +``` + +`container/secrets.rs` and `OnboardingSeedGenerate.vue` are **both in audit scope AND dirty**. +Read them **as they are on disk**. Do NOT modify them, do NOT revert them, do NOT `git stash`, +do NOT `git checkout` them. If the audit finds something in them, write it up as a finding with +a note that the file had uncommitted third-party changes at audit time. + +**That agent is committing to these paths live** (`4b5367eb` federation/storage.rs, `bc9a210c` +Cloud.vue, `3288a02d` pip.ts all landed after this plan was written). So expect the tree and the +log to move underneath you. That is normal and is not your problem to fix. The invariant you owe +is narrow and absolute: **no commit you author may contain any of those paths.** Do not rebase, +do not reset, do not revert their commits, and re-read a file rather than trusting a stale read +if you see it change. + +**Staging rule (CLAUDE.md, non-negotiable):** always `git add `. Never +`git add -A`, never `git add .`, never `git commit -a`. + +## Honesty requirements — apply to all three tasks + +- **Never claim a path is safe without `file:line` evidence.** "I grepped and found nothing" is + a valid finding only if you state the grep and the directories it covered. +- **Anything that cannot be verified from this environment is `UNVERIFIED`**, listed in the + on-node verification checklist, never asserted as checked. Real hardware (`.228` / dev-box) + is not reachable from this task. +- **Never put a real secret into a document.** Reference the path or variable name + (`master_seed.enc`, `STRIPE_SECRET_KEY`), never a value. This includes example/illustrative + mnemonics — use `<24 words>` or `word1 … word24` placeholders, never a real wordlist. +- Mark research-derived claims that you could not confirm in-tree as `[FROM RESEARCH, + NOT RE-VERIFIED]` rather than laundering them into audit findings. + + + + + + Task 1: Entropy & seed-generation security audit against the real codebase + docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md + + core/archipelago/src/seed.rs (full — the fan-out root), + core/archipelago/src/api/rpc/seed_rpc.rs (the network boundary, [ARCHY-4]), + core/archipelago/src/container/secrets.rs (DIRTY — read only), + neode-ui/src/views/OnboardingSeedGenerate.vue (DIRTY — read only), + image-recipe/build-debian-iso.sh + image-recipe/archipelago-scripts/install-to-disk.sh ([ARCHY-3]), + docs/adr/ (for the ADR-005 Argon2 parameter cross-check) + + +Perform a real audit and write it to `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` (create +`docs/security/` first). Do the evidence collection with the bounded pipeline below FIRST, then +write — do not write findings before you have their line numbers. + +**Step A — collect evidence (bounded; ~10 bash calls, do not wander outside these paths).** +Run these and keep the output as your evidence corpus. `core/*/src` deliberately excludes +`core/target/`; exclude `image-recipe/_archived/` from conclusions: + +``` +grep -rnE 'SmallRng|seed_from_u64|::from_seed\(|rand::rngs::mock|StdRng' core/*/src --include=*.rs +grep -rnE 'OsRng|thread_rng|rand::random|getrandom|SystemRandom' core/*/src --include=*.rs +grep -rn -B3 -A3 -E 'SystemTime::now|as_nanos|Instant::now' core/*/src --include=*.rs | grep -iE 'key|seed|nonce|salt|token|secret|password|mnemonic' +grep -rn -B2 -A2 -E 'Math\.random|getRandomValues|crypto\.subtle|jsbn|SecureRandom\(' neode-ui/src --include=*.ts --include=*.vue +grep -rnE '\$RANDOM|/dev/urandom|/dev/random|openssl rand|uuidgen|random\.random|random\.randint|shuf ' scripts/ image-recipe/ --include=*.sh --include=*.py +grep -rniE 'random-seed|urandom|jitterentropy|haveged|rng-tools|rngd|crng' image-recipe/ --include=*.sh --include=*.service --include=*.conf +find image-recipe -name 'random-seed' -o -name '*.seed' +grep -rniE '(info|warn|error|debug|trace)!\(.*(mnemonic|seed|privkey|private_key|passphrase|aezeed)' core/*/src --include=*.rs +grep -rn 'derive(Debug' core/archipelago/src/seed.rs core/archipelago/src/identity.rs core/archipelago/src/credentials/store.rs +cd core && cargo tree -i rand | head -40 && cargo tree -i getrandom | head -40 +``` +Run `cargo audit` only if `command -v cargo-audit` succeeds; if absent, record that as a gap +(the research explicitly recommends `cargo audit`/`cargo deny` in CI rather than a snapshot). + +**Step B — trace every secret class.** For each of these, trace from the syscall to the consumer +and record `file:line` at every hop. Any hop where the entropy source is a *default* rather than +an *argument* is a T1-shaped structural risk and must be called out as such: +(1) user Bitcoin/LND wallet seed; (2) LND aezeed (`HKDF(seed, "archipelago/lnd/entropy/v1")`); +(3) **fleet release-root signing key** + catalog/manifest signing keys; (4) node identity keys +(nostr / FIPS / Reticulum); (5) `container::secrets` `generated_secrets` materialisation +(0600/rootless per CLAUDE.md — verify the mode is actually set, don't assume); +(6) session tokens + CSRF; (7) onboarding seed generation in the UI. + +**Step C — adjudicate [ARCHY-1] … [ARCHY-4] individually.** Each gets its own subsection headed +with the tag and a verdict of exactly `CONFIRMED`, `REFUTED`, or `UNVERIFIED`: +- **[ARCHY-1]** — check `core/archipelago/src/seed.rs` around line 92 for the `bip39::Mnemonic::generate(24)` + call, then confirm the transitive default by reading the vendored crate at + `~/.cargo/registry/src/*/bip39-2.1.0/src/lib.rs` (research cites line 297). State whether the + entropy source is chosen at the call site or by the dependency. +- **[ARCHY-2]** — verify the `kernel_csprng_ready()` probe near `seed.rs:52-91` really uses the + nonblocking flag *as a probe only* and that no key material is drawn from that path. +- **[ARCHY-3]** — the highest-unknown item. From `image-recipe/` evidence answer: does the build + bake a populated seed file into the image; is there any first-boot regeneration unit; does the + image install `jitterentropy-rngd`/`haveged`/`rng-tools`; can onboarding key generation run + before the kernel CSPRNG is initialised on freshly-flashed hardware. Answer what the tree can + answer with `file:line`. Everything else goes to the on-node checklist as `UNVERIFIED`. +- **[ARCHY-4]** — confirm whether the generated mnemonic crosses the JSON-RPC boundary + (`seed_rpc.rs` ~line 147), the in-memory TTL and whether it is cleared at verify time + (~lines 205-209), and whether the daemon can be served over plaintext HTTP. + +If a research finding does not survive contact with the code, write `REFUTED` and say why +plainly. Do not soften it. Also cross-check open question 9: whether `Argon2::default()` in +`seed.rs` matches ADR-005's stated 64MB/3-iteration profile — report the actual numbers. + +**Step D — write the document.** Structure: +1. Scope + method (directories covered, greps run, what was explicitly excluded and why — + name `image-recipe/_archived/` and `core/target/`). +2. Executive summary — the honest one-paragraph verdict. +3. Findings table, then one subsection per finding. Every finding carries: + `Severity` (Critical/High/Medium/Low/Informational) | Evidence (`file:line`) | Exploitability | + Blast radius | Concrete remediation. +4. `[ARCHY-1]`…`[ARCHY-4]` adjudication (Step C). +5. **What we do right** — a real section, giving credit where the code is correct + (zeroization, encrypted-at-rest envelope, the CSPRNG-readiness probe, 24-word enforcement, + the correct browser RNG call sites — each with `file:line`). +6. **On-node verification checklist (UNVERIFIED)** — the discrete checks that need real hardware, + each written as a runnable command an operator can paste on `.228`/dev-box, including the + cross-node same-ISO collision test from the research. +7. Leave a placeholder heading `## Remediation Backlog` — Task 3 fills it. + +Severity must reflect *this* codebase, not the Coldcard incident. Do not inflate: a benign +`Math.random()` that only picks which word to quiz is Low or Informational, and the audit should +say so and annotate it so the next auditor does not re-derive that it is benign. + +Do not put any secret value in the document (see the secret-shaped-string gate in `verify`). +Commit with `git add docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` only. + + + AUD=docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md; test -f "$AUD" || { echo "MISSING FILE"; exit 1; }; for t in ARCHY-1 ARCHY-2 ARCHY-3 ARCHY-4 Severity "What we do right" UNVERIFIED; do grep -qi "$t" "$AUD" || { echo "MISSING: $t"; exit 1; }; done; n=$(grep -cE '\.(rs|vue|ts|sh|py|yml|toml):[0-9]+' "$AUD"); [ "$n" -ge 20 ] || { echo "FAIL: only $n file:line evidence refs, need >=20"; exit 1; }; grep -qE '(xprv|xpub|nsec|npub)[A-Za-z0-9]{40,}|BEGIN [A-Z ]*PRIVATE KEY' "$AUD" && { echo "FAIL: secret-shaped string in audit doc"; exit 1; }; echo OK + + `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` exists; every finding has `file:line` evidence and a severity; all four ARCHY tags carry an explicit CONFIRMED/REFUTED/UNVERIFIED verdict; all six mandated secret classes are traced; the on-node checklist exists and is labelled UNVERIFIED; a "What we do right" section exists; no secret value appears anywhere; committed with explicit paths. + + + + Task 2: PSBT / watch-only / multisig architecture spec + docs/security/PSBT-SIGNING-ARCHITECTURE.md + + docs/hardware-signer-design.md (existing TROPIC01 signer stub — cross-link, do not duplicate), + core/archipelago/src/api/rpc/bitcoin.rs (integration point), + core/archipelago/src/seed.rs lines ~214-233 (BIP-84 derivation + LND aezeed HKDF), + apps/bitcoin-core/manifest.yml + apps/bitcoin-knots/manifest.yml + apps/lnd/manifest.yml (pinned versions), + the RESEARCH.md Part C tables (Core RPCs, LND capability matrix, air-gap formats) + + +Write `docs/security/PSBT-SIGNING-ARCHITECTURE.md` — a spec, not a tutorial, and not an +implementation. Ground every architectural claim in either RESEARCH.md Part C or a `file:line` +from this tree; mark anything from neither as `[UNVERIFIED]`. + +Required sections: + +1. **Target architecture.** Watch-only **descriptor** wallet on the node (Core), created with + private keys disabled and populated via descriptor import — descriptor-only from day one, + because Core 30 removed BDB legacy wallets. Name the actual Core RPCs for each step of the + loop (create funded PSBT -> export -> sign offline -> import -> combine -> finalize -> + broadcast) and say which RPCs are wallet-scoped vs node-scoped. Recommend driving UI state + from `analyzepsbt` (it reports which role must act next) rather than guessing. Record the + current versions the node actually runs from the manifests, and flag `bitcoin-knots:latest` + as an unpinned tag at odds with ADR-009 (in-scope to flag, out-of-scope to fix). + +2. **Where each step lives.** Map the loop across the three surfaces: Rust orchestrator + (`core/archipelago`), `neode-ui`, and the companion app. Be explicit that the BIP-84 private + key stays in the daemon's encrypted store and the **xpub only** goes into the Core descriptor + wallet — the private key is never imported into Core. + +3. **Tiers.** Tier 1 single-sig with an external hardware signer; Tier 2 `wsh(sortedmulti(k,...))` + multisig with BIP-48 paths (`m/48'/coin'/account'/2'` for P2WSH) and descriptor exchange. + State why `sortedmulti` over ordered `multi`. Mandate key-origin annotation + `[fingerprint/derivation]` — hardware signers cannot locate their key without it. Treat + taproot/MuSig2 multisig as future work and say why (unconfirmed 2026 support). + +4. **Air-gapped transport.** Pick a format and justify it against the companion app's *existing* + QR scanner and shipped SeedQR capability. Compare BBQr (sequential) vs BC-UR v2 (fountain-coded, + order-independent, degrades gracefully in poor light) vs microSD/file. Be honest about QR + density: a real multi-input multisig PSBT exceeds single-QR capacity, so animated multi-frame + is mandatory and a file fallback must always be offered. Cross-link + `docs/hardware-signer-design.md` as the future first-party signer and keep the format choice + consistent with it. + +5. **LND — what is and is not achievable.** Reproduce the capability matrix as a decision table: + watch-only + remote signer YES; signer fully offline NO (it must accept a live inbound gRPC + connection); air-gapping channel/revocation/HTLC keys NO; PSBT channel funding YES; opening a + channel with zero LND wallet balance YES; self-broadcasting the funding transaction NEVER + (encode that as a hard UI rule — funds can be lost). Name the required xpub accounts and the + taproot import gotcha. Then split the whole design into **on-chain balance: genuinely + PSBT-protectable** vs **lightning balance: necessarily hot**, and give the exact honest + user-facing sentence the UI should use. Any copy implying a routing node's channel keys are + cold is misleading — say so. + +6. **Hot wallet as the explicitly-secondary option.** Hard separation of on-chain and Lightning + balances in the data model and the UI (never one blended number); server-enforced per-tx and + rolling-daily spend limits with anything above forced onto the PSBT path; reuse of the + existing at-rest encryption envelope; zeroization; and the explicit cold/warm/hot tiering in + the UI. State the design principle from the incident: T1's survivors were the users who took + the *optional* extra step, so the safe path must be the **default**, not the option. Also + spell out how to nudge toward PSBT without making the hot path feel broken or punitive. + +7. **Migration for existing hot-seed users.** The honest advice implied by the incident: a + software fix does not repair an already-generated seed. Specify the sequence — generate a new + key, verify the backup and a receive address, send a test transaction, migrate funds, retain + the old backup until confirmed — and state clearly which Archipelago users this does and does + not apply to based on Task 1's findings (do not over-alarm if Task 1 found no defect; do not + under-state if it did). + +8. **Phased rollout.** Concrete, plannable phases with dependencies and what each unlocks. This + document is the input to a future `/gsd-plan-phase`, so each phase needs a name, a goal + sentence, its dependencies, and 2-5 candidate requirement lines. Suggested shape (adjust with + reasoning): descriptor watch-only read path -> PSBT construct/export -> external-signer import + and finalize -> air-gap transport -> multisig -> LND remote signing -> hot-wallet limits. + Note explicitly which phases need real-hardware verification. + +Do NOT write implementation code. Do NOT add dependencies. Do NOT modify wallet or signing +behaviour anywhere in the tree. Commit with `git add docs/security/PSBT-SIGNING-ARCHITECTURE.md` +only. + + + SPEC=docs/security/PSBT-SIGNING-ARCHITECTURE.md; test -f "$SPEC" || { echo "MISSING FILE"; exit 1; }; for t in walletcreatefundedpsbt analyzepsbt finalizepsbt importdescriptors disable_private_keys sortedmulti BIP-48 remotesigner "necessarily hot" migration Phase hardware-signer-design; do grep -qi "$t" "$SPEC" || { echo "MISSING: $t"; exit 1; }; done; grep -qE '(xprv|xpub|nsec|npub)[A-Za-z0-9]{40,}|BEGIN [A-Z ]*PRIVATE KEY' "$SPEC" && { echo "FAIL: secret-shaped string in spec"; exit 1; }; echo OK + + `docs/security/PSBT-SIGNING-ARCHITECTURE.md` exists covering all eight required sections; the LND section states plainly that channel/revocation/HTLC keys cannot be air-gapped and that the funding transaction must never be self-broadcast; on-chain vs lightning are split into separately-protectable tiers; the hot wallet is framed as explicitly secondary; the rollout is phased with dependencies and candidate requirements; `docs/hardware-signer-design.md` is cross-linked; no code changed; committed with explicit paths. + + + + Task 3: Remediation backlog, tracker update, and the one gated hardening fix + docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md, docs/UNIFIED-TASK-TRACKER.md, core/archipelago/src/seed.rs + + Only if [ARCHY-1] was CONFIRMED in Task 1, the mnemonic-generation path becomes RNG-injectable + and the following test exists and passes (it cannot exist before the change, because there is + no seam to inject through): + - `mnemonic_generation_uses_injected_rng`: driving generation with a deterministic + `CryptoRng + RngCore` test RNG produces a stable, asserted 24-word mnemonic (known-answer), + proving the passed RNG — not an implicit transitive default — is the one actually consumed. + - `mnemonic_generation_is_256_bit`: generated mnemonics are 24 words and two successive + productions from the real entropy source differ. + Existing seed tests stay green. + + +**Part A — remediation backlog (always).** Fill the `## Remediation Backlog` placeholder left in +`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`. Prioritise by severity x effort, fastest/most +valuable first. Each item needs: the finding it closes, the concrete change, the file(s), an +effort estimate, and whether it needs real-hardware verification. Anything that needs a proper +phase (PSBT work, ISO first-boot entropy regeneration, confining seed-bearing RPCs to +loopback/TLS) is listed here as a backlog item and explicitly **not** implemented in this task. + +**Part B — tracker (always).** Add the resulting open items to `docs/UNIFIED-TASK-TRACKER.md` +using that file's existing conventions: `- [ ]` checkboxes, bold lead sentence, indented +continuation lines, placed in the correct existing Tier (Tier 0 = quick/mechanical/no blockers, +Tier 1 = medium effort/unblocked, etc.) rather than in a new section at the top. Link back to +`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` and `docs/security/PSBT-SIGNING-ARCHITECTURE.md` +so the tracker stays the single entry point. Use `Edit` for scoped insertions — never rewrite the +whole file. + +**Part C — the one gated fix (conditional).** Apply this **only if Task 1 recorded [ARCHY-1] as +CONFIRMED**. It is the only code change authorised by this plan. + +Refactor `core/archipelago/src/seed.rs` so the mnemonic-generation call site takes its RNG as an +**injected parameter** instead of inheriting a transitive dependency's default: an internal +helper that accepts `&mut (impl CryptoRng + RngCore)` and generates a 24-word English mnemonic +through the injectable `bip39` entry point, with the production caller passing `OsRng`. Add a +comment at the call site pinning the rationale (a bare reference to this audit and to T1 — an +explicit source beats an implicit one, and a future `rand`/`bip39` bump must not silently rebind +it). Then add the two tests from `` to the existing test module (`seed.rs:479`). The +known-answer test is what makes this a fix rather than a comment: it is impossible to write +against the pre-refactor code because there is no seam to inject through. + +Do **not** bump `bip39` or `rand` versions. Do **not** change the derivation, the word count, +the empty-passphrase decision, the at-rest encryption, or anything else in `seed.rs`. + +You MAY additionally fix the `core/archipelago/src/totp.rs` modulo bias (rejection sampling or +`SliceRandom::choose` in place of `% charset.len()`) **only if** it is a <=10-line change with its +own test and Task 1 confirmed it. Anything beyond these two goes to the backlog. If neither is +applied, write a greppable line `ARCHY-1: NOT APPLIED` into the audit doc with the reason. + +Build and test from `core/` (workspace root). If the build hits a `rust-lld: undefined hidden +symbol` error, that is incremental-cache corruption — rebuild with `CARGO_INCREMENTAL=0`. + +**Part D — commit and push.** Commit Part A+B together and Part C separately (if applied), always +with explicit paths: `git add docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md docs/UNIFIED-TASK-TRACKER.md` +then `git add core/archipelago/src/seed.rs`. Never `git add -A`. Then push all three commits with +`git push gitea-ai main` (`main` is protected; `gitea-ai` is the push account). If the push fails, +report the exact error — do not force-push, do not retarget another remote, do not amend history. + + + AUD=docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md; grep -qi "Remediation Backlog" "$AUD" || { echo "FAIL: no remediation backlog"; exit 1; }; grep -q "ENTROPY-SEED-AUDIT-2026-07-31" docs/UNIFIED-TASK-TRACKER.md || { echo "FAIL: tracker missing audit link"; exit 1; }; grep -q "PSBT-SIGNING-ARCHITECTURE" docs/UNIFIED-TASK-TRACKER.md || { echo "FAIL: tracker missing spec link"; exit 1; }; FORBID='container/secrets\.rs|federation/storage\.rs|OnboardingSeedGenerate\.vue|utils/pip\.ts|views/Cloud\.vue'; for c in $(git log --format=%H -10 -- "$AUD" docs/UNIFIED-TASK-TRACKER.md core/archipelago/src/seed.rs core/archipelago/src/totp.rs); do git show --name-only --pretty=format: "$c" | grep -qE "$FORBID" && { echo "FAIL: commit $c mixed in the other agent's files"; exit 1; }; done; if grep -q "ARCHY-1: NOT APPLIED" "$AUD"; then echo "OK (fix deliberately not applied, reason recorded)"; else ( cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago mnemonic_generation_uses_injected_rng 2>&1 | grep -qE 'test result: ok\. [1-9]' ) || { echo "FAIL: injected-rng known-answer test missing or failing"; exit 1; }; ( cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago seed:: 2>&1 | grep -q 'test result: ok' ) || { echo "FAIL: existing seed tests not green"; exit 1; }; echo OK; fi + Show the reviewer the full `git diff` of `core/archipelago/src/seed.rs` (and `totp.rs` if touched) BEFORE pushing, plus the output of `git log --oneline -3` and `git show --stat HEAD`. This is master-seed generation code for every new node — get explicit confirmation that the diff is limited to making the RNG explicit + adding tests, and that no derivation, word count, passphrase, or at-rest-encryption behaviour changed. If [ARCHY-1] was not applied, show the recorded reason instead and confirm that is the right call. + + The audit doc has a prioritised, actionable remediation backlog; `docs/UNIFIED-TASK-TRACKER.md` carries the new open items in its existing tier/checkbox format and links both new docs; either the injectable-RNG fix is applied in `seed.rs` with a known-answer test that passes and could not exist before the change, or `ARCHY-1: NOT APPLIED` plus a reason is recorded; no commit authored by this plan contains any of the other agent's five files; all commits staged by explicit path; the `seed.rs` diff was human-reviewed before push; commits pushed via `gitea-ai`. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| kernel CSPRNG -> userspace key generation | every secret in the system crosses here; a weak or unready source here is unrecoverable | +| daemon -> JSON-RPC/websocket client | the master mnemonic currently crosses this boundary ([ARCHY-4]); plaintext HTTP is in use on LAN in places | +| build host -> flashed ISO -> N nodes | a single image is written to many nodes; anything entropy-bearing baked in is shared fleet-wide ([ARCHY-3]) | +| this task -> committed documentation | audit/spec artifacts are public-facing repo content and could leak secrets or false assurance | +| this task -> shared working tree | a concurrent agent's uncommitted work can be clobbered by careless staging | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-UPZ-01 | Information Disclosure | produced audit/spec docs | critical | mitigate | No secret values in any doc — paths/variable names only; enforced by the secret-shaped-string negative gate in Tasks 1 and 2 `` | +| T-UPZ-02 | Repudiation / false assurance | audit findings without evidence | high | mitigate | Every finding requires `file:line`; `>=20` evidence refs enforced by Task 1 ``; unverifiable items forced into an `UNVERIFIED` on-node checklist | +| T-UPZ-03 | Tampering | `core/archipelago/src/seed.rs` (master-seed generation for every node) | critical | mitigate | Fix is gated on [ARCHY-1] being CONFIRMED, bounded to RNG injection + tests, proven by a known-answer test, and human-reviewed via `` before push | +| T-UPZ-04 | Tampering | concurrent agent's uncommitted work in the shared tree | high | mitigate | Explicit-path staging only (never `git add -A`); dirty files are read-only; Task 3 `` asserts the five dirty files are still dirty | +| T-UPZ-05 | Information Disclosure | master mnemonic over plaintext-HTTP JSON-RPC ([ARCHY-4]) | high | transfer | Audited and written up with remediation (loopback/TLS confinement, shorter TTL); implementation deferred to a proper phase, not done here | +| T-UPZ-06 | Spoofing | fleet release-root signing key derived from the same mnemonic | critical | mitigate | Explicitly traced as a first-class secret class in Task 1 Step B — a seed defect forges release manifests fleet-wide, strictly larger blast radius than a wallet | +| T-UPZ-07 | Elevation of Privilege | one-ISO-many-nodes entropy correlation ([ARCHY-3]) | high | mitigate | `image-recipe/` evidence grep answers what the tree can answer; the rest becomes a runnable on-node checklist including the cross-node same-ISO collision test | +| T-UPZ-SC | Tampering | package installs | low | accept | This plan adds no dependencies and installs nothing; `cargo audit` is invoked read-only if already present | + + + +1. Both documents exist under `docs/security/` and pass their automated gates. +2. Every audit finding carries `file:line` evidence and a severity; all four ARCHY tags are adjudicated. +3. Nothing that requires real hardware is claimed as verified — it appears in the UNVERIFIED on-node checklist instead. +4. No secret value appears in any produced document. +5. `docs/UNIFIED-TASK-TRACKER.md` carries the new open items in its existing format and links both docs. +6. If code changed: `cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago seed::` is green and the new known-answer test passes. +7. No commit authored by this plan contains any of the concurrent agent's five files (they commit to those paths themselves — that is expected and is not a failure). +8. All commits pushed via `gitea-ai`, or the exact push failure reported. + + + +- `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` is an audit a security reviewer would accept: evidence-backed, severity-classified, honest about what it could not verify, and generous where the code is right. +- `docs/security/PSBT-SIGNING-ARCHITECTURE.md` is directly plannable — a future `/gsd-plan-phase` can pick up its phase breakdown without re-deriving the architecture. +- The remediation backlog is in `docs/UNIFIED-TASK-TRACKER.md`, so the work is not stranded in a doc nobody reads. +- At most one small, human-reviewed, test-proven code change landed; everything larger is queued as a backlog item. +- Zero disruption to the concurrent agent's uncommitted work. + + + +Create `.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-SUMMARY.md` when done. +