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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
201ef474e7
commit
ff6902dd9d
@@ -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://<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_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/<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
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user