Files
archy/.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-SUMMARY.md
T

211 lines
12 KiB
Markdown
Raw Normal View History

2026-08-12 10:55:50 +00:00
---
phase: quick-260731-upz
plan: 01
subsystem: security
status: complete
tags: [security, entropy, bip39, seed, psbt, audit, bitcoin, lnd]
requires: []
provides:
- docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md
- docs/security/PSBT-SIGNING-ARCHITECTURE.md
- injectable-RNG seam in core/archipelago/src/seed.rs
affects:
- core/archipelago/src/seed.rs
- docs/UNIFIED-TASK-TRACKER.md
tech-stack:
added: []
patterns:
- "Key-generation entropy source is passed as an argument, never inherited from a dependency default"
- "Injection seam + deterministic test RNG as the regression guard for entropy-source rebinding"
key-files:
created:
- docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md
- docs/security/PSBT-SIGNING-ARCHITECTURE.md
modified:
- core/archipelago/src/seed.rs
- docs/UNIFIED-TASK-TRACKER.md
decisions:
- "image-recipe/_archived/ is NOT dead code — build-debian-iso.sh execs it; it is the live ISO builder and therefore in audit scope"
- "ARCHY-1 fix applied as an injectable-RNG seam with a known-answer test; no derivation, word-count, passphrase or at-rest-encryption behaviour changed"
- "ARCHY-5 refuted as a present defect (32 divides 256, so no modulo bias today) but retained as a latent one"
- "PSBT spec ships wsh(sortedmulti) and defers taproot/MuSig2 as UNVERIFIED; BC-UR v2 chosen over BBQr on graceful-degradation grounds"
- "Migration section deliberately does NOT tell Archipelago users to rotate seeds — the audit found no entropy defect, and over-alarming has real cost"
metrics:
duration: ~75min
completed: 2026-08-01
---
# Quick Task 260731-upz: Entropy/Seed Audit + PSBT Signing Architecture — Summary
Turned the confirmed 2026-07-30 Coinkite COLDCARD low-entropy incident into an
evidence-backed audit of Archipelago's own entropy paths, a plannable PSBT-first signing
spec, a prioritised remediation backlog wired into the tracker, and one small, test-proven
hardening fix to master-seed generation.
## ⚠️ Push deliberately withheld
**Nothing was pushed.** Per explicit instruction, the `core/archipelago/src/seed.rs` diff is
held for human review before it leaves this machine — it is master-seed generation code for
every new node.
**Review commands:**
```bash
git show 8b51b7e2 # the full seed.rs diff (122 insertions, 2 deletions)
git log --oneline -4 # this plan's four commits
git show --stat 8b51b7e2
```
**Commits awaiting review, all on `main`, none pushed:**
| Commit | Type | Contents |
|---|---|---|
| `f11db4ea` | docs | `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` (the audit) |
| `5faf1a3c` | docs | `docs/security/PSBT-SIGNING-ARCHITECTURE.md` (the spec) |
| `5ba80e49` | docs | Remediation backlog + F-13 + tracker items |
| `8b51b7e2` | **fix** | **`core/archipelago/src/seed.rs` — the diff to review** |
**What to check in `8b51b7e2`:** that the change is limited to (a) routing mnemonic
generation through a helper that takes its RNG as a parameter, with `OsRng` passed at the
production call site, and (b) two new tests — and that **no** derivation path, word count,
BIP-39 passphrase decision, or at-rest encryption behaviour changed. It does not, but that
is the thing worth confirming with your own eyes.
## Headline result
**No Coldcard-class entropy defect exists in this codebase.** Every first-party
key-generation call site draws from a genuine CSPRNG. There is no Mersenne Twister, no
clock-seeded key, no `SmallRng`, no `seed_from_u64`, and no `Math.random()` in any browser
key path. The code also does several things better than most implementations (audit §5).
**But the audit found something more urgent than anything entropy-related.**
## The Critical finding (F-01) — not what we went looking for
`seed.generate` and `seed.restore` are in `UNAUTHENTICATED_METHODS`
(`core/archipelago/src/api/rpc/middleware.rs:24-28`), which skips session, RBAC **and** CSRF.
Neither handler checks whether onboarding is already complete, and
`NodeIdentity::from_seed` (`core/archipelago/src/identity.rs:79-114`) overwrites `node_key`,
`nostr_secret` and the FIPS mesh key **unconditionally**. There is no rate limit. The
endpoint is proxied to the LAN over plaintext HTTP
(`image-recipe/configs/nginx-archipelago.conf:11`, `:165`, `:192`) and mesh peers can reach
it too (`core/archipelago/src/server.rs:2080`).
**One unauthenticated POST can take over or destroy a live node's identity**, and
`seed.restore` lets the attacker choose the mnemonic. The guard already exists and is simply
never called — `NodeIdentity::key_exists` (`identity.rs:117`).
Surfaced by tracing secret classes (3) and (4) end-to-end rather than only checking where
their bits come from. Queued as backlog **R-01** and as a Tier 2 tracker item; it changes an
authentication boundary on a live fleet and needs its own phase.
## ARCHY findings — adjudicated
| Tag | Verdict | Note |
|---|---|---|
| **[ARCHY-1]** | **CONFIRMED** | `seed.rs:92``bip39-2.1.0/src/lib.rs:311-313``:296-298` (`&mut rand::thread_rng()`) → `:267-283`. **FIXED.** |
| **[ARCHY-2]** | **CONFIRMED (positive)** | The `GRND_NONBLOCK` probe is used as a probe only; its byte is discarded; no key material comes from it. Better than most. |
| **[ARCHY-3]** | **PARTIALLY CONFIRMED** | The feared version does not exist. Three of four sub-questions answered from the tree; the rest is an UNVERIFIED on-node checklist. |
| **[ARCHY-4]** | **CONFIRMED, and worse** | Every claim checks out, plus it is an integrity/availability exposure too — that is F-01. |
| **[ARCHY-5]** | **REFUTED as a present defect** | `totp.rs:305`'s charset is 32 chars and 32 divides 256, so bias is **zero** today. Latent, not live. Stated plainly rather than dropped. |
| Open Q9 | **DIVERGENCE CONFIRMED** | `Argon2::default()` = 19 MiB / t=2 / p=1; ADR-005 says 64 MB / 3. |
## Two findings the research did not predict
- **F-03 (High)** — the installed rootfs is a **cached container export shared by every
node**, baking SSH host keys and a TLS keypair. Per-device regeneration exists and is
correct in intent, but both branches are **fail-open** and `touch "$MARKER"` runs
**unconditionally** (`image-recipe/_archived/build-auto-installer-iso.sh:1647`, `:1659`,
`:1663`), so one transient failure permanently leaves that node on the image-wide shared
keys, visible only in a log file.
- **F-13 (High)** — `bitcoin.rs:203` passes `disable_private_keys=false` and `:229-231`
imports `wpkh(xprv/...)`, so the BIP-84 account **private** key is persisted in Bitcoin
Core's `wallet.dat` (with an empty wallet passphrase) in addition to the Argon2 envelope.
The descriptors also carry no key-origin annotation, so no hardware signer could use them.
## Scoping correction worth carrying forward
`image-recipe/_archived/` is **not dead code**. `image-recipe/build-debian-iso.sh:19-40`
copies `_archived/build-auto-installer-iso.sh` to a temp path, rewrites its relative paths,
and `exec`s it. **The "archived" auto-installer is the live ISO builder.** The plan scoped it
out; treating it as dead would have made [ARCHY-3] unanswerable and hidden F-03 entirely.
## Deliverables
**`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`** — 13 findings, each with severity,
`file:line` evidence, exploitability, blast radius and concrete remediation; all five ARCHY
tags adjudicated; all six mandated secret classes traced; a 13-item "What we do right"
section; a 7-item UNVERIFIED on-node checklist with paste-ready commands; and an R-00…R-15
remediation backlog. **103 `file:line` evidence references** (gate required ≥20).
**`docs/security/PSBT-SIGNING-ARCHITECTURE.md`** — watch-only descriptor wallets, the full
Core RPC loop with wallet- vs node-scoped RPCs, `analyzepsbt`-driven UI state, Tier 1
single-sig and Tier 2 `wsh(sortedmulti)` on BIP-48, BC-UR v2 vs BBQr vs file transport, the
honest LND capability matrix, the hot wallet as an explicitly-secondary tier, migration
guidance, and a 7-phase rollout with dependencies and candidate requirements. Cross-links
and answers two open items in `docs/hardware-signer-design.md`.
**`docs/UNIFIED-TASK-TRACKER.md`** — 9 new items in the file's existing tier/checkbox format:
4 in Tier 0, 3 in Tier 1, 4 in Tier 2 (including the Critical F-01 item and PSBT Phase 1).
## The one code change
`core/archipelago/src/seed.rs``generate_mnemonic_with<R: CryptoRng + RngCore>` calls
bip39's **injectable** `generate_in_with`; `MasterSeed::generate()` passes `OsRng` explicitly.
`mnemonic_generation_uses_injected_rng` asserts the result equals
`bip39::Mnemonic::from_entropy(<the exact bytes the test RNG emitted>)` — the direct proof
that the **injected** RNG, not bip39's transitive default, is the one consumed — plus a
known-answer pin and a determinism check. **This test cannot be written against the previous
code**, because `Mnemonic::generate(24)` exposes no seam.
**Verified:** `CARGO_INCREMENTAL=0 cargo test -p archipelago seed::` → **25 passed, 0
failed** (23 pre-existing + 2 new).
**Honest limitation, recorded in the audit:** this removes a *future* failure mode. It does
not retroactively change seeds generated before it, which came from `rand::thread_rng()`
a genuine CSPRNG, so nothing is weakened, but their guarantee rests on `rand 0.8.5`'s
behaviour rather than on this call site.
## Deviations from plan
1. **`image-recipe/_archived/` brought into scope** (plan said excluded). Justified above;
documented in the audit's §1 so the next auditor does not re-derive it.
2. **F-13 added to the audit during Task 3.** Discovered while reading `bitcoin.rs` for the
PSBT spec. It belongs to secret class (1), which Task 1 was required to trace, so it was
written up rather than left in the spec alone.
3. **R-12 (`totp.rs` modulo bias) NOT applied**, though the plan permitted it. `[ARCHY-5]`
was refuted as a present defect — 32 divides 256, so there is no bias today. Changing
working crypto code for a latent-only issue did not meet the plan's "small and obviously
correct" bar during a security-sensitive pass. Queued as R-12.
4. **`cargo audit` not run** — `cargo-audit` is not installed. Recorded as gap F-07 with
CI remediation R-05 rather than silently skipped.
## Not done, deliberately
- **No push, no tag, no deploy** (see the banner above).
- **No PSBT/watch-only/multisig implementation** — the spec is a spec.
- **`core/archipelago/src/container/secrets.rs` untouched** (backlog R-13) — it carried
another agent's uncommitted work. Read-only for the audit, as required.
## Concurrent-agent hygiene
All four commits verified against the forbidden-path list: **no commit authored by this plan
contains any of the other agents' files.** Every commit staged by explicit path; no
`git add -A`, no `git add .`, no `git commit -a`. The submodule guard (`indeedhub`) ran
before each commit and passed. Their uncommitted work (`ScreensaverRing.vue`,
`SendBitcoinModal.vue`, `WalletScanModal.vue`, and the earlier set) is intact.
## Self-Check: PASSED
- `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` — FOUND
- `docs/security/PSBT-SIGNING-ARCHITECTURE.md` — FOUND
- `core/archipelago/src/seed.rs` — modified, tests green
- Commits `f11db4ea`, `5faf1a3c`, `5ba80e49`, `8b51b7e2` — all FOUND in `git log`
- Task 1 verify gate — OK (103 evidence refs, all required tokens present, no secret-shaped
strings)
- Task 2 verify gate — OK (all 12 required tokens present, no secret-shaped strings)
- Task 3 verify gate — OK (backlog present, both tracker links present, no forbidden paths in
any of the four commits)
- No real secret value appears in any produced document — verified by pattern scan on both.