# 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.