1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
|
package jwtbundle
import (
"crypto"
"encoding/json"
"errors"
"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"
"github.com/zeebo/errs"
)
var jwtbundleErr = errs.Class("jwtbundle")
// 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, jwtbundleErr.New("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, jwtbundleErr.New("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, jwtbundleErr.New("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, jwtbundleErr.New("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 jwtbundleErr.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, jwtbundleErr.New("no JWT bundle for trust domain %q", trustDomain)
}
return b, nil
}
|