211 lines
12 KiB
Markdown
211 lines
12 KiB
Markdown
---
|
|
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.
|