Documentation
¶
Overview ¶
Package grant encodes CapacityGrant as a signed JWT and verifies it offline. Phase 1 holds only the proto <-> core conversion and an insecure development verifier; Phase 2 adds signing, JWKS and the issuer.
Index ¶
- Constants
- Variables
- func Deadline(ts *timestamppb.Timestamp, now time.Time) (time.Duration, bool)
- func FromProto(p *grantv1.CapacityGrant) core.Grant
- func GenerateKey(alg jose.SignatureAlgorithm) (*jose.JSONWebKey, error)
- func LoadKey(path string) (*jose.JSONWebKey, error)
- func PeekExpiry(token string) (time.Time, error)
- func PeekUID(token string) (string, error)
- func PublicJWKS(keys ...*jose.JSONWebKey) jose.JSONWebKeySet
- func SaveKey(path string, k *jose.JSONWebKey) error
- func Sign(g core.Grant, key *jose.JSONWebKey, now time.Time) (string, error)
- func ToProto(g core.Grant) *grantv1.CapacityGrant
- func Verify(token string, pub *jose.JSONWebKey, opts VerifyOptions) (core.Grant, error)
- type Cache
- type InsecureJSONVerifier
- type Issuer
- type KeyRing
- type KeyStore
- type StoredKey
- type Verifier
- type VerifyOptions
Constants ¶
const MaxTokenBytes = 8 << 10
MaxTokenBytes bounds a token before any parsing.
Variables ¶
var ( ErrNoKID = errors.New("grant: token has no kid header") ErrClaimMismatch = errors.New("grant: registered claims disagree with the grant claim") // ErrWrongAudience is core's, so one sentinel covers the verifier's // and the home's refusals of a grant for another home. ErrWrongAudience = core.ErrWrongAudience ErrWrongIssuer = errors.New("grant: issuer mismatch") ErrMissingGrant = errors.New("grant: token carries no grant claim") ErrKeyMismatchAlg = errors.New("grant: key algorithm does not match token") ErrTokenTooLarge = fmt.Errorf("grant: token exceeds %d bytes", MaxTokenBytes) ErrLeaseTooLong = errors.New("grant: lease longer than this home accepts") ErrEmptyGrant = errors.New("grant: grant_uid is empty") ErrBadUID = errors.New("grant: grant_uid must be a DNS-1123 label (lowercase alphanumerics and '-', at most 63)") ErrBadTenant = errors.New("grant: tenant must be one path segment (alphanumerics, '.', '_' and '-', at most 253)") )
Token errors.
var ( ErrUnknownKey = errors.New("grant: no key for kid") ErrNeverLoaded = errors.New("grant: key set never loaded") ErrJWKSStale = errors.New("grant: key set older than the lease TTL") )
Key set errors.
var ( // ErrNoKeyRing is KeyStore.Load's answer when nothing is stored. ErrNoKeyRing = errors.New("grant: no key ring is stored") // ErrKeyRingExists is KeyStore.Create's answer when a ring is already // stored, such as when another replica created it first. ErrKeyRingExists = errors.New("grant: a key ring is already stored") )
Key store errors.
var AllowedAlgorithms = []jose.SignatureAlgorithm{jose.EdDSA, jose.ES256}
Algorithms the protocol accepts. Anything else, including "none" and every symmetric algorithm, is rejected before the signature is looked at, so a key-confusion token cannot reach verification.
Functions ¶
func FromProto ¶
func FromProto(p *grantv1.CapacityGrant) core.Grant
FromProto converts the wire message to the home-invariant core type.
func GenerateKey ¶
func GenerateKey(alg jose.SignatureAlgorithm) (*jose.JSONWebKey, error)
GenerateKey creates a private signing key as a JWK with a thumbprint key id, ready for Sign and for publishing its public half in a JWKS.
func LoadKey ¶
func LoadKey(path string) (*jose.JSONWebKey, error)
LoadKey reads a JWK written by SaveKey (or any JWK JSON).
func PeekExpiry ¶
PeekExpiry reads the lease expiry (exp) WITHOUT verifying the token. Only for bookkeeping that never trusts the value, such as deciding whether a grant file left on disk is worth starting on before the file is delivered and verified. It bounds the token's size and algorithm as Verify does, before any parsing. A token without exp is an error.
func PeekUID ¶
PeekUID reads the grant UID (jti) WITHOUT verifying the token. Only for bookkeeping that never trusts the value, such as mapping a removed grant file back to the UID to revoke. It bounds the token's size and algorithm as Verify does, before any parsing.
func PublicJWKS ¶
func PublicJWKS(keys ...*jose.JSONWebKey) jose.JSONWebKeySet
PublicJWKS is the key set an issuer publishes: the public half of each key, kid and alg preserved.
func SaveKey ¶
func SaveKey(path string, k *jose.JSONWebKey) error
SaveKey writes the private JWK as JSON, readable by the owner only.
func Sign ¶
Sign mints the token for g with key (a private JWK from GenerateKey or LoadKey). The key's kid lands in the header so verifiers can pick it from a JWKS.
func ToProto ¶
func ToProto(g core.Grant) *grantv1.CapacityGrant
ToProto converts the core type back to the wire message.
func Verify ¶
func Verify(token string, pub *jose.JSONWebKey, opts VerifyOptions) (core.Grant, error)
Verify checks the signature with pub (a public JWK), then the registered claims against opts and against the grant claim, and returns the grant. It does not reject an expired token; see the package note.
Types ¶
type Cache ¶
type Cache struct {
IssuerURL string
// MinRefresh rate-limits refreshes triggered by unknown kids or
// staleness (default one minute). Explicit Refresh calls ignore it.
MinRefresh time.Duration
// OnRefresh, when set, is called after every successful refresh. The
// standalone home uses it as its control-plane liveness signal.
OnRefresh func(at time.Time)
Now func() time.Time
// contains filtered or unexported fields
}
Cache holds an issuer's public keys, found through OIDC discovery: <issuer>/.well-known/openid-configuration names the jwks_uri, which serves the key set. Keys are looked up by kid. An unknown kid triggers one refresh, rate-limited, so a flood of bad tokens cannot hammer the issuer. Every successful refresh is timestamped; the Verifier turns a timestamp older than the lease TTL into a hard failure.
func (*Cache) Key ¶
Key returns the public key for kid. A miss triggers a rate-limited refresh; a miss after that is ErrUnknownKey. If the set was never loaded and cannot be loaded now, the fetch error is returned wrapped in ErrNeverLoaded so callers can tell "offline" from "bad kid".
func (*Cache) LastRefresh ¶
LastRefresh is when the key set was last fetched successfully; zero if never.
func (*Cache) Refresh ¶
Refresh performs discovery (once; the jwks_uri is remembered) and reloads the key set.
func (*Cache) RefreshIfDue ¶
RefreshIfDue refreshes unless a refresh was attempted within MinRefresh. It reports whether an attempt was made.
type InsecureJSONVerifier ¶
type InsecureJSONVerifier struct{}
InsecureJSONVerifier accepts a CapacityGrant encoded as protobuf JSON in place of a signed JWT. It performs NO signature check and exists only for local development and for exercising the ledger before an issuer is configured. fiberd refuses to start with it unless told explicitly.
type Issuer ¶
type Issuer struct {
// Key is the one signing key of an issuer without a Ring. It may be
// nil when Ring is set.
Key *jose.JSONWebKey
// Ring, when set, holds every key the issuer publishes. Its Signing
// key is the one Mint and Renew sign with.
Ring KeyRing
// URL is the issuer identifier. It is what goes in `iss`, and where
// discovery is served.
URL string
Now func() time.Time
}
Issuer mints grants and publishes its public keys the way verifiers expect, with OIDC discovery at <URL>/.well-known/openid-configuration and the key set at <URL>/openid/v1/jwks. It is the reference issuer for every home, and the Kubernetes controller wraps the same type.
An issuer signs with Ring when Ring is set and with Key otherwise. The Kubernetes controller fills Ring from its Secret-backed KeyStore through LoadOrCreate, so a rotated key keeps verifying while its grants live.
func (*Issuer) JWKS ¶
func (i *Issuer) JWKS() jose.JSONWebKeySet
JWKS is the public key set. It is every key of Ring when Ring is set, else the public half of Key.
func (*Issuer) Mint ¶
Mint signs g. g.Issuer is set to the issuer URL, since a grant cannot claim another issuer. Everything else is the caller's, and g must pass core.Grant.Validate.
func (*Issuer) Renew ¶
Renew signs the grant in previous again for a fresh lease of the same length. previous must be a token this issuer signed with a key it still publishes, and its registered claims must agree with its grant claim as Verify requires. The lease length is exp minus iat of previous, so a token without either claim is refused. The renewed token has iat now and exp now plus that length, cut to whole seconds, and is signed with the current signing key.
An expired previous token may still be renewed. Renewal is the lease's continuation, and whether a lapsed lease is continued is the issuer's decision. A home treats the lapse as capacity it no longer holds until the renewed token arrives.
type KeyRing ¶
type KeyRing []StoredKey
KeyRing is an issuer's signing keys, newest first. Index 0 signs today. Every later entry is retired, and the ring keeps it so that its public half stays in the JWKS while grants it signed are still live.
func DecodeKeyRing ¶
DecodeKeyRing reads a ring that Encode wrote. It refuses an empty ring, an entry that is not a private signing key fiberd accepts, a repeated kid, and an order that is not newest first. Only index 0 may lack a SignedUntil, and no SignedUntil is later than the one before it. Every error names the entry by index. None carries key material.
func LoadOrCreate ¶
func LoadOrCreate(ctx context.Context, store KeyStore, generate func() (KeyRing, error)) (KeyRing, error)
LoadOrCreate returns the ring in store. When nothing is stored it stores the ring generate makes. A nil generate makes one EdDSA key. A create that loses a race to another replica re-reads the store and returns what it holds, so every replica signs with the same key.
A stored ring that does not decode is an error. It is never overwritten, because that would drop keys homes still trust.
func NewKeyRing ¶
func NewKeyRing(alg jose.SignatureAlgorithm) (KeyRing, error)
NewKeyRing is a ring of one fresh key for alg, made by GenerateKey.
func (KeyRing) Public ¶
func (r KeyRing) Public() jose.JSONWebKeySet
Public is the public half of every key, retired ones included. It is the JWKS an issuer with this ring serves.
A ring assigned directly rather than read by DecodeKeyRing may hold an invalid or symmetric key. Its public copy would be an empty JWK, served as {}. Public skips such an entry.
type KeyStore ¶
type KeyStore interface {
Load(ctx context.Context) ([]byte, error)
Create(ctx context.Context, raw []byte) error
}
KeyStore keeps an encoded KeyRing. The Kubernetes controller backs it with a Secret. Load returns ErrNoKeyRing, possibly wrapped, when nothing is stored. Create stores raw only when nothing is stored yet, and returns ErrKeyRingExists, possibly wrapped, otherwise. Create never overwrites a stored ring.
type StoredKey ¶
type StoredKey struct {
JWK jose.JSONWebKey `json:"jwk"`
SignedUntil *time.Time `json:"signedUntil,omitempty"`
}
StoredKey is one entry of a KeyRing. SignedUntil is unset for the key that signs today and holds a time once the key has been superseded. A superseded key stays in the ring so that homes can still verify the grants it signed.
JWK holds private key material, and json.Marshal writes it out because the store needs it. String, GoString and MarshalLog show only the kid, the alg and SignedUntil, so fmt and a logr logger print a StoredKey without key material. A KeyRing handed to a logger is still encoded with json.Marshal, so log the kids of a ring and never the ring itself.
func (StoredKey) GoString ¶
GoString implements fmt.GoStringer, which %#v uses, without key material.
func (StoredKey) MarshalLog ¶
MarshalLog implements logr.Marshaler. A logr sink encodes what it returns in place of the StoredKey, so a logged key never carries key material.
type Verifier ¶
type Verifier struct {
Cache *Cache
Audience string
// MaxStale is how old the key set may be (default one hour). Set it
// to the lease TTL grants are minted with.
MaxStale time.Duration
// MaxLease bounds a grant's signed lifetime. Zero accepts any.
MaxLease time.Duration
Now func() time.Time
}
Verifier implements core.Verifier against a JWKS Cache. It never makes a control-plane round trip on the request path beyond a rate-limited key refresh: known keys verify from memory.
Two failures are not authentication failures but reachability ones, and are reported wrapped in core.ErrVerifyUnavailable so fiberd answers SHED (back off) instead of Unauthenticated (give up):
- the key set was never loaded and the issuer cannot be reached;
- the key set is older than MaxStale (the lease TTL): a rotation or revocation could have been missed for longer than any grant lives, so nothing is trusted until a refresh succeeds.
type VerifyOptions ¶
VerifyOptions constrain Verify. Audience is required: a home only accepts grants addressed to it. Issuer, when set, must match. MaxLease, when set, bounds the signed lifetime (exp - iat). A grant missing either claim is then refused.