Documentation
¶
Overview ¶
Package token — one JWK Set member, with its binding already derived.
Package token — the three numeric limits a verification policy resolves.
Package token — the claims <-> JSON conversion shared by both formats.
JWT and PASETO agree on the seven registered claim NAMES and disagree on their value encodings: JWT writes NumericDate (RFC 7519 §2) and allows "aud" to be a string or an array of strings; PASETO writes RFC 3339 timestamps and allows exactly one audience. So the traversal is written once and the two encodings are a [claimShape] each — rather than two claim decoders that would drift apart on everything except the part that actually differs.
Package token — the one-pass, string-aware JSON nesting counter.
Package token — the Ed25519 signing binding.
Package token — the bounded wire primitives every format here goes through: segment splitting, strict base64url, JSON depth and duplicate-member checks, and PASETO's pre-authentication encoding.
Every function in this file runs on ATTACKER-CONTROLLED bytes before any key is touched, so each one is O(len(input)) with a bound checked first and no allocation proportional to a count the attacker chooses.
Package token — the ECDSA P-256 signing binding.
Package token — the HMAC-SHA-256 binding.
Package token — the JWT issuing policy.
Package token — the JWT/JOSE claim-value encoding.
Package token — JSON string rendering that cannot fail.
encoding/json's Marshal returns an error no string or []string can ever produce, and discarding it at a dozen call sites is how a real failure somewhere else eventually gets discarded too. Rendering the two shapes this package actually emits by hand removes the error channel instead of ignoring it — and removes a reflective encode from every mint.
Package token — verification against a JWK or a JWK Set.
This file holds the ONLY run-time algorithm selection in the package, and the selector is the KEY, never the token. A relying party fetched the key set from a publisher it trusts; the "kty"/"crv" members are that publisher's statement about their own key, and deriving the algorithm from them is exactly as trustworthy as the key itself. Deriving it from the token's "alg" header would be trusting the attacker's statement about the relying party's key — which is algorithm confusion, spelled out.
Package token — JWS Compact Serialization (RFC 7515): the header, and the algorithm gate every verifier runs before a key reaches a primitive.
Package token — the JWS compact issuer.
Package token — the parsed, not-yet-authenticated shape of a compact token.
Package token — the single-key JWS compact verifier.
Package token — the JWT constructor set: one per algorithm, each accepting only the Go key type its algorithm can use.
That is not documentation. NewHS256Verifier's parameter is a core/crypto.Key — a struct with an unexported field — so the classic confusion bug, handing it an EC or RSA public key so the attacker can sign with the public half, is a call that does not compile.
Package token — the algorithm-bound key contracts and their constructors.
This file is where algorithm confusion is made unwritable. Each binding holds ONE Go key type and reports ONE algorithm, and the bind* helpers below are the only way to build one. There is no `bind(alg, []byte)`, because a function that takes an algorithm name and a bag of bytes is a function whose caller can be talked into passing a public key where a shared secret goes.
Package token — PASETO v4.public: the constructors and the scheme gate.
PASETO is the format that answers the question "what if the algorithm were not negotiable". Its header is a version and a purpose — "v4.public." — baked into the signed bytes through the pre-authentication encoding, so there is no "alg" field to substitute and algorithm confusion has nothing to grip. This implementation still binds the key type at construction, because the property should hold for the same reason on both formats rather than by accident on one of them.
Only v4.public ships. See the package CLAUDE.md §PASETO for v4.local.
Package token — the PASETO v4.public issuer.
Package token — the PASETO v4.public issuing policy.
Package token — the PASETO claim-value encoding.
Package token — the PASETO v4.public verifier.
Package token — the PASETO v4.public verification policy.
Package token — the validated, defaults-applied form of a verification config, so the checks run once at construction rather than once per token.
Package token implements the two concrete security-token formats behind the core/security/token ports: JWT over JWS Compact Serialization (RFC 7519 + RFC 7515) and PASETO v4.public. It is stdlib-only and composes the SDK's own crypto schemes — HMAC-SHA-256 from service/crypto/mac/hmacsha2, Ed25519 from service/crypto/sign/ed25519sig — so it adds no dependency to internal/service.
Algorithm confusion is prevented by the constructor set, not by a check ¶
There is no NewVerifier(alg, key) here, and there is no verifier that reads the token's "alg" header to decide what to do. There is one constructor per algorithm, each accepting only the ONE Go type that algorithm can use:
NewHS256Verifier(secret crypto.Key, cfg VerifierConfig) NewES256Verifier(pub *ecdsa.PublicKey, cfg VerifierConfig) NewEdDSAVerifier(pub ed25519.PublicKey, cfg VerifierConfig)
The textbook algorithm-confusion bug — an RSA/EC public key handed to HMAC-SHA-256 as a shared secret, so that anybody holding the public key can mint tokens — is a call that does not compile here: *ecdsa.PublicKey is not a crypto.Key and never converts to one. Verification then compares the header against the binding and refuses a mismatch with coretoken.AlgorithmMismatch, before any key reaches any primitive.
NewVerifierFromJWK and NewSetVerifier are the only places an algorithm is chosen at run time, and they choose it from the KEY's "kty"/"crv" — material the relying party fetched from a trusted publisher — never from the token.
What is refused, always ¶
- "alg":"none" in any capitalisation (RFC 7519 §6), with no option to enable it: the core Algorithm enum has no value that spells it.
- a non-empty "crit" header (RFC 7515 §4.1.11).
- a token past the configured size, or claims past the configured nesting depth, both checked before any decode.
- a repeated JSON member in the header or the claims (RFC 8725 §2.6).
- a token with no "exp", unless the profile opts out explicitly.
See the package CLAUDE.md for the RFC 8725 coverage table, including the sections this package does NOT address.
Package token — hosts compile-time interface assertions — keeping them out of the production source so the runtime binary carries no diagnostic-only declarations.
Package token — the claim validation both formats share.
Everything here runs AFTER the signature has verified, and that ordering is a security property, not an implementation detail: reporting "expired" for a token whose signature was never checked tells an attacker what is inside a forgery, and hands the application a claim set it may log.
Package token — the JWT verification policy.
Index ¶
- Constants
- func NewES256Issuer(priv *ecdsa.PrivateKey, cfg IssuerConfig) (issuer coretoken.Issuer, err error)
- func NewES256Verifier(pub *ecdsa.PublicKey, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
- func NewEdDSAIssuer(priv ed25519.PrivateKey, cfg IssuerConfig) (issuer coretoken.Issuer, err error)
- func NewEdDSAVerifier(pub ed25519.PublicKey, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
- func NewHS256Issuer(secret corecrypto.Key, cfg IssuerConfig) (issuer coretoken.Issuer, err error)
- func NewHS256Verifier(secret corecrypto.Key, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
- func NewPasetoV4Issuer(priv ed25519.PrivateKey, cfg PasetoIssuerConfig) (issuer coretoken.Issuer, err error)
- func NewPasetoV4Verifier(pub ed25519.PublicKey, cfg PasetoVerifierConfig) (verifier coretoken.Verifier, err error)
- func NewSetVerifier(set jwk.Set, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
- func NewVerifierFromJWK(key jwk.KeyValue, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
- type IssuerConfig
- type PasetoIssuerConfig
- type PasetoVerifierConfig
- type VerifierConfig
Constants ¶
const ( // DefaultMaxTokenLen is the token-string bound applied when a config // leaves MaxTokenLen at zero. 8 KiB comfortably holds an OIDC ID token // with a certificate thumbprint; it does not hold an attack. DefaultMaxTokenLen int = 8 << 10 // MinTokenLen is the floor a configured MaxTokenLen may not go below. A // bound smaller than this would reject every real token, which is a // configuration mistake worth naming rather than a policy. MinTokenLen int = 64 // MaxTokenLenCeiling is the ceiling a configured MaxTokenLen may not // exceed. There is no legitimate multi-megabyte bearer token, and the // ceiling is what keeps "configurable" from meaning "unbounded". MaxTokenLenCeiling int = 1 << 20 // DefaultMaxClaimDepth is the JSON nesting bound applied when a config // leaves MaxClaimDepth at zero. DefaultMaxClaimDepth int = 16 // MaxClaimDepthCeiling is the ceiling a configured MaxClaimDepth may not // exceed. MaxClaimDepthCeiling int = 64 // DefaultMaxKeyCandidates is how many keys a set verifier will try for one // "kid" when the config leaves MaxKeyCandidates at zero. A rotation window // holds two; four leaves room for a slow rollover without turning a // published JWK Set into a signature-verification multiplier. DefaultMaxKeyCandidates int = 4 // MaxKeyCandidatesCeiling caps MaxKeyCandidates. Trying keys IS signature // verification, the expensive half of this package; a publisher who can // name the candidate count can name the cost. MaxKeyCandidatesCeiling int = 16 )
const MaxLeeway time.Duration = 5 * time.Minute
MaxLeeway is the largest clock-skew allowance a verifier accepts.
Leeway exists because two hosts disagree about the time by seconds. A leeway measured in hours is not skew tolerance, it is an expiry claim that has been quietly disabled — so the constructor refuses it rather than honouring a value whose effect the caller almost certainly did not intend (ADR 0031).
Variables ¶
This section is empty.
Functions ¶
func NewES256Issuer ¶
func NewES256Issuer(priv *ecdsa.PrivateKey, cfg IssuerConfig) (issuer coretoken.Issuer, err error)
NewES256Issuer returns a JWT issuer signing with ECDSA P-256 + SHA-256, emitting the fixed-width R||S signature of RFC 7518 §3.4, always in its low-S form, so a token it signs has one spelling.
func NewES256Verifier ¶
func NewES256Verifier(pub *ecdsa.PublicKey, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
NewES256Verifier returns a JWT verifier bound to ECDSA P-256 under pub.
pub is validated as a curve point, not merely measured: an off-curve point is a documented route to key recovery (RFC 8725 §3.4).
It accepts a high-S signature as well as a low-S one: RFC 7518 §3.4 does not require low S, and most producers emit a high s about half the time. Since (r, s) and (r, n−s) both verify, a token from another issuer has two valid spellings, so a revocation or replay list is keyed on jti, or on the claims, never on the token string.
func NewEdDSAIssuer ¶
func NewEdDSAIssuer(priv ed25519.PrivateKey, cfg IssuerConfig) (issuer coretoken.Issuer, err error)
NewEdDSAIssuer returns a JWT issuer signing with Ed25519 (JOSE "EdDSA").
func NewEdDSAVerifier ¶
func NewEdDSAVerifier(pub ed25519.PublicKey, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
NewEdDSAVerifier returns a JWT verifier bound to Ed25519 under pub.
func NewHS256Issuer ¶
func NewHS256Issuer(secret corecrypto.Key, cfg IssuerConfig) (issuer coretoken.Issuer, err error)
NewHS256Issuer returns a JWT issuer signing with HMAC-SHA-256 under secret.
HS256 is symmetric: anybody who can verify these tokens can also mint them. Reach for NewES256Issuer or NewEdDSAIssuer the moment the verifier is not the same trust domain as the issuer.
func NewHS256Verifier ¶
func NewHS256Verifier(secret corecrypto.Key, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
NewHS256Verifier returns a JWT verifier bound to HMAC-SHA-256 under secret.
func NewPasetoV4Issuer ¶
func NewPasetoV4Issuer(priv ed25519.PrivateKey, cfg PasetoIssuerConfig) (issuer coretoken.Issuer, err error)
NewPasetoV4Issuer returns a PASETO v4.public issuer signing with Ed25519.
func NewPasetoV4Verifier ¶
func NewPasetoV4Verifier(pub ed25519.PublicKey, cfg PasetoVerifierConfig) (verifier coretoken.Verifier, err error)
NewPasetoV4Verifier returns a PASETO v4.public verifier bound to pub.
func NewSetVerifier ¶
NewSetVerifier returns a JWT verifier that selects a key from set by the token's "kid" header.
The kid is a selector, not a credential ¶
The kid chooses which key to TRY. It never decides whether the token is valid: the signature does. So a kid naming an EC key does not let a token claiming HS256 through — the binding built from that key reports ES256, the header comparison refuses, and no key material reaches a MAC.
The verdicts are the single-key verifier's. A token with "alg": "none" is refused with AlgorithmNone before any key is selected, kid or no kid. A token whose header no candidate accepts — an algorithm none of them is bound to, a "crit" parameter, an unexpected "typ" — is refused with that header's own verdict, never reduced to SignatureInvalid, which means a candidate accepted the header and no signature verified.
Duplicate kids ¶
jwk.Set.ByKid refuses to choose between keys sharing a kid, because nothing in the document distinguishes them and picking the first would make the answer depend on member order. This verifier CAN choose, because it has something the key set does not: a signature. It tries the candidates in document order and accepts the first that verifies, which is the rotation path RFC 7517 §4.5 leaves open.
It tries a BOUNDED number of them. The candidate count is chosen by whoever published the JWK Set, and each attempt is a signature verification, so an unbounded loop would let a publisher — or anybody who can influence the document — price every request. Past MaxKeyCandidates the token is refused with KeyIDAmbiguous rather than verified slowly.
The set is bound ONCE, here ¶
Every member's verifying binding is derived at construction, not per token. A JWK is a key's SERIALISED form and the binding is the same key in the shape a primitive consumes; deriving it reads the key's own kty/crv/x/y and nothing a token supplies, so it is a fixed function of material that is fixed for this verifier's lifetime. Doing it per request cost a full PKIX round trip — `x509.MarshalPKIXPublicKey` immediately re-parsed by `x509.ParsePKIXPublicKey` — measured at 8.3 % of an ES256 JWKS verification's CPU and 28.2 % of its allocated objects. See `internal/service/crypto/key/jwk/BENCH.md`.
What deliberately did NOT move is the ORDER. Which key is selected, the candidate bound, and the algorithm comparison against the binding all still happen per token and in the same sequence, because that sequence is ADR 0042's security property. Nothing here is keyed on, or cached against, anything the token carries.
A set that can verify nothing is refused ¶
Binding at construction is also what lets a useless set be refused there (ADR 0031): an empty set, and one whose every member either has no kid or is unusable, are both PolicyMisconfigured rather than a verifier that refuses every token. The unusable members of a set that IS accepted still count against MaxKeyCandidates — see indexByKid.
func NewVerifierFromJWK ¶
func NewVerifierFromJWK(key jwk.KeyValue, cfg VerifierConfig) (verifier coretoken.Verifier, err error)
NewVerifierFromJWK returns a JWT verifier bound to the algorithm key's own type and curve imply:
kty=oct -> HS256 kty=EC, crv=P-256 -> ES256 kty=OKP, crv=Ed25519 -> EdDSA
P-384 and P-521 are represented by the jwk package and refused HERE with KeyUnsuitable. RSA never gets this far: jwk.Parse refuses it first, as UNSUPPORTED_KEY_TYPE, because the SDK models no RSA scheme at all. The SDK signs with none of them, and a verifier that pretends otherwise would promise an interop the crypto domain cannot honour.
The key's own advisory members are enforced, not ignored: a "use" other than "sig", a "key_ops" that excludes "verify", or an "alg" that disagrees with what the key type implies, all refuse the key. A publisher who says "this key is for encryption" has said something, and honouring it costs nothing.
Types ¶
type IssuerConfig ¶
type IssuerConfig struct {
// Issuer, when non-empty, is stamped as "iss" on claims that carry none.
Issuer string
// Type overrides the JWS "typ" header. Empty means "JWT" (RFC 7519 §5.1).
// Set it to an explicit media type such as "at+jwt" when this issuer mints
// more than one kind of token (RFC 8725 §3.11 / §3.12).
Type string
// KeyID, when non-empty, is stamped as the JWS "kid" header — the value a
// recipient's key-set verifier selects on. An issuer that publishes a JWK
// Set needs one; an issuer whose verifier holds a single key does not.
KeyID string
// Lifetime, when positive, is stamped as "exp" = now + Lifetime on claims
// that carry no expiry of their own. Zero means the claims must bring one.
Lifetime time.Duration
// AllowMissingExpiry permits minting a token with no "exp" at all. The
// zero value is false, so the dangerous shape is the one you have to ask
// for by name: a bearer token that never expires is a password with worse
// rotation (ADR 0030).
AllowMissingExpiry bool
// Clock is the time source for "iat" and the derived "exp". Nil means
// clock.System. It is a clock.Clock — the frozen read-only half of the
// time port (ADR 0039) — because issuing stamps instants and never waits.
Clock clock.Clock
}
IssuerConfig configures a JWT issuer.
The zero value is usable and safe: it stamps "iat", requires the caller to supply an expiry, and refuses to mint a token that would never expire.
type PasetoIssuerConfig ¶
type PasetoIssuerConfig struct {
// Issuer, when non-empty, is stamped as "iss" on claims that carry none.
Issuer string
// covered by the signature through the pre-authentication encoding, and it
// is where a key identifier belongs in this format.
Footer []byte
// ImplicitAssertion is v4's optional out-of-band context: it is covered by
// the signature but never transmitted, so a token only verifies for a
// recipient who already knows it. Both sides must configure the same value.
ImplicitAssertion []byte
// Lifetime, when positive, is stamped as "exp" = now + Lifetime on claims
// that carry no expiry of their own. Zero means the claims must bring one.
Lifetime time.Duration
// AllowMissingExpiry permits minting a token with no "exp" at all. The
// zero value is false, so the dangerous shape is the one you have to ask
// for by name (ADR 0030).
AllowMissingExpiry bool
// Clock is the time source for "iat" and the derived "exp". Nil means
// clock.System.
Clock clock.Clock
}
PasetoIssuerConfig configures a PASETO v4.public issuer.
It is a flat struct rather than an IssuerConfig plus two extras, and the missing fields are the point: PASETO has no header parameters, so there is nothing for Type or KeyID to set. Embedding the JWS config would have published two knobs this format silently ignores, and a configuration field that does nothing is a bug waiting for somebody to set it. The equivalent of a "kid" here is PasetoIssuerConfig.Footer, which the signature covers.
The zero value is usable and safe: it stamps "iat", requires the caller to supply an expiry, and refuses to mint a token that would never expire.
type PasetoVerifierConfig ¶
type PasetoVerifierConfig struct {
// Issuer, when non-empty, must equal the token's "iss" (RFC 8725 §3.8).
Issuer string
// Audience, when non-empty, must equal the token's "aud". PASETO's
// audience is a single string, so there is no list to search.
Audience string
// Empty means the token must carry NO footer.
//
// The Verifier port returns claims, not a footer, so an unconstrained
// footer would be data this package authenticated and then dropped on the
// floor. Refusing is the only answer that does not quietly lose it.
Footer []byte
// ImplicitAssertion must equal the value the issuer signed with.
ImplicitAssertion []byte
// Leeway is the clock-skew allowance applied to "exp" and "nbf". It must
// be in [0, MaxLeeway].
Leeway time.Duration
// MaxLifetime, when positive, refuses an authenticated token whose exp-iat
// span exceeds it. It requires "iat".
MaxLifetime time.Duration
// MaxTokenLen bounds the token string. Zero means DefaultMaxTokenLen.
MaxTokenLen int
// MaxClaimDepth bounds the claims' JSON nesting. Zero means
// DefaultMaxClaimDepth.
MaxClaimDepth int
// AllowMissingExpiry accepts a token with no "exp".
AllowMissingExpiry bool
// Clock is the time source for the temporal claims. Nil means
// clock.System; a clock.ManualClock makes an expiry test deterministic.
Clock clock.Clock
}
PasetoVerifierConfig configures a PASETO v4.public verifier.
Like its issuing counterpart it is flat rather than an embedded VerifierConfig, and the absent knobs are deliberate: PASETO has no "typ" to require and no "kid" to select on, so RequireType and MaxKeyCandidates would be fields this format ignores.
The zero value is usable and strict: it requires an expiry, allows no clock skew, checks no issuer or audience, expects no footer, and applies the default bounds.
type VerifierConfig ¶
type VerifierConfig struct {
// Issuer, when non-empty, must equal the token's "iss" (RFC 8725 §3.8).
Issuer string
// Audience, when non-empty, must appear in the token's "aud"
// (RFC 8725 §3.9). A correctly signed token minted for another API is not
// a valid token here.
Audience string
// RequireType, when non-empty, must equal the JWS "typ" header. It is the
// mechanism RFC 8725 §3.12 asks for: two kinds of token from one issuer
// need validation rules that reject each other's tokens.
RequireType string
// Leeway is the clock-skew allowance applied to "exp" and "nbf". It must
// be in [0, MaxLeeway].
Leeway time.Duration
// MaxLifetime, when positive, refuses an authenticated token whose
// exp-iat span exceeds it. It requires "iat": a lifetime that cannot be
// computed is not a lifetime this recipient has accepted.
MaxLifetime time.Duration
// MaxTokenLen bounds the token string. Zero means DefaultMaxTokenLen; a
// value outside [MinTokenLen, MaxTokenLenCeiling] is refused.
MaxTokenLen int
// MaxClaimDepth bounds the claims' JSON nesting. Zero means
// DefaultMaxClaimDepth; a value outside [1, MaxClaimDepthCeiling] is
// refused.
MaxClaimDepth int
// MaxKeyCandidates bounds how many keys a set verifier tries for one kid.
// Zero means DefaultMaxKeyCandidates. Ignored by single-key verifiers.
MaxKeyCandidates int
// AllowMissingExpiry accepts a token with no "exp". The zero value is
// false, so the profile that RFC 7519 §4.1.4 leaves optional is required
// here unless the caller opts out by name.
AllowMissingExpiry bool
// Clock is the time source for the temporal claims. Nil means
// clock.System; a clock.ManualClock makes an expiry test deterministic
// without sleeping.
Clock clock.Clock
}
VerifierConfig configures a JWT verifier.
The zero value is usable and strict: it requires an expiry, allows no clock skew, checks no issuer or audience (there is none to check against), and applies the default bounds.
Source Files
¶
- bound_key_value.go
- bounds_value.go
- claims_codec.go
- depth_scan.go
- doc.go
- ed25519.go
- encoding.go
- es256.go
- hs256_binding.go
- hs256_verifier.go
- issuer_config.go
- jose_shape.go
- json_scan.go
- jsonstring.go
- jwkbridge.go
- jws.go
- jws_issuer.go
- jws_parts.go
- jws_verifier.go
- jwt.go
- keys.go
- paseto.go
- paseto_issuer.go
- paseto_issuer_config.go
- paseto_shape.go
- paseto_verifier.go
- paseto_verifier_config.go
- policy.go
- token.go
- token_compliance.go
- validate.go
- verifier_config.go