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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user