Skip to content

Add WIT bundle functionality#367

Draft
jsnctl wants to merge 7 commits intospiffe:mainfrom
jsnctl:jsnctl/wit-bundle
Draft

Add WIT bundle functionality#367
jsnctl wants to merge 7 commits intospiffe:mainfrom
jsnctl:jsnctl/wit-bundle

Conversation

@jsnctl
Copy link
Copy Markdown

@jsnctl jsnctl commented Oct 16, 2025

implements #365

Opening draft PR ahead of merge of upstream spiffe/spiffe#327 (per discussion here)

// AddWITAuthority adds a WIT authority to the bundle. If a WIT authority already exists
// under the given key ID, it is replaced. A key ID must be specified.
func (b *Bundle) AddWITAuthority(keyID string, witAuthority crypto.PublicKey) error {
if keyID == "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constraint that a keypair must have a kid is interesting - I have raised this for discussion on spiffe/spiffe#327 (comment)

Today, the JWT-SVID/WIT-SVID profiles of the Workload API do not make the kid member of the JWKS mandatory - but it looks like go-spiffe treats it as such. I do think that the behaviour of go-spiffe here is reasonable, but, we should consider at least modifying the WIP WIT-SVID profile to express that the kid member is mandatory.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the existing jwtsvid implementation, it also concretely relies on the kid in the FindJWTAuthority call for verification (the corresponding witsvid changes will be in a separate merge)

// Obtain the key ID from the header
keyID := tok.Headers[0].KeyID
if keyID == "" {
return nil, wrapJwtsvidErr(errors.New("token header missing key id"))
}
// Get JWT Bundle
bundle, err := bundles.GetJWTBundleForTrustDomain(trustDomain)
if err != nil {
return nil, wrapJwtsvidErr(fmt.Errorf("no bundle found for trust domain %q", trustDomain))
}
// Find JWT authority using the key ID from the token header
authority, ok := bundle.FindJWTAuthority(keyID)
if !ok {
return nil, wrapJwtsvidErr(fmt.Errorf("no JWT authority %q found for trust domain %q", keyID, trustDomain))
}

Presumably JWKS usage without a kid in each authority would just require iterating over all available JWKs, so this seems like a nice pattern to maintain here.

As for the spec, you can imagine (less tidy) downstream implementations which use valid JWKS without the kid? Perhaps the opinion of go-spiffe here isn't the only valid consideration?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the opinion of go-spiffe here isn't the only valid consideration?

Perhaps so - although it definitely carries a fair bit of weight in terms of the "real-world" usage of the specification. Today, if a Workload API implementation does not provide the kid member of the trust bundle JWKS, then it looks like go-spiffe/jwtbundle would fail to unmarshal it. Fortunately, I think all "real-world" implementations of the SPIFFE specification are likely to include the kid member.

However, as we're introducing a new profile for WIT, it does seem like a good opportunity to make the spec a little more restrictive if it means that all proper implementations of the spec will be compatible with the SDKs. At the moment the gray area where an implementation could be spec-compliant but not work with go-spiffe seems a little problematic.

Copy link
Copy Markdown
Member

@arndt-s arndt-s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started a review last week but got distracted. Publishing my comments so far.


// Load loads a bundle from a file on disk. The file must contain a standard RFC 7517 JWKS document.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
bundleBytes, err := os.ReadFile(path)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you could use Read() here and supply the file as io.Reader.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this would be a nicer implementation - do we want to keep things consistent with the methods in the other *bundle implementations though?

// Load loads a bundle from a file on disk. The file must contain a standard RFC 7517 JWKS document.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
bundleBytes, err := os.ReadFile(path)
if err != nil {
return nil, wrapJwtbundleErr(fmt.Errorf("unable to read JWT bundle: %w", err))
}
return Parse(trustDomain, bundleBytes)
}

func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
fileBytes, err := os.ReadFile(path)
if err != nil {
return nil, wrapX509bundleErr(fmt.Errorf("unable to load X.509 bundle file: %w", err))
}
return Parse(trustDomain, fileBytes)
}

// Load loads a bundle from a file on disk. The file must contain a JWKS
// document following the SPIFFE Trust Domain and Bundle specification.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
bundleBytes, err := os.ReadFile(path)
if err != nil {
return nil, wrapSpiffebundleErr(fmt.Errorf("unable to read SPIFFE bundle: %w", err))
}
return Parse(trustDomain, bundleBytes)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think consistency is likely more important here - that being said - it'd be nice if we did extend the API of all the profiles to include LoadFromReader or similar? and perhaps refactor Load to leverage that under the hood.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should ideally be a generic. Curious to get the maintainers POV on this. But maybe something for another day.

@jsnctl jsnctl force-pushed the jsnctl/wit-bundle branch from 25c8efc to 029eaa3 Compare October 28, 2025 20:58
Jason Costello added 7 commits October 28, 2025 20:59
* Note this commit still has JWTs in testdata. These will be replaced
  with WIMSE-compliant WITs in a later push

Signed-off-by: Jason Costello <jason@cofide.io>
Signed-off-by: Jason Costello <jason@cofide.io>
Signed-off-by: Jason Costello <jason@cofide.io>
Signed-off-by: Jason Costello <jason@cofide.io>
Signed-off-by: Jason Costello <jason@cofide.io>
Signed-off-by: Jason Costello <jason@cofide.io>
Signed-off-by: Jason Costello <jason@cofide.io>
@jsnctl jsnctl force-pushed the jsnctl/wit-bundle branch from 029eaa3 to 20e33c7 Compare October 28, 2025 20:59
@jsnctl
Copy link
Copy Markdown
Author

jsnctl commented Nov 26, 2025

We'd originally said to hold off a merge until spiffe/spiffe#327 was merged (which it is now), do with think that's still applicable here?

@jsnctl
Copy link
Copy Markdown
Author

jsnctl commented Mar 23, 2026

Not sure of the best way to stack this, but I've also got a branch based off this diff for the corresponding witsvid changes too jsnctl#1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants