grant

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

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

View Source
const MaxTokenBytes = 8 << 10

MaxTokenBytes bounds a token before any parsing.

Variables

View Source
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.

View Source
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.

View Source
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.

View Source
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 Deadline

func Deadline(ts *timestamppb.Timestamp, now time.Time) (time.Duration, bool)

Deadline converts a proto timestamp deadline into a duration from now.

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

func PeekExpiry(token string) (time.Time, error)

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

func PeekUID(token string) (string, error)

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

func Sign(g core.Grant, key *jose.JSONWebKey, now time.Time) (string, error)

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

func (c *Cache) Key(ctx context.Context, kid string) (*jose.JSONWebKey, error)

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

func (c *Cache) LastRefresh() time.Time

LastRefresh is when the key set was last fetched successfully; zero if never.

func (*Cache) Refresh

func (c *Cache) Refresh(ctx context.Context) error

Refresh performs discovery (once; the jwks_uri is remembered) and reloads the key set.

func (*Cache) RefreshIfDue

func (c *Cache) RefreshIfDue(ctx context.Context) (bool, error)

RefreshIfDue refreshes unless a refresh was attempted within MinRefresh. It reports whether an attempt was made.

func (*Cache) Run

func (c *Cache) Run(ctx context.Context, every time.Duration)

Run refreshes every interval until ctx ends, keeping the cache warm and driving OnRefresh. Failures are logged by the caller through the hook's absence; Run itself never stops on error.

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.

func (InsecureJSONVerifier) Verify

func (InsecureJSONVerifier) Verify(_ context.Context, token []byte) (core.Grant, error)

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) Handler

func (i *Issuer) Handler() http.Handler

Handler serves discovery and the JWKS. Mount it at the root of URL.

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

func (i *Issuer) Mint(g core.Grant) (string, error)

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

func (i *Issuer) Renew(previous string, now time.Time) (string, error)

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

func DecodeKeyRing(raw []byte) (KeyRing, error)

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) Encode

func (r KeyRing) Encode() ([]byte, error)

Encode is the ring as JSON, private keys included, for a KeyStore.

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.

func (KeyRing) Signing

func (r KeyRing) Signing() (*jose.JSONWebKey, error)

Signing returns a copy of the key that signs today, which is index 0 while it is not retired. It is an error when the ring is empty, when its first key is retired, or when that key is not a private key fiberd accepts.

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

func (k StoredKey) GoString() string

GoString implements fmt.GoStringer, which %#v uses, without key material.

func (StoredKey) MarshalLog

func (k StoredKey) MarshalLog() any

MarshalLog implements logr.Marshaler. A logr sink encodes what it returns in place of the StoredKey, so a logged key never carries key material.

func (StoredKey) String

func (k StoredKey) String() string

String implements fmt.Stringer without 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.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, token []byte) (core.Grant, error)

Verify checks token against the issuer at Cache.IssuerURL. It parses the token once, for the key lookup and the signature check.

type VerifyOptions

type VerifyOptions struct {
	Audience string
	Issuer   string
	MaxLease time.Duration
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL