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