Files
zeekay bae63692f0 the corporate name is Lux Industries Inc
Lux Partners Limited is the Isle of Man company and does not appear on a
public surface. Lux Industries Inc, a Delaware C corporation, is the one
these copyright lines, citations and terms name.

A bare "Lux Partners" is left alone unless the line says who owns
something — elsewhere it is the name of a site, not a party.
2026-08-28 16:43:59 -07:00

403 lines
14 KiB
Go

// Copyright 2024-2026 Lux Industries Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
// An issuer's published signing keys: how they are read, how long they are held,
// and what counts as a key at all.
//
// The algorithms accepted and the key types decoded are stated together, in this
// one file, because they have to agree. An algorithm accepted with no key type
// that can verify it locks out the brand that rotates to it; a key type decoded
// with no accepted algorithm is dead weight that reads as support. IAM publishes
// RS256/RS512, ES256/ES384/ES512 and MLDSA65 (internal/oidc/jwks.go signingAlgs),
// and every one of those is verifiable here.
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/big"
"net/http"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/luxfi/crypto/pq/mldsa/mldsa65"
)
// jwksPath is where a Hanzo IAM issuer publishes its signing keys, relative to
// the issuer itself. The issuer is authoritative for its own keys, so this is the
// only place they are ever read from.
const jwksPath = "/v1/iam/.well-known/jwks"
// methods are the signing algorithms accepted.
//
// It is an allow-list of families whose verification key is a public key, which is
// what refuses HS256 — the algorithm-confusion attack that would have the verifier
// treat a published RSA modulus as a shared secret. An algorithm outside this list
// has no key in any JWKS and is refused before a signature is ever checked.
//
// The RSA-PSS and wider-hash rows are here so that a rotation to a stronger hash
// under the same key does not need a release; MLDSA65 is here because this stack is
// post-quantum-forward and IAM already signs with it where a Cert says so.
var methods = []string{
"RS256", "RS384", "RS512",
"PS256", "PS384", "PS512",
"ES256", "ES384", "ES512",
algMLDSA65,
}
// minModulus is the smallest RSA modulus whose signature counts as evidence. An
// issuer publishing a shorter key cannot be told apart from a JWKS that was
// tampered with in transit, and a signature under a forgeable key proves nothing
// about who is calling.
const minModulus = 2048
// Keyset is an issuer's public signing keys, addressed by JWK `kid`. A key is an
// RSA, EC or ML-DSA-65 public key — whichever the issuer published.
type Keyset map[string]crypto.PublicKey
// Keysets resolves an issuer to the keyset it signs with.
//
// It is a seam so that the validator holds no network: a deployment passes JWKS,
// a test passes the key it signed with, and both exercise the same validation.
// (Distinct from Handler.Keys, which is the tokenisation keyring — that holds
// secrets, this holds published verification keys.)
type Keysets func(issuer string) (Keyset, error)
// verify supplies the keys a token's signature may be checked against: the one its
// `kid` names, and nothing else. A token that names a published key must be signed
// by that key — pointing at one key while being signed by another is a key
// substitution, not a rotation, and there is no reading of it that is legitimate.
//
// A token naming no `kid`, or naming one this issuer does not publish, is checked
// against every key the issuer does publish. That is what keeps a token minted
// either side of a rotation working: the label may be stale, but the signature is
// the evidence and it still has to hold under a key the issuer stands behind.
func (k Keyset) verify(t *jwt.Token) (any, error) {
if kid, _ := t.Header["kid"].(string); kid != "" {
if key, ok := k[kid]; ok {
return jwt.VerificationKeySet{Keys: []jwt.VerificationKey{key}}, nil
}
}
out := make([]jwt.VerificationKey, 0, len(k))
for _, key := range k {
out = append(out, key)
}
if len(out) == 0 {
return nil, errors.New("no signing key to verify against")
}
return jwt.VerificationKeySet{Keys: out}, nil
}
// JWKS resolves each issuer's keyset from its own /v1/iam/.well-known/jwks,
// holding it for ttl.
//
// Keys are served past ttl while the issuer is unreachable, because a momentary
// blip must not refuse every caller at once. That grace is bounded by stale: past
// it the keys are no longer something this instance can confirm, and it refuses
// rather than authenticating against a set it can no longer see. The fleet edge
// holds a stale set indefinitely for availability; a record plane would rather
// stop answering than admit a caller on evidence it cannot check.
func JWKS(ttl, stale time.Duration) Keysets {
if ttl <= 0 {
ttl = 15 * time.Minute
}
if stale < ttl {
stale = ttl
}
c := &cache{
ttl: ttl,
stale: stale,
retry: retry,
sets: map[string]*issuerKeys{},
// The timeout is the ceiling on how long one issuer can hold an auth path:
// a caller with no usable keys waits for the fetch in front of it rather
// than being refused on a cold start, so that wait has to be short. An
// issuer answers this in milliseconds in-cluster.
client: &http.Client{Timeout: 5 * time.Second},
now: time.Now,
}
return c.get
}
// retry is how long a failed fetch is remembered. Inside it the failure is
// answered from memory instead of being attempted again.
//
// Without it an unreachable issuer costs a fetch per request — so a caller who can
// choose the issuer, by choosing the Host, can point every request at whichever
// brand is unreachable and make the auth path pay a network timeout each time. It
// is seconds rather than minutes because it also bounds how long a recovered
// issuer stays refused for a caller holding no stale keys.
const retry = 15 * time.Second
// issuerKeys is one issuer's state: the keys, when they were confirmed, and the
// last failure to confirm them.
type issuerKeys struct {
// mu serialises this issuer's fetches, and only this issuer's. A burst against
// a cold cache asks the issuer once rather than once per request, and — because
// the map is not locked while a fetch is outstanding — an issuer that does not
// answer holds up nothing but itself. Holding one process-wide lock across the
// HTTP fetch is how one unreachable issuer came to stall every request in the
// process, including those for issuers that were answering.
mu sync.Mutex
keys Keyset
fetchedAt time.Time
failedAt time.Time
err error
}
type cache struct {
// mu guards the map of issuers and is never held across a fetch.
mu sync.Mutex
sets map[string]*issuerKeys
ttl time.Duration
stale time.Duration
retry time.Duration
client *http.Client
// now is the clock, so the staleness bound is testable without waiting for it.
now func() time.Time
}
// issuer returns the state for an issuer, creating it if this is the first time it
// has been asked for. This is the only critical section over the map.
func (c *cache) issuer(url string) *issuerKeys {
c.mu.Lock()
defer c.mu.Unlock()
i, ok := c.sets[url]
if !ok {
i = &issuerKeys{}
c.sets[url] = i
}
return i
}
func (c *cache) get(issuer string) (Keyset, error) {
i := c.issuer(issuer)
i.mu.Lock()
defer i.mu.Unlock()
now := c.now()
if i.keys != nil && now.Sub(i.fetchedAt) < c.ttl {
return i.keys, nil
}
// A failure this recent is answered from memory. Stale keys are still preferred
// to a refusal while they are inside the bound — the issuer being unreachable
// is not evidence against keys it published.
if i.err != nil && now.Sub(i.failedAt) < c.retry {
if i.keys != nil && now.Sub(i.fetchedAt) < c.stale {
return i.keys, nil
}
return nil, i.err
}
keys, err := c.fetch(issuer)
now = c.now()
if err != nil {
i.failedAt, i.err = now, err
if i.keys != nil && now.Sub(i.fetchedAt) < c.stale {
return i.keys, nil
}
return nil, err
}
i.keys, i.fetchedAt = keys, now
i.err, i.failedAt = nil, time.Time{}
return keys, nil
}
func (c *cache) fetch(issuer string) (Keyset, error) {
resp, err := c.client.Get(strings.TrimSuffix(issuer, "/") + jwksPath)
if err != nil {
return nil, fmt.Errorf("fetch: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
// Bounded read: an issuer's key set is a few kilobytes, and an unbounded read
// makes the size of the answer the caller's problem.
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("read: %w", err)
}
return decodeKeys(body)
}
// jwk is one published key, in the shapes IAM emits: RSA (n, e), EC (crv, x, y)
// and ML-DSA-65 (x), each with kty, kid, use and alg (internal/oidc/certkey.go).
type jwk struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
Alg string `json:"alg"`
Crv string `json:"crv"`
N string `json:"n"`
E string `json:"e"`
X string `json:"x"`
Y string `json:"y"`
}
// decodeKeys reads the signing keys out of a JWKS document.
//
// A key that cannot verify anything is left out rather than loaded, so it cannot
// be the reason a forged token passes — but it is left out LOUDLY. A silent skip
// is how an EC or ML-DSA rotation locks out a whole brand with nothing in the log
// to say why: every token then names a kid the verifier does not have, the set
// looks merely empty, and the operator sees an issuer that publishes keys and a
// service that refuses everybody.
func decodeKeys(body []byte) (Keyset, error) {
var doc struct {
Keys []jwk `json:"keys"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
out := Keyset{}
for _, k := range doc.Keys {
key, err := k.public()
if err != nil {
log.Printf("[aml] jwks: unsupported key kid=%q kty=%q alg=%q crv=%q: %v", k.Kid, k.Kty, k.Alg, k.Crv, err)
continue
}
// A duplicate kid is kept first-wins and announced, not overwritten
// silently. Two keys under one kid means a token naming that kid verifies
// against only one of them; a silent last-wins would refuse a token the
// other key signed while logging nothing — the quiet partial lockout this
// package's "left out loudly" principle exists to prevent. The upstream IAM
// dedupes by cert name, so this is a guard against a malformed set, and it
// says so when it fires.
if _, dup := out[k.Kid]; dup {
log.Printf("[aml] jwks: duplicate kid=%q — keeping the first, ignoring the rest", k.Kid)
continue
}
out[k.Kid] = key
}
if len(out) == 0 {
return nil, errors.New("no usable signing key")
}
return out, nil
}
// public is the key a JWK stands for, or why it is not one.
func (k jwk) public() (crypto.PublicKey, error) {
// `use` is optional, but a key published for encryption is published for
// something other than proving who is calling.
if k.Use != "sig" && k.Use != "" {
return nil, fmt.Errorf("published for %q, not for signing", k.Use)
}
switch strings.ToUpper(k.Kty) {
case "RSA":
return k.rsa()
case "EC":
return k.ec()
case ktyMLDSA:
return k.mldsa()
default:
return nil, errors.New("no verifier for this key type")
}
}
func (k jwk) rsa() (*rsa.PublicKey, error) {
n, err := unpad(k.N)
if err != nil {
return nil, fmt.Errorf("modulus: %w", err)
}
e, err := unpad(k.E)
if err != nil {
return nil, fmt.Errorf("exponent: %w", err)
}
if len(e) == 0 || len(e) > 8 {
return nil, fmt.Errorf("exponent is %d bytes", len(e))
}
modulus := new(big.Int).SetBytes(n)
if modulus.BitLen() < minModulus {
return nil, fmt.Errorf("modulus is %d bits, need %d", modulus.BitLen(), minModulus)
}
return &rsa.PublicKey{N: modulus, E: int(new(big.Int).SetBytes(e).Int64())}, nil
}
// ec decodes an EC signing key (RFC 7518 §6.2). The coordinates are checked to be
// a point on the named curve — a pair of integers that is not on the curve is not
// a public key, and feeding one to a verifier is undefined rather than false.
func (k jwk) ec() (*ecdsa.PublicKey, error) {
var curve elliptic.Curve
var size int
switch k.Crv {
case "P-256":
curve, size = elliptic.P256(), 32
case "P-384":
curve, size = elliptic.P384(), 48
case "P-521":
curve, size = elliptic.P521(), 66
default:
return nil, fmt.Errorf("curve %q is not one this verifies", k.Crv)
}
x, err := coordinate(k.X, size)
if err != nil {
return nil, fmt.Errorf("x: %w", err)
}
y, err := coordinate(k.Y, size)
if err != nil {
return nil, fmt.Errorf("y: %w", err)
}
// SEC 1 uncompressed point, which is the form that gets validated on the way in.
point := append([]byte{4}, append(x, y...)...)
pub, err := ecdsa.ParseUncompressedPublicKey(curve, point)
if err != nil {
return nil, fmt.Errorf("not a point on %s: %w", k.Crv, err)
}
return pub, nil
}
// coordinate decodes one fixed-width EC coordinate. RFC 7518 requires the full
// width; a shorter value is left-padded because stripping leading zeros is a
// common encoder bug and the integer it names is the same one, while a longer
// value is not a coordinate on this curve at all.
func coordinate(s string, size int) ([]byte, error) {
raw, err := unpad(s)
if err != nil {
return nil, err
}
if len(raw) == 0 || len(raw) > size {
return nil, fmt.Errorf("%d bytes, want at most %d", len(raw), size)
}
out := make([]byte, size)
copy(out[size-len(raw):], raw)
return out, nil
}
// unpad decodes base64url with or without padding: RFC 7517 omits it, and
// implementations that send it are common enough that refusing them would refuse
// a working issuer.
func unpad(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(strings.TrimRight(s, "="))
}
// mldsa decodes an ML-DSA-65 signing key: the packed public key in `x`, as IAM
// publishes it (internal/oidc/certkey.go certToJWK).
func (k jwk) mldsa() (*mldsa65.PublicKey, error) {
raw, err := unpad(k.X)
if err != nil {
return nil, err
}
if len(raw) != mldsa65.PublicKeySize {
return nil, fmt.Errorf("%d bytes, want %d", len(raw), mldsa65.PublicKeySize)
}
pub := new(mldsa65.PublicKey)
if err := pub.UnmarshalBinary(raw); err != nil {
return nil, err
}
return pub, nil
}