Help us shape the future of Go! Take the 2026 Go Developer Survey

dpop

package
v1.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package dpop implements RFC 9449 Demonstrating Proof of Possession.

A DPoP proof is a short-lived JWT signed by a key the client holds. Binding an issued token to that key's thumbprint means a token on its own is not enough to use it, which is the property bearer tokens lack.

Index

Constants

View Source
const (
	DefaultIatLeewayPast   = 60 * time.Second
	DefaultIatLeewayFuture = 30 * time.Second
)

Default freshness window.

Lopsided on purpose. A proof dated in the future means somebody minted it ahead of time and deserves almost no slack. A proof dated in the past is nearly always a client with a drifting clock, and tightening that side buys support tickets rather than security.

View Source
const DefaultNonceTTL = 5 * time.Minute

DefaultNonceTTL is how long an issued nonce stays acceptable.

View Source
const MaxProofBytes = 4096

MaxProofBytes caps the DPoP header before any parsing happens. A proof is a few hundred bytes; anything larger is either broken or an attempt to make us do expensive work on garbage.

Variables

View Source
var (
	ErrMalformedProof = errors.New("dpop: malformed proof")
	ErrUnsupportedAlg = errors.New("dpop: unsupported proof algorithm")
	ErrBadSignature   = errors.New("dpop: proof signature invalid")
	ErrMethodMismatch = errors.New("dpop: htm does not match request method")
	ErrURIMismatch    = errors.New("dpop: htu does not match request URI")
	ErrStaleProof     = errors.New("dpop: proof iat outside acceptable window")
	ErrNonceRequired  = errors.New("dpop: nonce required")
	ErrNonceMismatch  = errors.New("dpop: nonce mismatch")
	ErrATHMismatch    = errors.New("dpop: ath does not match presented token")
	ErrKeyMismatch    = errors.New("dpop: proof key does not match bound token")
	ErrReplayed       = errors.New("dpop: proof jti replayed")
)

Sentinel errors. Callers map these onto the RFC 9449 wire codes: everything here becomes invalid_dpop_proof at the token endpoint and invalid_token at a protected resource, except ErrNonceRequired which becomes use_dpop_nonce.

View Source
var ErrNonceSecretMissing = errors.New("dpop: nonce signing secret not configured")

ErrNonceSecretMissing is returned when the signing secret is absent or too short.

Callers must treat this as fatal when nonces are switched on. Engine .NonceSecret returns nil with no HMAC JWT key and no environment override, and falling back to a per-process random secret would mint nonces that no other replica can verify: a security feature that presents as an intermittent outage the moment you scale past one instance.

Functions

func AccessTokenHash

func AccessTokenHash(token string) string

AccessTokenHash returns the ath claim value for a token: the base64url SHA-256 of its ASCII bytes.

func SupportedAlgs

func SupportedAlgs() []string

SupportedAlgs returns the advertised algorithm list for server metadata.

func WithRequestScope

func WithRequestScope(ctx context.Context) context.Context

WithRequestScope returns a context that remembers which proofs have already been validated under it, so a second enforcement point on the same request does not report the request's own proof as a replay.

Installing it twice is a no-op: the first scope wins, which keeps the record shared across every check on the request rather than split between nested contexts.

Types

type Config

type Config struct {
	IatLeewayPast   time.Duration
	IatLeewayFuture time.Duration
	Replay          ReplayCache
	Nonce           NonceVerifier
	// Now defaults to time.Now. Tests inject a fixed clock so iat boundaries
	// can be asserted exactly.
	Now func() time.Time
}

Config configures a Validator.

type Expectation

type Expectation struct {
	// Method is the HTTP method the request arrived on.
	Method string
	// URL is the request URL. Query and fragment are ignored during
	// comparison, so callers may pass the full URL.
	URL string
	// AccessToken is the token presented alongside the proof. Empty at the
	// token endpoint, where no token exists yet, and an ath claim is then
	// neither required nor checked.
	AccessToken string
	// ExpectedJKT is the thumbprint the presented token is bound to. Empty at
	// issuance, where the key is learned from the proof instead of checked.
	ExpectedJKT string
	// NonceRequired demands a valid server-issued nonce.
	NonceRequired bool
}

Expectation describes what a proof has to match for this request.

type MemoryReplayCache

type MemoryReplayCache struct {
	// contains filtered or unexported fields
}

MemoryReplayCache is a bounded in-process cache with TTL expiry and oldest-first eviction.

In-process means per-instance. Behind a load balancer a proof replayed against a different instance is not caught here, which is why the nonce exists and why NewCeremonyReplayCache is offered for deployments that run a distributed ceremony store.

func NewMemoryReplayCache

func NewMemoryReplayCache(maxEntries int) *MemoryReplayCache

NewMemoryReplayCache creates a cache holding at most maxEntries.

func (*MemoryReplayCache) Evictions

func (c *MemoryReplayCache) Evictions() uint64

Evictions returns how many entries have been dropped for capacity. A non-zero and climbing value means proofs are being accepted that should have been checked, so surface it rather than leaving it to be discovered later.

func (*MemoryReplayCache) Seen

func (c *MemoryReplayCache) Seen(_ context.Context, key string, ttl time.Duration) (bool, error)

type Mode

type Mode string

Mode is how strictly DPoP applies at an issuance point.

const (
	// ModeOff ignores proofs entirely. Tokens are issued unbound.
	ModeOff Mode = "off"
	// ModeOptional binds when the client presents a valid proof and issues an
	// ordinary bearer token when it does not. This is the migration setting.
	ModeOptional Mode = "optional"
	// ModeRequired refuses to issue an unbound token.
	ModeRequired Mode = "required"
)

func MaxMode

func MaxMode(a, b Mode) Mode

MaxMode returns the stricter of two modes.

Resolution is monotonic on purpose. Setting an app to required states a mandate for every client under it, and a per-client field able to quietly undo that would turn the strongest setting in the system into a suggestion. A legacy client that cannot cope is handled by moving the app back to optional, which is explicit and audited.

func ParseMode

func ParseMode(s string) Mode

ParseMode converts a stored string into a Mode. Anything unrecognised, including the empty string, becomes ModeOff, so a typo or a corrupt row fails towards today's behaviour rather than towards locking clients out.

type NonceSigner

type NonceSigner struct {
	// contains filtered or unexported fields
}

NonceSigner mints and verifies server nonces without storing them.

The construction follows dashboard/nonce.go, with one deliberate difference: a DPoP nonce is NOT single use. Consuming it on first sight would break the client on its second request, because one nonce covers every request made during its lifetime. Do not reach for nonceSigner.Consume here.

func NewNonceSigner

func NewNonceSigner(secret []byte, ttl time.Duration) (*NonceSigner, error)

NewNonceSigner returns a signer, or ErrNonceSecretMissing if the secret is unusable.

func (*NonceSigner) Issue

func (s *NonceSigner) Issue(jkt string) string

Issue returns a nonce bound to jkt at the current time.

func (*NonceSigner) NeedsRefresh

func (s *NonceSigner) NeedsRefresh(nonce string) bool

NeedsRefresh reports whether a nonce is past half its TTL, so responses can carry a replacement before the client's current one expires. An unparseable nonce always needs replacing. A nil signer can verify nothing, so it can't claim a nonce is still fresh either; it always needs replacing too.

func (*NonceSigner) Verify

func (s *NonceSigner) Verify(jkt, nonce string) bool

Verify reports whether nonce was issued by this signer for jkt and is still inside its TTL. It does not consume the nonce, so it may return true many times for the same value.

Guarded against a nil receiver on purpose. Config.Nonce is a NonceVerifier interface; assigning a nil *NonceSigner into it produces a non-nil interface wrapping a nil pointer, so a caller's `cfg.Nonce == nil` check sees a value and calls Verify anyway. Without this guard that call dereferences s.secret in sign() and panics on every nonce-required request. With it, a nil signer just fails the check, which is what "no nonce support configured" is supposed to look like.

type NonceVerifier

type NonceVerifier interface {
	Verify(jkt, nonce string) bool
}

NonceVerifier checks a server-issued nonce against the key it was minted for. NonceSigner implements it.

type Proof

type Proof struct {
	Raw string

	Typ string
	Alg string
	JWK json.RawMessage

	PublicKey crypto.PublicKey
	JKT       string

	JTI      string
	HTM      string
	HTU      string
	Nonce    string
	ATH      string
	IssuedAt time.Time
}

Proof is a parsed and signature-verified DPoP proof. Holding one means the signature checked out; it says nothing yet about whether the proof matches the request it arrived on. That is Validator's job.

func Parse

func Parse(raw string) (*Proof, error)

Parse decodes a compact DPoP proof and verifies its signature against the public key carried in its own jwk header.

Self-signed verification looks circular and is not. The signature proves the sender holds the private key for the jwk they supplied; binding that key to a token is what makes the claim mean anything, and that check happens in Validator against the token's thumbprint.

type ReplayCache

type ReplayCache interface {
	Seen(ctx context.Context, key string, ttl time.Duration) (bool, error)
}

ReplayCache records proof jti values so a captured proof cannot be replayed inside its freshness window.

Seen atomically records the key and reports whether it was already present. Implementations must be safe for concurrent use, and Seen sits on the hot path of every authenticated request, so it needs to be cheap.

func NewCeremonyReplayCache

func NewCeremonyReplayCache(s ceremony.Store) ReplayCache

NewCeremonyReplayCache returns a ReplayCache backed by a ceremony store. See ceremonyReplayCache for what this does and does not guarantee under concurrent replay attempts.

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator checks proofs against requests. It holds no HTTP types and no store, so its whole behaviour is reachable from a table-driven test.

func NewValidator

func NewValidator(cfg Config) *Validator

NewValidator returns a Validator, filling in defaults for anything unset.

func (*Validator) Validate

func (v *Validator) Validate(ctx context.Context, p *Proof, e Expectation) error

Validate checks a parsed proof against the request it arrived on.

The jti replay check runs last, after every other check has passed. Recording it earlier would let an attacker burn a client's jti values using proofs that were going to be rejected anyway, turning a replay defence into a denial of service against the legitimate holder.

Jump to

Keyboard shortcuts

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