| 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) | |
| } |