iop/apps/control-plane/internal/credentialops/service_test.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

2034 lines
66 KiB
Go

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)
}
})
}