Add escrow.Consume(blob, R, expectedFingerprint, keyDest): Unwrap -> fingerprint gate -> atomic 0600 install. Bakes in the spike findings — wrong R fails closed (no write), the fingerprint gate runs BEFORE any restore (no install on mismatch), the input blob is read-only (retryable), K is never mutated, R/key bytes never logged. Zero-knowledge holds: the hub serves all but R (by hand). --selftest=escrow-consume invokes the real path live. Agent-only; no hub change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.0 KiB
REPORT — slice 10C: escrow consumption — productionize the spike (v0.17.0) (2026-06-10)
Overwrite-latest report. Cumulative history: CHANGELOG.md.
What was implemented
The 10C spike proved the crypto + real-data restore on a key-less box (recover K from (blob, R),
restore real data, GO). 10C turns that throwaway harness into a real, tested Consume path.
Agent-only — no hub change (10C reads its four inputs as parameters; 10D sources blob/fingerprint/
PBS-connection from the hub and prompts for R).
escrow.Consume(ctx, blob, R, expectedFingerprint, keyDest)
The consumption contract, baking in the spike findings:
- Unwrap the blob (operating on a copy — F-C6: the input blob is read-only, so a failed
Consume is retryable) with
R; a wrong R fails closed at the scrypt KDF (F-C3) → a clear, R-free error, nothing written. - Fingerprint gate (F-C4) —
KeyFingerprint(recovered)must equal the expected (the hub knows it); a mismatch fails fast + loud, no install, no restore attempted (the cheap correctness check before any multi-GB restore). - Atomic install (F-C2) at
keyDest(0600, write-temp-sibling → rename); any failure leaves no partial install. The recovered key lives only in a0700tempdir that is always removed.
Secret discipline: R and key bytes are never logged/persisted (only fingerprint prefixes); the
input blob is read-only; K is never mutated. Zero-knowledge holds: the hub serves the blob +
expected fingerprint + PBS connection; R is by-hand from the customer, never the hub — a hub
compromise alone still cannot decrypt.
--selftest=escrow-consume
-blob -fingerprint -keydest, R via env FELHOM_RECOVERY_CODE (off the command line / ps). Invokes
the real Consume live — the spike's S3 via the production path, not a harness.
Tests (non-hollow)
- valid → key installed +
KeyFingerprint(dest) == expected+0600+ blob byte-unchanged; wrong R → error + no file at dest + blob unchanged; fingerprint mismatch → fail fast + no install (the gate runs before any restore); input validation; format-tolerant fingerprint compare (no empty-fingerprint gate-bypass); atomic-install.go test ./...green (integration round-trip gated to a host withproxmox-backup-client).
Versioning / docs
- Version
0.16.0 → 0.17.0;CHANGELOG.md. Doc 03 §8a (consumption implemented; zero-knowledge) + §9 (10C done) updated. No cross-repo golden (no wire change).
Out of scope (per the task)
- DR orchestration (re-enroll in restore mode, which guests, identity restore) → 10D; populating/serving the restore directive from the hub → 10D.
Pending
- Live validation on the demo: create a fresh escrow from the live
K→Consumeon a key-less scratch → restore a real encrypted backup with the consumed key; wrong R → clean failure, nothing installed; the liveKstays byte-unchanged.