package credentialops import ( "bytes" "context" "crypto/sha256" "database/sql" "encoding/base64" "encoding/hex" "errors" "fmt" "os" "path/filepath" "reflect" "strings" "sync" "testing" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest" "iop/apps/control-plane/internal/credentialseal" "iop/apps/control-plane/internal/credentialstore" ) // fakeSealer is a deterministic test sealer that produces a fixed envelope // from any input. It records each Seal call for assertion. type fakeSealer struct { mu sync.Mutex calls []sealCall envelope credentialstore.SecretEnvelope err error } type sealCall struct { plaintext []byte ctx_ credentialseal.Context } func newFakeSealer(t *testing.T) *fakeSealer { t.Helper() return &fakeSealer{ envelope: credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "fake-key", KeyVersion: 1, Nonce: []byte("fake-nonce-01234"), Ciphertext: []byte("fake-ciphertext-val"), AAD: []byte("fake-aad-val"), }, } } func (f *fakeSealer) Seal(_ context.Context, plaintext []byte, ctx_ credentialseal.Context) (credentialstore.SecretEnvelope, error) { f.mu.Lock() defer f.mu.Unlock() if f.err != nil { return credentialstore.SecretEnvelope{}, f.err } f.calls = append(f.calls, sealCall{plaintext: append([]byte(nil), plaintext...), ctx_: ctx_}) return f.envelope, nil } func (f *fakeSealer) callCount() int { f.mu.Lock() defer f.mu.Unlock() return len(f.calls) } type logCapture struct { mu sync.Mutex buf bytes.Buffer } func (lc *logCapture) Write(p []byte) (n int, err error) { lc.mu.Lock() defer lc.mu.Unlock() return lc.buf.Write(p) } func (lc *logCapture) String() string { lc.mu.Lock() defer lc.mu.Unlock() return lc.buf.String() } func newTestServiceWithLogs(t *testing.T, sealer SecretSealer) (*Service, *credentialstore.Store, *credentialstore.IssuedPrincipal, *logCapture) { t.Helper() ctx := context.Background() reg := credentialstore.EnvelopeKeyRegistry(newFakeKeyRegistryForService()) dbPath := "file:iop_credentialops_test_" + strings.ReplaceAll(t.Name(), "/", "_") + ".db?mode=memory&cache=shared" store, err := credentialstore.Open(ctx, dbPath, credentialstore.WithEnvelopeKeyRegistry(reg)) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { _ = store.Close() }) issued, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "test-principal"}) if err != nil { t.Fatalf("create principal: %v", err) } capture := &logCapture{} core := zapcore.NewCore(zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), zapcore.AddSync(capture), zapcore.DebugLevel) logger := zap.New(core) svc := NewService(store, logger, sealer, nil) return svc, store, issued, capture } func newTestService(t *testing.T, sealer SecretSealer) (*Service, *credentialstore.Store, *credentialstore.IssuedPrincipal) { t.Helper() svc, store, issued, _ := newTestServiceWithLogs(t, sealer) return svc, store, issued } type fakeKeyRegistryForService struct { keys map[string]map[uint64]bool } func newFakeKeyRegistryForService() *fakeKeyRegistryForService { f := &fakeKeyRegistryForService{keys: make(map[string]map[uint64]bool)} f.RegisterKey("fake-key", 1) return f } func (f *fakeKeyRegistryForService) RegisterKey(keyID string, version uint64) { if f.keys[keyID] == nil { f.keys[keyID] = make(map[uint64]bool) } f.keys[keyID][version] = true } func (f *fakeKeyRegistryForService) HasEnvelopeKey(_ context.Context, keyID string, keyVersion uint64) (bool, error) { if versions, ok := f.keys[keyID]; ok { return versions[keyVersion], nil } return false, nil } var _ credentialstore.EnvelopeKeyRegistry = (*fakeKeyRegistryForService)(nil) func digestOf(raw []byte) string { sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) } // --------------------------------------------------------------------------- // API-1: Own-principal lifecycle & Create/Rotate Secret Handoff // --------------------------------------------------------------------------- func TestServiceCreateAndRotatePassSuppliedSecretOnlyToSealer(t *testing.T) { fake := newFakeSealer(t) svc, store, issued := newTestService(t, fake) ctx := context.Background() secret1 := []byte("provider-secret-payload-alpha") created, err := svc.CreateSlot(ctx, []byte(issued.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "create-slot-1", ProviderSecret: secret1, }) if err != nil { t.Fatalf("CreateSlot: %v", err) } if created.Alias != "create-slot-1" { t.Fatalf("CreateSlot alias mismatch: got %q want create-slot-1", created.Alias) } if created.Status != credentialstore.StatusDraft { t.Fatalf("CreateSlot initial status: got %q want draft", created.Status) } if fake.callCount() != 1 { t.Fatalf("sealer call count after create: got %d want 1", fake.callCount()) } if string(fake.calls[0].plaintext) != "provider-secret-payload-alpha" { t.Fatalf("sealer received plaintext mismatch: got %q", string(fake.calls[0].plaintext)) } if fake.calls[0].ctx_.PrincipalID != issued.Principal.ID { t.Fatalf("sealer ctx PrincipalID mismatch: got %q want %q", fake.calls[0].ctx_.PrincipalID, issued.Principal.ID) } storedSlot, err := store.GetSlot(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("GetSlot from store: %v", err) } if string(storedSlot.Envelope.Ciphertext) != "fake-ciphertext-val" { t.Fatalf("stored envelope ciphertext mismatch: got %q", string(storedSlot.Envelope.Ciphertext)) } _, err = svc.CreateRoute(ctx, []byte(issued.RawToken), CreateRouteInput{ SlotID: created.ID, Alias: "route-1", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if err != nil { t.Fatalf("CreateRoute: %v", err) } activeSlot, err := svc.GetSlot(ctx, []byte(issued.RawToken), created.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } secret2 := []byte("provider-secret-payload-beta") rotated, err := svc.RotateSlot(ctx, []byte(issued.RawToken), RotateSlotInput{ SlotID: created.ID, Revision: activeSlot.Revision, ProviderSecret: secret2, }) if err != nil { t.Fatalf("RotateSlot: %v", err) } if rotated.Revision != activeSlot.Revision+1 { t.Fatalf("RotateSlot revision: got %d want %d", rotated.Revision, activeSlot.Revision+1) } if fake.callCount() != 2 { t.Fatalf("sealer call count after rotate: got %d want 2", fake.callCount()) } if string(fake.calls[1].plaintext) != "provider-secret-payload-beta" { t.Fatalf("sealer received plaintext mismatch on rotate: got %q", string(fake.calls[1].plaintext)) } if fake.calls[1].ctx_.SlotID != created.ID { t.Fatalf("sealer ctx SlotID mismatch: got %q want %q", fake.calls[1].ctx_.SlotID, created.ID) } } func TestServiceScopesOperationsToAuthenticatedPrincipal(t *testing.T) { svc, _, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() slot, err := svc.CreateSlot(ctx, []byte(issued.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "lifecycle-slot", ProviderSecret: []byte("secret-data"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } route, err := svc.CreateRoute(ctx, []byte(issued.RawToken), CreateRouteInput{ SlotID: slot.ID, Alias: "lifecycle-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if err != nil { t.Fatalf("CreateRoute: %v", err) } _ = route activeSlot, err := svc.GetSlot(ctx, []byte(issued.RawToken), slot.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } if activeSlot.Status != credentialstore.StatusActive { t.Fatalf("GetSlot status: got %q want active", activeSlot.Status) } disabled, err := svc.DisableSlot(ctx, []byte(issued.RawToken), slot.ID, activeSlot.Revision) if err != nil { t.Fatalf("DisableSlot: %v", err) } if disabled.Status != credentialstore.StatusDisabled { t.Fatalf("DisableSlot status: got %q want disabled", disabled.Status) } enabled, err := svc.EnableSlot(ctx, []byte(issued.RawToken), slot.ID, disabled.Revision) if err != nil { t.Fatalf("EnableSlot: %v", err) } if enabled.Status != credentialstore.StatusActive { t.Fatalf("EnableSlot status: got %q want active", enabled.Status) } rotated, err := svc.RotateSlot(ctx, []byte(issued.RawToken), RotateSlotInput{ SlotID: slot.ID, Revision: enabled.Revision, ProviderSecret: []byte("new-secret-data"), }) if err != nil { t.Fatalf("RotateSlot: %v", err) } if rotated.Revision != enabled.Revision+1 { t.Fatalf("RotateSlot revision: got %d want %d", rotated.Revision, enabled.Revision+1) } revoked, err := svc.RevokeSlot(ctx, []byte(issued.RawToken), slot.ID, rotated.Revision) if err != nil { t.Fatalf("RevokeSlot: %v", err) } if revoked.Status != credentialstore.StatusRevoked { t.Fatalf("RevokeSlot status: got %q want revoked", revoked.Status) } if revoked.RevokedAt == nil { t.Fatal("RevokeSlot must set RevokedAt") } slots, err := svc.ListSlots(ctx, []byte(issued.RawToken)) if err != nil { t.Fatalf("ListSlots: %v", err) } if len(slots) != 1 { t.Fatalf("ListSlots count: got %d want 1", len(slots)) } if slots[0].Status != credentialstore.StatusRevoked { t.Fatalf("ListSlots status: got %q want revoked", slots[0].Status) } } func TestServiceTokenCreateListLifecycle(t *testing.T) { svc, _, issued1 := newTestService(t, newFakeSealer(t)) ctx := context.Background() token1 := []byte(issued1.RawToken) tok2, err := svc.CreateToken(ctx, token1) if err != nil { t.Fatalf("CreateToken 2: %v", err) } if tok2.RawToken == "" { t.Fatal("CreateToken must return raw token once") } tok3, err := svc.CreateToken(ctx, token1) if err != nil { t.Fatalf("CreateToken 3: %v", err) } if tok3.RawToken == "" { t.Fatal("CreateToken must return raw token once") } tokens, err := svc.ListTokens(ctx, token1) if err != nil { t.Fatalf("ListTokens: %v", err) } if len(tokens) != 3 { t.Fatalf("ListTokens count: got %d want 3", len(tokens)) } for _, tok := range tokens { if tok.Status != credentialstore.StatusActive { t.Fatalf("token status: got %q want active", tok.Status) } } disabledTok2, err := svc.DisableToken(ctx, token1, tok2.Token.TokenRef, tok2.Token.Revision) if err != nil { t.Fatalf("DisableToken tok2: %v", err) } if disabledTok2.Status != credentialstore.StatusDisabled { t.Fatalf("DisableToken status: got %q want disabled", disabledTok2.Status) } _, err = svc.DisableToken(ctx, token1, tok2.Token.TokenRef, tok2.Token.Revision) if !errors.Is(err, ErrStaleRevision) { t.Fatalf("expected ErrStaleRevision for stale DisableToken, got: %v", err) } revokedTok2, err := svc.RevokeToken(ctx, token1, tok2.Token.TokenRef, disabledTok2.Revision) if err != nil { t.Fatalf("RevokeToken tok2: %v", err) } if revokedTok2.Status != credentialstore.StatusRevoked { t.Fatalf("RevokeToken status: got %q want revoked", revokedTok2.Status) } if revokedTok2.RevokedAt == nil { t.Fatal("RevokeToken must set RevokedAt") } _, err = svc.DisableToken(ctx, token1, issued1.Token.TokenRef, issued1.Token.Revision) if err != nil { t.Fatalf("DisableToken tok1: %v", err) } _, err = svc.ListSlots(ctx, token1) if !errors.Is(err, ErrUnauthorized) { t.Fatalf("expected ErrUnauthorized using disabled tok1, got: %v", err) } slotsTok3, err := svc.ListSlots(ctx, []byte(tok3.RawToken)) if err != nil { t.Fatalf("ListSlots using active tok3: %v", err) } if len(slotsTok3) != 0 { t.Fatalf("expected 0 slots, got %d", len(slotsTok3)) } } func TestServiceRejectsCrossPrincipalTokenReferences(t *testing.T) { svc, store, issued1 := newTestService(t, newFakeSealer(t)) ctx := context.Background() issued2, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "p2-principal"}) if err != nil { t.Fatalf("create principal 2: %v", err) } token1 := []byte(issued1.RawToken) token2 := []byte(issued2.RawToken) tok1_2, err := svc.CreateToken(ctx, token1) if err != nil { t.Fatalf("CreateToken for p1: %v", err) } _, err = svc.DisableToken(ctx, token2, tok1_2.Token.TokenRef, tok1_2.Token.Revision) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal DisableToken, got: %v", err) } _, err = svc.RevokeToken(ctx, token2, tok1_2.Token.TokenRef, tok1_2.Token.Revision) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal RevokeToken, got: %v", err) } } func TestServiceRouteCreateUpdateLifecycle(t *testing.T) { svc, _, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() token := []byte(issued.RawToken) slot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "route-slot", ProviderSecret: []byte("secret-payload"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } route, err := svc.CreateRoute(ctx, token, CreateRouteInput{ SlotID: slot.ID, Alias: "route-alias", ProfileID: "openai", UpstreamModel: "gpt-4o", ResourceSelector: "default", }) if err != nil { t.Fatalf("CreateRoute: %v", err) } if route.Alias != "route-alias" { t.Fatalf("Route alias mismatch: got %q want route-alias", route.Alias) } updatedRoute, err := svc.UpdateRoute(ctx, token, UpdateRouteInput{ RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: "updated-route-alias", ProfileID: "openai", UpstreamModel: "gpt-4o-mini", ResourceSelector: "default", }) if err != nil { t.Fatalf("UpdateRoute: %v", err) } if updatedRoute.Alias != "updated-route-alias" { t.Fatalf("Updated route alias: got %q want updated-route-alias", updatedRoute.Alias) } if updatedRoute.UpstreamModel != "gpt-4o-mini" { t.Fatalf("Updated route model: got %q want gpt-4o-mini", updatedRoute.UpstreamModel) } if updatedRoute.Revision != route.Revision+1 { t.Fatalf("Updated route revision: got %d want %d", updatedRoute.Revision, route.Revision+1) } _, err = svc.UpdateRoute(ctx, token, UpdateRouteInput{ RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: "stale-update", ProfileID: "openai", UpstreamModel: "gpt-4o", ResourceSelector: "default", }) if !errors.Is(err, ErrStaleRevision) { t.Fatalf("expected ErrStaleRevision on UpdateRoute with stale rev, got: %v", err) } } // --------------------------------------------------------------------------- // API-1: Cross-principal denial // --------------------------------------------------------------------------- func TestServiceRejectsCrossPrincipalTargets(t *testing.T) { svc, store, issued1 := newTestService(t, newFakeSealer(t)) ctx := context.Background() issued2, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "other-principal"}) if err != nil { t.Fatalf("create second principal: %v", err) } token1 := []byte(issued1.RawToken) token2 := []byte(issued2.RawToken) slot, err := svc.CreateSlot(ctx, token1, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "cross-slot", ProviderSecret: []byte("p1-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } route, err := svc.CreateRoute(ctx, token1, CreateRouteInput{ SlotID: slot.ID, Alias: "cross-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if err != nil { t.Fatalf("CreateRoute: %v", err) } _, err = svc.GetSlot(ctx, token2, slot.ID) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal GetSlot, got: %v", err) } _, err = svc.DisableSlot(ctx, token2, slot.ID, slot.Revision) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal DisableSlot, got: %v", err) } _, err = svc.RotateSlot(ctx, token2, RotateSlotInput{SlotID: slot.ID, Revision: slot.Revision, ProviderSecret: []byte("hack")}) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal RotateSlot, got: %v", err) } slots, err := svc.ListSlots(ctx, token2) if err != nil { t.Fatalf("ListSlots for other principal: %v", err) } if len(slots) != 0 { t.Fatalf("expected empty list for cross-principal, got %d slots", len(slots)) } _, err = svc.GetRoute(ctx, token2, route.ID) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal GetRoute, got: %v", err) } _, err = svc.UpdateRoute(ctx, token2, UpdateRouteInput{ RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: "p2-alias", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal UpdateRoute, got: %v", err) } _, err = svc.CreateRoute(ctx, token2, CreateRouteInput{ SlotID: slot.ID, Alias: "cross-bind", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for cross-principal CreateRoute targeting p1 slot, got: %v", err) } } // --------------------------------------------------------------------------- // API-1: Stale revision conflict // --------------------------------------------------------------------------- func TestServiceRejectsStaleRevision(t *testing.T) { svc, _, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() token := []byte(issued.RawToken) slot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "stale-slot", ProviderSecret: []byte("stale-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } route, err := svc.CreateRoute(ctx, token, CreateRouteInput{ SlotID: slot.ID, Alias: "stale-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if err != nil { t.Fatalf("CreateRoute: %v", err) } activeSlot, err := svc.GetSlot(ctx, token, slot.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } disabledSlot, err := svc.DisableSlot(ctx, token, slot.ID, activeSlot.Revision) if err != nil { t.Fatalf("first disable: %v", err) } _, err = svc.DisableSlot(ctx, token, slot.ID, activeSlot.Revision) if !errors.Is(err, ErrStaleRevision) { t.Fatalf("expected ErrStaleRevision on second DisableSlot, got: %v", err) } _, err = svc.RotateSlot(ctx, token, RotateSlotInput{SlotID: slot.ID, Revision: activeSlot.Revision, ProviderSecret: []byte("new-sec")}) if !errors.Is(err, ErrStaleRevision) { t.Fatalf("expected ErrStaleRevision on RotateSlot, got: %v", err) } enabledSlot, err := svc.EnableSlot(ctx, token, slot.ID, disabledSlot.Revision) if err != nil { t.Fatalf("EnableSlot: %v", err) } updatedRoute, err := svc.UpdateRoute(ctx, token, UpdateRouteInput{ RouteID: route.ID, CurrentRevision: route.Revision, SlotID: enabledSlot.ID, Alias: "updated-route", ProfileID: "openai", UpstreamModel: "gpt-4o-mini", }) if err != nil { t.Fatalf("first UpdateRoute: %v", err) } _ = updatedRoute _, err = svc.UpdateRoute(ctx, token, UpdateRouteInput{ RouteID: route.ID, CurrentRevision: route.Revision, SlotID: enabledSlot.ID, Alias: "stale-update", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if !errors.Is(err, ErrStaleRevision) { t.Fatalf("expected ErrStaleRevision on UpdateRoute with stale rev, got: %v", err) } } // --------------------------------------------------------------------------- // API-1: Missing sealer failure (fail-closed) // --------------------------------------------------------------------------- func TestServiceRequiresSealerForSecretMutation(t *testing.T) { ctx := context.Background() reg := credentialstore.EnvelopeKeyRegistry(newFakeKeyRegistryForService()) dbPath := "file:iop_credentialops_nosealer_" + strings.ReplaceAll(t.Name(), "/", "_") + ".db?mode=memory&cache=shared" store, err := credentialstore.Open(ctx, dbPath, credentialstore.WithEnvelopeKeyRegistry(reg)) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { _ = store.Close() }) issued, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "nosealer-principal"}) if err != nil { t.Fatalf("create principal: %v", err) } capture := &logCapture{} core := zapcore.NewCore(zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), zapcore.AddSync(capture), zapcore.DebugLevel) logger := zap.New(core) svcWithoutSealer := NewService(store, logger, nil, nil) _, err = svcWithoutSealer.CreateSlot(ctx, []byte(issued.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "nosealer-slot", ProviderSecret: []byte("nosealer-secret"), }) if !errors.Is(err, ErrSealerUnavailable) { t.Fatalf("expected ErrSealerUnavailable on CreateSlot without sealer, got: %v", err) } slots, err := store.ListSlots(ctx, issued.Principal.ID) if err != nil { t.Fatalf("ListSlots: %v", err) } if len(slots) != 0 { t.Fatalf("expected 0 slots after failed CreateSlot, got %d", len(slots)) } svcWithSealer := NewService(store, logger, newFakeSealer(t), nil) slot, err := svcWithSealer.CreateSlot(ctx, []byte(issued.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "sealer-slot", ProviderSecret: []byte("sealer-secret"), }) if err != nil { t.Fatalf("CreateSlot with sealer: %v", err) } _, err = svcWithoutSealer.RotateSlot(ctx, []byte(issued.RawToken), RotateSlotInput{ SlotID: slot.ID, Revision: slot.Revision, ProviderSecret: []byte("new-secret"), }) if !errors.Is(err, ErrSealerUnavailable) { t.Fatalf("expected ErrSealerUnavailable on RotateSlot without sealer, got: %v", err) } slotAfter, err := store.GetSlot(ctx, issued.Principal.ID, slot.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } if slotAfter.Revision != slot.Revision { t.Fatalf("revision changed after failed RotateSlot: got %d want %d", slotAfter.Revision, slot.Revision) } } // --------------------------------------------------------------------------- // API-1: Secret-blind responses & Redaction // --------------------------------------------------------------------------- func TestServiceResponsesAreSecretBlind(t *testing.T) { if _, ok := reflect.TypeOf(SlotRecord{}).FieldByName("Envelope"); ok { t.Fatal("secret-blind SlotRecord must not expose an envelope") } rawIOPTokenSentinel := "RAW_IOP_TOKEN_SENTINEL_998877" providerSecretSentinel := "PROVIDER_SECRET_SENTINEL_665544" ciphertextSentinel := "CIPHERTEXT_SENTINEL_332211" nonceSentinel := "NONCE_SENTINEL_009988" aadSentinel := "AAD_SENTINEL_776655" digestSentinel := digestOf([]byte(rawIOPTokenSentinel)) fake := &fakeSealer{ envelope: credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "fake-key", KeyVersion: 1, Nonce: []byte(nonceSentinel), Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), }, } svc, store, issued, capture := newTestServiceWithLogs(t, fake) ctx := context.Background() // Direct DB transaction to set digest sentinel on token row for auth test setup. err := store.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error { _, err := tx.ExecContext(ctx, "UPDATE tokens SET digest=?, token_ref=? WHERE principal_id=?", digestSentinel, tokenRefForSentinel(digestSentinel), issued.Principal.ID) return err }) if err != nil { t.Fatalf("update token digest sentinel: %v", err) } token := []byte(rawIOPTokenSentinel) slot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "blind-slot", ProviderSecret: []byte(providerSecretSentinel), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } route, err := svc.CreateRoute(ctx, token, CreateRouteInput{ SlotID: slot.ID, Alias: "blind-route", ProfileID: "openai", UpstreamModel: "gpt-4o", }) if err != nil { t.Fatalf("CreateRoute: %v", err) } activeSlot, err := svc.GetSlot(ctx, token, slot.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } rotatedSlot, err := svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: slot.ID, Revision: activeSlot.Revision, ProviderSecret: []byte("SECOND_SECRET_SENTINEL"), }) if err != nil { t.Fatalf("RotateSlot: %v", err) } gotSlot, err := svc.GetSlot(ctx, token, slot.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } listedSlots, err := svc.ListSlots(ctx, token) if err != nil { t.Fatalf("ListSlots: %v", err) } gotRoute, err := svc.GetRoute(ctx, token, route.ID) if err != nil { t.Fatalf("GetRoute: %v", err) } listedRoutes, err := svc.ListRoutes(ctx, token) if err != nil { t.Fatalf("ListRoutes: %v", err) } _, errNotFound := svc.GetSlot(ctx, token, "nonexistent-slot") var rendered strings.Builder rendered.WriteString(fmt.Sprintf("%+v\n", slot)) rendered.WriteString(fmt.Sprintf("%+v\n", rotatedSlot)) rendered.WriteString(fmt.Sprintf("%+v\n", gotSlot)) rendered.WriteString(fmt.Sprintf("%+v\n", listedSlots)) rendered.WriteString(fmt.Sprintf("%+v\n", gotRoute)) rendered.WriteString(fmt.Sprintf("%+v\n", listedRoutes)) if errNotFound != nil { rendered.WriteString(errNotFound.Error() + "\n") } rendered.WriteString(capture.String() + "\n") renderedStr := rendered.String() for _, forbidden := range []string{rawIOPTokenSentinel, providerSecretSentinel, ciphertextSentinel, nonceSentinel, aadSentinel, digestSentinel, "SECOND_SECRET_SENTINEL"} { if strings.Contains(renderedStr, forbidden) { t.Fatalf("rendered responses/errors/logs contains sensitive sentinel %q", forbidden) } } } func tokenRefForSentinel(digest string) string { if len(digest) > 16 { return "tok_" + digest[:16] } return "tok_" + digest } func TestServiceRedact(t *testing.T) { svc, _, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() token := []byte(issued.RawToken) _, err := svc.GetSlot(ctx, token, "nonexistent") if err == nil { t.Fatal("expected error") } if strings.Contains(err.Error(), issued.RawToken) { t.Fatalf("error leaks raw token: %v", err) } } type retainingSealer struct { retained []byte envelope credentialstore.SecretEnvelope err error } func (r *retainingSealer) Seal(_ context.Context, plaintext []byte, _ credentialseal.Context) (credentialstore.SecretEnvelope, error) { r.retained = plaintext return r.envelope, r.err } func TestServiceZeroizesOwnedProviderSecret(t *testing.T) { ctx := context.Background() validEnv := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "fake-key", KeyVersion: 1, Nonce: []byte("fake-nonce-01234"), Ciphertext: []byte("fake-ciphertext-val"), } t.Run("success paths zeroize owned and preserve caller", func(t *testing.T) { retSealer := &retainingSealer{envelope: validEnv} svc, _, issued := newTestService(t, retSealer) token := []byte(issued.RawToken) secret := []byte("super-secret-provider-key") secretCopy := append([]byte(nil), secret...) slot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "zeroize-slot", ProviderSecret: secret, }) if err != nil { t.Fatalf("CreateSlot: %v", err) } if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext slice") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d is non-zero (%d), expected zeroization", i, b) } } if !bytes.Equal(secret, secretCopy) { t.Fatalf("caller secret buffer mutated on CreateSlot: got %q want %q", secret, secretCopy) } retSealer.retained = nil rotateSecret := []byte("rotated-super-secret-key") rotateCopy := append([]byte(nil), rotateSecret...) _, err = svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: slot.ID, Revision: slot.Revision, ProviderSecret: rotateSecret, }) if err != nil { t.Fatalf("RotateSlot: %v", err) } if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext slice on rotate") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d on rotate is non-zero (%d), expected zeroization", i, b) } } if !bytes.Equal(rotateSecret, rotateCopy) { t.Fatalf("caller rotate secret buffer mutated on RotateSlot: got %q want %q", rotateSecret, rotateCopy) } }) t.Run("failing paths zeroize owned and preserve caller", func(t *testing.T) { retSealer := &retainingSealer{ envelope: validEnv, err: errors.New("sealer error"), } svc, _, issued := newTestService(t, retSealer) token := []byte(issued.RawToken) failCreateSecret := []byte("fail-create-secret") failCreateCopy := append([]byte(nil), failCreateSecret...) _, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "fail-create-slot", ProviderSecret: failCreateSecret, }) if err == nil { t.Fatal("expected error on CreateSlot with failing sealer") } if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext slice on failing CreateSlot") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d on failing CreateSlot is non-zero (%d), expected zeroization", i, b) } } if !bytes.Equal(failCreateSecret, failCreateCopy) { t.Fatalf("caller secret buffer mutated on failing CreateSlot: got %q want %q", failCreateSecret, failCreateCopy) } retSealer.err = nil retSealer.retained = nil validSlot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "valid-slot-for-rotate", ProviderSecret: []byte("valid-secret"), }) if err != nil { t.Fatalf("CreateSlot for valid slot: %v", err) } retSealer.err = errors.New("rotate sealer error") retSealer.retained = nil failRotateSecret := []byte("fail-rotate-secret") failRotateCopy := append([]byte(nil), failRotateSecret...) _, err = svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: validSlot.ID, Revision: validSlot.Revision, ProviderSecret: failRotateSecret, }) if err == nil { t.Fatal("expected error on RotateSlot with failing sealer") } if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext slice on failing RotateSlot") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d on failing RotateSlot is non-zero (%d), expected zeroization", i, b) } } if !bytes.Equal(failRotateSecret, failRotateCopy) { t.Fatalf("caller secret buffer mutated on failing RotateSlot: got %q want %q", failRotateSecret, failRotateCopy) } }) } func TestServiceSecretHandlingFailureIsBlindAndDoesNotMutate(t *testing.T) { ctx := context.Background() plaintextSentinel := "REVIEWER_PLAINTEXT_SECRET_SENTINEL_12345" errorSentinel := "REVIEWER_SEALER_ERROR_SENTINEL_67890" keyIDSentinel := "REVIEWER_KEY_ID_SENTINEL_UNKNOWN_999" keyVersionSentinel := uint64(9998877) keyVersionSentinelStr := "9998877" ciphertextSentinel := "REVIEWER_CIPHERTEXT_SENTINEL_112233" nonceSentinel := "REVIEWER_NONCE_SENTINEL_445566" aadSentinel := "REVIEWER_AAD_SENTINEL_778899" forbiddenSentinels := []string{ plaintextSentinel, errorSentinel, keyIDSentinel, keyVersionSentinelStr, ciphertextSentinel, nonceSentinel, aadSentinel, } validEnv := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: "fake-key", KeyVersion: 1, Nonce: []byte("fake-nonce-01234"), Ciphertext: []byte("fake-ciphertext-val"), } mustGetSlot := func(t *testing.T, store *credentialstore.Store, pid, slotID string) *credentialstore.CredentialSlotRecord { t.Helper() s, err := store.GetSlot(ctx, pid, slotID) if err != nil { t.Fatalf("GetSlot: %v", err) } return s } mustListSlots := func(t *testing.T, store *credentialstore.Store, pid string) []credentialstore.CredentialSlotRecord { t.Helper() slots, err := store.ListSlots(ctx, pid) if err != nil { t.Fatalf("ListSlots: %v", err) } return slots } mustListSlotRevisions := func(t *testing.T, store *credentialstore.Store, pid, slotID string) []credentialstore.SlotRevisionRecord { t.Helper() revs, err := store.ListSlotRevisions(ctx, pid, slotID) if err != nil { t.Fatalf("ListSlotRevisions: %v", err) } return revs } mustProjectionGeneration := func(t *testing.T, store *credentialstore.Store) uint64 { t.Helper() gen, err := store.ProjectionGeneration(ctx) if err != nil { t.Fatalf("ProjectionGeneration: %v", err) } return gen } assertBlindAndNoSentinels := func(t *testing.T, err error, capture fmt.Stringer) { t.Helper() if !errors.Is(err, ErrSecretHandlingFailed) { t.Fatalf("expected ErrSecretHandlingFailed, got: %v", err) } errMsg := "" if err != nil { errMsg = err.Error() } logMsg := "" if capture != nil { logMsg = capture.String() } for _, s := range forbiddenSentinels { if s == "" { continue } if strings.Contains(errMsg, s) { t.Fatalf("error leaks sensitive sentinel %q: %v", s, err) } if strings.Contains(logMsg, s) { t.Fatalf("logs leak sensitive sentinel %q: %s", s, logMsg) } } } t.Run("CreateSlot failing sealer", func(t *testing.T) { failingEnvWithSentinels := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: keyIDSentinel, KeyVersion: keyVersionSentinel, Nonce: []byte(nonceSentinel), Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), } retSealer := &retainingSealer{ envelope: failingEnvWithSentinels, err: fmt.Errorf("sealer failed with sensitive error %s, plaintext %s, key %s, ver %d, nonce %s, cipher %s, aad %s", errorSentinel, plaintextSentinel, keyIDSentinel, keyVersionSentinel, nonceSentinel, ciphertextSentinel, aadSentinel), } svc, store, issued, capture := newTestServiceWithLogs(t, retSealer) token := []byte(issued.RawToken) genBefore := mustProjectionGeneration(t, store) slotsBefore := mustListSlots(t, store, issued.Principal.ID) callerSecret := []byte("secret-payload-" + plaintextSentinel) callerCopy := append([]byte(nil), callerSecret...) _, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "failing-slot", ProviderSecret: callerSecret, }) assertBlindAndNoSentinels(t, err, capture) if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d is non-zero (%d)", i, b) } } if !bytes.Equal(callerSecret, callerCopy) { t.Fatalf("caller secret buffer mutated on failed CreateSlot") } slotsAfter := mustListSlots(t, store, issued.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) { t.Fatalf("slots mutated on failed CreateSlot: got %+v want %+v", slotsAfter, slotsBefore) } genAfter := mustProjectionGeneration(t, store) if genAfter != genBefore { t.Fatalf("ProjectionGeneration changed on failed CreateSlot: got %d want %d", genAfter, genBefore) } }) t.Run("RotateSlot failing sealer", func(t *testing.T) { retSealer := &retainingSealer{envelope: validEnv} svc, store, issued, capture := newTestServiceWithLogs(t, retSealer) token := []byte(issued.RawToken) validSlot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "valid-slot-for-rotate-failure", ProviderSecret: []byte("initial-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } genBefore := mustProjectionGeneration(t, store) slotBefore := mustGetSlot(t, store, issued.Principal.ID, validSlot.ID) historyBefore := mustListSlotRevisions(t, store, issued.Principal.ID, validSlot.ID) slotsBefore := mustListSlots(t, store, issued.Principal.ID) failingEnvWithSentinels := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: keyIDSentinel, KeyVersion: keyVersionSentinel, Nonce: []byte(nonceSentinel), Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), } retSealer.envelope = failingEnvWithSentinels retSealer.err = fmt.Errorf("sealer rotate failed with sensitive error %s, plaintext %s, key %s, ver %d, nonce %s, cipher %s, aad %s", errorSentinel, plaintextSentinel, keyIDSentinel, keyVersionSentinel, nonceSentinel, ciphertextSentinel, aadSentinel) retSealer.retained = nil callerSecret := []byte("rotate-payload-" + plaintextSentinel) callerCopy := append([]byte(nil), callerSecret...) _, err = svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: validSlot.ID, Revision: validSlot.Revision, ProviderSecret: callerSecret, }) assertBlindAndNoSentinels(t, err, capture) if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext on failing RotateSlot") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d on failing RotateSlot is non-zero (%d)", i, b) } } if !bytes.Equal(callerSecret, callerCopy) { t.Fatalf("caller secret buffer mutated on failed RotateSlot") } slotAfter := mustGetSlot(t, store, issued.Principal.ID, validSlot.ID) if !reflect.DeepEqual(slotAfter, slotBefore) { t.Fatalf("slot mutated on failed RotateSlot: got %+v want %+v", slotAfter, slotBefore) } historyAfter := mustListSlotRevisions(t, store, issued.Principal.ID, validSlot.ID) if !reflect.DeepEqual(historyAfter, historyBefore) { t.Fatalf("slot revision history mutated on failed RotateSlot: got %+v want %+v", historyAfter, historyBefore) } slotsAfter := mustListSlots(t, store, issued.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) { t.Fatalf("slots mutated on failed RotateSlot: got %+v want %+v", slotsAfter, slotsBefore) } genAfter := mustProjectionGeneration(t, store) if genAfter != genBefore { t.Fatalf("ProjectionGeneration mutated on failed RotateSlot: got %d want %d", genAfter, genBefore) } }) t.Run("CreateSlot unknown key ID", func(t *testing.T) { unknownEnv := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: keyIDSentinel, KeyVersion: keyVersionSentinel, Nonce: []byte(nonceSentinel), Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), } retSealer := &retainingSealer{envelope: unknownEnv} svc, store, issued, capture := newTestServiceWithLogs(t, retSealer) token := []byte(issued.RawToken) genBefore := mustProjectionGeneration(t, store) slotsBefore := mustListSlots(t, store, issued.Principal.ID) callerSecret := []byte("secret-payload-" + plaintextSentinel) callerCopy := append([]byte(nil), callerSecret...) _, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "unknown-key-slot", ProviderSecret: callerSecret, }) assertBlindAndNoSentinels(t, err, capture) if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext on unknown key CreateSlot") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d is non-zero (%d)", i, b) } } if !bytes.Equal(callerSecret, callerCopy) { t.Fatalf("caller secret buffer mutated on unknown key CreateSlot") } slotsAfter := mustListSlots(t, store, issued.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) { t.Fatalf("slots mutated on unknown key CreateSlot: got %+v want %+v", slotsAfter, slotsBefore) } genAfter := mustProjectionGeneration(t, store) if genAfter != genBefore { t.Fatalf("ProjectionGeneration changed on unknown key CreateSlot: got %d want %d", genAfter, genBefore) } }) t.Run("RotateSlot unknown key ID", func(t *testing.T) { retSealer := &retainingSealer{envelope: validEnv} svc, store, issued, capture := newTestServiceWithLogs(t, retSealer) token := []byte(issued.RawToken) validSlot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "valid-slot-for-unknown-key-rotate", ProviderSecret: []byte("initial-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } genBefore := mustProjectionGeneration(t, store) slotBefore := mustGetSlot(t, store, issued.Principal.ID, validSlot.ID) historyBefore := mustListSlotRevisions(t, store, issued.Principal.ID, validSlot.ID) slotsBefore := mustListSlots(t, store, issued.Principal.ID) unknownEnv := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: keyIDSentinel, KeyVersion: keyVersionSentinel, Nonce: []byte(nonceSentinel), Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), } retSealer.envelope = unknownEnv retSealer.retained = nil callerSecret := []byte("rotate-payload-" + plaintextSentinel) callerCopy := append([]byte(nil), callerSecret...) _, err = svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: validSlot.ID, Revision: validSlot.Revision, ProviderSecret: callerSecret, }) assertBlindAndNoSentinels(t, err, capture) if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext on unknown key RotateSlot") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d is non-zero (%d)", i, b) } } if !bytes.Equal(callerSecret, callerCopy) { t.Fatalf("caller secret buffer mutated on unknown key RotateSlot") } slotAfter := mustGetSlot(t, store, issued.Principal.ID, validSlot.ID) if !reflect.DeepEqual(slotAfter, slotBefore) { t.Fatalf("slot mutated on unknown key RotateSlot: got %+v want %+v", slotAfter, slotBefore) } historyAfter := mustListSlotRevisions(t, store, issued.Principal.ID, validSlot.ID) if !reflect.DeepEqual(historyAfter, historyBefore) { t.Fatalf("slot revision history mutated on unknown key RotateSlot: got %+v want %+v", historyAfter, historyBefore) } slotsAfter := mustListSlots(t, store, issued.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) { t.Fatalf("slots mutated on unknown key RotateSlot: got %+v want %+v", slotsAfter, slotsBefore) } genAfter := mustProjectionGeneration(t, store) if genAfter != genBefore { t.Fatalf("ProjectionGeneration mutated on unknown key RotateSlot: got %d want %d", genAfter, genBefore) } }) t.Run("CreateSlot invalid envelope variant", func(t *testing.T) { invalidEnv := credentialstore.SecretEnvelope{ Algorithm: "", KeyID: keyIDSentinel, KeyVersion: keyVersionSentinel, Nonce: []byte(nonceSentinel), Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), } retSealer := &retainingSealer{envelope: invalidEnv} svc, store, issued, capture := newTestServiceWithLogs(t, retSealer) token := []byte(issued.RawToken) genBefore := mustProjectionGeneration(t, store) slotsBefore := mustListSlots(t, store, issued.Principal.ID) callerSecret := []byte("secret-payload-" + plaintextSentinel) callerCopy := append([]byte(nil), callerSecret...) _, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "invalid-env-slot", ProviderSecret: callerSecret, }) assertBlindAndNoSentinels(t, err, capture) if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d is non-zero (%d)", i, b) } } if !bytes.Equal(callerSecret, callerCopy) { t.Fatalf("caller secret buffer mutated on invalid envelope CreateSlot") } slotsAfter := mustListSlots(t, store, issued.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) { t.Fatalf("slots mutated on invalid envelope CreateSlot: got %+v want %+v", slotsAfter, slotsBefore) } genAfter := mustProjectionGeneration(t, store) if genAfter != genBefore { t.Fatalf("ProjectionGeneration changed on invalid envelope CreateSlot: got %d want %d", genAfter, genBefore) } }) t.Run("RotateSlot invalid envelope variant", func(t *testing.T) { retSealer := &retainingSealer{envelope: validEnv} svc, store, issued, capture := newTestServiceWithLogs(t, retSealer) token := []byte(issued.RawToken) validSlot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "valid-slot-for-invalid-env-rotate", ProviderSecret: []byte("initial-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } genBefore := mustProjectionGeneration(t, store) slotBefore := mustGetSlot(t, store, issued.Principal.ID, validSlot.ID) historyBefore := mustListSlotRevisions(t, store, issued.Principal.ID, validSlot.ID) slotsBefore := mustListSlots(t, store, issued.Principal.ID) invalidEnv := credentialstore.SecretEnvelope{ Algorithm: "AES-256-GCM", KeyID: keyIDSentinel, KeyVersion: keyVersionSentinel, Nonce: nil, Ciphertext: []byte(ciphertextSentinel), AAD: []byte(aadSentinel), } retSealer.envelope = invalidEnv retSealer.retained = nil callerSecret := []byte("rotate-payload-" + plaintextSentinel) callerCopy := append([]byte(nil), callerSecret...) _, err = svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: validSlot.ID, Revision: validSlot.Revision, ProviderSecret: callerSecret, }) assertBlindAndNoSentinels(t, err, capture) if len(retSealer.retained) == 0 { t.Fatal("retainingSealer did not capture plaintext") } for i, b := range retSealer.retained { if b != 0 { t.Fatalf("retained slice byte at index %d is non-zero (%d)", i, b) } } if !bytes.Equal(callerSecret, callerCopy) { t.Fatalf("caller secret buffer mutated on invalid envelope RotateSlot") } slotAfter := mustGetSlot(t, store, issued.Principal.ID, validSlot.ID) if !reflect.DeepEqual(slotAfter, slotBefore) { t.Fatalf("slot mutated on invalid envelope RotateSlot: got %+v want %+v", slotAfter, slotBefore) } historyAfter := mustListSlotRevisions(t, store, issued.Principal.ID, validSlot.ID) if !reflect.DeepEqual(historyAfter, historyBefore) { t.Fatalf("slot revision history mutated on invalid envelope RotateSlot: got %+v want %+v", historyAfter, historyBefore) } slotsAfter := mustListSlots(t, store, issued.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) { t.Fatalf("slots mutated on invalid envelope RotateSlot: got %+v want %+v", slotsAfter, slotsBefore) } genAfter := mustProjectionGeneration(t, store) if genAfter != genBefore { t.Fatalf("ProjectionGeneration mutated on invalid envelope RotateSlot: got %d want %d", genAfter, genBefore) } }) } // --------------------------------------------------------------------------- // API-3: Service construction has no network side effect // --------------------------------------------------------------------------- func TestServiceConstructionHasNoNetworkSideEffect(t *testing.T) { logger := zaptest.NewLogger(t) reg := credentialstore.EnvelopeKeyRegistry(newFakeKeyRegistryForService()) dbPath := "file:iop_credentialops_no_net_" + strings.ReplaceAll(t.Name(), "/", "_") + ".db?mode=memory&cache=shared" store, err := credentialstore.Open(context.Background(), dbPath, credentialstore.WithEnvelopeKeyRegistry(reg)) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { _ = store.Close() }) svcWithSealer := NewService(store, logger, newFakeSealer(t), nil) if svcWithSealer == nil { t.Fatal("NewService returned nil with sealer") } svcWithoutSealer := NewService(store, logger, nil, nil) if svcWithoutSealer == nil { t.Fatal("NewService returned nil without sealer") } issued, err := store.CreatePrincipalWithToken(context.Background(), credentialstore.CreatePrincipalInput{Alias: "no-net-principal"}) if err != nil { t.Fatalf("create principal: %v", err) } slots, err := svcWithoutSealer.ListSlots(context.Background(), []byte(issued.RawToken)) if err != nil { t.Fatalf("ListSlots without wire server: %v", err) } if len(slots) != 0 { t.Fatalf("expected 0 slots, got %d", len(slots)) } } // --------------------------------------------------------------------------- // API-1: Authenticate rejects empty/invalid tokens // --------------------------------------------------------------------------- func TestServiceRejectsEmptyToken(t *testing.T) { svc, _, _ := newTestService(t, newFakeSealer(t)) ctx := context.Background() _, err := svc.GetSlot(ctx, nil, "any-id") if !errors.Is(err, ErrUnauthorized) { t.Fatalf("expected ErrUnauthorized for nil token, got: %v", err) } _, err = svc.GetSlot(ctx, []byte{}, "any-id") if !errors.Is(err, ErrUnauthorized) { t.Fatalf("expected ErrUnauthorized for empty token, got: %v", err) } _, err = svc.ListSlots(ctx, []byte("nonexistent-token")) if !errors.Is(err, ErrUnauthorized) { t.Fatalf("expected ErrUnauthorized for unknown token, got: %v", err) } } // --------------------------------------------------------------------------- // API-1: Concurrent access (race test) // --------------------------------------------------------------------------- func TestServiceConcurrentOperations(t *testing.T) { svc, _, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() token := []byte(issued.RawToken) slot, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "concurrent-slot", ProviderSecret: []byte("concurrent-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } var wg sync.WaitGroup errs := make(chan error, 10) for range 5 { wg.Add(1) go func() { defer wg.Done() _, err := svc.GetSlot(ctx, token, slot.ID) if err != nil { errs <- err } }() } for range 5 { wg.Add(1) go func() { defer wg.Done() _, err := svc.ListSlots(ctx, token) if err != nil { errs <- err } }() } wg.Wait() close(errs) for err := range errs { t.Fatalf("concurrent error: %v", err) } } func TestServiceMapsStoreErrorsCorrectly(t *testing.T) { svc, _, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() _, err := svc.GetSlot(ctx, []byte(issued.RawToken), "does-not-exist") if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound, got: %v", err) } _, err = svc.GetRoute(ctx, []byte(issued.RawToken), "does-not-exist") if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound for route, got: %v", err) } } func TestServiceAuthenticatesViaDigest(t *testing.T) { svc, store, issued := newTestService(t, newFakeSealer(t)) ctx := context.Background() expectedDigest := digestOf([]byte(issued.RawToken)) p, tkr, err := store.LookupTokenByDigest(ctx, expectedDigest) if err != nil { t.Fatalf("lookup by expected digest: %v", err) } if p.ID != issued.Principal.ID { t.Fatalf("principal ID mismatch: got %q want %q", p.ID, issued.Principal.ID) } if tkr.ID != issued.Token.ID { t.Fatalf("token ID mismatch: got %q want %q", tkr.ID, issued.Token.ID) } slot, err := svc.CreateSlot(ctx, []byte(issued.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "digest-slot", ProviderSecret: []byte("digest-secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } slots, err := svc.ListSlots(ctx, []byte(issued.RawToken)) if err != nil { t.Fatalf("ListSlots with raw token: %v", err) } if len(slots) != 1 { t.Fatalf("expected 1 slot, got %d", len(slots)) } if slots[0].ID != slot.ID { t.Fatalf("slot ID mismatch: got %q want %q", slots[0].ID, slot.ID) } } func TestServiceHasNoProtoDependency(t *testing.T) { svc, _, _ := newTestService(t, newFakeSealer(t)) if svc == nil { t.Fatal("service must not be nil") } } func TestServiceImplementsExpectedInterface(t *testing.T) { var _ SecretSealer = (*fakeSealer)(nil) svc, _, _ := newTestService(t, nil) if svc == nil { t.Fatal("NewService must not return nil") } } // --------------------------------------------------------------------------- // API-2: create-time AAD binds to the persisted slot id // --------------------------------------------------------------------------- func TestServiceCreateBindsSealContextToPersistedSlotID(t *testing.T) { fake := newFakeSealer(t) svc, _, issued := newTestService(t, fake) ctx := context.Background() created, err := svc.CreateSlot(ctx, []byte(issued.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "bound-slot", ProviderSecret: []byte("secret"), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } if fake.callCount() != 1 { t.Fatalf("sealer call count: got %d want 1", fake.callCount()) } if fake.calls[0].ctx_.SlotID == "" { t.Fatal("create-time seal context must carry a slot id") } if fake.calls[0].ctx_.SlotID != created.ID { t.Fatalf("seal context slot id mismatch: got %q want persisted %q", fake.calls[0].ctx_.SlotID, created.ID) } if fake.calls[0].ctx_.Kind != credentialstore.CredentialKindBearer { t.Fatalf("seal context kind mismatch: got %q", fake.calls[0].ctx_.Kind) } } // --------------------------------------------------------------------------- // API-1/API-2: production keyring end-to-end at-rest encryption // --------------------------------------------------------------------------- func newKeyringForTest(t *testing.T) *credentialseal.Keyring { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "manifest.yaml") material := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x5A}, 32)) body := "keys:\n - id: \"primary\"\n version: 1\n material: \"" + material + "\"\n" if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatalf("write manifest: %v", err) } kr, err := credentialseal.LoadFile(credentialseal.FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1}) if err != nil { t.Fatalf("load keyring: %v", err) } return kr } func TestServiceKeyringStoresInspectableCiphertextAndDecrypts(t *testing.T) { ctx := context.Background() dir := t.TempDir() dbPath := filepath.Join(dir, "encrypted.db") keyring := newKeyringForTest(t) store, err := credentialstore.Open(ctx, dbPath, credentialstore.WithEnvelopeKeyRegistry(keyring)) if err != nil { t.Fatalf("open store: %v", err) } logger := zaptest.NewLogger(t) svc := NewService(store, logger, keyring, nil) issued, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "keyring-principal"}) if err != nil { t.Fatalf("create principal: %v", err) } token := []byte(issued.RawToken) const secret1 = "PROVIDER_SECRET_SENTINEL_AAA_111" const secret2 = "PROVIDER_SECRET_SENTINEL_BBB_222" created, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer, Alias: "keyring-slot", ProviderSecret: []byte(secret1), }) if err != nil { t.Fatalf("CreateSlot: %v", err) } rotated, err := svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: created.ID, Revision: created.Revision, ProviderSecret: []byte(secret2), }) if err != nil { t.Fatalf("RotateSlot: %v", err) } // Inspect the persisted row: only algorithm/key metadata/nonce/ciphertext/aad, // and no plaintext, are stored. var algorithm, keyID string var keyVersion uint64 var nonce, ciphertext, aad []byte err = store.DB().QueryRowContext(ctx, `SELECT algorithm, key_id, key_version, nonce, ciphertext, aad FROM credential_slots WHERE id=?`, created.ID, ).Scan(&algorithm, &keyID, &keyVersion, &nonce, &ciphertext, &aad) if err != nil { t.Fatalf("inspect slot row: %v", err) } if algorithm != credentialseal.AlgorithmAES256GCM { t.Fatalf("stored algorithm: got %q", algorithm) } if keyID != "primary" || keyVersion != 1 { t.Fatalf("stored key metadata: got id=%q version=%d", keyID, keyVersion) } if len(nonce) != 12 || len(ciphertext) == 0 || len(aad) == 0 { t.Fatalf("stored envelope shape: nonce=%d ciphertext=%d aad=%d", len(nonce), len(ciphertext), len(aad)) } for _, blob := range [][]byte{nonce, ciphertext, aad} { if strings.Contains(string(blob), secret1) || strings.Contains(string(blob), secret2) { t.Fatal("stored envelope leaks plaintext secret") } } sealCtx := credentialseal.Context{PrincipalID: issued.Principal.ID, SlotID: created.ID, Kind: credentialstore.CredentialKindBearer} // Every historical revision decrypts to its original plaintext with the // exact context. revisions, err := store.ListSlotRevisions(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("ListSlotRevisions: %v", err) } if len(revisions) != 2 { t.Fatalf("revision count: got %d want 2", len(revisions)) } wantByRevision := map[int64]string{0: secret1, 1: secret2} for _, rev := range revisions { got, err := keyring.Open(ctx, rev.Envelope, sealCtx) if err != nil { t.Fatalf("decrypt revision %d: %v", rev.Revision, err) } if string(got) != wantByRevision[rev.Revision] { t.Fatalf("revision %d plaintext mismatch: got %q want %q", rev.Revision, got, wantByRevision[rev.Revision]) } } // A swapped principal/slot/kind context cannot decrypt the current envelope. current, err := store.GetSlot(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("GetSlot current: %v", err) } _ = rotated for _, wrong := range []credentialseal.Context{ {PrincipalID: "other", SlotID: created.ID, Kind: credentialstore.CredentialKindBearer}, {PrincipalID: issued.Principal.ID, SlotID: "other-slot", Kind: credentialstore.CredentialKindBearer}, {PrincipalID: issued.Principal.ID, SlotID: created.ID, Kind: credentialstore.CredentialKindAPIKey}, } { if _, err := keyring.Open(ctx, current.Envelope, wrong); !errors.Is(err, credentialseal.ErrOpenFailed) { t.Fatalf("expected ErrOpenFailed for swapped context, got: %v", err) } } // Close and reopen the DB with a freshly reloaded keyring holding the same // external material: the current revision still decrypts (restart + reload). if err := store.Close(); err != nil { t.Fatalf("close store: %v", err) } keyring2 := newKeyringForTest(t) store2, err := credentialstore.Open(ctx, dbPath, credentialstore.WithEnvelopeKeyRegistry(keyring2)) if err != nil { t.Fatalf("reopen store: %v", err) } t.Cleanup(func() { _ = store2.Close() }) reloaded, err := store2.GetSlot(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("GetSlot after reopen: %v", err) } got, err := keyring2.Open(ctx, reloaded.Envelope, sealCtx) if err != nil { t.Fatalf("decrypt after reopen: %v", err) } if string(got) != secret2 { t.Fatalf("decrypt after reopen mismatch: got %q want %q", got, secret2) } } // --------------------------------------------------------------------------- // REVIEW_API-1: canonical credential kind binds AAD to persisted kind // --------------------------------------------------------------------------- func TestServiceCreateCanonicalizesCredentialKindForAAD(t *testing.T) { ctx := context.Background() // Table-driven: each input kind normalizes to a canonical value. kindCases := []struct { name string input string canonical string }{ {"bearer-lowercase", "bearer", credentialstore.CredentialKindBearer}, {"bearer-uppercase", "BEARER", credentialstore.CredentialKindBearer}, {"bearer-mixed-case", "BeArEr", credentialstore.CredentialKindBearer}, {"bearer-leading-trailing-space", " bearer ", credentialstore.CredentialKindBearer}, {"bearer-tabs-and-newlines", "\tbearer\n", credentialstore.CredentialKindBearer}, {"api-key-lowercase", "api_key", credentialstore.CredentialKindAPIKey}, {"api-key-uppercase", "API_KEY", credentialstore.CredentialKindAPIKey}, {"api-key-mixed-case", "Api_Key", credentialstore.CredentialKindAPIKey}, {"api-key-spaces", " api_key ", credentialstore.CredentialKindAPIKey}, } for _, tc := range kindCases { t.Run(tc.name, func(t *testing.T) { subDir := t.TempDir() dbPath := filepath.Join(subDir, "canonical.db") keyring := newKeyringForTest(t) store, err := credentialstore.Open(ctx, dbPath, credentialstore.WithEnvelopeKeyRegistry(keyring)) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { _ = store.Close() }) logger := zaptest.NewLogger(t) svc := NewService(store, logger, keyring, nil) issued, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "canonical-principal"}) if err != nil { t.Fatalf("create principal: %v", err) } token := []byte(issued.RawToken) created, err := svc.CreateSlot(ctx, token, CreateSlotInput{ Vendor: "openai", CredentialKind: tc.input, Alias: "canonical-" + tc.name, ProviderSecret: []byte("canonical-secret-payload"), }) if err != nil { t.Fatalf("CreateSlot with kind %q: %v", tc.input, err) } // Persisted kind must equal the canonical value. if created.CredentialKind != tc.canonical { t.Fatalf("persisted CredentialKind: got %q want %q", created.CredentialKind, tc.canonical) } // Stored slot row must carry the canonical kind. stored, err := store.GetSlot(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("GetSlot: %v", err) } if stored.CredentialKind != tc.canonical { t.Fatalf("stored CredentialKind: got %q want %q", stored.CredentialKind, tc.canonical) } // Initial envelope must open with the exact persisted context. sealCtx := credentialseal.Context{ PrincipalID: issued.Principal.ID, SlotID: created.ID, Kind: stored.CredentialKind, } got, err := keyring.Open(ctx, stored.Envelope, sealCtx) if err != nil { t.Fatalf("open initial envelope with persisted context: %v", err) } if string(got) != "canonical-secret-payload" { t.Fatalf("initial envelope plaintext: got %q", got) } // Rotate with the same canonical kind. rotated, err := svc.RotateSlot(ctx, token, RotateSlotInput{ SlotID: created.ID, Revision: created.Revision, ProviderSecret: []byte("canonical-rotated-secret"), }) if err != nil { t.Fatalf("RotateSlot: %v", err) } if rotated.CredentialKind != tc.canonical { t.Fatalf("rotated CredentialKind: got %q want %q", rotated.CredentialKind, tc.canonical) } // All revisions must open with the persisted context. revisions, err := store.ListSlotRevisions(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("ListSlotRevisions: %v", err) } for _, rev := range revisions { g, err := keyring.Open(ctx, rev.Envelope, sealCtx) if err != nil { t.Fatalf("open revision %d: %v", rev.Revision, err) } if rev.Revision == 0 && string(g) != "canonical-secret-payload" { t.Fatalf("revision 0 plaintext: got %q", g) } if rev.Revision == 1 && string(g) != "canonical-rotated-secret" { t.Fatalf("revision 1 plaintext: got %q", g) } } // Close and reopen: every revision must still decrypt with the row context. if err := store.Close(); err != nil { t.Fatalf("close store: %v", err) } keyring2 := newKeyringForTest(t) store2, err := credentialstore.Open(ctx, dbPath, credentialstore.WithEnvelopeKeyRegistry(keyring2)) if err != nil { t.Fatalf("reopen store: %v", err) } t.Cleanup(func() { _ = store2.Close() }) reloaded, err := store2.GetSlot(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("GetSlot after reopen: %v", err) } if reloaded.CredentialKind != tc.canonical { t.Fatalf("reloaded CredentialKind: got %q want %q", reloaded.CredentialKind, tc.canonical) } reopenedCtx := credentialseal.Context{ PrincipalID: issued.Principal.ID, SlotID: created.ID, Kind: reloaded.CredentialKind, } reopenedRevs, err := store2.ListSlotRevisions(ctx, issued.Principal.ID, created.ID) if err != nil { t.Fatalf("ListSlotRevisions after reopen: %v", err) } for _, rev := range reopenedRevs { g, err := keyring2.Open(ctx, rev.Envelope, reopenedCtx) if err != nil { t.Fatalf("decrypt revision %d after reopen: %v", rev.Revision, err) } if len(g) == 0 { t.Fatalf("empty plaintext after reopen for revision %d", rev.Revision) } } }) } // Unknown kind must be rejected before the sealer is called. t.Run("unknown-kind-rejected-before-sealer", func(t *testing.T) { fake := newFakeSealer(t) svcFake, storeFake, issuedFake := newTestService(t, fake) credentialState := func(t *testing.T, store *credentialstore.Store, pid string) ([]credentialstore.CredentialSlotRecord, int, uint64) { t.Helper() slots, err := store.ListSlots(ctx, pid) if err != nil { t.Fatalf("ListSlots: %v", err) } var revCount int if err := store.DB().QueryRowContext(ctx, "SELECT COUNT(*) FROM credential_slot_revisions").Scan(&revCount); err != nil { t.Fatalf("QueryRowContext credential_slot_revisions count: %v", err) } gen, err := store.ProjectionGeneration(ctx) if err != nil { t.Fatalf("ProjectionGeneration: %v", err) } return slots, revCount, gen } slotsBefore, revisionsBefore, genBefore := credentialState(t, storeFake, issuedFake.Principal.ID) _, err := svcFake.CreateSlot(ctx, []byte(issuedFake.RawToken), CreateSlotInput{ Vendor: "openai", CredentialKind: "unknown_kind", Alias: "unknown-kind-slot", ProviderSecret: []byte("unknown-secret"), }) if !errors.Is(err, ErrInvalidCredentialKind) { t.Fatalf("expected ErrInvalidCredentialKind, got: %v", err) } if fake.callCount() != 0 { t.Fatalf("sealer must not be called for unknown kind, got %d calls", fake.callCount()) } slotsAfter, revisionsAfter, genAfter := credentialState(t, storeFake, issuedFake.Principal.ID) if !reflect.DeepEqual(slotsAfter, slotsBefore) || revisionsAfter != revisionsBefore || genAfter != genBefore { t.Fatalf("unknown credential kind mutated durable state: slotsBefore=%+v slotsAfter=%+v revsBefore=%d revsAfter=%d genBefore=%d genAfter=%d", slotsBefore, slotsAfter, revisionsBefore, revisionsAfter, genBefore, genAfter) } if len(slotsAfter) != 0 || revisionsAfter != 0 { t.Fatalf("expected empty slots and zero revisions, got slots=%d revs=%d", len(slotsAfter), revisionsAfter) } }) }