Skip to main content

Module dpop

Module dpop 

Source
Expand description

DPoP proof verification (RFC 9449), the verifier side.

jacquard’s OAuth client (jacquard-oauth, pinned alongside jacquard-common) builds DPoP proofs and never checks one; this module is the other half. It exists so that an authorization server or resource server built on top of didbot-serve – sibling work under crates/didbot-serve/src/oauth/, see plan/oauth.md – can hold one DpopVerifier and call DpopVerifier::verify once per protected request, rather than re-deriving RFC 9449 §4.3’s checklist at each call site.

§What a proof is

A DPoP proof is a compact JWS: three base64url segments, header. payload.signature, with no . inside a segment. This verifier accepts exactly the shape jacquard’s client mints (jacquard-oauth-0.12.1’s dpop::build_dpop_proof), because that shape is this ecosystem’s proof of what a conformant client actually sends:

  • Header: {"alg":"ES256","typ":"dpop+jwt","jwk":{"kty":"EC","crv": "P-256","x":..,"y":..}} – an EC P-256 public key embedded in the header, never a kid pointing elsewhere. Only ES256 is accepted; nothing else is a DPoP key jacquard’s client can produce, and none is never entertained.
  • Payload: jti, iat, htm, htu, and optionally ath (a hash of the access token this proof accompanies) and nonce (echoing a server- issued value). jacquard’s proofs carry no exp – RFC 9449 does not require one, freshness is iat plus the verifier’s own window instead, which is DpopVerifier::verify’s clock-skew check below.
  • Signature: a 64-byte compact r || s pair over header.payload, ES256.

§What this module checks, and in what order

presented_proof makes RFC 9449 §4.3’s first check before any other: a request carries one DPoP header. DpopVerifier::verify then checks typ, alg, the embedded key and the signature, so nothing in the payload is read before it is known to be signed. Then htm, htu, iat, the nonce and jti replay, and last the two bindings: the proof’s key against the thumbprint an access token is bound to, and ath against the token itself. RFC 9449 §4.3 allows any order; the nonce comes before replay and ath, as in the reference provider. Structure or signature failures are reported before content is trusted, and a content mismatch never reads as a signature failure – see DpopError’s variants, each of which names the one thing that did not check out.

§Replay is a two-step

verify reads the replay window and DpopVerifier::accept writes it. A caller verifies the proof, resolves the credential it arrived with, and only then accepts: that is what keeps an entry from being something an unauthenticated caller can spend, since every route that takes a proof can be reached with a freshly generated key and no credential at all.

The window is in memory, so a restart empties it. For the first clock-skew window after one, a proof stamped before the process started is refused with DpopError::PredatesStart — its own answer, so it never reads as a clock disagreeing. Past that window the clock check refuses every proof old enough to be worth replaying.

§Nonces

A verifier built with require_nonce refuses a proof that carries no nonce it accepts, and names the one to retry with. It hands out one nonce at a time, the same to every client, for nonce_ttl; the first time one is asked for after that, a fresh one replaces it. The replaced nonce is still accepted for one more nonce_ttl, so a request in flight across a rotation is not refused for it. A refusal replaces nothing, so a caller sending wrong nonces cannot move the one an honest client holds.

Structs§

DpopVerifier
Proof checking, nonce issuance, and the replay cache, held together because the replay window and the clock-skew window are the same number – see DpopVerifier::new.
NonceState 🔒
The nonce this verifier hands out, and the one it handed out before it, each with the instant it stops or stopped being handed out. Both are accepted until one nonce_ttl past that instant.
ProofClaims 🔒
The DPoP proof payload this verifier reads. RFC 9449 §4.2’s required claims, plus the two optional ones (ath, nonce) this module checks when a caller asks it to.
ProofHeader 🔒
The DPoP proof header this verifier is willing to read.
RawJwk 🔒
An embedded JWK, exactly as RFC 7517 §4 and RFC 7518 §6.2 name an EC public key’s members. Decoded lazily – x and y stay base64url text here, because Coordinate decoding has its own error to report and RawJwk itself has none to give it.
VerifiedProof
What a successfully verified proof establishes.

Enums§

DpopError
Why a presented DPoP proof, or the binding it claims, was refused.

Constants§

DPOP
DPoP, the request header a proof travels in – RFC 9449 §4.1.
DPOP_NONCE
DPoP-Nonce, the response header a server hands a client a nonce in – RFC 9449 §8.
JWK_CRV 🔒
JWK_KTY 🔒
JWT_ALG 🔒
The one JWS algorithm this verifier accepts.
JWT_TYP 🔒
The typ header RFC 9449 §4.2 requires on every DPoP proof JWT.

Functions§

decode_ec_point 🔒
Decodes a JWK’s x/y members into raw coordinates, refusing anything that is not an EC P-256 key with both present and exactly 32 bytes long.
jwk_thumbprint
The RFC 7638 JWK thumbprint of an EC P-256 public key, from its raw coordinates.
normalize_htu 🔒
Strips query and fragment and lowercases scheme and host, so that htu comparison follows RFC 9449 §4.3’s “the same server can use different forms” allowance without demanding byte-identical strings for two URLs that name the same resource.
p256_verifying_key 🔒
Builds a verifying key from raw coordinates, the same SEC1-uncompressed route didbot_key::secp256r1::VerifyingKey::from_multibase takes – see that module’s doc for why a key that arrives from elsewhere is judged rather than normalized.
presented_proof
The DPoP proof a request carries, if it carries one.
random_nonce 🔒
A fresh nonce: 18 CSPRNG-drawn bytes, base64url. The same entropy source didbot_pds::credential’s bearer tokens draw from.

Type Aliases§

Coordinate 🔒
A raw, uninterpreted 32-byte EC coordinate, base64url-decoded from a JWK’s x or y member.