From afb80de7ac106c85847b2cb6525cdf990f208b56 Mon Sep 17 00:00:00 2001 From: Marcos Yacob Date: Tue, 4 Aug 2026 12:38:24 -0300 Subject: [PATCH 1/2] allow custom claims on WIT-SVIDs Signed-off-by: Marcos Yacob --- exp/svid/witsvid/source.go | 6 +++++- exp/svid/witsvid/svid.go | 4 ++++ exp/svid/witsvid/svid_test.go | 14 +++++++++++++ workloadapi/witsource.go | 19 ++++++++++++++++++ workloadapi/witsource_test.go | 38 +++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/exp/svid/witsvid/source.go b/exp/svid/witsvid/source.go index 6ca789783..f47718261 100644 --- a/exp/svid/witsvid/source.go +++ b/exp/svid/witsvid/source.go @@ -2,8 +2,12 @@ package witsvid import "github.com/spiffe/go-spiffe/v2/spiffeid" -// Source is a source of WIT-SVIDs keyed by SPIFFE ID. +// Source is a source of WIT-SVIDs. type Source interface { + // GetWITSVID returns the default WIT-SVID from the source. It is used by + // code that presents a credential and therefore has no ID to look up. + GetWITSVID() (*SVID, error) + // GetWITSVIDForID returns the WIT-SVID for the given SPIFFE ID. GetWITSVIDForID(id spiffeid.ID) (*SVID, error) } diff --git a/exp/svid/witsvid/svid.go b/exp/svid/witsvid/svid.go index 0dbec410d..507f3345c 100644 --- a/exp/svid/witsvid/svid.go +++ b/exp/svid/witsvid/svid.go @@ -52,6 +52,9 @@ type SVID struct { // identity should be used by a workload when more than one SVID is returned. Hint string + // Claims is the parsed claims from the WIT-SVID token. + Claims map[string]interface{} + // token is the serialized JWS compact serialization. token string } @@ -167,6 +170,7 @@ func parse(token string, verify verifyFn) (*SVID, error) { Expiry: stdClaims.Expiry.Time().UTC(), PublicKey: publicKey, KeyID: keyID, + Claims: rawClaims, token: token, }, nil } diff --git a/exp/svid/witsvid/svid_test.go b/exp/svid/witsvid/svid_test.go index 83c83101c..0e1180421 100644 --- a/exp/svid/witsvid/svid_test.go +++ b/exp/svid/witsvid/svid_test.go @@ -67,6 +67,20 @@ func TestParseInsecure(t *testing.T) { c["nbf"] = jwt.NewNumericDate(time.Now().Add(-time.Minute)) }), }, + { + name: "issuer-supplied claims are exposed", + token: withClaims(func(c map[string]any) { + c["groups"] = []string{"admin", "ops"} + }), + check: func(t *testing.T, svid *witsvid.SVID) { + // Issuer-supplied claims are reachable by a verifier. + assert.Equal(t, []any{"admin", "ops"}, svid.Claims["groups"]) + // Standard and confirmation claims come through untouched. + assert.Equal(t, workload.String(), svid.Claims["sub"]) + assert.Contains(t, svid.Claims, "exp") + assert.Contains(t, svid.Claims, "cnf") + }, + }, { name: "malformed", token: func(*testing.T) string { return "not.a.valid.jwt" }, diff --git a/workloadapi/witsource.go b/workloadapi/witsource.go index 2fd4c55be..72a3276ca 100644 --- a/workloadapi/witsource.go +++ b/workloadapi/witsource.go @@ -125,6 +125,25 @@ func (s *WITSource) Close() error { return s.closeBase(closer) } +// GetWITSVID returns the default WIT-SVID, which is the first one in the list +// returned by the Workload API (see the SPIFFE Workload API specification ยง8). +// It implements the witsvid.Source interface. +// +// Experimental: subject to change. +func (s *WITSource) GetWITSVID() (*witsvid.SVID, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + + s.mtx.RLock() + defer s.mtx.RUnlock() + + if len(s.svids) == 0 { + return nil, errors.New("witsource: no WIT-SVID available") + } + return s.svids[0], nil +} + // GetWITSVIDForID returns the WIT-SVID for the given SPIFFE ID. // It implements the witsvid.Source interface. // diff --git a/workloadapi/witsource_test.go b/workloadapi/witsource_test.go index 1ab3206be..fa5501aec 100644 --- a/workloadapi/witsource_test.go +++ b/workloadapi/witsource_test.go @@ -71,6 +71,41 @@ func TestWITSourceLookup(t *testing.T) { }) } +func TestWITSourceGetWITSVID(t *testing.T) { + t.Run("returns the first SVID as the default identity", func(t *testing.T) { + api := fakeworkloadapi.New(t) + t.Cleanup(api.Stop) + + key := test.NewEC256Key(t) + kid := "key-1" + api.SetWITSVIDResponse(&workload.WITSVIDResponse{ + Svids: []*workload.WITSVID{ + makeWITSVIDProto(t, witFooID, key, test.NewEC256Key(t), kid, ""), + makeWITSVIDProto(t, witBarID, key, test.NewEC256Key(t), kid, ""), + }, + }) + api.SetWITBundles(makeWITBundle(t, witTD, key, kid)) + + src, err := workloadapi.NewWITSource(t.Context(), withAddr(api)) + require.NoError(t, err) + t.Cleanup(func() { src.Close() }) + + svid, err := src.GetWITSVID() + require.NoError(t, err) + assert.Equal(t, witFooID, svid.ID) + }) + + t.Run("errors when the source holds no SVIDs", func(t *testing.T) { + // Not reachable through NewWITSource: parseWITSVIDs rejects an empty + // response and watchWITSVIDs routes that to OnWITSVIDsWatchError, so + // OnWITSVIDsUpdate never delivers an empty slice. The branch is + // defensive; this pins its error rather than a nil SVID. + var src workloadapi.WITSource + _, err := src.GetWITSVID() + require.EqualError(t, err, "witsource: no WIT-SVID available") + }) +} + func TestWITSourceClose(t *testing.T) { api := fakeworkloadapi.New(t) t.Cleanup(api.Stop) @@ -82,6 +117,9 @@ func TestWITSourceClose(t *testing.T) { _, err := src.GetWITSVIDForID(witFooID) require.EqualError(t, err, "witsource: source is closed") + _, err = src.GetWITSVID() + require.EqualError(t, err, "witsource: source is closed") + _, err = src.GetWITBundleForTrustDomain(witTD) require.EqualError(t, err, "witsource: source is closed") }) From 555d7513e11e99fe9610c48c32ba598ae2ffbcbb Mon Sep 17 00:00:00 2001 From: Marcos Yacob Date: Mon, 24 Aug 2026 11:30:52 -0300 Subject: [PATCH 2/2] simplify comment Signed-off-by: Marcos Yacob --- exp/svid/witsvid/source.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exp/svid/witsvid/source.go b/exp/svid/witsvid/source.go index f47718261..a0f2d4d93 100644 --- a/exp/svid/witsvid/source.go +++ b/exp/svid/witsvid/source.go @@ -4,8 +4,7 @@ import "github.com/spiffe/go-spiffe/v2/spiffeid" // Source is a source of WIT-SVIDs. type Source interface { - // GetWITSVID returns the default WIT-SVID from the source. It is used by - // code that presents a credential and therefore has no ID to look up. + // GetWITSVID returns the default WIT-SVID from the source. GetWITSVID() (*SVID, error) // GetWITSVIDForID returns the WIT-SVID for the given SPIFFE ID.