docs(quick-260731-upz): plan entropy/seed audit + PSBT signing architecture

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) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-31 22:29:23 -04:00
parent b5628d968e
commit 0c63a8518c

View File

@ -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"
---
<objective>
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**.
</objective>
<context>
@/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 (T1T7), 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 <explicit paths>`. 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.
</context>
<tasks>
<task type="auto">
<name>Task 1: Entropy &amp; seed-generation security audit against the real codebase</name>
<files>docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<done>`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.</done>
</task>
<task type="auto">
<name>Task 2: PSBT / watch-only / multisig architecture spec</name>
<files>docs/security/PSBT-SIGNING-ARCHITECTURE.md</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<done>`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.</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Remediation backlog, tracker update, and the one gated hardening fix</name>
<files>docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md, docs/UNIFIED-TASK-TRACKER.md, core/archipelago/src/seed.rs</files>
<behavior>
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.
</behavior>
<action>
**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 `<behavior>` 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.
</action>
<verify>
<automated>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</automated>
<human-check>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.</human-check>
</verify>
<done>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`.</done>
</task>
</tasks>
<threat_model>
## 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 `<verify>` |
| T-UPZ-02 | Repudiation / false assurance | audit findings without evidence | high | mitigate | Every finding requires `file:line`; `>=20` evidence refs enforced by Task 1 `<verify>`; 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 `<human-check>` 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 `<verify>` 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 |
</threat_model>
<verification>
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.
</verification>
<success_criteria>
- `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.
</success_criteria>
<output>
Create `.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-SUMMARY.md` when done.
</output>