From ff6902dd9ddb42689f6efb00d0044d157e0c2f24 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 2 Aug 2026 09:14:27 -0400 Subject: [PATCH] docs(10): add independent verification guide for auditors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/security/PHASE-10-VERIFICATION-GUIDE.md | 267 +++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 docs/security/PHASE-10-VERIFICATION-GUIDE.md diff --git a/docs/security/PHASE-10-VERIFICATION-GUIDE.md b/docs/security/PHASE-10-VERIFICATION-GUIDE.md new file mode 100644 index 00000000..a0a8dd09 --- /dev/null +++ b/docs/security/PHASE-10-VERIFICATION-GUIDE.md @@ -0,0 +1,267 @@ +# Phase 10 — Independent Verification Guide + +**Audience:** third-party security auditors, and the Archipelago team. +**Purpose:** verify the Phase 10 security claims *independently*, without trusting the +project's own test harness. +**Status:** LIVING — sections are marked ✅ verifiable now, ⏳ pending a plan still in +execution, or 🔒 hardware-gated. Do not read an unmarked absence as a passing result. + +--- + +## 0. How to use this document + +Every claim below follows the same four-part structure, and **all four parts matter**: + +| Part | Why it exists | +|---|---| +| **Claim** | Stated so it can be falsified. A claim you cannot disprove is not a security claim. | +| **Reproduce the defect** | Check out the parent commit and demonstrate the bug. *A test that passes on both the fixed and unfixed code proves nothing.* | +| **Verify the fix** | Command + expected output, runnable without our harness wherever possible. | +| **Negative control** | Break the fix deliberately; confirm the check goes red on **exactly** that and nothing else. This is what separates verification from demonstration. | + +**Do not skip "Reproduce the defect".** It is the only step that proves the fix addresses +something real, and it is the step most often omitted in security theatre. + +### Trust posture + +Where a claim can be checked from *outside* the codebase — an HTTP request from another host, +a `tar` listing, a file comparison across two machines — **prefer that over running our tests.** +Our tests are offered as convenience and as evidence of intent, not as proof. Every claim below +that can be externally checked says so explicitly. + +--- + +## 1. Scope + +### In scope — what Phase 10 claims + +| ID | Claim | Severity | Status | +|---|---|---|---| +| KEY-01 | An already-provisioned node refuses every unauthenticated RPC that can mutate identity or credentials | **Critical** | ⏳ `10-01` in execution | +| KEY-02 | First-boot per-device secret generation is fail-closed, retried, self-healing, and has exactly one producer; the shipped rootfs contains no fleet-shared identity material | **High** | ✅ partially landed (`21043096`, `408b328c`), ⏳ single-producer + self-heal in progress | +| KEY-03 | The BIP-84 account private key is never imported into Bitcoin Core; the dead import path is deleted | **High** | ⏳ `10-05` in execution | +| KEY-04 | On-node evidence for C-3 / C-4 / C-6 | — | 🔒 hardware-gated | +| KEY-05 | A defaulted RNG cannot be inherited anywhere in the crate | Medium | ⏳ `10-06` not started | + +### Explicitly NOT claimed + +State these plainly so an auditor is not left inferring them: + +- **Lightning custody is not air-gappable.** Channel, revocation and HTLC keys must sign in real + time to answer counterparty commitments. LND remote signing *relocates* those keys; it does not + make them cold. Any document implying otherwise is wrong. +- **No claim against a compromised kernel CSPRNG**, a malicious dependency in the supply chain, + memory disclosure on a running node, or physical access. +- **KEY-05 fixes a structural risk, not a live vulnerability.** `rand::random()`/`thread_rng()` + are ChaCha12 seeded from `getrandom(2)`; nothing in that finding is exploitable today. The + mitigation targets *future silent rebinding* of the entropy source. +- **Findings F-04 through F-12 are out of scope** for this phase and remain open. See + `ENTROPY-SEED-AUDIT-2026-07-31.md` remediation register (R-05..R-15) and + `docs/UNIFIED-TASK-TRACKER.md`. + +--- + +## 2. Provenance + +```bash +# The audit that motivated this phase +docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md # 103 file:line references + +# The entropy fix that preceded the phase +git show 8b51b7e2 # seed.rs — explicit OsRng at the call site + +# Phase 10 plans and locked decisions +.planning/phases/10-key-material-hardening/ +``` + +`.planning/` is committed deliberately: an auditor can read *why* each decision was made, +including the ones that were reversed. `10-CONTEXT.md` records D-01..D-11 plus three +in-flight corrections (D-03a, D-07a/b/c) where our own earlier reasoning was wrong. + +--- + +## 3. Tier 0 — verifiable on any checkout, no node required ✅ + +No hardware, no deploy. Start here. + +### 3.1 First-boot secrets are fail-closed (KEY-02) + +**Claim.** If per-device secret generation fails, the completion marker is **not** written and +the boot does not proceed as if it had succeeded. + +**Reproduce the defect:** +```bash +git log --oneline -1 21043096 # the fix commit +git show 21043096^:image-recipe/_archived/build-auto-installer-iso.sh > /tmp/pre-fix.sh +grep -n 'touch .*MARKER' /tmp/pre-fix.sh +# Observe: the marker write is NOT inside the success branch — it runs regardless of outcome. +``` + +**Verify the fix:** +```bash +bash tests/first-boot-secrets/run-tests.sh +# Expect: passed: 3 failed: 0 (more cases once the self-heal work lands) +``` +The harness extracts the heredoc body **from the builder itself**, so it exercises the bytes +that ship rather than a copy. Confirm that for yourself: +```bash +grep -n 'extracted .* lines from the builder' tests/first-boot-secrets/run-tests.sh +``` + +**Negative control:** +```bash +# Move `touch "$MARKER"` outside the success branch in the builder, then: +bash tests/first-boot-secrets/run-tests.sh +# Expect: FAIL: openssl fails every attempt -> MARKER-SET-ON-FAILURE +# passed: 2 failed: 1 EXIT=1 +git checkout image-recipe/_archived/build-auto-installer-iso.sh +``` +It must fail on **that case only**. A negative control that reddens everything is measuring +nothing. + +### 3.2 Master-seed entropy is explicit (F-02, shipped) + +**Claim.** Mnemonic generation draws from an explicitly-passed `OsRng`, not a +transitive-dependency default, and a test proves the injected RNG is the one consumed. + +```bash +git show 8b51b7e2 -- core/archipelago/src/seed.rs # ~6 lines of production change +cd core && cargo test -p archipelago seed:: # expect 25 passed; 0 failed +``` + +**Reproduce the defect:** on `8b51b7e2^`, `MasterSeed::generate` calls +`bip39::Mnemonic::generate(24)`, which resolves to `&mut rand::thread_rng()` *inside* the bip39 +crate — there is no seam to inject through, so the proving test cannot be written at all. + +**Note for auditors:** the test module implements `rand::CryptoRng` for a counter RNG. That is a +deliberately false marker-trait promise, confined to `#[cfg(test)]` (`seed.rs:502`). KEY-05 +retires it. Confirm containment: +```bash +grep -n 'CountingRng' core/archipelago/src/seed.rs # all hits must be after the cfg(test) at :502 +``` + +### 3.3 Unauthenticated method inventory (KEY-01 context) + +Read the authoritative list rather than trusting prose: +```bash +sed -n '/UNAUTHENTICATED_METHODS/,/];/p' core/archipelago/src/api/rpc/middleware.rs +``` +Every entry is reachable without a session, RBAC check, or CSRF token. KEY-01's claim is that +those which can mutate identity or credentials refuse once the node is provisioned. + +--- + +## 4. Tier 1 — requires a running node ⏳ + +Pending `10-01` and `10-02`. `10-02` produces `scripts/security/rpc-exposure-probe.sh` and +`docs/security/KEY-01-ON-NODE-VERIFICATION.md`. + +**The external check that matters most (C-6).** From a *different host* on the same network, +against a node that has completed onboarding: + +```bash +curl -sS -X POST http:///rpc \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"seed.restore","params":{"words":["<24 words>"]}}' +``` + +- **Before the fix:** the node accepts attacker-supplied words and overwrites `node_key`, + `nostr_secret` and the FIPS mesh key. This is the Critical finding. +- **After the fix:** refused, and the node's identity is byte-identical afterwards. + +Verify byte-identity yourself rather than trusting a log line: +```bash +sha256sum /var/lib/archipelago/identity/node_key /var/lib/archipelago/identity/nostr_secret +# run before and after the request; the hashes must be unchanged +``` + +> ⚠️ **Do not run the "before" case against a node you care about.** It really does overwrite the +> identity. Use a disposable node — see `.planning/todos/pending/2026-08-01-archi-dev-box-as-fresh-test-node-without-iso.md` +> for standing up an isolated instance without flashing an ISO. + +**Do not probe with `seed.status`.** The original audit's C-6 command used it; `seed.status` is +**not** in `UNAUTHENTICATED_METHODS`, so it returns 401 by design and would report the surface +closed while the real door stands open. Probe with a method that is genuinely on the +unauthenticated list. + +**Non-regression, equally important:** a *fresh, un-onboarded* node must still complete +onboarding. The gate distinguishes provisioned from fresh; a fix that refuses on a fresh node +bricks first boot fleet-wide. + +--- + +## 5. Tier 2 — ISO build host 🔒 + +Full procedure: `docs/security/KEY-02-ROOTFS-EVIDENCE.md` (C-4). + +```bash +UNBUNDLED=1 bash image-recipe/build-debian-iso.sh --rebuild +# then follow steps 2/4/5/6 in KEY-02-ROOTFS-EVIDENCE.md +``` + +**Claim.** The shipped rootfs tar contains no SSH host keys, no TLS private key, and no +machine-id — so no two nodes flashed from one image can share them. + +**Gotcha, recorded because it will waste your afternoon:** read `RECIPE_HASH` from +`image-recipe/build/auto-installer/archipelago-rootfs.recipe.sha256`, **not** by hashing the +repo file. The wrapper rewrites 35 path expressions and absolutises `SCRIPT_DIR` before exec, +so the hash is host- and checkout-specific. + +**Note the inverted expectation.** The original audit expected these artefacts to be *present*. +This check passes when they are *absent*. + +--- + +## 6. Tier 3 — two physical nodes 🔒 + +**C-3 — host-key uniqueness.** Flash two machines from the *same* ISO, then compare: +```bash +# on each node +sha256sum /etc/ssh/ssh_host_*_key.pub +sha256sum /etc/ssl/private/ # path per the nginx config +cat /etc/machine-id +``` +Every value must differ between the two nodes. Any match is a finding. + +SSH host keys and the TLS key are equally sharp signals once the single-producer work lands +(before it, TLS had an installer fallback and SSH did not — see `KEY-02-ROOTFS-EVIDENCE.md`). + +--- + +## 7. Tier 4 — pre-release gate + +```bash +# ON the node, not over RPC — it uses local podman/systemctl/bitcoin probes +ARCHY_ITERATIONS=5 bash tests/lifecycle/run-gate.sh +``` +Install / UI / stop / start / restart / reinstall / reboot-survive / +archipelago-restart-survive / uninstall, 5× green. See `tests/lifecycle/TESTING.md`. + +Frontend: `cd neode-ui && npm run test` (vitest) and `npm run build`. +Rust: `cd core && cargo test -p archipelago`. + +--- + +## 8. Known-accepted risks + +Recorded so an auditor does not have to discover them by reading commit messages. + +| Risk | Decision | Where | +|---|---|---| +| A node whose first-boot secret generation can never succeed will not serve TLS | Accepted. Mitigated by a build-time assertion on generator binaries, retry-with-backoff, and self-heal on subsequent boots — leaving genuinely-broken hardware as the residual | `10-03` | +| Rotating host keys on already-deployed nodes invalidates `known_hosts` fleet-wide | Accepted, rated one-way, gated behind a decision checkpoint | D-06, `10-04` | +| KEY-01's fix ships on the next scheduled OTA, not an emergency release | Deliberate. The Critical finding stays live on the fleet until that OTA | D-10 | +| `#[cfg(test)]` code implements `rand::CryptoRng` falsely | Accepted until KEY-05 retires it; contained to test builds | `seed.rs:656` | + +--- + +## 9. Reporting a finding + +If any check above fails, or you find something not covered: the audit format that produced this +work is `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` — evidence as `file:line`, an explicit +severity, and a stated confidence. Findings that cannot be verified without hardware belong in an +UNVERIFIED section rather than being asserted. + +Two corrections in that document are worth reading as calibration, because both were ours: F-10 +**understated** its scope by a factor of 20, and the correction to it then **overstated** the +severity of two files within a day. Both are struck in place rather than rewritten.