blob: 78cdaddfcad10c1da6e25366d720d23e29148018 [file]
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)
}