131 lines
8.3 KiB
Markdown
131 lines
8.3 KiB
Markdown
# Phase 10: Key-Material Hardening - Discussion Log
|
|
|
|
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
|
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
|
|
|
**Date:** 2026-08-01
|
|
**Phase:** 10-key-material-hardening
|
|
**Areas discussed:** Re-key policy for a live node, Gate signal, First-boot fail-closed behavior, Fleet scope, Wallet migration, Signing path, Rollout, 10-01 scope
|
|
|
|
---
|
|
|
|
## Re-key policy for a live node (KEY-01)
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Hard refuse; reset first | Endpoints permanently closed once the node holds identity keys; re-key via the authenticated `system.factory-reset`, then normal onboarding restore. No authorization logic on the pre-auth path at all. | ✓ |
|
|
| Allow with an authenticated session | Keep endpoints usable post-onboarding behind a session + password re-entry. Convenient for in-place recovery, but a session-fixation or CSRF bug becomes a node takeover. | |
|
|
| Allow only with physical presence | Local console / button-press window. Strongest guarantee, needs new plumbing, awkward for headless nodes reached over Tailscale. | |
|
|
|
|
**User's choice:** Hard refuse; reset first
|
|
**Notes:** Verified mid-discussion that `system.factory-reset` (`dispatcher.rs:469`) and `auth.resetOnboarding` (`auth.rs:272`) are both absent from `UNAUTHENTICATED_METHODS`, i.e. already authenticated. This made "reset first" a real existing path rather than something the phase would have to build, and confirmed it is not itself a gate bypass.
|
|
|
|
---
|
|
|
|
## Gate signal (KEY-01)
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Refuse if either says onboarded | Check both `key_exists` (on-disk key file) and `is_onboarding_complete` (JSON flag); refuse if either indicates onboarded. Fails safe when they disagree. | ✓ |
|
|
| Key file on disk only | Trust only `key_exists` — the artefact actually being protected, unflippable without filesystem access. | |
|
|
| Onboarding flag only | Trust only `is_onboarding_complete` — matches how the rest of the app reasons, but the flag is writable by `auth.resetOnboarding` and can drift. | |
|
|
|
|
**User's choice:** Refuse if either says onboarded
|
|
**Notes:** The drift case is real, not hypothetical — `auth.rs:196-207` already contains auto-heal logic that infers completion from `setup_complete` + `password_hash` precisely because the flag can be wrong.
|
|
|
|
---
|
|
|
|
## First-boot fail-closed behavior (KEY-02)
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Retry, then fail closed | Retry with backoff; on continued failure refuse to start and show a loud error. Survives transient causes without ever silently shipping fleet-shared keys. | ✓ |
|
|
| Fail closed immediately | First failure refuses to start. Absolutely safe; a transient condition bricks a brand-new node with no self-recovery. | |
|
|
| Boot locked, with a visible alert | Starts but refuses onboarding/network exposure until fixed. Most forgiving; risks a dismissed warning and a node running on shared keys. | |
|
|
|
|
**User's choice:** Retry, then fail closed
|
|
**Notes:** Current behaviour is the inverse of all three options — fail-open, with the completion marker written even when regeneration fails (`build-auto-installer-iso.sh:1647`, `:1659`, `:1663`).
|
|
|
|
---
|
|
|
|
## Fleet scope (KEY-02)
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Fix builder + remediate existing | Fix the ISO builder for future flashes and add boot-time detection + one-time regeneration reaching existing nodes via OTA. | ✓ |
|
|
| Fix the ISO builder only | Future flashes get unique keys; deployed nodes keep what they have. | |
|
|
| Builder now, remediation as its own phase | Ship the builder fix here; scope live-node rotation separately since it can break `known_hosts` and Tailscale-based access. | |
|
|
|
|
**User's choice:** Fix builder + remediate existing
|
|
**Notes:** Chosen as the only option that ends the exposure rather than stopping it from growing. The `known_hosts` / remote-access risk raised in option 3 does not disappear with this choice — it was carried into CONTEXT.md as D-06's `one-way` reversibility rating with a required checkpoint, rather than being deferred.
|
|
|
|
---
|
|
|
|
## Wallet migration (KEY-03)
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Migrate with parity proof | Recreate watch-only (`disable_private_keys=true`, xpub + key origin), verify balance and UTXO-set parity before/after, retain old `wallet.dat` for rollback. | ✓ |
|
|
| New wallets only | Future initialisations are watch-only; existing nodes keep the private key until the user re-creates their wallet. | |
|
|
| Migrate, but user-confirmed per node | Same migration behind an explicit UI opt-in. | |
|
|
|
|
**User's choice:** Migrate with parity proof
|
|
**Notes:** Matches the audit's R-04 and CLAUDE.md's "migrations never destroy data". New-wallets-only was rejected because it leaves the exposure on exactly the nodes holding real funds.
|
|
|
|
---
|
|
|
|
## Signing path (KEY-03)
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Daemon signs PSBTs, air-gap opt-in | Daemon signs with the seed it already holds in the encrypted envelope; send UX unchanged; the spending key exists in one place instead of two. External-signer path stays opt-in. | ✓ |
|
|
| Daemon signs, nothing else | Remove the duplicate copy, one hot signing path, defer all external-signer work. | |
|
|
| External signer required | Core goes watch-only, signing happens off-node. Strongest posture, changes how every user spends, needs hardware. | |
|
|
|
|
**User's choice:** Daemon signs PSBTs, air-gap opt-in
|
|
**Notes:** Because the opt-in path must actually be reachable, the missing `[fingerprint/derivation]` key-origin annotation on the current descriptors (`bitcoin.rs:230-231`) was pulled into scope as D-09 rather than left as a follow-up — without it no hardware signer can locate its key in a PSBT.
|
|
|
|
---
|
|
|
|
## Rollout
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| Emergency point release | Cut a release carrying 10-01 alone once verified on the dev pair. | |
|
|
| Dev pair now, decide after | Deploy and verify, then choose emergency vs scheduled after seeing the real diff. | |
|
|
| Ride the next scheduled OTA | Fold into the next planned release with the rest of Phase 10 and pending 1.7.119 work. | ✓ |
|
|
|
|
**User's choice:** Ride the next scheduled OTA
|
|
**Notes:** Claude recommended the emergency release; the user chose the scheduled OTA. Recorded in CONTEXT.md as D-10 with its consequence stated plainly — the exposure window for a Critical, remotely-reachable finding is now set by OTA cadence rather than by when the fix is verified. D-11 keeps 10-01 independently shippable so this stays a scheduling choice, not a technical constraint, if the timing changes.
|
|
|
|
---
|
|
|
|
## 10-01 scope
|
|
|
|
| Option | Description | Selected |
|
|
|--------|-------------|----------|
|
|
| One gate over all identity-mutating endpoints | Sweep every `UNAUTHENTICATED_METHODS` entry that mutates identity or credentials behind the same gate with one shared test suite. | ✓ |
|
|
| Only seed.generate + seed.restore | Keep 10-01 minimal — exactly the endpoints F-01 names — others get their own plan. | |
|
|
| Gate the mutating ones, document the rest | Gate writers; record an evidence-backed verdict for read-only-ish endpoints. | |
|
|
|
|
**User's choice:** One gate over all identity-mutating endpoints
|
|
**Notes:** The "document the rest" behaviour from option 3 was folded into the chosen option for endpoints that turn out not to mutate — a verdict with evidence rather than a behaviour change.
|
|
|
|
---
|
|
|
|
## Claude's Discretion
|
|
|
|
- Error code / JSON-RPC response shape for a refused call.
|
|
- Rate-limit shape and thresholds (constrained by the retry-storm trap).
|
|
- Test organisation and file placement.
|
|
- Whether the shared gate is middleware-layer, a per-handler helper, or both.
|
|
|
|
## Deferred Ideas
|
|
|
|
- F-04 mnemonic-over-RPC exposure (R-07) — PHASE-sized in its own right.
|
|
- F-05 Argon2 params vs ADR-005 (R-06) — needs a versioned envelope migration or an ADR amendment.
|
|
- F-06 ceremony mnemonic via env/stdout (R-08) — deliberately scheduled separately.
|
|
- F-07 no `cargo audit`/`cargo deny` in CI (R-05).
|
|
- F-09 / F-10 / F-11 low+informational items (R-12, R-13 — blocked on another agent's uncommitted work — and R-14).
|
|
- archi-dev-box as a fresh test node, shapes A and B — sequenced after this phase; shape A is the natural harness for KEY-04.
|