e4dfe5ccc7
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>
84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|