454388226c7d5b140fec656acc6708f2f68653d9
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
262998747e |
feat(10-05): report BIP-32 key origin on lnd.create-psbt, and record the honest signing posture (D-07b/D-09)
With Bitcoin Core's wallet deleted, LND's PSBT round trip is the only external-signer path Archipelago has, and D-09's key-origin protection moves from Core descriptors (of which none remain) to the PSBT itself. Adds `psbt_key_origin_report(&str) -> Result<PsbtKeyOriginReport>` to lnd/wallet.rs, reporting `input_count`, `inputs_with_key_origin` and `all_inputs_have_key_origin`. An input counts as carrying key origin when either its `bip32_derivation` or `tap_key_origins` map is non-empty. A PSBT with zero inputs reports false rather than vacuous truth. Parsed with the already-present `bitcoin` and `base64` crates; no dependency added. `lnd.create-psbt` gains an additive `key_origin` object on its response and a `tracing::warn!` with the counts when key origin is missing, because that is the exact condition under which a hardware signer refuses the PSBT. Computed best-effort: a decode failure degrades to `null`, never to an error, so a user's send cannot fail because an inspection helper could not parse something. `handle_lnd_finalize_psbt` and `handle_lnd_create_raw_tx` (which deliberately auto-signs with LND's hot keys) are untouched. Three tests, with fixtures built programmatically from the `bitcoin` crate rather than pasted as opaque base64: with-derivations, without-derivations, and malformed-is-an-error-not-a-panic. KEY-03-SIGNING-POSTURE.md gains an honest per-step coverage map of the fund -> export -> sign offline -> import -> finalize -> broadcast round trip. Of six steps, only the new inspection has automated coverage; steps 1, 4, 5 and 6 have none, and there is no air-gap transport (no animated QR, no .psbt file exchange) — export/import is copy-paste of base64. Untested paths are named as untested. Records the verdict that decides whether any of this is an air gap: on a default node an external signer CANNOT meaningfully sign a PSBT from `lnd.create-psbt`, because LND holds the keys for every input it selects. Evidence: the PSBT is funded from LND's own wallet; `ensure_wallet_initialized` creates a full key-holding wallet via /v1/initwallet; the generated lnd.conf carries no `remotesigner.*` block; and a search of apps/, scripts/, core/archipelago/src and image-recipe/ for remotesigner/createwatchonly/ nochainbackend returns zero matches. No fleet node is provisioned watch-only. What ships is PSBT transport, not air-gapped custody — the gap is provisioning, not plumbing. Adds the standing honesty statement in its own subsection: Lightning channel, revocation and HTLC keys are NOT air-gappable at all. They must sign in real time to answer counterparty commitments; remote signing relocates them to a hardened host, it does not cool them. Also adds a status banner to PSBT-SIGNING-ARCHITECTURE.md recording that its Phase 1 was superseded by deletion rather than delivered, so §0's "single highest-value change" and §2.1's invariant now read against a code path that no longer exists. Banner only; §5.4's honesty table is byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9622926868 |
fix(10-05): delete the Bitcoin Core wallet path that duplicated the spending key (F-13, D-07b)
`handle_bitcoin_init_wallet_from_seed` derived the BIP-84 account extended *private* key, stringified it, and imported `wpkh(xprv/0/*)` / `wpkh(xprv/1/*)` into a Bitcoin Core descriptor wallet created with `disable_private_keys=false` and an empty passphrase. That put a second copy of the node's spending key in Core's `wallet.dat`, outside the daemon's Argon2 + ChaCha20-Poly1305 envelope. That duplication into weaker protection was audit finding F-13 (High). Deleted rather than rewritten watch-only (D-07b supersedes D-07/D-07a): - No caller anywhere. Repo-wide search leaves exactly one occurrence of the method name (its own dispatcher registration) and two of the symbol in code (definition + dispatch call); every other hit is prose in docs. - LND is the wallet the product drives. Across neode-ui/src every `bitcoin.*` call is read-only status (getinfo/prune-status/onion); the wallet UI sends via `lnd.sendcoins`. - It never ran on archi-dev-box: no wallet named `archipelago` exists there, and the one loaded wallet reports blank=true, keypoolsize=0, txcount=0. - It was authenticated AND password-gated, so F-13 was key-at-rest duplication, not an exposed endpoint. No migration is performed and none is planned. This removes code, not wallets: nothing on disk is touched, no funds move, no wallet.dat is modified. If a node is ever found holding a wallet this handler created, that is a finding to surface and stop on, not a trigger to auto-migrate. `seed::derive_bitcoin_xprv` loses its only non-test caller and is retained deliberately with `#[allow(dead_code)]` and a stated reason: it keeps its existing test coverage and it is the derivation D-07c's deferred BDK cold vault will need. Records the evidence, the D-08/D-09 consequences and the D-07c deferral in docs/security/KEY-03-SIGNING-POSTURE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2efab5f219 |
fix(10-03): unify secret generation to a single producer + self-heal (F-03)
Unify rather than delete. The defect in F-03 was never "a second attempt to
create a key exists" — it was that failure was silent and the completion marker
lied about it. A second attempt is only dangerous when it is an unaudited second
PRODUCER carrying its own idea of success, its own absent retry policy and its
own absent failure record.
Single producer. gen_tls() is now the only code in the ISO build that creates
/etc/archipelago/ssl/archipelago.{key,crt}; gen_ssh() the only code that creates
/etc/ssh/ssh_host_*. Two secondary producers are gone:
- the Dockerfile's `openssl req` layer, which baked a keypair the strip layer
deleted moments later in the same build;
- the installer's "ensure SSL cert exists for nginx HTTPS" block, which before
the strip almost never fired and after it would have fired on every install.
Proof is mechanical, not a claim: every executable `openssl req` / `ssh-keygen
-A` invocation in the builder now lives inside the generator heredoc, and the
test suite fails if one appears outside it.
Build-time assertion. The one realistic total failure is a missing generator
binary, which is deterministic — no retry or reboot fixes it. A rootfs RUN layer
now fails the build if openssl or ssh-keygen is missing or non-executable.
openssl and openssh-server are both already in the package list (and
openssh-server hard-depends openssh-client, which ships ssh-keygen), so today
this is cheap insurance; it earns its place the first time someone edits that
list.
Self-heal, never dead-end. Fail-closed governs SERVING; retry governs
RECOVERING, and they are different things. Adds
archipelago-first-boot-secrets.timer (OnBootSec=5min, OnUnitActiveSec=15min),
installed and enabled with a hand-written symlink fallback because chroot
systemctl enable can fail silently. The service's own ConditionPathExists=!
makes every trigger a no-op once the marker exists, so a healthy node pays
nothing. On success the script now restarts consumers that are in `failed` —
try-reload-or-restart is a no-op on a failed unit, so without this a recovered
node would have valid keys on disk and nginx still down.
Never serve a bogus key. gen_tls parses both halves back with `openssl pkey`
and `openssl x509` before the swap, so a truncated or half-written artefact is
never what nginx reads.
Tests: 6 cases, each with an isolated negative control (transcripts in SUMMARY).
- case 4, TLS fails every attempt on a stripped root -> no key from any source.
Control: reintroduce a fallback key creation -> only case 4 red.
- case 5, self-heal: a failed run then a later successful run -> key present,
marker set, failed units restarted. Control: dead-end on a node that already
failed -> only case 5 red.
- case 6, single-producer invariant. Control: reintroduce the installer block
-> only case 6 red, naming the line.
Residual risk, stated plainly: a machine where generation can never succeed
still ends up with no SSH and no TLS. Build-time assertion removes the
deterministic cause, retry plus timer removes the transient ones, so what
remains is genuinely broken hardware — and it says so on the console and in
/var/lib/archipelago/first-boot-secrets.failed rather than quietly serving a
key nobody audited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ff6902dd9d |
docs(10): add independent verification guide for auditors
A guide a third-party security auditor can use to verify Phase 10's claims without trusting our test harness — and that we use ourselves. Every claim carries four parts, all required: the claim stated falsifiably; how to REPRODUCE THE DEFECT on the parent commit; how to verify the fix; and a negative control that must go red on exactly that defect and nothing else. A test passing on both fixed and unfixed code proves nothing, and reproduce-first is the step most often omitted in security theatre. Prefers external checks (curl from another host, tar listing, cross-node file comparison) over our own tests wherever a claim can be checked from outside. Tiered by hardware needed: Tier 0 any checkout, Tier 1 running node, Tier 2 ISO build host, Tier 3 two physical nodes, Tier 4 pre-release gate. Status marked per claim — verifiable now, pending a plan, or hardware-gated — so an unmarked absence is never read as a pass. States what is explicitly NOT claimed (Lightning custody is not air-gappable; no claim against a compromised kernel CSPRNG or supply chain; KEY-05 is structural not exploitable), the known-accepted risks with where each was decided, and carries the C-6 warning that probing with seed.status reports the surface closed while the real door stands open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
201ef474e7 |
docs(10-03): record C-4 build-host evidence procedure (UNVERIFIED)
Task 3 of 10-03 is a blocking checkpoint: proving the shipped rootfs tar is identity-free needs a real ISO build host with podman/docker and disk for a full rootfs rebuild. This commits the prepared evidence document with the exact command sequence, marked UNVERIFIED, rather than claiming the check passed. The document states the inverted expectation explicitly. The audit's C-4 entry expected SSH host keys and the TLS key to be PRESENT — that described the broken state it was measuring. After the strip layer those must be ABSENT, so the audit's stated expectation is now the failure condition. A future reader comparing the two would otherwise conclude the check regressed. Also records two things the operator would otherwise get wrong: - RECIPE_HASH must be read from the stamp file, not computed from the repo file. build-debian-iso.sh rewrites the builder's relative paths into a temp copy before exec, and the hash covers "$0"; the hashed region has 35 such rewritten expressions plus an absolutised SCRIPT_DIR, so the value is specific to the build host and checkout path. - C-4 is a build-host check only. Two-node key divergence is C-3 and stays separately UNVERIFIED; the note explains why SSH host keys are the sharper signal there than TLS, given the installer's per-install TLS fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90ce4bcc46 |
docs(10): correct F-10a's own overstatement — x3dh sites are identifiers, not key material
The F-10a scope correction committed hours earlier asserted semantics its evidence did not support. The KEY-05 planner caught it against the code: - mesh/x3dh.rs:100/:114 are u32 prekey IDENTIFIERS (spk_id, otk_id), not key agreement material. The X25519 secrets come from crypto::generate_x25519_ephemeral() at :99/:113 and were never in scope. - session.rs's 16 raw matches read as 16 production token sites; #[cfg(test)] begins at :470, so it is 4 production + 12 test. - wallet/bdhke.rs is 2 production of 4 (#[cfg(test)] at :143) — and those two ARE genuine key material: generate_secret() :133 and random_blinding_factor() :139. The Medium rating still holds, on narrower grounds: bdhke's two production sites plus storage_crypto.rs:39's AEAD nonce. It no longer rests on x3dh. Struck rather than silently rewritten. F-10 was corrected on the grounds that understatement misleads the next reader; overstatement does the same, and this table managed both within a day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5cf44c9a58 |
docs(10): correct F-10's scope and add KEY-05 — crate-wide CSPRNG enforcement
The audit recorded F-10 as two call sites in container/secrets.rs. The real defaulted-RNG surface is 41 sites across 15 files: session.rs (16), pine_ha.rs (6), wallet/bdhke.rs (4 — ecash key material), mesh/x3dh.rs (2 — key-agreement material), storage_crypto.rs (1 — AEAD nonce), +10 more. Nothing is broken today: rand::random()/thread_rng() are ChaCha12 seeded from getrandom(2). What changes is blast radius — F-10's Low rating rested on 'per-app credentials rather than the master key hierarchy', which does not survive the true scope. Re-rated Medium as F-10a. Records why the original audit missed it: F-10 was reached by tracing the manifest-secrets path, and no step enumerated defaulted-RNG use across the crate independently of the traced paths. F-10's original text is left unedited so the correction is auditable rather than retroactive. R-13 superseded by R-16; tracker item replaced. Adds KEY-05 to Phase 10: sealed allowlist trait at key-gen seams, clippy disallowed-methods ban (compile-time, CI-enforced), cargo-deny on duplicate rand majors, degenerate-entropy runtime check, persisted CSPRNG-readiness verdict. Also retires the false 'impl CryptoRng for CountingRng' at seed.rs:656. Records the user's execution gate: Phase 10 does not start until the concurrent Phase 1 agent is finished and their changes are synced. KEY-05 is unplanned — the existing 5 plans predate it and a 6th is required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5ba80e49b7 |
docs(quick-260731-upz): entropy-audit remediation backlog + tracker items
- Adds F-13 (High) to the audit: the BIP-84 account PRIVATE key is imported into Bitcoin Core (bitcoin.rs:203 disable_private_keys=false, :229-231 wpkh(xprv/...)), so the spending key is persisted outside the Argon2 envelope in a wallet with an empty passphrase; the descriptors also carry no [fingerprint/derivation] key origin, so no hardware signer could ever use them. Found by tracing secret class (1) end-to-end - Fills the Remediation Backlog: R-00..R-15, prioritised severity x effort, each with the finding it closes, files, effort, and hardware gating; plus an explicit "not implemented here, and why" section - Records ARCHY-1 as APPLIED with the exact test evidence and an honest note that making the source explicit removes a future failure mode rather than repairing a past one - Wires the resulting open items into docs/UNIFIED-TASK-TRACKER.md in its existing tier/checkbox format: Tier 0 (cargo audit/deny CI, ceremony mnemonic input, a five-item hygiene batch, secrets.rs OsRng), Tier 1 (ISO fail-open first-boot secrets, Argon2 vs ADR-005, the on-node checklist), Tier 2 (the Critical unauthenticated seed RPCs, PSBT Phase 1, PSBT phases 2-7, seed-RPC transport confinement) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5faf1a3c5f |
docs(quick-260731-upz): PSBT-first signing architecture spec
Watch-only descriptor wallets, external signers, multisig, air-gap transport and honest LND limits — a spec a future /gsd-plan-phase can consume. - Names the current gap: bitcoin.rs:203 passes disable_private_keys=false and bitcoin.rs:229-231 imports wpkh(xprv/...), so Core holds the BIP-84 account PRIVATE key today. Closing that is Phase 1 and unblocks everything else - Full Core RPC loop with wallet- vs node-scoped RPCs; analyzepsbt drives UI - Tier 1 single-sig with mandatory [fingerprint/derivation] key origin; Tier 2 wsh(sortedmulti) on BIP-48; taproot/MuSig2 deferred as UNVERIFIED - BC-UR v2 primary (fountain-coded, degrades gracefully), BBQr for Coldcard, file fallback always; animated multi-frame is mandatory, not optional - LND: channel/revocation/HTLC keys CANNOT be air-gapped; funding tx must NEVER be self-broadcast (type-level refusal, not a boolean) - On-chain (PSBT-protectable) vs lightning (necessarily hot) split, with the exact user-facing sentence the UI must use - Hot wallet kept as explicitly-secondary with server-enforced limits; safe path is the DEFAULT, per T1's survivors - Migration section refuses to over-alarm: the audit found no entropy defect, so no Archipelago user needs to rotate a seed - 7 phases with dependencies, candidate requirements, and hardware gating - Answers two open items in docs/hardware-signer-design.md - Flags bitcoin-knots:latest as an unpinned tag vs ADR-009 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f11db4ea1d |
docs(quick-260731-upz): entropy & seed-generation security audit
Evidence-backed audit of every secret class against the real tree, prompted by the 2026-07-30 Coinkite COLDCARD entropy incident. - No Coldcard-class entropy defect exists: no non-cryptographic PRNG, no clock-seeded key, no Math.random() in any browser key path - F-01 (Critical, NOT entropy): seed.generate/seed.restore are unauthenticated, unrated, and unconditionally overwrite a live node's Ed25519/Nostr/FIPS keys - F-02 [ARCHY-1] CONFIRMED: mnemonic entropy source is a bip39 transitive default, not a call-site argument — the exact structural shape of T1 - F-03 (High) [ARCHY-3]: first-boot TLS/SSH regeneration is fail-open and its completion marker is set even on failure, over a fleet-shared cached rootfs - ARCHY-2 confirmed good; ARCHY-5 refuted as a present defect (32 | 256) - Argon2::default() is 19MiB/t=2, not ADR-005's stated 64MB/3 - Corrects the scoping assumption that image-recipe/_archived/ is dead: it is the live ISO builder, exec'd by build-debian-iso.sh - Adds a "What we do right" section and an UNVERIFIED on-node checklist Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |