| package jwtbundle |
| |
| import ( |
| "crypto" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "sync" |
| |
| "github.com/go-jose/go-jose/v4" |
| "github.com/spiffe/go-spiffe/v2/internal/jwtutil" |
| "github.com/spiffe/go-spiffe/v2/spiffeid" |
| ) |
| |
| // Bundle is a collection of trusted JWT authorities for a trust domain. |
| type Bundle struct { |
| trustDomain spiffeid.TrustDomain |
| |
| mtx sync.RWMutex |
| jwtAuthorities map[string]crypto.PublicKey |
| } |
| |
| // New creates a new bundle. |
| func New(trustDomain spiffeid.TrustDomain) *Bundle { |
| return &Bundle{ |
| trustDomain: trustDomain, |
| jwtAuthorities: make(map[string]crypto.PublicKey), |
| } |
| } |
| |
| // FromJWTAuthorities creates a new bundle from JWT authorities |
| func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle { |
| return &Bundle{ |
| trustDomain: trustDomain, |
| jwtAuthorities: jwtutil.CopyJWTAuthorities(jwtAuthorities), |
| } |
| } |
| |
| // 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) |
| } |
| |
| // Read decodes a bundle from a reader. The contents must contain a standard RFC 7517 JWKS document. |
| func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) { |
| b, err := io.ReadAll(r) |
| if err != nil { |
| return nil, wrapJwtbundleErr(fmt.Errorf("unable to read: %v", err)) |
| } |
| |
| return Parse(trustDomain, b) |
| } |
| |
| // Parse parses a bundle from bytes. The data must be a standard RFC 7517 JWKS document. |
| func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) { |
| jwks := new(jose.JSONWebKeySet) |
| if err := json.Unmarshal(bundleBytes, jwks); err != nil { |
| return nil, wrapJwtbundleErr(fmt.Errorf("unable to parse JWKS: %v", err)) |
| } |
| |
| bundle := New(trustDomain) |
| for i, key := range jwks.Keys { |
| if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil { |
| return nil, wrapJwtbundleErr(fmt.Errorf("error adding authority %d of JWKS: %v", i, errors.Unwrap(err))) |
| } |
| } |
| |
| return bundle, nil |
| } |
| |
| // TrustDomain returns the trust domain that the bundle belongs to. |
| func (b *Bundle) TrustDomain() spiffeid.TrustDomain { |
| return b.trustDomain |
| } |
| |
| // JWTAuthorities returns the JWT authorities in the bundle, keyed by key ID. |
| func (b *Bundle) JWTAuthorities() map[string]crypto.PublicKey { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| return jwtutil.CopyJWTAuthorities(b.jwtAuthorities) |
| } |
| |
| // FindJWTAuthority finds the JWT authority with the given key ID from the bundle. If the authority |
| // is found, it is returned and the boolean is true. Otherwise, the returned |
| // value is nil and the boolean is false. |
| func (b *Bundle) FindJWTAuthority(keyID string) (crypto.PublicKey, bool) { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| if jwtAuthority, ok := b.jwtAuthorities[keyID]; ok { |
| return jwtAuthority, true |
| } |
| return nil, false |
| } |
| |
| // HasJWTAuthority returns true if the bundle has a JWT authority with the given key ID. |
| func (b *Bundle) HasJWTAuthority(keyID string) bool { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| _, ok := b.jwtAuthorities[keyID] |
| return ok |
| } |
| |
| // AddJWTAuthority adds a JWT authority to the bundle. If a JWT authority already exists |
| // under the given key ID, it is replaced. A key ID must be specified. |
| func (b *Bundle) AddJWTAuthority(keyID string, jwtAuthority crypto.PublicKey) error { |
| if keyID == "" { |
| return wrapJwtbundleErr(errors.New("keyID cannot be empty")) |
| } |
| |
| b.mtx.Lock() |
| defer b.mtx.Unlock() |
| |
| b.jwtAuthorities[keyID] = jwtAuthority |
| return nil |
| } |
| |
| // RemoveJWTAuthority removes the JWT authority identified by the key ID from the bundle. |
| func (b *Bundle) RemoveJWTAuthority(keyID string) { |
| b.mtx.Lock() |
| defer b.mtx.Unlock() |
| |
| delete(b.jwtAuthorities, keyID) |
| } |
| |
| // SetJWTAuthorities sets the JWT authorities in the bundle. |
| func (b *Bundle) SetJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) { |
| b.mtx.Lock() |
| defer b.mtx.Unlock() |
| |
| b.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities) |
| } |
| |
| // Empty returns true if the bundle has no JWT authorities. |
| func (b *Bundle) Empty() bool { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| return len(b.jwtAuthorities) == 0 |
| } |
| |
| // Marshal marshals the JWT bundle into a standard RFC 7517 JWKS document. The |
| // JWKS does not contain any SPIFFE-specific parameters. |
| func (b *Bundle) Marshal() ([]byte, error) { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| jwks := jose.JSONWebKeySet{} |
| for keyID, jwtAuthority := range b.jwtAuthorities { |
| jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ |
| Key: jwtAuthority, |
| KeyID: keyID, |
| }) |
| } |
| |
| return json.Marshal(jwks) |
| } |
| |
| // Clone clones the bundle. |
| func (b *Bundle) Clone() *Bundle { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| return FromJWTAuthorities(b.trustDomain, b.jwtAuthorities) |
| } |
| |
| // Equal compares the bundle for equality against the given bundle. |
| func (b *Bundle) Equal(other *Bundle) bool { |
| if b == nil || other == nil { |
| return b == other |
| } |
| |
| return b.trustDomain == other.trustDomain && |
| jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities) |
| } |
| |
| // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust |
| // domain. It implements the Source interface. An error will be returned if |
| // the trust domain does not match that of the bundle. |
| func (b *Bundle) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { |
| b.mtx.RLock() |
| defer b.mtx.RUnlock() |
| |
| if b.trustDomain != trustDomain { |
| return nil, wrapJwtbundleErr(fmt.Errorf("no JWT bundle for trust domain %q", trustDomain)) |
| } |
| |
| return b, nil |
| } |
| |
| func wrapJwtbundleErr(err error) error { |
| return fmt.Errorf("jwtbundle: %w", err) |
| } |