Documentation
¶
Overview ¶
Package clortho supplies the keys a service needs to verify JWS signatures, such as those on JWTs, as a jws.KeyProvider for github.com/lestrrat-go/jwx/v4.
It makes three kinds of provider, for three ways a service can come by its keys.
A KeySetProvider is for a server that publishes its keys as a set. It polls each configured source for its complete key set, a JWK set or a single JWK, keeps the keys in one map by key ID, and answers jwx's FetchKeys from that map. It never fetches a key on demand, so a token cannot cause it to contact anything; see KeySetConfig.RefreshOnUnknownKeyID for the one opt-in exception, which is rate-limited.
A PerKeyProvider is for a server that serves one key per request and no key set, as themis does. It fetches a key when a token names it, and then holds it. The key ID it asks for comes from a token nobody has verified yet, so a token can make it send a request. PerKeyConfig bounds what that can cost, and a service that can list its key IDs should.
A FixedKeyProvider is for keys written into the service's own configuration, as PEM or as JWKs. It never sends a request at all.
Prefer a KeySetProvider wherever the server offers a key set. A service that needs several providers, of any kind, passes each one to jwx, which asks them in the order given. The package example shows all three kinds used together.
Configuration ¶
Each kind is configured by a plain struct with no options and no struct tags: KeySetConfig, PerKeyConfig, and FixedKeyConfig. A service unmarshals its own settings and maps them onto one of these. For the two kinds that reach a server, it also hands over the *http.Client to reach it with. That client is required, and clortho uses it exactly as given; the example on NewKeySetProvider shows one suited to fetching keys.
p, err := clortho.NewKeySetProvider(clortho.KeySetConfig{
Sources: []clortho.RefreshSource{{
URI: "https://issuer.example.com/keys",
Client: client,
}},
})
if err != nil {
return err
}
p.AddListener(zapListener)
if err := p.Start(ctx); err != nil {
return err
}
defer p.Stop(ctx)
parser, err := basculejwt.NewTokenParser(jwt.WithKeyProvider(p))
A KeySetProvider in service ¶
Start returns once the refresh loops are running; it does not wait for the first fetch. Status reports each source's last outcome, so a readiness check can decide when the KeySetProvider has keys. On a refresh failure the last good keys keep serving; Status exposes their age. A failed refresh is retried well before the next scheduled one, and when a server asks for a pause with Retry-After that is the wait used; see RefreshSource.
Every key a source serves must carry a kid. A key ID served by two sources is an error, not a merge: a KeySetProvider is one map, and a deployment that needs separate key spaces builds separate KeySetProviders.
A PerKeyProvider in service ¶
A PerKeyProvider has nothing to start or stop, and holds no keys until a token asks, so it has no readiness to report. The first token that names a key waits for that key to be fetched. A key ID the operator listed in PerKeyConfig.AllowedKeyIDs may always be fetched; any other is fetched no more often than PerKeyConfig.FetchRateLimit allows, so that a flood of invented key IDs cannot become a flood of requests to the key server. A key whose time is up keeps serving while the server cannot be reached, and is dropped as soon as the server says it is gone.
A FixedKeyProvider in service ¶
A FixedKeyProvider is complete as soon as it is built. It has nothing to start, stop, or watch, and what it holds changes only when the service's configuration does. Every key is given its key ID in the configuration, since a PEM has nowhere to carry one.
Every provider ¶
No key may be symmetric, and a private key is reduced to its public half. By default a key's "use" must be "sig" and its "alg", when present, must match the token; VerifyConfig turns either check off.
Errors carry sentinels for errors.Is. clorthozap and clorthometrics log and count what the two kinds of provider that reach a server do. clorthofx builds any number of providers of every kind for a go.uber.org/fx application, and presents them to it as one.
Example ¶
A service that takes keys from more than one place builds a provider for each and gives them all to jwx, which asks them in the order given. The three kinds can be mixed, and there can be several of any of them.
package main
import (
"fmt"
"net/http"
"time"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/xmidt-org/clortho"
)
func main() {
client := &http.Client{Timeout: 30 * time.Second}
// A key written into this service's own configuration.
fixed, err := clortho.NewFixedKeyProvider(clortho.FixedKeyConfig{
Keys: []clortho.FixedKey{{KeyID: "break-glass", Key: examplePEM}},
})
if err != nil {
fmt.Println(err)
return
}
// A server that publishes its keys as a set.
keySets, err := clortho.NewKeySetProvider(clortho.KeySetConfig{
Sources: []clortho.RefreshSource{
{URI: "https://issuer.example.com/keys", Client: client},
},
})
if err != nil {
fmt.Println(err)
return
}
// Two servers that each serve one key per request.
themis, err := clortho.NewPerKeyProvider(clortho.PerKeyConfig{
Template: "https://themis.example.com/keys/{keyID}",
Client: client,
// This service knows every key themis has, so no other key ID is
// ever asked for.
AllowedKeyIDs: []string{"themis-2026"},
AllowedKeyIDsOnly: true,
})
if err != nil {
fmt.Println(err)
return
}
partner, err := clortho.NewPerKeyProvider(clortho.PerKeyConfig{
Template: "https://keys.partner.example.net/{keyID}/key.json",
Client: client,
})
if err != nil {
fmt.Println(err)
return
}
// jwx asks the providers in this order and stops at the first key that
// verifies the token. The two kinds that a token cannot make send a
// request go first, and the per-key providers last.
options := []jws.VerifyOption{
jws.WithKeyProvider(fixed),
jws.WithKeyProvider(keySets),
jws.WithKeyProvider(themis),
jws.WithKeyProvider(partner),
}
// Verifying a token is then jws.Verify(token, options...). A service
// using bascule passes the same providers to its token parser instead,
// each as a jwt.WithKeyProvider option.
fmt.Println(len(options), "providers")
}
// examplePEM and exampleJWK are the same public key, written the two ways a
// fixed key may be.
const examplePEM = `
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/TjDSjYqcGZtvtmV+gjGmdLqO03q
XmnwqIK24dVkPClST1e1NM7fxHBy/CwPKk/ZXxbFSdgFjGGO3kr+wcGIpA==
-----END PUBLIC KEY-----`
Output: 4 providers
Index ¶
- Constants
- Variables
- type FetchEvent
- type FetchListener
- type FixedKey
- type FixedKeyConfig
- type FixedKeyProvider
- type HTTPError
- type KeySetConfig
- type KeySetProvider
- func (p *KeySetProvider) AddListener(l Listener) (cancel func())
- func (p *KeySetProvider) FetchKeys(ctx context.Context, sink jws.KeySink, sig *jws.Signature, _ *jws.Message) error
- func (p *KeySetProvider) KeyIDs() []string
- func (p *KeySetProvider) Start(context.Context) error
- func (p *KeySetProvider) Status() []SourceStatus
- func (p *KeySetProvider) Stop(ctx context.Context) error
- type Listener
- type PerKeyConfig
- type PerKeyProvider
- type RefreshEvent
- type RefreshSource
- type SourceStatus
- type VerifyConfig
Examples ¶
Constants ¶
const ( // DefaultRefreshInterval is the time between refreshes when a source gives // no hint and RefreshSource.RefreshInterval is zero. DefaultRefreshInterval = 24 * time.Hour // DefaultMinRefreshInterval is the shortest time between refreshes when // RefreshSource.MinRefreshInterval is zero. DefaultMinRefreshInterval = 10 * time.Minute // DefaultMaxRefreshInterval is the longest time between refreshes when // RefreshSource.MaxRefreshInterval is zero. DefaultMaxRefreshInterval = 7 * 24 * time.Hour // DefaultJitterPercentage is the jitter applied when // RefreshSource.JitterPercentage is zero or out of range. DefaultJitterPercentage = 10.0 // DefaultMaxResponseBytes is the largest response body read from an http // or https source when RefreshSource.MaxResponseBytes is zero. At one // mebibyte it is far larger than any key set in ordinary use, so that a // set which grows during a rotation does not start failing refreshes. DefaultMaxResponseBytes int64 = 1024 * 1024 )
const ( // DefaultFetchRateLimit is the shortest time between fetches for key IDs // that are not allowed key IDs, when PerKeyConfig.FetchRateLimit is zero. DefaultFetchRateLimit = 10 * time.Second // DefaultCacheTime is how long a fetched key is held when the server does // not say and PerKeyConfig.CacheTime is zero. DefaultCacheTime = 24 * time.Hour // DefaultMinCacheTime is the shortest time a fetched key is held when // PerKeyConfig.MinCacheTime is zero. DefaultMinCacheTime = 10 * time.Minute // DefaultMaxCacheTime is the longest time a fetched key is held when // PerKeyConfig.MaxCacheTime is zero. DefaultMaxCacheTime = 7 * 24 * time.Hour // DefaultNotFoundCoolDown is the wait before a key ID is asked for again // after the server said there is no such key, when // PerKeyConfig.NotFoundCoolDown is zero. DefaultNotFoundCoolDown = 10 * time.Minute // DefaultFailureCoolDown is the wait before a key ID is asked for again // after the server could not answer, when PerKeyConfig.FailureCoolDown is // zero. DefaultFailureCoolDown = 10 * time.Second )
Variables ¶
var ( // ErrNoKeySources is returned by NewKeySetProvider when the KeySetConfig has // no sources. ErrNoKeySources = errors.New("at least one source is required") // ErrUnsupportedScheme is returned by NewKeySetProvider when a source URI is // not a file path or a file, http, or https URI. That includes a source // whose URI cannot be parsed at all, since no supported scheme can be read // from it. NewPerKeyProvider returns it for a template that is not http or // https. ErrUnsupportedScheme = errors.New("source URI scheme is not supported; use file, http, or https") // ErrMissingClient is returned by NewKeySetProvider when an http or https // source has no Client, and by NewPerKeyProvider when the config has none. // clortho does not supply one: the client decides the timeout, redirects, // TLS, and proxies, and those are the caller's to choose. ErrMissingClient = errors.New("an http or https source requires a Client") // ErrUnusedClient is returned by NewKeySetProvider when a file source has a // Client, which it would never use. The usual cause is an http or https URI // written without its scheme, such as keys.example.com/jwks, which reads as a // file path. ErrUnusedClient = errors.New("a file source does not use a Client") // ErrInvalidTemplate is returned by NewPerKeyProvider when the URL template // cannot be used: it does not hold the {keyID} placeholder exactly once, // or holds it where a key ID could change which server is asked. ErrInvalidTemplate = errors.New("the URL template is not usable") // ErrInvalidKeyID marks a key ID that may not be put into a URL. A key ID // is letters, digits, '-', '_', and '.', at most 128 characters, with no // "..". NewPerKeyProvider returns it for such an ID in AllowedKeyIDs. A // PerKeyProvider's FetchKeys returns it, together with ErrKeyNotFound, for // such an ID in a token, and makes no request. ErrInvalidKeyID = errors.New("key ID is not valid") // ErrNoAllowedKeyIDs is returned by NewPerKeyProvider when // AllowedKeyIDsOnly is set and AllowedKeyIDs is empty, since such a // provider could never fetch anything. ErrNoAllowedKeyIDs = errors.New("AllowedKeyIDsOnly requires at least one allowed key ID") // ErrKeyIDMismatch marks a key that carries a key ID other than the one it // was wanted under. It is a fetch error when a server returns such a key // for the key ID it was asked for, and NewFixedKeyProvider returns it when // a fixed key written as a JWK carries a kid other than its FixedKey.KeyID. ErrKeyIDMismatch = errors.New("the key is not the one asked for") // ErrNoFixedKeys is returned by NewFixedKeyProvider when the config has no // keys. ErrNoFixedKeys = errors.New("at least one fixed key is required") // ErrInvalidFixedKey is returned by NewFixedKeyProvider when a fixed key's // text cannot be used: it is empty, it is neither PEM nor a JWK, it holds // more than one PEM block, or it does not parse. ErrInvalidFixedKey = errors.New("fixed key is not usable") // ErrAlreadyStarted is returned by Start when the KeySetProvider is already // running. ErrAlreadyStarted = errors.New("the provider has already been started") // ErrNotStarted is returned by Stop when the KeySetProvider is not running. ErrNotStarted = errors.New("the provider is not running") // ErrDuplicateKeyID is a refresh error: a source served a key ID that is // already on the ring from another source, or served the same key ID twice. // A KeySetProvider is one map, so a key ID names exactly one key; a // deployment that needs separate key spaces builds separate // KeySetProviders. The refresh that would introduce the duplicate fails // and leaves the ring untouched. // // NewFixedKeyProvider returns it when two fixed keys are given the same // KeyID. ErrDuplicateKeyID = errors.New("key ID is already supplied by another source") // ErrMissingKeyID is returned by FetchKeys when the protected header has no // kid, and is a refresh error when a source serves a key with no kid. // NewFixedKeyProvider returns it for a fixed key with no KeyID. ErrMissingKeyID = errors.New("no key ID") // ErrSymmetricKey is a refresh error: a source served a symmetric (oct) // key. Such keys are never accepted, since a secret published in a key set // is known to everyone who fetched it. The refresh fails and leaves the // ring untouched. ErrSymmetricKey = errors.New("symmetric keys are not accepted") // ErrResponseTooLarge is a refresh error: an HTTP response body exceeded // the source's MaxResponseBytes. ErrResponseTooLarge = errors.New("response body exceeds the source's maximum") // ErrKeyNotFound is returned by FetchKeys when no key on the ring has the // protected header's kid. ErrKeyNotFound = errors.New("no key with that key ID") // ErrMissingAlgorithm is returned by FetchKeys when the protected header has // no alg. ErrMissingAlgorithm = errors.New(`protected header must contain an "alg" field`) // ErrKeyUsage is returned by FetchKeys when the key's "use" member is set to // something other than "sig". See VerifyConfig.IgnoreKeyUsage. ErrKeyUsage = errors.New("key is not marked for signature use") // ErrKeyAlgorithm is returned by FetchKeys when the key's "alg" member is set // and does not match the protected header's alg. See // VerifyConfig.IgnoreKeyAlgorithm. ErrKeyAlgorithm = errors.New("key algorithm does not match the protected header") )
Functions ¶
This section is empty.
Types ¶
type FetchEvent ¶ added in v0.5.0
type FetchEvent struct {
// KeyID is the key ID that was asked for. It comes from a token that had
// not been verified, though it has passed the check on what a key ID may
// hold.
KeyID string
// URI is the provider's URL template, with any password redacted. It
// names the provider and is the same for every key, so it is safe to use
// as a metric label where KeyID is not.
URI string
// Err is why the fetch failed, or nil.
Err error
}
FetchEvent describes one fetch of one key by a PerKeyProvider. It is dispatched to every FetchListener after every fetch, successful or not. A lookup that needed no request, or was refused one, produces no event.
type FetchListener ¶ added in v0.5.0
type FetchListener interface {
OnFetchEvent(FetchEvent)
}
FetchListener is a sink for FetchEvents. OnFetchEvent is called from the goroutine that made the fetch and must not panic.
type FixedKey ¶ added in v0.5.0
type FixedKey struct {
// KeyID is the key ID that tokens name this key by. Required: a PEM has
// nowhere to carry one, and a JWK may not. If Key is a JWK that does carry
// a kid, it must be this one.
KeyID string
// Key is the key itself, as text: either PEM, starting with
// "-----BEGIN", or a JWK, starting with "{". Which it is does not have to
// be said. White space around it, and indentation on each line of a PEM,
// are ignored, so it can be an indented block in a configuration file.
//
// It should be a public key. A private key is reduced to its public half,
// and a symmetric key is refused.
Key string
}
FixedKey is one key written into a service's own configuration.
type FixedKeyConfig ¶ added in v0.5.0
type FixedKeyConfig struct {
// Keys are the keys to serve. At least one is required, and no two may
// share a KeyID.
Keys []FixedKey
// Verify is the set of checks applied to a key before it is offered to
// jwx.
Verify VerifyConfig
}
FixedKeyConfig is the only way to configure a FixedKeyProvider. Like the other configs it is a plain struct with no struct tags.
type FixedKeyProvider ¶ added in v0.5.0
type FixedKeyProvider struct {
// contains filtered or unexported fields
}
FixedKeyProvider supplies verification keys that were written into the service's own configuration. It never sends a request, for any reason, and what it holds changes only when the configuration does.
It suits a service whose keys are few and seldom change, and one that must verify tokens without depending on a key server being reachable. Rotating a key means changing the configuration of every such service.
A FixedKeyProvider has nothing to start, stop, or watch: once built, it is complete.
func NewFixedKeyProvider ¶ added in v0.5.0
func NewFixedKeyProvider(cfg FixedKeyConfig) (*FixedKeyProvider, error)
NewFixedKeyProvider builds a FixedKeyProvider from a FixedKeyConfig. It rejects a config with no keys (ErrNoFixedKeys), a key with no KeyID (ErrMissingKeyID), two keys with the same KeyID (ErrDuplicateKeyID), a key whose text cannot be used (ErrInvalidFixedKey), a symmetric key (ErrSymmetricKey), and a JWK that carries a kid other than its KeyID (ErrKeyIDMismatch). Every problem is reported, joined, rather than just the first.
No error quotes the text of a key.
Example ¶
A FixedKeyProvider serves keys written into the service's own configuration. A key may be PEM or a JWK, and each is given the key ID that tokens will name it by.
package main
import (
"fmt"
"github.com/xmidt-org/clortho"
)
// examplePEM and exampleJWK are the same public key, written the two ways a
// fixed key may be.
const (
examplePEM = `
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/TjDSjYqcGZtvtmV+gjGmdLqO03q
XmnwqIK24dVkPClST1e1NM7fxHBy/CwPKk/ZXxbFSdgFjGGO3kr+wcGIpA==
-----END PUBLIC KEY-----`
exampleJWK = `{
"kty": "EC",
"crv": "P-256",
"x": "_TjDSjYqcGZtvtmV-gjGmdLqO03qXmnwqIK24dVkPCk",
"y": "Uk9XtTTO38RwcvwsDypP2V8WxUnYBYxhjt5K_sHBiKQ"
}`
)
func main() {
p, err := clortho.NewFixedKeyProvider(clortho.FixedKeyConfig{
Keys: []clortho.FixedKey{
// PEM has nowhere to say what its key ID is, so KeyID says it.
{KeyID: "2026", Key: examplePEM},
// A JWK may carry a kid of its own. If it does, it must match.
{KeyID: "2027", Key: exampleJWK},
},
})
if err != nil {
fmt.Println(err)
return
}
// Nothing is ever fetched: the keys are held from the start.
fmt.Println(p.KeyIDs())
}
Output: [2026 2027]
Example (SymmetricKey) ¶
A key that cannot be used is refused when the provider is built, and the error says which key without quoting it.
package main
import (
"errors"
"fmt"
"github.com/xmidt-org/clortho"
)
func main() {
_, err := clortho.NewFixedKeyProvider(clortho.FixedKeyConfig{
Keys: []clortho.FixedKey{
{KeyID: "shared", Key: `{"kty":"oct","k":"c2VjcmV0"}`},
},
})
fmt.Println(errors.Is(err, clortho.ErrSymmetricKey))
fmt.Println(err)
}
Output: true fixed key 0, "shared": symmetric keys are not accepted: "shared"
func (*FixedKeyProvider) FetchKeys ¶ added in v0.5.0
func (p *FixedKeyProvider) FetchKeys(_ context.Context, sink jws.KeySink, sig *jws.Signature, _ *jws.Message) error
FetchKeys satisfies jws.KeyProvider, and is the only way a key leaves the FixedKeyProvider. It finds the key for the protected header's kid, applies the VerifyConfig checks, and offers the key to jwx under the header's alg.
Errors carry sentinels for errors.Is: ErrMissingKeyID, ErrKeyNotFound, ErrMissingAlgorithm, ErrKeyUsage, and ErrKeyAlgorithm.
func (*FixedKeyProvider) KeyIDs ¶ added in v0.5.0
func (p *FixedKeyProvider) KeyIDs() []string
KeyIDs lists the key IDs of the keys held, sorted, for health endpoints. The list never changes.
type HTTPError ¶ added in v0.5.0
HTTPError is a refresh error: an http or https source answered with a status other than 200 or 304. Location has any password redacted.
type KeySetConfig ¶ added in v0.5.0
type KeySetConfig struct {
// Sources are polled on a schedule. At least one is required.
//
// All sources feed one map keyed by key ID. A key ID served by more than one
// source is reported as ErrDuplicateKeyID by the refresh that would introduce
// it, not merged. Use separate KeySetProviders for separate key spaces.
Sources []RefreshSource
// RefreshOnUnknownKeyID makes a lookup for a key ID that is not on the ring
// trigger an early refresh of every source, and wait for it, so a token
// signed with a newly rotated key verifies without waiting for the next
// scheduled refresh. The early refresh is rate-limited by each source's
// MinRefreshInterval; a lookup that arrives inside that window fails
// immediately with ErrKeyNotFound, so a flood of unknown key IDs costs at
// most one request per source per MinRefreshInterval. A source that
// answered 429 or 503 with a Retry-After is also left alone until that wait
// has passed.
//
// The wait has no limit of its own. A lookup that triggers an early
// refresh, or that arrives while a refresh is already running, waits until
// every source has answered. That can take as long as the slowest source's
// Client allows, and is without limit for a client that has no timeout.
// The request being verified is held for all of that time. The lookup
// stops waiting sooner only when the context jwx was given, and passes to
// FetchKeys, ends. A service that needs a tighter bound puts a deadline on
// that context. A refresh already under way carries on either way.
//
// Off, which is the default, an unknown key ID fails with ErrKeyNotFound and
// no request is made: a token can never cause the KeySetProvider to contact
// anything.
RefreshOnUnknownKeyID bool
// Verify is the policy applied to every key before it is offered to jwx.
Verify VerifyConfig
}
KeySetConfig is the only way to configure a KeySetProvider. It is a plain struct with no struct tags: serialization is the caller's concern. A service unmarshals its own settings and fills this in, which is also how it supplies things no configuration file can hold, such as an *http.Client.
type KeySetProvider ¶ added in v0.5.0
type KeySetProvider struct {
// contains filtered or unexported fields
}
KeySetProvider supplies verification keys. It is the one thing clortho makes, and it is named for the role it fills: it is what a caller hands to jwt.WithKeyProvider.
A KeySetProvider polls its sources for their complete key sets and serves lookups from that. It never fetches a key on demand, so a token cannot cause a request; see KeySetConfig.RefreshOnUnknownKeyID for the one opt-in exception.
func NewKeySetProvider ¶ added in v0.5.0
func NewKeySetProvider(cfg KeySetConfig) (*KeySetProvider, error)
NewKeySetProvider builds a KeySetProvider from a KeySetConfig. It rejects a KeySetConfig with no sources (ErrNoKeySources), a source whose URI cannot be parsed or whose scheme is not file, http, or https (ErrUnsupportedScheme), an http or https source with no Client (ErrMissingClient), a file source that has one (ErrUnusedClient), and a duplicate source URI. Every problem is reported, joined, rather than just the first.
The returned KeySetProvider is not running; call Start.
Example ¶
A source reached over http or https must be given the client to reach it with, and clortho uses that client exactly as given. This one is a sound starting point for fetching keys.
package main
import (
"crypto/tls"
"fmt"
"net/http"
"time"
"github.com/xmidt-org/clortho"
)
func main() {
client := &http.Client{
// Bound the whole exchange. Without a timeout, a key set server that
// stops answering stalls the refresh, and with RefreshOnUnknownKeyID
// on it also holds every request that is waiting for that refresh.
Timeout: 30 * time.Second,
// Do not follow redirects. (do not add this to allow redirects)
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
// Give the client a transport of its own. http.DefaultTransport is
// shared by the whole process, and anything in it can change it.
Transport: &http.Transport{
// Decide on purpose whether a proxy applies. Leaving Proxy nil
// means none, even when the environment names one; use
// http.ProxyFromEnvironment to honor the environment.
Proxy: nil,
// The trust settings for the key set server belong here: a floor
// on the TLS version, and a private CA or a client certificate
// when the server calls for them.
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
},
}
p, err := clortho.NewKeySetProvider(clortho.KeySetConfig{
Sources: []clortho.RefreshSource{
{URI: "https://issuer.example.com/keys", Client: client},
// A file source takes no client, and giving it one is an error.
{URI: "/etc/example/keys.json"},
},
})
if err != nil {
fmt.Println(err)
return
}
for _, status := range p.Status() {
fmt.Println(status.URI)
}
}
Output: https://issuer.example.com/keys /etc/example/keys.json
Example (MissingClient) ¶
There is no default client. An http or https source without one is rejected when the KeySetProvider is built, not when the first refresh fails.
package main
import (
"errors"
"fmt"
"github.com/xmidt-org/clortho"
)
func main() {
_, err := clortho.NewKeySetProvider(clortho.KeySetConfig{
Sources: []clortho.RefreshSource{{URI: "https://issuer.example.com/keys"}},
})
fmt.Println(errors.Is(err, clortho.ErrMissingClient))
fmt.Println(err)
}
Output: true an http or https source requires a Client: "https://issuer.example.com/keys"
func (*KeySetProvider) AddListener ¶ added in v0.5.0
func (p *KeySetProvider) AddListener(l Listener) (cancel func())
AddListener registers a sink for RefreshEvents. Only events after this call are delivered. The returned closure removes the listener; calling it more than once has no further effect.
func (*KeySetProvider) FetchKeys ¶ added in v0.5.0
func (p *KeySetProvider) FetchKeys(ctx context.Context, sink jws.KeySink, sig *jws.Signature, _ *jws.Message) error
FetchKeys satisfies jws.KeyProvider, and is the only way a key leaves the KeySetProvider. It looks the protected header's kid up on the ring, applies the VerifyConfig checks, and offers the key to jwx under the header's alg. jwx then decides whether the key can serve that algorithm.
Errors carry sentinels for errors.Is: ErrMissingKeyID, ErrKeyNotFound, ErrMissingAlgorithm, ErrKeyUsage, and ErrKeyAlgorithm.
func (*KeySetProvider) KeyIDs ¶ added in v0.5.0
func (p *KeySetProvider) KeyIDs() []string
KeyIDs lists the key IDs currently on the ring, sorted, for health endpoints.
func (*KeySetProvider) Start ¶ added in v0.5.0
func (p *KeySetProvider) Start(context.Context) error
Start begins refreshing every source. It returns once the refresh loops are running; it does not wait for the first fetch. Use Status to decide when the KeySetProvider is ready to serve. The context governs only this call, not the life of the loops; see Stop.
Start returns ErrAlreadyStarted if the KeySetProvider is running.
func (*KeySetProvider) Status ¶ added in v0.5.0
func (p *KeySetProvider) Status() []SourceStatus
Status reports the state of every source, in KeySetConfig order. A service's readiness check decides what ready means from this: for example, every source has a non-zero LastRetrieved, or at least one does.
func (*KeySetProvider) Stop ¶ added in v0.5.0
func (p *KeySetProvider) Stop(ctx context.Context) error
Stop ends the refresh loops and waits for them to exit, or for ctx to end. The ring keeps its keys, and Start may be called again.
A refresh that Stop interrupts is abandoned, not failed: Status keeps the outcome of the last attempt that finished, and no RefreshEvent is dispatched for the interrupted one.
Stop returns ErrNotStarted if the KeySetProvider is not running.
type Listener ¶ added in v0.5.0
type Listener interface {
OnRefreshEvent(RefreshEvent)
}
Listener is a sink for RefreshEvents. OnRefreshEvent is called from the refresh goroutine and must not panic.
type PerKeyConfig ¶ added in v0.5.0
type PerKeyConfig struct {
// Template is the URL a key is fetched from, with {keyID} where the key ID
// goes: "https://keys.example.com/keys/{keyID}". Required. The scheme is
// http or https. Every {keyID} is replaced by the key ID. The placeholder
// may only be in the path or the query, so that no key ID can change which
// server is asked.
Template string
// Client makes the requests, and is required. It is used exactly as
// given; see RefreshSource.Client, which follows the same rules. A client
// with no timeout lets a server that stops answering hold every lookup
// waiting on that fetch.
Client *http.Client
// AllowedKeyIDs are the key IDs the operator knows the server to hold.
// They are exempt from FetchRateLimit, so a flood of invented key IDs
// cannot crowd out a real one. Each must be a valid key ID.
AllowedKeyIDs []string
// AllowedKeyIDsOnly makes AllowedKeyIDs the complete list: a key ID that
// is not on it is never fetched, and a token naming one costs no request
// at all. It requires AllowedKeyIDs to hold at least one key ID.
AllowedKeyIDsOnly bool
// FetchRateLimit is the shortest time between fetches for key IDs that are
// not in AllowedKeyIDs, counted across all of them. It is what a flood of
// invented key IDs can cost the server: one request per FetchRateLimit.
// If zero, DefaultFetchRateLimit is used. Ignored when AllowedKeyIDsOnly
// is set.
//
// The limit cannot tell an invented key ID from a real one it has not seen
// before, so during a flood a real new key may not be fetched until the
// flood eases. AllowedKeyIDs is the way to protect a key from that.
FetchRateLimit time.Duration
// CacheTime is how long a fetched key is held when the server does not
// say. A response's Cache-Control max-age, when present, is used instead.
// Either way the time is kept between MinCacheTime and MaxCacheTime. If
// zero, DefaultCacheTime is used.
//
// When the time is up, the next token that names the key causes it to be
// fetched again. If the server then says there is no such key, the key
// is dropped at once. If the server cannot answer, the key keeps
// serving, so that an outage of the key server is not an outage of every
// service that verifies with it.
CacheTime time.Duration
// MinCacheTime is the shortest time a fetched key is held, whatever the
// server says. Without it, a server that gave a tiny max-age would be
// asked on nearly every token. If zero, DefaultMinCacheTime is used.
MinCacheTime time.Duration
// MaxCacheTime is the longest time a fetched key is held, whatever the
// server says, and so bounds how long a key the server has removed can
// still verify. If zero, DefaultMaxCacheTime is used. A value below the
// effective MinCacheTime is raised to it.
MaxCacheTime time.Duration
// NotFoundCoolDown is the wait before a key ID is asked for again after
// the server said there is no such key, or served a key that was
// rejected. The server answered, so asking again soon would get the same
// answer. If zero, DefaultNotFoundCoolDown is used.
NotFoundCoolDown time.Duration
// FailureCoolDown is the wait before a key ID is asked for again after the
// server could not answer: a timeout, a refused connection, a 500. The
// outage may be brief, so this is short. A Retry-After on a 429 or 503
// is used instead, up to MaxCacheTime. If zero, DefaultFailureCoolDown
// is used.
FailureCoolDown time.Duration
// MaxResponseBytes caps the body read from the server. A larger body
// fails the fetch with ErrResponseTooLarge. If less than 1,
// DefaultMaxResponseBytes is used.
MaxResponseBytes int64
// Verify is the set of checks applied to a key before it is offered to
// jwx.
Verify VerifyConfig
}
PerKeyConfig is the only way to configure a PerKeyProvider. Like KeySetConfig it is a plain struct with no struct tags.
A PerKeyProvider fetches a key because a token named it, before that token has been verified. Most of this config is about what such a token may cost:
- A key ID in AllowedKeyIDs may always be fetched.
- Any other key ID is fetched no more often than FetchRateLimit, across all such IDs, or never when AllowedKeyIDsOnly is set.
- Every key ID, allowed or not, is fetched by one request at a time, and after a fetch fails is left alone for a cool-down: NotFoundCoolDown when the server said there is no such key, FailureCoolDown when the server could not answer.
- A fetched key is held, and not fetched again, for the time the server gave or else CacheTime.
A lookup that would need a fetch which is not allowed right now fails at once. It never waits for the limit to pass.
type PerKeyProvider ¶ added in v0.5.0
type PerKeyProvider struct {
// contains filtered or unexported fields
}
PerKeyProvider supplies verification keys by fetching each one from a server that serves single keys, named by the key ID in a token. It is what a service uses against a server, such as themis, that offers no key set.
Unlike a KeySetProvider, a PerKeyProvider can be made to send a request by a token, since the key ID it asks for comes from one. PerKeyConfig bounds what that can cost. A service that can list its key IDs should, with AllowedKeyIDs.
A PerKeyProvider has no background work: it fetches only when a token asks. There is nothing to start or stop.
func NewPerKeyProvider ¶ added in v0.5.0
func NewPerKeyProvider(cfg PerKeyConfig) (*PerKeyProvider, error)
NewPerKeyProvider builds a PerKeyProvider from a PerKeyConfig. It rejects a template that is missing or unusable (ErrInvalidTemplate), a template whose scheme is not http or https (ErrUnsupportedScheme), a missing Client (ErrMissingClient), an allowed key ID that is not a valid key ID (ErrInvalidKeyID), and AllowedKeyIDsOnly with no allowed key IDs (ErrNoAllowedKeyIDs). Every problem is reported, joined, rather than just the first.
Example ¶
A PerKeyProvider fetches each key from a server that serves one key per request, at the URL the template gives for that key's ID.
package main
import (
"fmt"
"net/http"
"time"
"github.com/xmidt-org/clortho"
)
func main() {
p, err := clortho.NewPerKeyProvider(clortho.PerKeyConfig{
// {keyID} is replaced by the key ID a token names.
Template: "https://themis.example.com/keys/{keyID}",
// The client is required, as it is for a KeySetProvider, and the
// example on NewKeySetProvider shows how to build a good one.
Client: &http.Client{Timeout: 30 * time.Second},
// A key ID comes from a token nobody has verified yet. Listing the
// ones this service expects means those can always be fetched, even
// while a flood of invented key IDs is using up the limit that
// applies to every other.
AllowedKeyIDs: []string{"themis-2026", "themis-2027"},
})
if err != nil {
fmt.Println(err)
return
}
// Nothing is fetched until a token asks, so nothing is held yet.
fmt.Println(len(p.KeyIDs()), "keys held")
}
Output: 0 keys held
Example (TemplateInTheServerName) ¶
The key ID goes into a URL, so the template may only put it where it cannot change which server is asked.
package main
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/xmidt-org/clortho"
)
func main() {
_, err := clortho.NewPerKeyProvider(clortho.PerKeyConfig{
Template: "https://{keyID}.example.com/keys",
Client: &http.Client{Timeout: 30 * time.Second},
})
fmt.Println(errors.Is(err, clortho.ErrInvalidTemplate))
}
Output: true
func (*PerKeyProvider) AddListener ¶ added in v0.5.0
func (p *PerKeyProvider) AddListener(l FetchListener) (cancel func())
AddListener registers a sink for FetchEvents. Only events after this call are delivered. The returned closure removes the listener; calling it more than once has no further effect.
func (*PerKeyProvider) FetchKeys ¶ added in v0.5.0
func (p *PerKeyProvider) FetchKeys(ctx context.Context, sink jws.KeySink, sig *jws.Signature, _ *jws.Message) error
FetchKeys satisfies jws.KeyProvider, and is the only way a key leaves the PerKeyProvider. It finds the key for the protected header's kid, fetching it if it is not held and a fetch is allowed, applies the VerifyConfig checks, and offers the key to jwx under the header's alg.
When the key is not held and has to be fetched, FetchKeys waits for that fetch, for as long as the Client allows or until ctx ends.
Errors carry sentinels for errors.Is: ErrMissingKeyID, ErrKeyNotFound, ErrInvalidKeyID, ErrMissingAlgorithm, ErrKeyUsage, and ErrKeyAlgorithm.
func (*PerKeyProvider) KeyIDs ¶ added in v0.5.0
func (p *PerKeyProvider) KeyIDs() []string
KeyIDs lists the key IDs of the keys currently held, sorted, for health endpoints. A PerKeyProvider holds nothing until a token asks, so an empty list is its normal state after start-up and says nothing about readiness.
type RefreshEvent ¶
type RefreshEvent struct {
// URI is the source's URI with any password redacted.
URI string
// Err is why the refresh failed, or nil. On failure the ring is untouched
// and KeyIDs lists what this source still supplies.
Err error
// KeyIDs are the key IDs now on the ring from this source, sorted.
KeyIDs []string
// NewKeyIDs are the key IDs this refresh added, sorted.
NewKeyIDs []string
// DeletedKeyIDs are the key IDs this refresh removed, sorted.
DeletedKeyIDs []string
// KeySetBytes is the size of the key set the keys in KeyIDs came from: what
// this source served at its last successful load. Like KeyIDs it describes
// what is on the ring, so a failed refresh leaves it unchanged, as does a
// refresh the server answered with a 304. Zero if the source has never
// loaded.
KeySetBytes int64
}
RefreshEvent describes one refresh of one source. It is dispatched to every Listener after every attempt, successful or not. The one exception is an attempt that Stop interrupts, which is abandoned without an event.
Events carry key IDs, never keys. A string identifier is always a KeyID in this package, and "key" alone always means the key material.
type RefreshSource ¶
type RefreshSource struct {
// URI is a file path, or a file, http, or https URI. Required.
//
// Every key the source serves must carry a kid, and none may be symmetric;
// a refresh that finds otherwise fails and leaves the ring untouched.
URI string
// RefreshInterval is the time between refreshes when the server gives no
// hint. An http source's Cache-Control max-age, when present, is used
// instead. If zero, DefaultRefreshInterval is used.
RefreshInterval time.Duration
// MinRefreshInterval is the shortest time between refreshes, whatever the
// server's Cache-Control says and whatever an unknown key ID asks for. It
// is also the wait before most failed refreshes are retried. The one
// exception is a retry the server itself scheduled, with Retry-After on a
// 429 or 503, which is honored even when it is sooner. If zero,
// DefaultMinRefreshInterval is used.
MinRefreshInterval time.Duration
// MaxRefreshInterval is the longest time between refreshes, whatever the
// server says. A refresh happens at least this often even when the content
// never changes, so it bounds how long a key the issuer has removed can
// still verify. If zero, DefaultMaxRefreshInterval is used. A value below the
// effective MinRefreshInterval is raised to it.
MaxRefreshInterval time.Duration
// JitterPercentage is a percentage, plus or minus, applied to every refresh
// interval: 10 means each refresh fires at a random point between ten
// percent early and ten percent late. When the interval comes from a
// server TTL the late half is dropped, since the server said the content
// is stale after that, so 10 then means up to ten percent early. The
// result is clipped to MinRefreshInterval and MaxRefreshInterval. A retry
// after a failed refresh drops the early half instead, so 10 then means up
// to ten percent late.
//
// Valid values are greater than zero and less than one hundred; anything
// else, including zero, gets DefaultJitterPercentage. Jitter cannot be
// turned off, since its purpose is to keep a fleet that started together
// from polling the key server in lockstep.
JitterPercentage float64
// Client makes the requests for an http or https URI, and is required for
// one: NewKeySetProvider fails with ErrMissingClient without it.
//
// clortho uses the client exactly as given. The client owns the timeout,
// redirects, TLS, proxies, and any authorization its transport adds, and
// clortho neither adds to those nor overrides them. What the client leaves
// out is therefore left out: a client with no timeout lets a server that
// stops answering stall the refresh until Stop, and a client that follows
// redirects lets the server, rather than the URI configured here, decide
// where key material comes from. The example on NewKeySetProvider shows a
// client suited to fetching keys.
//
// A file source takes no client, and NewKeySetProvider fails with
// ErrUnusedClient if it is given one. A client on a file source nearly
// always means an http or https URI was written without its scheme, which
// reads as a file path.
Client *http.Client
// MaxResponseBytes caps the body read from an http or https URI. A larger
// body fails the refresh with ErrResponseTooLarge. If less than 1,
// DefaultMaxResponseBytes is used. Ignored for a file source.
MaxResponseBytes int64
}
RefreshSource is one key set source: a location that serves a JWK set or a single JWK. It is almost always a key set server, reached over http or https, and otherwise a file.
A refresh that fails leaves the ring as it was. When the source is tried again depends on why the refresh failed:
- The key set server asked for a pause, with a Retry-After header on a 429 or 503 response. The retry happens when the server said, even if that is sooner than MinRefreshInterval, but no later than MaxRefreshInterval.
- The source answered, but clortho rejected the key set. That happens when the set holds a symmetric key, a key with no kid, or a key ID that appears twice or that another source already supplies, and when the body is larger than MaxResponseBytes. Asking again would return the same key set until someone corrects it at the source, so there is no early retry: the source is next read after its usual RefreshInterval. One case does get an early retry. A source that has never loaded successfully has supplied no keys to fall back on, so it is retried after MinRefreshInterval.
- Anything else went wrong, such as a timeout, a 500, a body that is not a key set, or a missing file. The retry happens after MinRefreshInterval.
type SourceStatus ¶ added in v0.5.0
type SourceStatus struct {
// URI is the source's URI with any password redacted. It matches
// RefreshSource.URI otherwise.
URI string
// LastRetrieved is when the source last loaded successfully. A 304 counts.
// Zero if it never has.
LastRetrieved time.Time
// LastStatusCode is the status of the last HTTP response, including a 304
// or an error status. Zero for a file source, or when the last attempt got
// no response at all.
LastStatusCode int
// LastErr is the error from the last attempt, or nil if it succeeded.
LastErr error
}
SourceStatus is the last outcome for one source, as reported by KeySetProvider.Status.
type VerifyConfig ¶ added in v0.5.0
type VerifyConfig struct {
// IgnoreKeyUsage turns off the check that a key's "use", when present, is
// "sig". Off by default: a key marked for anything else is rejected with
// ErrKeyUsage.
IgnoreKeyUsage bool
// IgnoreKeyAlgorithm turns off the check that a key's "alg", when present,
// matches the token header's alg. Off by default: a mismatch is rejected
// with ErrKeyAlgorithm.
IgnoreKeyAlgorithm bool
}
VerifyConfig is the set of checks FetchKeys applies to a key before it offers that key to jwx. It holds only checks on a key, so that it means the same thing wherever it is used. The zero value is the strict default.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package clorthofx provides integration with go.uber.org/fx.
|
Package clorthofx provides integration with go.uber.org/fx. |
|
Package clorthometrics tallies what clortho's providers do as Prometheus metrics.
|
Package clorthometrics tallies what clortho's providers do as Prometheus metrics. |
|
Package clorthozap logs what clortho's providers do through go.uber.org/zap: the refreshes of a KeySetProvider and the fetches of a PerKeyProvider.
|
Package clorthozap logs what clortho's providers do through go.uber.org/zap: the refreshes of a KeySetProvider and the fetches of a PerKeyProvider. |
|
cmd
|
|
|
keyset
command
Command keyset reads one key set the way a clortho.KeySetProvider does and prints what it found.
|
Command keyset reads one key set the way a clortho.KeySetProvider does and prints what it found. |
|
perkey
command
Command perkey fetches single keys the way a clortho.PerKeyProvider does and prints what it found.
|
Command perkey fetches single keys the way a clortho.PerKeyProvider does and prints what it found. |