Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/plugin_server_upstreamauthority_spire.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The `spire` plugin uses credentials fetched from the Workload API to call an ups

The SVIDs minted in a nested configuration are valid in the entire trust domain, not only in the scope of the server that originated the SVID.

In the case of X509-SVID, this is easily achieved because of the chaining semantics that X.509 has. On the other hand, for JWT-SVID, this capability is accomplished by propagating every JWT-SVID public signing key to the whole topology.
In the case of X509-SVID, this is easily achieved because of the chaining semantics that X.509 has. On the other hand, for JWT-SVID and WIT-SVID, this capability is accomplished by propagating every JWT-SVID and WIT-SVID public signing key to the whole topology.

The plugin accepts the following configuration options:

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ require (
github.com/smallstep/pkcs7 v0.2.3
github.com/spiffe/go-spiffe/v2 v2.8.2
github.com/spiffe/spire-api-sdk v1.2.5-0.20260924193931-38b883fcd6d1
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20260617144146-5dcde407c4d1
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20260929185137-e961422db712
github.com/stretchr/testify v1.12.1
github.com/uber-go/tally/v4 v4.1.17
github.com/valyala/fastjson v1.6.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,8 @@ github.com/spiffe/go-spiffe/v2 v2.8.2 h1:jUEsvCMD6fH25J8K/w3q/XnIx8W1lb8+YLaEEHI
github.com/spiffe/go-spiffe/v2 v2.8.2/go.mod h1:w2CLWKLMTX/PPYUEUPv3ltH0RXsw5S8suwNF46w9/Aw=
github.com/spiffe/spire-api-sdk v1.2.5-0.20260924193931-38b883fcd6d1 h1:O0ZhDye/pIEZGKsaGnczAClIG0SK6jS5IwH+mfhIXdE=
github.com/spiffe/spire-api-sdk v1.2.5-0.20260924193931-38b883fcd6d1/go.mod h1:9hXJcMzatM1KwAtBDO3s6HccDCic++/5c2yOc5Iln8Y=
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20260617144146-5dcde407c4d1 h1:gN8nWPdRSxYIACrgdPBuVwhNe6ABzpvUVza7cP8Yy38=
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20260617144146-5dcde407c4d1/go.mod h1:QvrRDiBlXiJ7kNd176ZHsF5eklxxeTRgJSu2CXe0MKw=
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20260929185137-e961422db712 h1:dMxwHOpAc6jH4NtU4mgqHLE/daLwa2gDW04Wz4lIAIQ=
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20260929185137-e961422db712/go.mod h1:QvrRDiBlXiJ7kNd176ZHsF5eklxxeTRgJSu2CXe0MKw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
Expand Down
43 changes: 34 additions & 9 deletions pkg/server/api/bundle/v1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,7 @@ import (
// UpstreamPublisher defines the publisher interface.
type UpstreamPublisher interface {
PublishJWTKey(ctx context.Context, jwtKey *common.PublicKey) ([]*common.PublicKey, error)
}

// UpstreamPublisherFunc defines the function.
type UpstreamPublisherFunc func(ctx context.Context, jwtKey *common.PublicKey) ([]*common.PublicKey, error)

// PublishJWTKey publishes the JWT key with the given function.
func (fn UpstreamPublisherFunc) PublishJWTKey(ctx context.Context, jwtKey *common.PublicKey) ([]*common.PublicKey, error) {
return fn(ctx, jwtKey)
PublishWITKey(ctx context.Context, witKey *common.PublicKey) ([]*common.PublicKey, error)
}

// Config defines the bundle service configuration.
Expand Down Expand Up @@ -189,8 +182,40 @@ func (s *Service) PublishJWTAuthority(ctx context.Context, req *bundlev1.Publish

// PublishWITAuthority published the WIT key on the server.
func (s *Service) PublishWITAuthority(ctx context.Context, req *bundlev1.PublishWITAuthorityRequest) (*bundlev1.PublishWITAuthorityResponse, error) {
parseRequest := func() logrus.Fields {
fields := logrus.Fields{}
if req.WitAuthority != nil {
fields[telemetry.WITAuthorityExpiresAt] = req.WitAuthority.ExpiresAt
fields[telemetry.WITAuthorityKeyID] = req.WitAuthority.KeyId
fields[telemetry.WITAuthorityPublicKeySHA256] = api.HashByte(req.WitAuthority.PublicKey)
}
return fields
}
rpccontext.AddRPCAuditFields(ctx, parseRequest())
log := rpccontext.Logger(ctx)
return nil, commonapi.MakeErr(log, codes.Unimplemented, "WIT-SVID functionality is not yet implemented", nil)

if err := rpccontext.RateLimit(ctx, 1); err != nil {
return nil, commonapi.MakeErr(log, status.Code(err), "rejecting request due to key publishing rate limiting", err)
}

if req.WitAuthority == nil {
return nil, commonapi.MakeErr(log, codes.InvalidArgument, "missing WIT authority", nil)
}

keys, err := api.ParseWITAuthorities([]*types.WITKey{req.WitAuthority})
if err != nil {
return nil, commonapi.MakeErr(log, codes.InvalidArgument, "invalid WIT authority", err)
}

resp, err := s.up.PublishWITKey(ctx, keys[0])
if err != nil {
return nil, commonapi.MakeErr(log, codes.Internal, "failed to publish WIT key", err)
}
rpccontext.AuditRPC(ctx)

return &bundlev1.PublishWITAuthorityResponse{
WitAuthorities: api.PublicKeysToWITKeys(resp),
}, nil
}

// ListFederatedBundles returns an optionally paginated list of federated bundles.
Expand Down
246 changes: 246 additions & 0 deletions pkg/server/api/bundle/v1/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,242 @@ func TestPublishJWTAuthority(t *testing.T) {
}
}

func TestPublishWITAuthority(t *testing.T) {
test := setupServiceTest(t)
defer test.Cleanup()

pkixBytes, err := base64.StdEncoding.DecodeString("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYSlUVLqTD8DEnA4F1EWMTf5RXc5lnCxw+5WKJwngEL3rPc9i4Tgzz9riR3I/NiSlkgRO1WsxBusqpC284j9dXA==")
pkixHashed := api.HashByte(pkixBytes)
require.NoError(t, err)
expiresAt := time.Now().Unix()
expiresAtStr := strconv.FormatInt(expiresAt, 10)
witKey1 := &types.WITKey{
ExpiresAt: expiresAt,
KeyId: "key1",
PublicKey: pkixBytes,
}

_, expectedWITErr := x509.ParsePKIXPublicKey([]byte("malformed key"))
require.Error(t, expectedWITErr)

for _, tt := range []struct {
name string

code codes.Code
err string
expectLogs []spiretest.LogEntry
resultKeys []*types.WITKey
fakeErr error
fakeExpectKey *common.PublicKey
witKey *types.WITKey
rateLimiterErr error
}{
{
name: "success",
witKey: witKey1,
fakeExpectKey: &common.PublicKey{
PkixBytes: pkixBytes,
Kid: "key1",
NotAfter: expiresAt,
},
resultKeys: []*types.WITKey{
{
ExpiresAt: expiresAt,
KeyId: "key1",
PublicKey: pkixBytes,
},
},
expectLogs: []spiretest.LogEntry{
{
Level: logrus.InfoLevel,
Message: "API accessed",
Data: logrus.Fields{
telemetry.Status: "success",
telemetry.Type: "audit",
telemetry.WITAuthorityKeyID: "key1",
telemetry.WITAuthorityPublicKeySHA256: pkixHashed,
telemetry.WITAuthorityExpiresAt: expiresAtStr,
},
},
},
},
{
name: "rate limit fails",
witKey: witKey1,
rateLimiterErr: status.Error(codes.Internal, "limit error"),
code: codes.Internal,
err: "rejecting request due to key publishing rate limiting: limit error",
expectLogs: []spiretest.LogEntry{
{
Level: logrus.ErrorLevel,
Message: "Rejecting request due to key publishing rate limiting",
Data: logrus.Fields{
logrus.ErrorKey: "rpc error: code = Internal desc = limit error",
},
},
{
Level: logrus.InfoLevel,
Message: "API accessed",
Data: logrus.Fields{
telemetry.Status: "error",
telemetry.StatusCode: "Internal",
telemetry.StatusMessage: "rejecting request due to key publishing rate limiting: limit error",
telemetry.Type: "audit",
telemetry.WITAuthorityKeyID: "key1",
telemetry.WITAuthorityPublicKeySHA256: pkixHashed,
telemetry.WITAuthorityExpiresAt: expiresAtStr,
},
},
},
},
{
name: "missing WIT authority",
code: codes.InvalidArgument,
err: "missing WIT authority",
expectLogs: []spiretest.LogEntry{
{
Level: logrus.ErrorLevel,
Message: "Invalid argument: missing WIT authority",
},
{
Level: logrus.InfoLevel,
Message: "API accessed",
Data: logrus.Fields{
telemetry.Status: "error",
telemetry.StatusCode: "InvalidArgument",
telemetry.StatusMessage: "missing WIT authority",
telemetry.Type: "audit",
},
},
},
},
{
name: "malformed key",
code: codes.InvalidArgument,
err: "invalid WIT authority: asn1:",
witKey: &types.WITKey{
ExpiresAt: expiresAt,
KeyId: "key1",
PublicKey: []byte("malformed key"),
},
expectLogs: []spiretest.LogEntry{
{
Level: logrus.ErrorLevel,
Message: "Invalid argument: invalid WIT authority",
Data: logrus.Fields{
logrus.ErrorKey: expectedWITErr.Error(),
},
},
{
Level: logrus.InfoLevel,
Message: "API accessed",
Data: logrus.Fields{
telemetry.Status: "error",
telemetry.StatusCode: "InvalidArgument",
telemetry.StatusMessage: fmt.Sprintf("invalid WIT authority: %v", expectedWITErr),
telemetry.Type: "audit",
telemetry.WITAuthorityKeyID: "key1",
telemetry.WITAuthorityPublicKeySHA256: api.HashByte([]byte("malformed key")),
telemetry.WITAuthorityExpiresAt: expiresAtStr,
},
},
},
},
{
name: "missing key ID",
code: codes.InvalidArgument,
err: "invalid WIT authority: missing key ID",
witKey: &types.WITKey{
ExpiresAt: expiresAt,
PublicKey: witKey1.PublicKey,
},
expectLogs: []spiretest.LogEntry{
{
Level: logrus.ErrorLevel,
Message: "Invalid argument: invalid WIT authority",
Data: logrus.Fields{
logrus.ErrorKey: "missing key ID",
},
},
{
Level: logrus.InfoLevel,
Message: "API accessed",
Data: logrus.Fields{
telemetry.Status: "error",
telemetry.StatusCode: "InvalidArgument",
telemetry.StatusMessage: "invalid WIT authority: missing key ID",
telemetry.Type: "audit",
telemetry.WITAuthorityKeyID: "",
telemetry.WITAuthorityPublicKeySHA256: pkixHashed,
telemetry.WITAuthorityExpiresAt: expiresAtStr,
},
},
},
},
{
name: "fail to publish",
code: codes.Internal,
err: "failed to publish WIT key: publish error",
fakeErr: errors.New("publish error"),
witKey: witKey1,
expectLogs: []spiretest.LogEntry{
{
Level: logrus.ErrorLevel,
Message: "Failed to publish WIT key",
Data: logrus.Fields{
logrus.ErrorKey: "publish error",
},
},
{
Level: logrus.InfoLevel,
Message: "API accessed",
Data: logrus.Fields{
telemetry.Status: "error",
telemetry.StatusCode: "Internal",
telemetry.StatusMessage: "failed to publish WIT key: publish error",
telemetry.Type: "audit",
telemetry.WITAuthorityKeyID: "key1",
telemetry.WITAuthorityPublicKeySHA256: pkixHashed,
telemetry.WITAuthorityExpiresAt: expiresAtStr,
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
test.logHook.Reset()

// Setup fake
test.up.t = t
test.up.err = tt.fakeErr
test.up.expectKey = tt.fakeExpectKey

// Setup rate limiter
test.rateLimiter.count = 1
test.rateLimiter.err = tt.rateLimiterErr

resp, err := test.client.PublishWITAuthority(ctx, &bundlev1.PublishWITAuthorityRequest{
WitAuthority: tt.witKey,
})

spiretest.AssertLogs(t, test.logHook.AllEntries(), tt.expectLogs)
if err != nil {
spiretest.RequireGRPCStatusContains(t, err, tt.code, tt.err)
require.Nil(t, resp)

return
}

require.NoError(t, err)
require.NotNil(t, resp)

spiretest.RequireProtoEqual(t, &bundlev1.PublishWITAuthorityResponse{
WitAuthorities: tt.resultKeys,
}, resp)
})
}
}

func TestListFederatedBundles(t *testing.T) {
test := setupServiceTest(t)
defer test.Cleanup()
Expand Down Expand Up @@ -3081,6 +3317,16 @@ func (f *fakeUpstreamPublisher) PublishJWTKey(_ context.Context, jwtKey *common.
return []*common.PublicKey{jwtKey}, nil
}

func (f *fakeUpstreamPublisher) PublishWITKey(_ context.Context, witKey *common.PublicKey) ([]*common.PublicKey, error) {
if f.err != nil {
return nil, f.err
}

spiretest.AssertProtoEqual(f.t, f.expectKey, witKey)

return []*common.PublicKey{witKey}, nil
}

type fakeRateLimiter struct {
count int
err error
Expand Down
3 changes: 1 addition & 2 deletions pkg/server/api/svid/v1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,7 @@ func (s *Service) newWITSVID(ctx context.Context, param *svidv1.NewWITSVIDParams
Algorithm: param.SigningAlgorithm,
Key: publicKey,
},
// TODO: add WIT specific TTL (https://github.com/spiffe/spire/issues/6535)
TTL: time.Duration(entry.GetX509SvidTtl()) * time.Second,
TTL: time.Duration(entry.GetWitSvidTtl()) * time.Second,
})
if err != nil {
return &svidv1.BatchNewWITSVIDResponse_Result{
Expand Down
Loading
Loading