| package spiffebundle | |
| import ( | |
| "crypto" | |
| "crypto/x509" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "os" | |
| "sync" | |
| "time" | |
| "github.com/go-jose/go-jose/v4" | |
| "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" | |
| "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" | |
| "github.com/spiffe/go-spiffe/v2/exp/bundle/witbundle" | |
| "github.com/spiffe/go-spiffe/v2/internal/jwtutil" | |
| "github.com/spiffe/go-spiffe/v2/internal/x509util" | |
| "github.com/spiffe/go-spiffe/v2/spiffeid" | |
| ) | |
| const ( | |
| x509SVIDUse = "x509-svid" | |
| jwtSVIDUse = "jwt-svid" | |
| witSVIDUse = "wit-svid" | |
| ) | |
| type bundleDoc struct { | |
| jose.JSONWebKeySet | |
| SequenceNumber *uint64 `json:"spiffe_sequence,omitempty"` | |
| RefreshHint *int64 `json:"spiffe_refresh_hint,omitempty"` | |
| } | |
| // Bundle is a collection of trusted public key material for a trust domain, | |
| // conforming to the SPIFFE Bundle Format as part of the SPIFFE Trust Domain | |
| // and Bundle specification: | |
| // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md | |
| type Bundle struct { | |
| trustDomain spiffeid.TrustDomain | |
| mtx sync.RWMutex | |
| refreshHint *time.Duration | |
| sequenceNumber *uint64 | |
| jwtAuthorities map[string]crypto.PublicKey | |
| witAuthorities map[string]crypto.PublicKey | |
| x509Authorities []*x509.Certificate | |
| } | |
| // New creates a new bundle. | |
| func New(trustDomain spiffeid.TrustDomain) *Bundle { | |
| return &Bundle{ | |
| trustDomain: trustDomain, | |
| jwtAuthorities: make(map[string]crypto.PublicKey), | |
| witAuthorities: make(map[string]crypto.PublicKey), | |
| } | |
| } | |
| // 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) | |
| } | |
| // Read decodes a bundle from a reader. The contents must contain a JWKS | |
| // document following the SPIFFE Trust Domain and Bundle specification. | |
| func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) { | |
| b, err := io.ReadAll(r) | |
| if err != nil { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("unable to read: %v", err)) | |
| } | |
| return Parse(trustDomain, b) | |
| } | |
| // Parse parses a bundle from bytes. The data must be a JWKS document following | |
| // the SPIFFE Trust Domain and Bundle specification. | |
| func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) { | |
| jwks := &bundleDoc{} | |
| if err := json.Unmarshal(bundleBytes, jwks); err != nil { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("unable to parse JWKS: %v", err)) | |
| } | |
| bundle := New(trustDomain) | |
| if jwks.RefreshHint != nil { | |
| bundle.SetRefreshHint(time.Second * time.Duration(*jwks.RefreshHint)) | |
| } | |
| if jwks.SequenceNumber != nil { | |
| bundle.SetSequenceNumber(*jwks.SequenceNumber) | |
| } | |
| if jwks.Keys == nil { | |
| // The parameter keys MUST be present. | |
| // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#413-keys | |
| return nil, wrapSpiffebundleErr(errors.New("no authorities found")) | |
| } | |
| for i, key := range jwks.Keys { | |
| switch key.Use { | |
| // Two SVID types are supported: x509-svid and jwt-svid. | |
| case x509SVIDUse: | |
| if len(key.Certificates) != 1 { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("expected a single certificate in %s entry %d; got %d", x509SVIDUse, i, len(key.Certificates))) | |
| } | |
| bundle.AddX509Authority(key.Certificates[0]) | |
| case jwtSVIDUse: | |
| if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("error adding authority %d of JWKS: %w", i, errors.Unwrap(err))) | |
| } | |
| case witSVIDUse: | |
| if err := bundle.AddWITAuthority(key.KeyID, key.Key); err != nil { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("error adding WIT authority %d of JWKS: %w", i, errors.Unwrap(err))) | |
| } | |
| } | |
| } | |
| return bundle, nil | |
| } | |
| // FromX509Bundle creates a bundle from an X.509 bundle. | |
| // The function panics in case of a nil X.509 bundle. | |
| func FromX509Bundle(x509Bundle *x509bundle.Bundle) *Bundle { | |
| bundle := New(x509Bundle.TrustDomain()) | |
| bundle.x509Authorities = x509Bundle.X509Authorities() | |
| return bundle | |
| } | |
| // FromJWTBundle creates a bundle from a JWT bundle. | |
| // The function panics in case of a nil JWT bundle. | |
| func FromJWTBundle(jwtBundle *jwtbundle.Bundle) *Bundle { | |
| bundle := New(jwtBundle.TrustDomain()) | |
| bundle.jwtAuthorities = jwtBundle.JWTAuthorities() | |
| return bundle | |
| } | |
| // FromX509Authorities creates a bundle from X.509 certificates. | |
| func FromX509Authorities(trustDomain spiffeid.TrustDomain, x509Authorities []*x509.Certificate) *Bundle { | |
| bundle := New(trustDomain) | |
| bundle.x509Authorities = x509util.CopyX509Authorities(x509Authorities) | |
| return bundle | |
| } | |
| // FromJWTAuthorities creates a new bundle from JWT authorities. | |
| func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle { | |
| bundle := New(trustDomain) | |
| bundle.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities) | |
| return bundle | |
| } | |
| // FromWITAuthorities creates a new bundle from WIT authorities. | |
| func FromWITAuthorities(trustDomain spiffeid.TrustDomain, witAuthorities map[string]crypto.PublicKey) *Bundle { | |
| bundle := New(trustDomain) | |
| bundle.witAuthorities = jwtutil.CopyJWTAuthorities(witAuthorities) | |
| return bundle | |
| } | |
| // FromWITBundle creates a bundle from a WIT bundle. | |
| // The function panics in case of a nil WIT bundle. | |
| func FromWITBundle(witBundle *witbundle.Bundle) *Bundle { | |
| bundle := New(witBundle.TrustDomain()) | |
| bundle.witAuthorities = witBundle.WITAuthorities() | |
| return bundle | |
| } | |
| // TrustDomain returns the trust domain that the bundle belongs to. | |
| func (b *Bundle) TrustDomain() spiffeid.TrustDomain { | |
| return b.trustDomain | |
| } | |
| // X509Authorities returns the X.509 authorities in the bundle. | |
| func (b *Bundle) X509Authorities() []*x509.Certificate { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| return x509util.CopyX509Authorities(b.x509Authorities) | |
| } | |
| // AddX509Authority adds an X.509 authority to the bundle. If the authority already | |
| // exists in the bundle, the contents of the bundle will remain unchanged. | |
| func (b *Bundle) AddX509Authority(x509Authority *x509.Certificate) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| for _, r := range b.x509Authorities { | |
| if r.Equal(x509Authority) { | |
| return | |
| } | |
| } | |
| b.x509Authorities = append(b.x509Authorities, x509Authority) | |
| } | |
| // RemoveX509Authority removes an X.509 authority from the bundle. | |
| func (b *Bundle) RemoveX509Authority(x509Authority *x509.Certificate) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| for i, r := range b.x509Authorities { | |
| if r.Equal(x509Authority) { | |
| b.x509Authorities = append(b.x509Authorities[:i], b.x509Authorities[i+1:]...) | |
| return | |
| } | |
| } | |
| } | |
| // HasX509Authority checks if the given X.509 authority exists in the bundle. | |
| func (b *Bundle) HasX509Authority(x509Authority *x509.Certificate) bool { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| for _, r := range b.x509Authorities { | |
| if r.Equal(x509Authority) { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // SetX509Authorities sets the X.509 authorities in the bundle. | |
| func (b *Bundle) SetX509Authorities(authorities []*x509.Certificate) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.x509Authorities = x509util.CopyX509Authorities(authorities) | |
| } | |
| // 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() | |
| jwtAuthority, ok := b.jwtAuthorities[keyID] | |
| return jwtAuthority, ok | |
| } | |
| // 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 wrapSpiffebundleErr(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) | |
| } | |
| // WITAuthorities returns the WIT authorities in the bundle, keyed by key ID. | |
| func (b *Bundle) WITAuthorities() map[string]crypto.PublicKey { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| return jwtutil.CopyJWTAuthorities(b.witAuthorities) | |
| } | |
| // FindWITAuthority finds the WIT 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) FindWITAuthority(keyID string) (crypto.PublicKey, bool) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| witAuthority, ok := b.witAuthorities[keyID] | |
| return witAuthority, ok | |
| } | |
| // HasWITAuthority returns true if the bundle has a WIT authority with the given key ID. | |
| func (b *Bundle) HasWITAuthority(keyID string) bool { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| _, ok := b.witAuthorities[keyID] | |
| return ok | |
| } | |
| // 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 == "" { | |
| return wrapSpiffebundleErr(errors.New("keyID cannot be empty")) | |
| } | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.witAuthorities[keyID] = witAuthority | |
| return nil | |
| } | |
| // RemoveWITAuthority removes the WIT authority identified by the key ID from the bundle. | |
| func (b *Bundle) RemoveWITAuthority(keyID string) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| delete(b.witAuthorities, keyID) | |
| } | |
| // SetWITAuthorities sets the WIT authorities in the bundle. | |
| func (b *Bundle) SetWITAuthorities(witAuthorities map[string]crypto.PublicKey) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.witAuthorities = jwtutil.CopyJWTAuthorities(witAuthorities) | |
| } | |
| // Empty returns true if the bundle has no X.509, JWT, or WIT authorities. | |
| func (b *Bundle) Empty() bool { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| return len(b.x509Authorities) == 0 && len(b.jwtAuthorities) == 0 && len(b.witAuthorities) == 0 | |
| } | |
| // RefreshHint returns the refresh hint. If the refresh hint is set in | |
| // the bundle, it is returned and the boolean is true. Otherwise, the returned | |
| // value is zero and the boolean is false. | |
| func (b *Bundle) RefreshHint() (refreshHint time.Duration, ok bool) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| if b.refreshHint != nil { | |
| return *b.refreshHint, true | |
| } | |
| return 0, false | |
| } | |
| // SetRefreshHint sets the refresh hint. The refresh hint value will be | |
| // truncated to time.Second. | |
| func (b *Bundle) SetRefreshHint(refreshHint time.Duration) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.refreshHint = &refreshHint | |
| } | |
| // ClearRefreshHint clears the refresh hint. | |
| func (b *Bundle) ClearRefreshHint() { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.refreshHint = nil | |
| } | |
| // SequenceNumber returns the sequence number. If the sequence number is set in | |
| // the bundle, it is returned and the boolean is true. Otherwise, the returned | |
| // value is zero and the boolean is false. | |
| func (b *Bundle) SequenceNumber() (uint64, bool) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| if b.sequenceNumber != nil { | |
| return *b.sequenceNumber, true | |
| } | |
| return 0, false | |
| } | |
| // SetSequenceNumber sets the sequence number. | |
| func (b *Bundle) SetSequenceNumber(sequenceNumber uint64) { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.sequenceNumber = &sequenceNumber | |
| } | |
| // ClearSequenceNumber clears the sequence number. | |
| func (b *Bundle) ClearSequenceNumber() { | |
| b.mtx.Lock() | |
| defer b.mtx.Unlock() | |
| b.sequenceNumber = nil | |
| } | |
| // Marshal marshals the bundle according to the SPIFFE Trust Domain and Bundle | |
| // specification. The trust domain is not marshaled as part of the bundle and | |
| // must be conveyed separately. See the specification for details. | |
| func (b *Bundle) Marshal() ([]byte, error) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| jwks := bundleDoc{} | |
| if b.refreshHint != nil { | |
| tr := int64((*b.refreshHint + (time.Second - 1)) / time.Second) | |
| jwks.RefreshHint = &tr | |
| } | |
| jwks.SequenceNumber = b.sequenceNumber | |
| for _, x509Authority := range b.x509Authorities { | |
| jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ | |
| Key: x509Authority.PublicKey, | |
| Certificates: []*x509.Certificate{x509Authority}, | |
| Use: x509SVIDUse, | |
| }) | |
| } | |
| for keyID, jwtAuthority := range b.jwtAuthorities { | |
| jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ | |
| Key: jwtAuthority, | |
| KeyID: keyID, | |
| Use: jwtSVIDUse, | |
| }) | |
| } | |
| for keyID, witAuthority := range b.witAuthorities { | |
| jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ | |
| Key: witAuthority, | |
| KeyID: keyID, | |
| Use: witSVIDUse, | |
| }) | |
| } | |
| return json.Marshal(jwks) | |
| } | |
| // Clone clones the bundle. | |
| func (b *Bundle) Clone() *Bundle { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| return &Bundle{ | |
| trustDomain: b.trustDomain, | |
| refreshHint: copyRefreshHint(b.refreshHint), | |
| sequenceNumber: copySequenceNumber(b.sequenceNumber), | |
| x509Authorities: x509util.CopyX509Authorities(b.x509Authorities), | |
| jwtAuthorities: jwtutil.CopyJWTAuthorities(b.jwtAuthorities), | |
| witAuthorities: jwtutil.CopyJWTAuthorities(b.witAuthorities), | |
| } | |
| } | |
| // X509Bundle returns an X.509 bundle containing the X.509 authorities in the SPIFFE | |
| // bundle. | |
| func (b *Bundle) X509Bundle() *x509bundle.Bundle { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| // FromX509Authorities makes a copy, so we can pass our internal slice directly. | |
| return x509bundle.FromX509Authorities(b.trustDomain, b.x509Authorities) | |
| } | |
| // JWTBundle returns a JWT bundle containing the JWT authorities in the SPIFFE bundle. | |
| func (b *Bundle) JWTBundle() *jwtbundle.Bundle { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| // FromJWTBundle makes a copy, so we can pass our internal slice directly. | |
| return jwtbundle.FromJWTAuthorities(b.trustDomain, b.jwtAuthorities) | |
| } | |
| // GetBundleForTrustDomain returns the SPIFFE 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) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| if b.trustDomain != trustDomain { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("no SPIFFE bundle for trust domain %q", trustDomain)) | |
| } | |
| return b, nil | |
| } | |
| // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust | |
| // domain. It implements the x509bundle.Source interface. An error will be | |
| // returned if the trust domain does not match that of the bundle. | |
| func (b *Bundle) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| if b.trustDomain != trustDomain { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("no X.509 bundle for trust domain %q", trustDomain)) | |
| } | |
| return b.X509Bundle(), nil | |
| } | |
| // GetJWTBundleForTrustDomain returns the JWT bundle of the given trust domain. | |
| // It implements the jwtbundle.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) (*jwtbundle.Bundle, error) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| if b.trustDomain != trustDomain { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("no JWT bundle for trust domain %q", trustDomain)) | |
| } | |
| return b.JWTBundle(), nil | |
| } | |
| // WITBundle returns a WIT bundle containing the WIT authorities in the SPIFFE bundle. | |
| func (b *Bundle) WITBundle() *witbundle.Bundle { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| return witbundle.FromWITAuthorities(b.trustDomain, b.witAuthorities) | |
| } | |
| // GetWITBundleForTrustDomain returns the WIT bundle of the given trust domain. | |
| // It implements the witbundle.Source interface. An error will be returned if | |
| // the trust domain does not match that of the bundle. | |
| func (b *Bundle) GetWITBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*witbundle.Bundle, error) { | |
| b.mtx.RLock() | |
| defer b.mtx.RUnlock() | |
| if b.trustDomain != trustDomain { | |
| return nil, wrapSpiffebundleErr(fmt.Errorf("no WIT bundle for trust domain %q", trustDomain)) | |
| } | |
| return b.WITBundle(), nil | |
| } | |
| // 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 && | |
| refreshHintEqual(b.refreshHint, other.refreshHint) && | |
| sequenceNumberEqual(b.sequenceNumber, other.sequenceNumber) && | |
| jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities) && | |
| jwtutil.JWTAuthoritiesEqual(b.witAuthorities, other.witAuthorities) && | |
| x509util.CertsEqual(b.x509Authorities, other.x509Authorities) | |
| } | |
| func refreshHintEqual(a, b *time.Duration) bool { | |
| if a == nil || b == nil { | |
| return a == b | |
| } | |
| return *a == *b | |
| } | |
| func sequenceNumberEqual(a, b *uint64) bool { | |
| if a == nil || b == nil { | |
| return a == b | |
| } | |
| return *a == *b | |
| } | |
| func copyRefreshHint(refreshHint *time.Duration) *time.Duration { | |
| if refreshHint == nil { | |
| return nil | |
| } | |
| copied := *refreshHint | |
| return &copied | |
| } | |
| func copySequenceNumber(sequenceNumber *uint64) *uint64 { | |
| if sequenceNumber == nil { | |
| return nil | |
| } | |
| copied := *sequenceNumber | |
| return &copied | |
| } | |
| func wrapSpiffebundleErr(err error) error { | |
| return fmt.Errorf("spiffebundle: %w", err) | |
| } |