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>
12 KiB
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 fromgetrandom(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.mdremediation register (R-05..R-15) anddocs/UNIFIED-TASK-TRACKER.md.
2. Provenance
# 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:
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 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:
grep -n 'extracted .* lines from the builder' tests/first-boot-secrets/run-tests.sh
Negative control:
# 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.
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:
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:
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:
curl -sS -X POST http://<node>/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_secretand 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:
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.mdfor 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).
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:
# on each node
sha256sum /etc/ssh/ssh_host_*_key.pub
sha256sum /etc/ssl/private/<tls-key> # 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
# 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.