slice 10D (agent): DR capstone — identity escrow + restore-mode consumption (v0.18.0)

Identity escrow wraps {tunnel_token,pbs_token} under the SAME R via age
(scrypt+ChaCha20-Poly1305), reusing the K-escrow pty; wrong R fails closed.
escrow.Create optionally emits the identity blob; escrow-create uploads it +
the non-secret directive; identity-consume recovers it (R by hand, never
logged). K-escrow + 10C Consume untouched. Closes slice 10 with hub v0.11.0;
operator-side rotation model (hub holds no Cloudflare write-power).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:48:36 +02:00
parent 89e9f98a95
commit e4dfe5ccc7
6 changed files with 362 additions and 53 deletions
+16
View File
@@ -39,6 +39,9 @@ type CreateOptions struct {
// WantPaperkey: opt-in (a) — also return the RAW-key paperkey. Single-factor + unrevocable;
// the caller must surface the loud caveat. Off by default.
WantPaperkey bool
// IdentityBundle (slice 10D.1), when set, is ALSO wrapped under the SAME R (via age) → an
// IdentityBlob in the result. Additive: the K-escrow path is unchanged when nil.
IdentityBundle *IdentityBundle
}
// CreateResult is the non-secret output of escrow creation. NOTE: the recovery code R is returned
@@ -50,6 +53,7 @@ type CreateResult struct {
EntropyBits float64 // R's approximate entropy (for display; never R itself)
OfflineCopy []byte // (b) the same wrapped blob, if WantOfflineCopy (for the customer to print)
Paperkey string // (a) raw paperkey text, if WantPaperkey — SECRET-adjacent (single factor)
IdentityBlob []byte // (10D.1) the age-wrapped identity bundle under the same R, if IdentityBundle set
}
// Create generates a recovery code R, produces the R-wrapped escrow blob from the live key, and
@@ -121,6 +125,18 @@ func Create(ctx context.Context, opts CreateOptions) (recoveryCode string, res C
}
res.Paperkey = pk
}
// Slice 10D.1: ALSO wrap the identity bundle under the SAME R (via age), so DR can recover the
// box's identity with the one recovery code. Self-verify it round-trips before shipping.
if opts.IdentityBundle != nil {
idBlob, err := WrapIdentityBundle(ctx, *opts.IdentityBundle, R)
if err != nil {
return "", CreateResult{}, fmt.Errorf("escrow: identity wrap: %w", err)
}
if _, err := UnwrapIdentityBundle(ctx, idBlob, R); err != nil {
return "", CreateResult{}, fmt.Errorf("escrow: identity self-verify (not recoverable): %w", err)
}
res.IdentityBlob = idBlob
}
return R, res, nil
}