route 검증과 lease 발급이 같은 불변 profile snapshot을 사용해야 프로세스 내 호환 map 변경에도 유효한 credential route가 안정적으로 동작한다. 코드레벨 검토를 통과한 Milestone과 SDD도 완료 상태로 archive한다.
194 lines
7.6 KiB
Go
194 lines
7.6 KiB
Go
package credentiallease
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdh"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"google.golang.org/protobuf/proto"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"iop/apps/control-plane/internal/credentialseal"
|
|
"iop/apps/control-plane/internal/credentialstore"
|
|
"iop/packages/go/config"
|
|
lease "iop/packages/go/credentiallease"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
type acceptingRegistry struct{}
|
|
|
|
func (acceptingRegistry) HasEnvelopeKey(context.Context, string, uint64) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
type recordingOpener struct {
|
|
calls int
|
|
secret []byte
|
|
}
|
|
|
|
func (o *recordingOpener) Open(_ context.Context, _ credentialstore.SecretEnvelope, _ credentialseal.Context) ([]byte, error) {
|
|
o.calls++
|
|
return append([]byte(nil), o.secret...), nil
|
|
}
|
|
|
|
func TestAcquireChecksDurableBindingBeforeOpeningAndIssuesConsumableLease(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := credentialstore.Open(ctx, filepath.Join(t.TempDir(), "credential.db"), credentialstore.WithEnvelopeKeyRegistry(acceptingRegistry{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
principal, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "principal"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
slot, err := store.CreateSlot(ctx, credentialstore.CreateSlotInput{
|
|
PrincipalID: principal.Principal.ID, Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer,
|
|
Envelope: credentialstore.SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "at-rest", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route, err := store.CreateRoute(ctx, credentialstore.CreateRouteInput{PrincipalID: principal.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o", ResourceSelector: "default"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
slot, err = store.GetSlot(ctx, principal.Principal.ID, slot.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
generation, err := store.ProjectionGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
issuerPublic, issuerPrivate, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recipient, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Unix(1700000000, 0).UTC()
|
|
opener := &recordingOpener{secret: []byte("provider-secret-sentinel")}
|
|
service, err := New(store, opener, "issuer-1", issuerPrivate, 30*time.Second, 8, func() time.Time { return now }, rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
originalProfile, ok := config.BuiltInProtocolProfiles["openai"]
|
|
if !ok {
|
|
t.Fatal("missing built-in openai profile")
|
|
}
|
|
delete(config.BuiltInProtocolProfiles, "openai")
|
|
t.Cleanup(func() { config.BuiltInProtocolProfiles["openai"] = originalProfile })
|
|
binding := &iop.CredentialLeaseBinding{
|
|
PrincipalRef: principal.Principal.ID, CredentialSlotRef: slot.ID, RouteId: route.ID, ProfileId: route.ProfileID,
|
|
UpstreamTarget: route.UpstreamModel, NodeId: "node-1", RecipientKeyId: "recipient-1",
|
|
CredentialRevision: uint64(slot.Revision), RouteRevision: uint64(route.Revision), ProjectionGeneration: generation,
|
|
}
|
|
request := &iop.AcquireLeaseRequest{EdgeId: "edge-1", Binding: binding, RecipientPublicKey: recipient.PublicKey().Bytes()}
|
|
stale := proto.Clone(request).(*iop.AcquireLeaseRequest)
|
|
stale.Binding.ProjectionGeneration--
|
|
response, err := service.Acquire(ctx, stale)
|
|
if err != nil || response.GetError() == "" || opener.calls != 0 {
|
|
t.Fatalf("stale acquire response=%v err=%v opener_calls=%d", response, err, opener.calls)
|
|
}
|
|
response, err = service.Acquire(ctx, request)
|
|
if err != nil || response.GetLease() == nil || response.GetError() != "" || opener.calls != 1 {
|
|
t.Fatalf("valid acquire response=%v err=%v opener_calls=%d", response, err, opener.calls)
|
|
}
|
|
consumer, err := lease.NewConsumer("node-1", "recipient-1", recipient.Bytes(), "issuer-1", issuerPublic, 8, func() time.Time { return now })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
envelope, err := lease.FromProto(response.GetLease())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
material, err := consumer.Consume(ctx, envelope, lease.ExpectedFromProto(binding))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(material.Secret) != "provider-secret-sentinel" || material.HeaderName != "Authorization" || material.Scheme != "Bearer" {
|
|
t.Fatal("unexpected lease material")
|
|
}
|
|
material.Zero()
|
|
}
|
|
|
|
// TestAcquireCanonicalizesAPIKeyProfileHeader proves that a built-in profile
|
|
// declaring a lowercase API-key header (seulgi_messages -> "x-api-key") is
|
|
// issued and consumed as the canonical "X-Api-Key" scope with no scheme, so
|
|
// the managed Messages route no longer fails closed at lease issuance.
|
|
func TestAcquireCanonicalizesAPIKeyProfileHeader(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := credentialstore.Open(ctx, filepath.Join(t.TempDir(), "credential.db"), credentialstore.WithEnvelopeKeyRegistry(acceptingRegistry{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
principal, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "principal"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
slot, err := store.CreateSlot(ctx, credentialstore.CreateSlotInput{
|
|
PrincipalID: principal.Principal.ID, Vendor: "seulgi", CredentialKind: credentialstore.CredentialKindAPIKey,
|
|
Envelope: credentialstore.SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "at-rest", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route, err := store.CreateRoute(ctx, credentialstore.CreateRouteInput{PrincipalID: principal.Principal.ID, SlotID: slot.ID, ProfileID: "seulgi_messages", UpstreamModel: "seulgi-messages-1", ResourceSelector: "default"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
slot, err = store.GetSlot(ctx, principal.Principal.ID, slot.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
generation, err := store.ProjectionGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
issuerPublic, issuerPrivate, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recipient, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Unix(1700000000, 0).UTC()
|
|
opener := &recordingOpener{secret: []byte("api-key-secret-sentinel")}
|
|
service, err := New(store, opener, "issuer-1", issuerPrivate, 30*time.Second, 8, func() time.Time { return now }, rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
binding := &iop.CredentialLeaseBinding{
|
|
PrincipalRef: principal.Principal.ID, CredentialSlotRef: slot.ID, RouteId: route.ID, ProfileId: route.ProfileID,
|
|
UpstreamTarget: route.UpstreamModel, NodeId: "node-1", RecipientKeyId: "recipient-1",
|
|
CredentialRevision: uint64(slot.Revision), RouteRevision: uint64(route.Revision), ProjectionGeneration: generation,
|
|
}
|
|
request := &iop.AcquireLeaseRequest{EdgeId: "edge-1", Binding: binding, RecipientPublicKey: recipient.PublicKey().Bytes()}
|
|
response, err := service.Acquire(ctx, request)
|
|
if err != nil || response.GetLease() == nil || response.GetError() != "" || opener.calls != 1 {
|
|
t.Fatalf("api-key acquire response=%v err=%v opener_calls=%d", response, err, opener.calls)
|
|
}
|
|
consumer, err := lease.NewConsumer("node-1", "recipient-1", recipient.Bytes(), "issuer-1", issuerPublic, 8, func() time.Time { return now })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
envelope, err := lease.FromProto(response.GetLease())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
material, err := consumer.Consume(ctx, envelope, lease.ExpectedFromProto(binding))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(material.Secret) != "api-key-secret-sentinel" || material.HeaderName != "X-Api-Key" || material.Scheme != "" {
|
|
t.Fatalf("unexpected api-key lease material header=%q scheme=%q", material.HeaderName, material.Scheme)
|
|
}
|
|
material.Zero()
|
|
}
|