slice 10C: escrow consumption — productionize the spike (v0.17.0)
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>
This commit is contained in:
@@ -42,7 +42,7 @@ import (
|
||||
|
||||
// version is the agent version. Overridable at build time with
|
||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||
var version = "0.16.0"
|
||||
var version = "0.17.0"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
@@ -62,6 +62,9 @@ func main() {
|
||||
custDomain string
|
||||
custName string
|
||||
custEmail string
|
||||
blobPath string
|
||||
expectedFP string
|
||||
keyDest string
|
||||
showVersion bool
|
||||
)
|
||||
flag.StringVar(&cfgPath, "config", envOr("FELHOM_AGENT_CONFIG", "/etc/felhom-agent/agent.json"), "path to the agent config file (JSON)")
|
||||
@@ -76,6 +79,9 @@ func main() {
|
||||
flag.BoolVar(&paperkey, "paperkey", false, "for --selftest=escrow-create: ALSO emit the raw-key paperkey (opt-in (a); single-factor, unrevocable)")
|
||||
flag.BoolVar(&offline, "offline", false, "for --selftest=escrow-create: ALSO emit the R-wrapped offline copy to print (opt-in (b))")
|
||||
flag.BoolVar(&upload, "upload", false, "for --selftest=escrow-create: upload the opaque blob to the hub")
|
||||
flag.StringVar(&blobPath, "blob", "", "for --selftest=escrow-consume: path to the R-wrapped escrow blob file")
|
||||
flag.StringVar(&expectedFP, "fingerprint", "", "for --selftest=escrow-consume: the EXPECTED key fingerprint (the gate target)")
|
||||
flag.StringVar(&keyDest, "keydest", "", "for --selftest=escrow-consume: where to install the recovered key (0600)")
|
||||
flag.StringVar(&custID, "customer-id", "", "for --selftest=provision: the customer id to seed into the guest's bootstrap")
|
||||
flag.StringVar(&custDomain, "customer-domain", "", "for --selftest=provision: the customer domain to seed")
|
||||
flag.StringVar(&custName, "customer-name", "", "for --selftest=provision: the customer display name to seed (optional)")
|
||||
@@ -126,6 +132,8 @@ func main() {
|
||||
}))
|
||||
case "escrow-create":
|
||||
os.Exit(runSelftestEscrowCreate(context.Background(), cfg, logger, pbsStorage, paperkey, offline, upload))
|
||||
case "escrow-consume":
|
||||
os.Exit(runSelftestEscrowConsume(context.Background(), logger, blobPath, expectedFP, keyDest))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,6 +1131,40 @@ func runSelftestEscrowCreate(ctx context.Context, cfg config.Config, logger *slo
|
||||
return 0
|
||||
}
|
||||
|
||||
// runSelftestEscrowConsume exercises the slice-10C production Consume path live: recover K from an
|
||||
// R-wrapped blob, gate it on the expected fingerprint, install it at -keydest. R is taken BY HAND
|
||||
// from the env var FELHOM_RECOVERY_CODE (kept off the command line / ps) — never a flag, never
|
||||
// logged. The other three inputs (blob/fingerprint/keydest) are flags (10D sources blob+fingerprint
|
||||
// from the hub directive). This is the real Consume code, not a throwaway harness.
|
||||
func runSelftestEscrowConsume(ctx context.Context, logger *slog.Logger, blobPath, expectedFP, keyDest string) int {
|
||||
if blobPath == "" || expectedFP == "" || keyDest == "" {
|
||||
fmt.Fprintln(os.Stderr, "selftest=escrow-consume requires -blob, -fingerprint and -keydest (R via env FELHOM_RECOVERY_CODE)")
|
||||
return 2
|
||||
}
|
||||
R := os.Getenv("FELHOM_RECOVERY_CODE")
|
||||
if R == "" {
|
||||
fmt.Fprintln(os.Stderr, "selftest=escrow-consume: set the recovery code in env FELHOM_RECOVERY_CODE (by-hand input; never a flag/arg)")
|
||||
return 2
|
||||
}
|
||||
blob, err := os.ReadFile(blobPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "selftest=escrow-consume: reading blob %s: %v\n", blobPath, err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("=== felhom-agent %s selftest=escrow-consume (blob=%s → %s) ===\n", version, blobPath, keyDest)
|
||||
logger.Info("escrow: consuming R-wrapped escrow (Unwrap → fingerprint-gate → install)",
|
||||
"blob_bytes", len(blob), "key_dest", keyDest) // R is NOT logged
|
||||
if err := escrow.Consume(ctx, blob, R, expectedFP, keyDest); err != nil {
|
||||
R = "" // drop the reference
|
||||
fmt.Fprintln(os.Stderr, " [FAIL] consume:", err) // the error never contains R or key bytes
|
||||
return 1
|
||||
}
|
||||
R = "" // drop the reference promptly
|
||||
fmt.Printf(" [OK] recovered key installed at %s (fingerprint-gated, 0600) — ready for the PBS restore\n", keyDest)
|
||||
fmt.Println("=== selftest=escrow-consume OK ===")
|
||||
return 0
|
||||
}
|
||||
|
||||
// escrowUploadRequest is the agent→hub wire shape for the opaque escrow blob. MUST stay in lockstep
|
||||
// with the hub's ingest struct (felhom-hub api.escrowUploadRequest). The hub stores the bytes and
|
||||
// never decrypts them.
|
||||
@@ -1519,8 +1561,10 @@ func (f *selftestFlag) Set(v string) error {
|
||||
f.mode = "provision"
|
||||
case "escrow-create":
|
||||
f.mode = "escrow-create"
|
||||
case "escrow-consume":
|
||||
f.mode = "escrow-consume"
|
||||
default:
|
||||
return fmt.Errorf("invalid --selftest value %q (want read|task|hub|storage|backup|restore-test|pbs-verify|bring-up|provision|escrow-create)", v)
|
||||
return fmt.Errorf("invalid --selftest value %q (want read|task|hub|storage|backup|restore-test|pbs-verify|bring-up|provision|escrow-create|escrow-consume)", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user