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:
parent
b5628d968e
commit
0c63a8518c
@ -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 (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 <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 & 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user