From 0214114c7be070c4fbd6d190719ea80ba6011264 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 2 Aug 2026 14:26:57 -0400 Subject: [PATCH] =?UTF-8?q?docs(10-02):=20summary=20=E2=80=94=20probe=20bu?= =?UTF-8?q?ilt=20and=20committed,=20C-6=20still=20UNVERIFIED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 1 done. Tasks 2 and 3 are blocked on unmet preconditions and were NOT auto-approved: no fleet node runs 10-01's gate (installed binary predates 879de59e and lacks the refusal string), and no second machine was available to probe from. Nothing moved UNVERIFIED -> VERIFIED. Co-Authored-By: Claude Opus 5 (1M context) --- .../10-02-SUMMARY.md | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 .planning/phases/10-key-material-hardening/10-02-SUMMARY.md diff --git a/.planning/phases/10-key-material-hardening/10-02-SUMMARY.md b/.planning/phases/10-key-material-hardening/10-02-SUMMARY.md new file mode 100644 index 00000000..dfeaa7bb --- /dev/null +++ b/.planning/phases/10-key-material-hardening/10-02-SUMMARY.md @@ -0,0 +1,252 @@ +--- +phase: 10-key-material-hardening +plan: 02 +subsystem: security-verification +tags: [security, KEY-01, KEY-04, F-01, C-6, probe, unverified] +requires: + - "10-01 — api::rpc::onboarding_gate (the gate whose refusal this plan must prove on hardware)" +provides: + - "scripts/security/rpc-exposure-probe.sh — repeatable, read-only-by-default RPC exposure probe" + - "docs/security/KEY-01-ON-NODE-VERIFICATION.md — the evidence record for C-6 and the F-01 refusal" + - "The probe-method correction: auth.isOnboardingComplete, not seed.status, is the C-6 exposure signal" +affects: + - "audit item C-6 (still UNVERIFIED — narrowed, not closed)" + - "D-10 OTA gating: the refusal check is now provably blocked on deployment, not on repo work" +tech-stack: + added: [] + patterns: + - "Read-only by construction: probe methods come from a fixed array, never from an argument" + - "Mutating requests confined to a single explicit --destructive branch" + - "Published BIP-39 test vector instead of minting real mnemonics (the C-5 anti-pattern)" +key-files: + created: + - scripts/security/rpc-exposure-probe.sh + - docs/security/KEY-01-ON-NODE-VERIFICATION.md + modified: [] +decisions: + - "C-6 recorded as NOT VERIFIED rather than closed on loopback evidence — probing from the node measures the local stack, not the LAN" + - "The --destructive refusal check was NOT run: no node in the fleet carries 10-01's gate yet, so it would have replaced the dev-box's identity instead of being refused" + - "The /rpc/ nginx block returns 404 — the unauthenticated surface is reachable through /rpc/v1 only" +metrics: + duration: "~35m" + completed: 2026-08-02 +status: blocked +--- + +# Phase 10 Plan 02: On-Node Verification of KEY-01 / C-6 Summary + +The probe exists, is safe by construction, and has been run — but **audit item C-6 is still +UNVERIFIED and the KEY-01 refusal is still unproven on hardware**, because no node in the fleet +is running 10-01's gate and no second machine was available to probe from. + +## Status: Task 1 complete · Tasks 2 and 3 BLOCKED on unmet preconditions + +| Task | Type | Outcome | +|---|---|---| +| 1. Build the read-only-by-default probe | auto | **Done**, committed `527f6023` | +| 2. Measure C-6 on real nodes, prove the refusal | checkpoint (blocking) | **BLOCKED** — precondition unmet | +| 3. Fresh-node onboarding non-regression | checkpoint (blocking) | **BLOCKED** — precondition unmet | + +Per the plan's `autonomous: false` posture, neither checkpoint was auto-approved and no result +was recorded that was not observed. + +## What Was Built + +**`scripts/security/rpc-exposure-probe.sh`** (new, 296 lines, mode 755) + +| Flag / symbol | Contract | +|---|---| +| `--target ` | required; host, onion or ULA. Bare IPv6 is bracketed automatically so the mesh ULA can be probed | +| `--scheme http\|https` · `--port N` · `--label ` | defaults `http` / `80` / `unlabelled` | +| `--insecure` | **added beyond the plan** — accept a self-signed cert on `https`; without it every https vantage point is a false `UNREACHABLE` | +| `--destructive` | the single mutating branch: the KEY-01 refusal check | +| `READONLY_METHODS` | `health`, `auth.isOnboardingComplete`, `seed.status` — the only methods the default path can call | +| exit `0` / non-zero | all controls as expected / `seed.status` was not 401, or `--destructive` was not refused | + +Four requests per read-only run (well under 10-01's 10-per-300s floor, so T-10-15 does not fire): +the three methods on `/rpc/v1`, plus the exposure signal repeated on nginx's `/rpc/` block. + +Safety properties, as required by the threat model: + +- **T-10-11:** the method string is built from the fixed array, never from an argument; every + mutating request is inside one `if [ "$DESTRUCTIVE" = "1" ]` branch behind a red + disposable-nodes-only banner. +- **T-10-12:** the refusal check uses the published BIP-39 all-`abandon` + `art` vector (32 zero + bytes). The script never generates and never prints a mnemonic — the deliberate difference from + audit item C-5, which mints real ones. +- **T-10-13:** no node address, onion address, username or password is embedded + (`grep -nE '([0-9]{1,3}\.){3}[0-9]{1,3}|\.onion|password'` matches only the safety comment + that forbids them). + +The before/after byte-identity check is not attempted by the script (it has no node-local file +access); it prints the two `sha256sum` commands so they land in the operator's transcript. + +## The Probe-Method Correction (the substantive finding) + +The audit's C-6 command (`ENTROPY-SEED-AUDIT-2026-07-31.md:890-901`) probes with `seed.status` +and calls `200` a failure. `seed.status` is **not** in `UNAUTHENTICATED_METHODS` +(`middleware.rs:5-38`), so it is rejected at `api/rpc/mod.rs:293` with a **401 by design** — the +audit's failure criterion can never fire, and the probe reports the surface CLOSED while F-01's +door stands open. The probe therefore measures exposure with `auth.isOnboardingComplete` +(genuinely allowlisted at `middleware.rs:9`, read-only) and keeps `seed.status` as the +session-enforcement control. Recorded in the evidence document so it is not re-derived a third +time. + +## What Was Actually Measured + +Both runs originated **on the node under test**, so neither is a C-6 result — they are recorded +as `loopback` and `self-lan-ip`, not `lan`. + +| Vantage | `health` | `auth.isOnboardingComplete` | `seed.status` | `/rpc/` | +|---|---|---|---|---| +| `loopback` | 200 | **200 — EXPOSED** | **401 — PASS** | 404 | +| `self-lan-ip` | 200 | **200 — EXPOSED** | **401 — PASS** | 404 | + +- **`seed.status` returned 401 on every vantage tested** — no stop-the-plan finding. +- **Incidental finding:** `/rpc/` returns **404**. nginx's second proxy block + (`nginx-archipelago.conf:192`) forwards the full URI and the backend routes only `/rpc/v1`, so + the unauthenticated surface has exactly one path. This narrows F-01's exposure surface. +- **Corroborating, not measurement:** nginx binds `0.0.0.0:80` and `[::]:80`, the daemon is + loopback-only on `:5678`, and the host filter has no rule matching tcp/80 (`-P INPUT ACCEPT`, + nft ruleset is Tailscale chains only). A LAN `EXPOSED` result is very likely — but likely is + not measured, and C-6 stays open. + +## Why Tasks 2 and 3 Are Blocked (verified, not assumed) + +**No node in the fleet is running 10-01's gate.** Checked on the dev-box rather than inferred: + +``` +$ ls -l /usr/local/bin/archipelago +-rwxr-xr-x 1 root root 53437536 Aug 2 06:37 /usr/local/bin/archipelago +$ git log -1 --format='%ci' 879de59e +2026-08-02 13:05:35 -0400 +$ grep -qa "Not supported: this node is already provisioned" /usr/local/bin/archipelago && echo PRESENT || echo ABSENT +ABSENT +``` + +The installed binary was built ~6.5h before 10-01 landed, and the gate's refusal string is absent +from it. A `--destructive` run against this node would therefore **not** be refused — it would +replace `node_key`, `nostr_secret` and `fips_key` on a live dev-pair deploy target that is gated +before every OTA. It was not run. The plan's own threat model (T-10-11) and the phase brief +(which excludes deployment) make this a hard block, not a judgement call. + +Task 3's harness is shape (A) of +`.planning/todos/pending/2026-08-01-archi-dev-box-as-fresh-test-node-without-iso.md` — a second +daemon under its own `ARCHIPELAGO_DATA_DIR`. That todo is still **pending**: the harness does not +exist, and it would additionally need a binary built from `879de59e` or later. + +Task 2 also requires a second machine on the LAN. This session ran on the node itself, and +probing fleet nodes uninvited is out of bounds (`.228` is in real use). + +## Pre-OTA Fleet Check Carried Over from 10-01 + +10-01 flagged a state its gate makes unrecoverable — `onboarding.json` complete-true with no +`user.json` — and asked 10-02 to sweep the fleet before the OTA (D-10). + +| Node | `user.json` | `onboarding.json` | Verdict | +|---|---|---|---| +| dev-box | PRESENT | `{"complete": true}` | **safe** — provisioned normally | +| rest of fleet | — | — | **NOT CHECKED** | + +## Deviations from Plan + +**1. [Rule 1 — Bug] `curl` failure fallback produced a `000000` status code** + +- **Found during:** Task 1 verification against a dead port. +- **Issue:** `HTTP_CODE=$(curl … -w '%{http_code}' … || echo "000")` — curl already emits `000` + when no response arrives, so the fallback *appended* a second `000`. Every verdict then fell + through to the wildcard branch and an unreachable host was reported as + `seed.status … CRITICAL — session enforcement is NOT working`, exit 1. A false critical on an + unreachable transport is exactly the misreading this plan exists to prevent. +- **Fix:** the fallback now replaces rather than appends (`if ! HTTP_CODE=$(curl …); then + HTTP_CODE="000"; fi`) plus a three-digit normalisation. Retested: an unreachable target now + reports `UNREACHABLE` on all four lines and exits 0, since an unreachable vantage point is a + result, not a control failure. +- **Commit:** `527f6023` (fixed before the commit). + +**2. [Rule 2 — Missing critical functionality] `--insecure` flag added** + +- **Issue:** the plan's CLI contract has no way to accept a self-signed certificate, but + Archipelago nodes serve https with one. Every `--scheme https` probe would have reported a + false `UNREACHABLE`, silently under-measuring the exposure surface. +- **Fix:** opt-in `--insecure`, off by default, documented in `--help`. It does not alter the + plan's flag contract. + +**3. Tasks 2 and 3 not executed** — see the blocked section above. No result was recorded that +was not observed; nothing was marked verified. + +## Verification + +- `bash -n scripts/security/rpc-exposure-probe.sh` → exits 0. +- `bash scripts/security/rpc-exposure-probe.sh --help` → prints usage, exits 0. +- `test -x` → mode `755` (`-rwxr-xr-x`). +- `grep -c READONLY_METHODS` → **22** (≥1 required); `grep -c DESTRUCTIVE` → **5** (≥2 required). +- Missing `--target` → exit 2; unknown argument → exit 2; unreachable target → all `UNREACHABLE`, + exit 0; live daemon → `401` on the enforcement control, exit 0. +- **`shellcheck` is NOT installed on this host** (`command -v shellcheck` → empty). The + acceptance criterion's shellcheck run was therefore not performed, recorded here rather than + silently skipped. +- Both commits stage exactly one file each by explicit path; `git diff --diff-filter=D` over both + is empty. Three other agents are working in this tree (plans 10-04, 10-06, 01-18) and none of + their files were staged, reverted or modified. + +## Known Stubs + +None in code. The **evidence document is deliberately incomplete** and says so in its first +line — five of its rows are `NOT MEASURED` / `NOT PERFORMED` with the exact command needed to +close each. + +## Audit Checklist Movement + +| Item | Before | After | +|---|---|---| +| C-6 | UNVERIFIED | **still UNVERIFIED** — method corrected, tooling built, 2 non-qualifying vantage points recorded, 3 transports outstanding | +| KEY-01 refusal on hardware | unproven | **still unproven** — blocked on deploying 10-01 | +| Fresh-node onboarding non-regression | unproven | **still unproven** — blocked on the shape-A harness | + +**Nothing moved from UNVERIFIED to VERIFIED in this plan.** What changed is that the remaining +work is now mechanised (one command per transport), correctly specified (the method correction), +and provably blocked on deployment rather than on anything doable in the repository. + +## What Is Still Required + +1. LAN: `rpc-exposure-probe.sh --target --scheme http --port 80 --label lan` **from a second machine**. +2. Tor: `torsocks rpc-exposure-probe.sh --target --label tor`. +3. Mesh: `rpc-exposure-probe.sh --target --label mesh` from a peer node. +4. Deploy `879de59e`+ to a **disposable** node, capture + `sudo sha256sum /var/lib/archipelago/identity/{node_key,nostr_secret}`, run + `--destructive --label refusal` from a second machine, re-capture the digests. Response must + carry `Not supported:` and the digests must match character for character. +5. Build shape (A), walk the wizard end to end on a 10-01 binary (reloading once on the seed + screen to confirm the same 24 words return, and seeing no `Not supported:` / + `Rate limit exceeded`), then re-run step 4 against it. +6. Sweep the remaining fleet for `onboarding.json` complete-true without `user.json`. + +## Threat Flags + +None. No new endpoint, no dependency (T-10-SC did not fire — `curl`, `grep` and `mktemp` are +pre-existing host tools), no schema change. The one new surface is the `--destructive` branch, +which is the plan's own T-10-11 and is mitigated as specified. + +## Commits + +- `527f6023` — `feat(10-02): add read-only-by-default RPC exposure probe (C-6 / KEY-01)` — + `scripts/security/rpc-exposure-probe.sh` only. +- `f2f89b5f` — `docs(10-02): record C-6 evidence so far — probe-method correction, 3 transports still open` — + `docs/security/KEY-01-ON-NODE-VERIFICATION.md` only. + +Not pushed — the orchestrator pushes. + +## Self-Check: PASSED + +Both files exist on disk (`scripts/security/rpc-exposure-probe.sh` mode 755, 296 lines; +`docs/security/KEY-01-ON-NODE-VERIFICATION.md` 224 lines, contains `C-6` and +`rpc-exposure-probe`); both commits `527f6023` and `f2f89b5f` are present in `git log`; the probe +contains `DESTRUCTIVE` (5) and `READONLY_METHODS` (22); neither file contains a node address, +onion address or credential. + +**Not done by this agent, deliberately:** `STATE.md` / `ROADMAP.md` / `REQUIREMENTS.md` updates. +Three other agents are executing concurrently in this shared tree and `.planning/STATE.md` +already carries an uncommitted edit that is not mine; mutating shared planning state here would +entangle their work. Requirements KEY-01 and KEY-04 must **not** be marked complete — this plan +did not close them.