package credentiallease import ( "context" "crypto/ecdh" "crypto/ed25519" "crypto/rand" "errors" "strings" "testing" "time" "google.golang.org/protobuf/proto" ) func testKeys(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey, []byte, []byte) { t.Helper() 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) } return issuerPublic, issuerPrivate, recipient.Bytes(), recipient.PublicKey().Bytes() } func testScope(now time.Time, id string) Scope { return Scope{ LeaseID: id, PrincipalRef: "principal-1", CredentialSlotRef: "slot-1", RouteID: "route-1", ProfileID: "openai", UpstreamTarget: "gpt-4o", NodeID: "node-1", RecipientKeyID: "recipient-1", HeaderName: "Authorization", Scheme: "Bearer", CredentialRevision: 7, RouteRevision: 5, ProjectionGeneration: 11, IssuedAtUnixNano: now.UnixNano(), ExpiresAtUnixNano: now.Add(30 * time.Second).UnixNano(), } } func expected(scope Scope) ExpectedScope { return ExpectedScope{ PrincipalRef: scope.PrincipalRef, CredentialSlotRef: scope.CredentialSlotRef, RouteID: scope.RouteID, ProfileID: scope.ProfileID, UpstreamTarget: scope.UpstreamTarget, NodeID: scope.NodeID, RecipientKeyID: scope.RecipientKeyID, CredentialRevision: scope.CredentialRevision, RouteRevision: scope.RouteRevision, ProjectionGeneration: scope.ProjectionGeneration, } } func TestIssueConsumeExactScopeAndProtoRedaction(t *testing.T) { now := time.Unix(1700000000, 0).UTC() issuerPublic, issuerPrivate, recipientPrivate, recipientPublic := testKeys(t) secret := []byte("SENTINEL_PROVIDER_SECRET") env, err := Issue(testScope(now, "lease-1"), secret, recipientPublic, "issuer-1", issuerPrivate, rand.Reader) if err != nil { t.Fatal(err) } if strings.Contains(env.ToProto().String(), string(secret)) { t.Fatal("plaintext secret appeared in protobuf debug output") } wire, err := proto.Marshal(env.ToProto()) if err != nil { t.Fatal(err) } if strings.Contains(string(wire), string(secret)) { t.Fatal("plaintext secret appeared in protobuf wire bytes") } consumer, err := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer-1", issuerPublic, 8, func() time.Time { return now }) if err != nil { t.Fatal(err) } material, err := consumer.Consume(context.Background(), env, expected(env.Scope)) if err != nil { t.Fatal(err) } if got := string(material.Secret); got != string(secret) || material.HeaderName != "Authorization" || material.Scheme != "Bearer" { t.Fatalf("unexpected material metadata") } material.Zero() if material.Secret != nil { t.Fatal("material was not released") } if _, err := consumer.Consume(context.Background(), env, expected(env.Scope)); !errors.Is(err, ErrReplay) { t.Fatalf("duplicate consume error = %v, want replay", err) } } func TestScopeTamperAndExpectedMismatchFailBeforePlaintext(t *testing.T) { now := time.Unix(1700000000, 0).UTC() issuerPublic, issuerPrivate, recipientPrivate, recipientPublic := testKeys(t) base, err := Issue(testScope(now, "lease-tamper"), []byte("secret"), recipientPublic, "issuer-1", issuerPrivate, rand.Reader) if err != nil { t.Fatal(err) } fields := []func(*Scope){ func(s *Scope) { s.PrincipalRef += "x" }, func(s *Scope) { s.CredentialSlotRef += "x" }, func(s *Scope) { s.RouteID += "x" }, func(s *Scope) { s.ProfileID += "x" }, func(s *Scope) { s.UpstreamTarget += "x" }, func(s *Scope) { s.NodeID += "x" }, func(s *Scope) { s.RecipientKeyID += "x" }, func(s *Scope) { s.CredentialRevision++ }, func(s *Scope) { s.RouteRevision++ }, func(s *Scope) { s.ProjectionGeneration++ }, func(s *Scope) { s.ExpiresAtUnixNano++ }, } for i, mutate := range fields { copyEnvelope := *base mutate(©Envelope.Scope) consumer, _ := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer-1", issuerPublic, 8, func() time.Time { return now }) if material, err := consumer.Consume(context.Background(), ©Envelope, expected(copyEnvelope.Scope)); err == nil || material != nil { t.Fatalf("tamper case %d was accepted", i) } } consumer, _ := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer-1", issuerPublic, 8, func() time.Time { return now }) mismatch := expected(base.Scope) mismatch.RouteRevision++ if material, err := consumer.Consume(context.Background(), base, mismatch); !errors.Is(err, ErrScopeMismatch) || material != nil { t.Fatalf("expected scope mismatch, got material=%v err=%v", material, err) } } func TestTTLReplayBoundsOverflowAndExpiryPruning(t *testing.T) { now := time.Unix(1700000000, 0).UTC() _, issuerPrivate, _, recipientPublic := testKeys(t) for _, ttl := range []time.Duration{MinTTL - time.Nanosecond, MaxTTL + time.Nanosecond} { scope := testScope(now, "invalid-ttl") scope.ExpiresAtUnixNano = now.Add(ttl).UnixNano() if _, err := Issue(scope, []byte("secret"), recipientPublic, "issuer", issuerPrivate, rand.Reader); !errors.Is(err, ErrInvalid) { t.Fatalf("ttl %s error = %v", ttl, err) } } issuerPublic, issuerPrivate, recipientPrivate, recipientPublic := testKeys(t) clock := now.Add(time.Second) firstScope := testScope(now, "lease-first") firstScope.ExpiresAtUnixNano = now.Add(MinTTL).UnixNano() secondScope := testScope(now.Add(time.Second), "lease-second") secondScope.ExpiresAtUnixNano = now.Add(time.Second + MinTTL).UnixNano() first, _ := Issue(firstScope, []byte("one"), recipientPublic, "issuer", issuerPrivate, rand.Reader) second, _ := Issue(secondScope, []byte("two"), recipientPublic, "issuer", issuerPrivate, rand.Reader) consumer, err := NewConsumer("node-1", "recipient-1", recipientPrivate, "issuer", issuerPublic, 1, func() time.Time { return clock }) if err != nil { t.Fatal(err) } material, err := consumer.Consume(context.Background(), first, expected(first.Scope)) if err != nil { t.Fatal(err) } material.Zero() if _, err := consumer.Consume(context.Background(), second, expected(second.Scope)); !errors.Is(err, ErrOverflow) { t.Fatalf("live overflow error = %v", err) } clock = now.Add(MinTTL + time.Nanosecond) material, err = consumer.Consume(context.Background(), second, expected(second.Scope)) if err != nil { t.Fatalf("consume after expired prune: %v", err) } material.Zero() if _, err := NewConsumer("node", "key", recipientPrivate, "issuer", issuerPublic, 0, nil); !errors.Is(err, ErrInvalid) { t.Fatalf("zero replay bound error = %v", err) } if _, err := NewConsumer("node", "key", recipientPrivate, "issuer", issuerPublic, MaxSetSize+1, nil); !errors.Is(err, ErrInvalid) { t.Fatalf("oversize replay bound error = %v", err) } }