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:
@@ -0,0 +1,83 @@
|
||||
package escrow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ageAvailable() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath("age"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureAge(t *testing.T) {
|
||||
t.Helper()
|
||||
if !ageAvailable() {
|
||||
t.Skip("skipping: the `age` CLI + linux required (runs on the demo/build host)")
|
||||
}
|
||||
if p, err := exec.LookPath("age"); err == nil {
|
||||
ageBinary = p
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentity_InputValidation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if _, err := WrapIdentity(ctx, nil, "R"); err == nil {
|
||||
t.Error("empty bundle must error")
|
||||
}
|
||||
if _, err := WrapIdentity(ctx, []byte("x"), ""); err == nil {
|
||||
t.Error("empty R must error")
|
||||
}
|
||||
if _, err := UnwrapIdentity(ctx, nil, "R"); err == nil {
|
||||
t.Error("empty blob must error")
|
||||
}
|
||||
}
|
||||
|
||||
// Round-trip: a bundle wraps under R and recovers byte-identical (the identity analog of K-escrow).
|
||||
func TestIdentity_RoundTrip(t *testing.T) {
|
||||
ensureAge(t)
|
||||
ctx := context.Background()
|
||||
const R = "throwaway-correct-horse-battery-staple-words"
|
||||
bundle := IdentityBundle{TunnelToken: "eyJhIjoidGVzdCIsInQiOiJ4In0", PBSToken: "felhom@pbs!n100:deadbeefcafe"}
|
||||
|
||||
blob, err := WrapIdentityBundle(ctx, bundle, R)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapIdentityBundle: %v", err)
|
||||
}
|
||||
// the blob is opaque ciphertext, not the bundle.
|
||||
if bytes.Contains(blob, []byte(bundle.TunnelToken)) || bytes.Contains(blob, []byte(bundle.PBSToken)) {
|
||||
t.Fatal("the blob leaks plaintext token bytes — not encrypted")
|
||||
}
|
||||
got, err := UnwrapIdentityBundle(ctx, blob, R)
|
||||
if err != nil {
|
||||
t.Fatalf("UnwrapIdentityBundle: %v", err)
|
||||
}
|
||||
if got != bundle {
|
||||
t.Errorf("recovered bundle = %+v, want %+v", got, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
// Wrong R fails CLOSED — no bundle emitted.
|
||||
func TestIdentity_WrongRFailsClosed(t *testing.T) {
|
||||
ensureAge(t)
|
||||
ctx := context.Background()
|
||||
blob, err := WrapIdentity(ctx, []byte(`{"tunnel_token":"a","pbs_token":"b"}`), "the-correct-code")
|
||||
if err != nil {
|
||||
t.Fatalf("WrapIdentity: %v", err)
|
||||
}
|
||||
if _, err := UnwrapIdentity(ctx, blob, "DEFINITELY-the-wrong-code"); err == nil {
|
||||
t.Fatal("a wrong recovery code must fail closed (no bundle)")
|
||||
}
|
||||
// the blob is unchanged / retryable: the RIGHT code still works after a wrong attempt.
|
||||
if _, err := UnwrapIdentity(ctx, blob, "the-correct-code"); err != nil {
|
||||
t.Errorf("the blob was not retryable after a wrong-R attempt: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user