Skip to main content

oauth_as/
jwt.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! RFC 9068 JWT access tokens and the RFC 7517 key set that lets a resource server verify them.
5//! Compiled ONLY under the off-by-default `jwt` feature; with the feature off this module does not
6//! exist and the crate's dependency set is unchanged.
7//!
8//! # Verification: the three rules, and they are the whole of the trust boundary
9//!
10//! This module SIGNS in its first half and VERIFIES in its second, and the two jobs are not
11//! symmetric. The module doc used to be able to say this crate "never parses a JWT it did not
12//! make"; the `client-assertion` and `dpop` features ended that. An RFC 7523 client assertion and
13//! an RFC 9449 DPoP proof are both JWTs a CLIENT made, so [`CompactJws::parse`], [`PublicJwk`] and
14//! [`verify_es256`] are handling attacker-controlled input, and anyone verifying against them
15//! needs these three rules rather than a pointer at the source.
16//!
17//! They are the same three rules every published JWS confusion attack has been aimed at:
18//!
19//! 1. THE KEY IS CHOSEN BY THE VERIFIER, never by the token. A caller passes the key it already
20//!    decided to trust (a registered client's JWK, a registered client's secret); nothing here
21//!    resolves a key out of the header on its own authority, and there is no `jku`, `x5u` or `kid`
22//!    lookup. DPoP is the one apparent exception and is not really one: its key comes from the
23//!    proof, but the proof only ever proves possession of THAT key, and it is the `cnf.jkt`
24//!    binding, not this module, that decides whether the key means anything (see the `dpop`
25//!    module).
26//! 2. THE ALGORITHM IS CHOSEN BY THE VERIFIER, never by the token. [`verify_es256`] and
27//!    [`verify_hs256`] are separate functions taking separate key types, so there is no value of
28//!    `alg` a caller can be made to route an HMAC verification at a public key it already
29//!    published. `none` is not implemented at all: no code path here accepts an unsigned JWS. A
30//!    caller still has to check that the `alg` it was handed is the one the REGISTRATION expects,
31//!    which is why the `client-assertion` module's `AssertionKeys` holds one algorithm, not a
32//!    set.
33//! 3. NOTHING IS DECODED TWICE. The signature is verified over the EXACT received bytes of
34//!    `header.payload` ([`CompactJws::signing_input`] borrows them), never over a re-serialization
35//!    of the parsed claims, so a payload that serializes differently than it arrived cannot verify
36//!    under one reading and be interpreted under another.
37//!
38//! A JWK presented to this module is also refused outright if it carries any PRIVATE or symmetric
39//! member (`d`, the RSA CRT parameters, `k`): RFC 9449 section 4.3 makes that a requirement, and
40//! [`PublicJwk::from_json`] is the only route from JSON into the type, including through `serde`,
41//! whose `Deserialize` impl is routed through it rather than derived. The type's fields are sealed,
42//! so the other constructors are the only alternatives and neither can express a private member:
43//! [`PublicJwk::from_coordinates`] takes two P-256 coordinates and nothing else, and
44//! [`Jwk::to_public_jwk`] converts a key this crate PUBLISHED, which by construction has no private
45//! half in it. See [`PublicJwk`] on what each does and does not revalidate.
46//!
47//! # Why this is hand-rolled
48//!
49//! This crate ISSUES exactly one token shape. A general
50//! JOSE library brings a parser, a validation policy engine and a key-format zoo that an issuer
51//! never executes; the compact serialization of RFC 7515 section 3.1 is
52//! `BASE64URL(header) "." BASE64URL(payload) "." BASE64URL(signature)` and fits in this file on
53//! top of `serde_json` and `base64`, which the crate already depends on.
54//!
55//! # THE ES256 SEAM: the arithmetic is not in this feature
56//!
57//! The P-256 arithmetic is the one thing that genuinely needs an implementation, and after 0.9.0
58//! it is not one this feature brings. [`Es256Signer`] and [`Es256Verifier`] are the seam; the
59//! `jwt-p256` feature is the BACKEND this crate ships over `p256`, and a host may install its own
60//! instead. Two reasons, in the order they matter:
61//!
62//! 1. THE PRIVATE KEY NEED NOT BE IN THIS PROCESS. [`Es256Signer::sign`] is async precisely so it
63//!    can be a cloud KMS or a PKCS#11 token, where the key never leaves its boundary and this
64//!    process holds only a handle. The signing key is the one secret whose compromise forges every
65//!    token the deployment will ever issue, and "the key is in the process" is exactly the property
66//!    a regulated deployment must avoid. Through 0.9.0 this module made it structural.
67//! 2. It stops `jwt` adding a complete SECOND elliptic curve implementation (measured: 20 packages)
68//!    to a host that already has one through `rustls`, which is most Rust HTTP servers.
69//!
70//! [`Es256Verifier`] is SYNC, and the asymmetry is the design rather than an oversight: verifying
71//! holds only PUBLIC keys, so there is nothing to externalise, and it sits on the RFC 9449 DPoP hot
72//! path where an ES256 verification is already about 133 microseconds. Making it async would buy
73//! nothing and cost bytes on the token future.
74//!
75//! # What the host owns
76//!
77//! The signing key. This module will not invent one at startup and does not persist one: a key
78//! that appears from nowhere is a key nobody is managing, and a key regenerated on restart
79//! silently invalidates every live token. [`EcdsaP256Key::generate`] exists for tests and for a
80//! host's own key-provisioning tool, and the host is expected to store what it generates.
81
82use std::fmt;
83use std::future::Future;
84use std::pin::Pin;
85use std::sync::Arc;
86use std::time::{SystemTime, UNIX_EPOCH};
87
88use base64::engine::general_purpose::URL_SAFE_NO_PAD;
89use base64::Engine as _;
90#[cfg(feature = "jwt-p256")]
91use p256::ecdsa::signature::{Signer as _, Verifier as _};
92#[cfg(feature = "jwt-p256")]
93use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
94#[cfg(feature = "jwt-pkcs8")]
95use p256::pkcs8::{DecodePrivateKey as _, EncodePrivateKey as _};
96#[cfg(feature = "jwt-p256")]
97use p256::SecretKey;
98use serde::{Deserialize, Serialize};
99use sha2::{Digest as _, Sha256};
100
101/// A key could not be loaded or exported. The message never contains key material.
102#[cfg(feature = "jwt-p256")]
103#[cfg_attr(docsrs, doc(cfg(feature = "jwt-p256")))]
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct KeyError(String);
106
107#[cfg(feature = "jwt-p256")]
108impl fmt::Display for KeyError {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        write!(f, "signing key error: {}", self.0)
111    }
112}
113
114#[cfg(feature = "jwt-p256")]
115impl std::error::Error for KeyError {}
116
117/// A token could not be signed or serialized. The message never contains key material.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct JwtError(String);
120
121impl fmt::Display for JwtError {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "JWT signing error: {}", self.0)
124    }
125}
126
127impl std::error::Error for JwtError {}
128
129/// A host's ES256 backend could not produce a signature.
130///
131/// One opaque type rather than an enum, and no source error: the caller in
132/// [`JwtConfig::sign_access_token`] has exactly one reaction to any of them (mint no token, answer
133/// `server_error`), so distinguishing "the KMS was unreachable" from "the key was disabled" here
134/// would only invite somebody to treat one as recoverable on a path where neither is. The host
135/// already has the real detail, because the host wrote the signer.
136///
137/// The message MUST NOT contain key material; nothing in this crate ever prints it on the wire.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct SignerError(String);
140
141impl SignerError {
142    /// Describe a signing failure. Do not put key material in it.
143    pub fn new(message: impl Into<String>) -> Self {
144        SignerError(message.into())
145    }
146}
147
148impl fmt::Display for SignerError {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "ES256 signer error: {}", self.0)
151    }
152}
153
154impl std::error::Error for SignerError {}
155
156/// WHERE THIS SERVER'S SIGNING KEY LIVES. The host implements it; this crate holds only a handle.
157///
158/// Enable `jwt-p256` and use [`EcdsaP256Key`] if the key is a scalar in this process. Implement
159/// this if it is not: a cloud KMS, a PKCS#11 token, an HSM, a remote signing service.
160///
161/// # The two halves are deliberately different shapes
162///
163/// [`Es256Signer::sign`] is ASYNC because it holds a SECRET, so it is the half that wants to leave
164/// the process, and leaving the process is a network round trip (or, for PKCS#11, a blocking call
165/// that belongs on a blocking pool). [`Es256Verifier`] is SYNC because it holds only public keys,
166/// so there is nothing to externalise.
167///
168/// # `public_jwk` is SYNC, and that is a REQUIREMENT ON YOU
169///
170/// A KMS-backed signer may need a network round trip to learn its own public half, and this method
171/// gives it nowhere to await. That is deliberate, so it has to be said plainly:
172///
173/// **Fetch the public half ONCE, AT CONSTRUCTION, and return a cached value here.** Do not block a
174/// runtime thread inside this method, and do not panic if a fetch fails; neither is necessary,
175/// because construction is where the fetch belongs.
176///
177/// Sync is the right shape independently of KMS: this crate serialises the RFC 7517 JWKS document
178/// ONCE, at construction, exactly as it does the RFC 8414 metadata document. An async
179/// `public_jwk()` would invite a network call on a PUBLIC, UNAUTHENTICATED, CACHEABLE endpoint that
180/// any client may poll at any rate. Forcing the fetch to construction is the behaviour this crate
181/// wants, and making the method sync is how the type system asks for it.
182///
183/// Two consequences follow, and neither is papered over:
184///
185/// - **Construction becomes fallible, and may be slow.** Your signer reaches the KMS before you
186///   build [`JwtConfig`]. A KMS that is unreachable at boot is then a STARTUP failure, which is the
187///   correct time to find out, rather than a 500 on the first token request.
188/// - **A KEY ROTATED IN THE KMS BEHIND THIS PROCESS'S BACK GOES STALE SILENTLY.** The cached public
189///   half would advertise a key that no longer signs, so every token the deployment issues fails
190///   verification against its own published JWKS, and nothing in this process notices. **Rotating
191///   in the KMS alone is NOT enough.** Rotation must go through [`JwtConfig::rotate_to`], which
192///   keeps the retired PUBLIC half published so tokens minted before the swap keep verifying. This
193///   is the mistake an operator makes exactly once, in production, and its symptom (every token
194///   suddenly invalid) points nowhere near its cause.
195///
196/// # What this trait deliberately cannot do
197///
198/// There is NO method that returns a private key, and there must never be one.
199/// [`JwtConfig`]'s retired set holds public halves only, so a retired key cannot sign again BY
200/// CONSTRUCTION rather than by a promise the code keeps: retirement drops the signer, and a `Jwk`
201/// is all that is left.
202///
203/// # Before you deploy one
204///
205/// Run [`crate::signer_conformance`] against it, behind the `test-util` feature. A broken signer
206/// fails SILENTLY: a wrong signature is indistinguishable, at a resource server, from a tampered
207/// token. Emitting ASN.1 DER instead of the fixed-width form below is the obvious way to be wrong,
208/// and it is wrong in a way only a real client notices.
209#[cfg(feature = "jwt")]
210#[cfg_attr(docsrs, doc(cfg(feature = "jwt")))]
211pub trait Es256Signer: Send + Sync {
212    /// The `ES256` signature over `signing_input`, which is the JWS Signing Input of RFC 7515
213    /// section 5.1 step 5: the ASCII of `BASE64URL(header) "." BASE64URL(payload)`.
214    ///
215    /// The return is the FIXED-WIDTH `r || s` concatenation RFC 7518 section 3.4 mandates: 64
216    /// bytes, 32 per coordinate, leading zeros KEPT. It is **NOT** the ASN.1 DER
217    /// `SEQUENCE { r INTEGER, s INTEGER }` that OpenSSL and nearly every KMS return by default,
218    /// and converting is your job. The array type refuses the wrong LENGTH; it cannot refuse the
219    /// wrong ENCODING, which is what [`crate::signer_conformance`] is for.
220    ///
221    /// Sign the bytes as given. Do not hash them first: `ES256` is ECDSA/P-256/SHA-256, so the
222    /// SHA-256 is part of the signature scheme, and a KMS whose API wants a digest is a KMS you
223    /// hash for exactly once.
224    ///
225    /// # What you may assume about `signing_input`, and what you MUST NOT do
226    ///
227    /// `signing_input` is built by THIS crate, not by a client: it is non-empty printable ASCII,
228    /// it always contains exactly one `.`, and it is roughly a kilobyte. Its CONTENT is not
229    /// entirely this crate's, because the claims carry a `client_id`, a `sub` and a `scope` that
230    /// came from somewhere, but its SHAPE is. You may assume nothing further, and in particular
231    /// nothing about its length.
232    ///
233    /// **MUST NOT PANIC, for any input, ever.** Every failure you can have here (the KMS was
234    /// unreachable, the key was disabled, the credential expired, the response was the wrong
235    /// length) is `Err(SignerError)`, which this crate turns into an RFC 6749 section 5.2
236    /// `server_error`. A panic instead unwinds out of [`JwtConfig::sign_access_token`] and into
237    /// the host's token endpoint, where a runtime that aborts on panic takes the whole server
238    /// down and one that does not leaves a poisoned task; either way the deployment loses more
239    /// than the one request. Nothing about the difference is worth an `unwrap`.
240    fn sign(
241        &self,
242        signing_input: &[u8],
243    ) -> impl Future<Output = Result<[u8; 64], SignerError>> + Send;
244
245    /// The PUBLIC half, for the RFC 7517 JWKS document and the `kid` on every token header.
246    ///
247    /// Cached at construction. Read the trait docs above before implementing this one.
248    fn public_jwk(&self) -> Jwk;
249}
250
251/// Delegating impl so a host can share ONE signer between several [`JwtConfig`]s (two audiences,
252/// two servers in one process) without a newtype. `JwtConfig` erases to a `dyn` handle internally,
253/// so this costs nothing extra.
254#[cfg(feature = "jwt")]
255impl<T: Es256Signer + ?Sized> Es256Signer for Arc<T> {
256    fn sign(
257        &self,
258        signing_input: &[u8],
259    ) -> impl Future<Output = Result<[u8; 64], SignerError>> + Send {
260        (**self).sign(signing_input)
261    }
262
263    fn public_jwk(&self) -> Jwk {
264        (**self).public_jwk()
265    }
266}
267
268/// HOW THIS SERVER CHECKS A SIGNATURE SOMEBODY ELSE MADE: RFC 9449 DPoP proofs, RFC 9101 request
269/// objects, RFC 7523 client assertions.
270///
271/// Enable `jwt-p256` for the built-in [`P256Verifier`], or install your own with
272/// [`crate::AuthorizationServer::with_es256_verifier`]. With neither, every signed credential is
273/// REFUSED: a server that cannot check a signature must never behave as though it had checked one.
274///
275/// SYNC on purpose. This holds only PUBLIC keys, so there is no secret to externalise and nothing
276/// to be gained from a round trip; it also sits on the DPoP hot path, which runs once per token
277/// request. See the module docs on the asymmetry with [`Es256Signer`].
278///
279/// # The contract, and every clause of it is load bearing
280///
281/// `true` means, and may only mean: `signature` is a valid `ES256` (ECDSA/P-256/SHA-256) signature
282/// over exactly `signing_input`, under exactly `key`. In particular:
283///
284/// - `signature` MUST be the 64-byte fixed-width `r || s` of RFC 7518 section 3.4. Reject any
285///   other length, and do NOT also accept the ASN.1 DER form: two encodings of one signature is
286///   signature malleability, and a value a deployment recorded as unique stops being unique.
287/// - `key` must be checked to be ON THE CURVE. That check is what an invalid-curve attack needs to
288///   find missing, and it is the reason this crate hands you a [`PublicJwk`] rather than a parsed
289///   point: the coordinates arrived from a client.
290/// - There is no `false` you may return for an error and no error you may return at all. A
291///   malformed key, a wrong-length signature and a signature that simply does not verify all have
292///   the same and only safe answer, and distinguishing them would only invite a caller to treat
293///   one as recoverable.
294///
295/// # What you may assume about the arguments, which is LESS than it looks
296///
297/// The paragraph above says what `true` may mean. This one says what you are handed, because the
298/// clause "reject any other length" is the one an implementor reads as "the length will be 64".
299///
300/// - **`signature` IS ATTACKER-CONTROLLED BYTES OF ANY LENGTH, INCLUDING ZERO.** It is the third
301///   segment of a JWS somebody sent this server, base64url-decoded, and NOTHING between the wire
302///   and you checks its length. A DPoP proof, an RFC 9101 request object and an RFC 7523 client
303///   assertion all arrive this way; on a 4 kilobyte DPoP header the third segment decodes to
304///   anything from 0 to about 3000 bytes, and a token ending in a bare `.` decodes to an EMPTY
305///   slice, which parses fine and reaches you.
306/// - **`key` HAS PASSED SHAPE VALIDATION AND NOTHING MORE.** [`PublicJwk::from_json`] guarantees
307///   `kty` is `EC`, `crv` is `P-256`, and that `x` and `y` are each exactly 32 base64url-decoded
308///   bytes. It does NOT guarantee the point is on the curve, is not the point at infinity, or is a
309///   point at all: those 64 bytes came from a client. See the on-curve clause above.
310/// - **`signing_input` may be empty and is not required to be UTF-8** for your purposes. Hash the
311///   bytes as given.
312///
313/// # MUST NOT PANIC
314///
315/// **Return `false`. Do not panic, for any input, ever.** Every case above is a `false`: a
316/// zero-length signature, a 63-byte one, a 65-byte one, an off-curve key, an empty signing input.
317///
318/// This is not a formality, and it is the one clause a natural implementation breaks. Having read
319/// "MUST be the 64-byte fixed-width `r || s`", the obvious KMS-shaped verifier begins
320/// `&signature[..64]` or `Signature::from_slice(&signature[..64])`, and both PANIC on a token whose
321/// third segment is empty. The panic unwinds out of this crate and into the host's token endpoint,
322/// where it is reachable unauthenticated by anyone who can send a string with two dots in it. Test
323/// the length before you slice, or match on `signature.try_into()` into a `[u8; 64]`, which cannot
324/// be got wrong.
325///
326/// # Before you deploy one
327///
328/// Run [`crate::signer_conformance`] against it. It carries the RFC 7515 appendix A.3 vector,
329/// which neither side of your deployment produced, and it is the only thing that can tell a
330/// verifier that is right from one that agrees with your signer.
331#[cfg(feature = "jwt")]
332#[cfg_attr(docsrs, doc(cfg(feature = "jwt")))]
333pub trait Es256Verifier: Send + Sync {
334    /// Does `signature` verify over `signing_input` under `key`? See the trait docs for what
335    /// `true` is allowed to mean.
336    fn verify(&self, key: &PublicJwk, signing_input: &[u8], signature: &[u8]) -> bool;
337}
338
339/// The OBJECT-SAFE shadow of [`Es256Signer::sign`], so that [`JwtConfig`] can hold `Arc<dyn ...>`.
340///
341/// Only `sign` needs shadowing: `public_jwk` is called ONCE, on the concrete type, before the
342/// signer is erased, and the `Jwk` it returned is what [`JwtConfig`] keeps.
343///
344/// It exists because those two requirements are in tension in the language rather than in the
345/// design. `async fn` in a trait (return-position `impl Trait`, which is also what [`crate::Storage`]
346/// uses and what sets this crate's 1.75 MSRV floor) is what lets a host write a natural
347/// `async fn sign`, and it is exactly what makes a trait not object safe. Boxing the future in the
348/// PUBLIC trait would push that syntax onto every implementor forever; boxing it here, once, keeps
349/// the public shape and confines the cost to one line.
350///
351/// The cost is ONE allocation per signed access token, paid only by a host that configured RFC 9068
352/// tokens at all. The alternative is making [`JwtConfig`] generic over the signer, which is a third
353/// monomorphization axis on `AuthorizationServer`: MEASURED at 53,548 bytes per additional
354/// `(Storage, Clock)` pair, which is 27% of this crate's entire default binary surface. One
355/// allocation and one indirect call against a signing operation that may be a network round trip is
356/// not measurable; that is.
357#[cfg(feature = "jwt")]
358trait DynEs256Signer: Send + Sync {
359    fn dyn_sign<'a>(
360        &'a self,
361        signing_input: &'a [u8],
362    ) -> Pin<Box<dyn Future<Output = Result<[u8; 64], SignerError>> + Send + 'a>>;
363}
364
365#[cfg(feature = "jwt")]
366impl<T: Es256Signer> DynEs256Signer for T {
367    fn dyn_sign<'a>(
368        &'a self,
369        signing_input: &'a [u8],
370    ) -> Pin<Box<dyn Future<Output = Result<[u8; 64], SignerError>> + Send + 'a>> {
371        Box::pin(self.sign(signing_input))
372    }
373}
374
375/// A P-256 signing key plus the `kid` that names it.
376///
377/// The `kid` is what makes rotation possible: an AS publishes the old and new public keys in the
378/// same JWKS, signs new tokens under the new `kid`, and retires the old entry once every token
379/// signed under it has expired (RFC 7517 section 4.5; RFC 7515 section 4.1.4). Without a `kid` a
380/// verifier must trial every advertised key and rotation becomes a guessing game.
381///
382/// THE BUILT-IN BACKEND, behind `jwt-p256`. It is an [`Es256Signer`] like any other; what makes it
383/// the default choice is only that the key is a scalar in this process, which is the right answer
384/// for most deployments and the wrong one for a deployment whose policy says the key may not be.
385#[cfg(feature = "jwt-p256")]
386#[cfg_attr(docsrs, doc(cfg(feature = "jwt-p256")))]
387#[derive(Clone)]
388pub struct EcdsaP256Key {
389    kid: String,
390    signing: SigningKey,
391}
392
393#[cfg(feature = "jwt-p256")]
394impl EcdsaP256Key {
395    /// Load from a raw 32 byte big-endian private scalar (SEC 1: `1 <= d <= n-1`; out-of-range and
396    /// wrong-length input is rejected rather than reduced, because a silently reduced key is a key
397    /// the host did not choose).
398    pub fn from_scalar_bytes(kid: impl Into<String>, scalar: &[u8]) -> Result<Self, KeyError> {
399        // `SecretKey::from_slice` accepts SHORT inputs and left-pads them, so a truncated key file
400        // would load as a valid but different (and much weaker) key. A P-256 scalar is 32 bytes;
401        // anything else is a caller mistake worth failing loudly on.
402        if scalar.len() != 32 {
403            return Err(KeyError(
404                "a P-256 private scalar is exactly 32 bytes".into(),
405            ));
406        }
407        let secret = SecretKey::from_slice(scalar)
408            .map_err(|_| KeyError("not a valid P-256 private scalar".into()))?;
409        Ok(EcdsaP256Key {
410            kid: kid.into(),
411            signing: SigningKey::from(&secret),
412        })
413    }
414
415    /// Load from a PKCS#8 (RFC 5208) `PrivateKeyInfo` DER document, the format `openssl pkcs8`
416    /// and most KMS exports emit.
417    ///
418    /// Behind `jwt-pkcs8` rather than `jwt`, for the DEPENDENCY rather than for the bytes: the
419    /// split takes the `pkcs8` crate off a `--features jwt` tree. It does NOT save a host any
420    /// linked size, because a build with the feature on and these two constructors never called
421    /// measures byte for byte identical to one with it off; LTO deletes what nothing reaches. A
422    /// host whose key material arrives as a raw scalar uses [`EcdsaP256Key::from_scalar_bytes`]
423    /// and pays nothing either way.
424    #[cfg(feature = "jwt-pkcs8")]
425    #[cfg_attr(docsrs, doc(cfg(feature = "jwt-pkcs8")))]
426    pub fn from_pkcs8_der(kid: impl Into<String>, der: &[u8]) -> Result<Self, KeyError> {
427        let secret = SecretKey::from_pkcs8_der(der)
428            .map_err(|_| KeyError("not a valid PKCS#8 P-256 private key".into()))?;
429        Ok(EcdsaP256Key {
430            kid: kid.into(),
431            signing: SigningKey::from(&secret),
432        })
433    }
434
435    /// A fresh random key. For TESTS and for a host's own key-provisioning step: this crate never
436    /// calls it, because a key that materialises at startup is a key nobody is managing. Whatever
437    /// this returns must be exported ([`EcdsaP256Key::to_pkcs8_der`]) and stored by the host, or
438    /// the tokens signed with it die with the process.
439    ///
440    /// # Panics
441    /// If the OS refuses randomness, which the rest of this crate also treats as unrecoverable.
442    pub fn generate(kid: impl Into<String>) -> Self {
443        let kid = kid.into();
444        loop {
445            let mut buf = [0u8; 32];
446            getrandom::fill(&mut buf).expect("OS randomness for OAuth artifacts");
447            // Rejection sampling: a uniform 32 byte string is occasionally outside [1, n-1] for
448            // P-256's order n. Reducing it instead would bias the key; the probability of a redraw
449            // is about 2^-32, so this loop terminates immediately in practice.
450            if let Ok(key) = Self::from_scalar_bytes(kid.clone(), &buf) {
451                return key;
452            }
453        }
454    }
455
456    /// Export as PKCS#8 DER. PRIVATE KEY MATERIAL: the caller is responsible for where this goes.
457    /// Present so a host can persist a key it generated; nothing in this crate calls it.
458    ///
459    /// Behind `jwt-pkcs8`, for the reason on [`EcdsaP256Key::from_pkcs8_der`].
460    #[cfg(feature = "jwt-pkcs8")]
461    #[cfg_attr(docsrs, doc(cfg(feature = "jwt-pkcs8")))]
462    pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, KeyError> {
463        let doc = SecretKey::from(&self.signing)
464            .to_pkcs8_der()
465            .map_err(|_| KeyError("PKCS#8 encoding failed".into()))?;
466        Ok(doc.as_bytes().to_vec())
467    }
468
469    /// The key identifier published in the JWKS and in every token header.
470    pub fn kid(&self) -> &str {
471        &self.kid
472    }
473
474    /// The PUBLIC half as an RFC 7517 JWK. There is no method that produces a JWK containing `d`,
475    /// which is the point: the private parameter cannot be published by accident.
476    pub fn public_jwk(&self) -> Jwk {
477        let point = self.signing.verifying_key().to_encoded_point(false);
478        // Uncompressed SEC 1 form guarantees both affine coordinates are present and each is the
479        // FIXED 32 byte width RFC 7518 section 6.2.1.2 requires (left-padded, never trimmed: a
480        // trimmed coordinate is the classic JWK interoperability bug).
481        let x = point.x().expect("uncompressed point has an x coordinate");
482        let y = point.y().expect("uncompressed point has a y coordinate");
483        Jwk {
484            kty: "EC",
485            crv: "P-256",
486            x: URL_SAFE_NO_PAD.encode(x),
487            y: URL_SAFE_NO_PAD.encode(y),
488            kid: self.kid.clone(),
489            use_: "sig",
490            alg: "ES256",
491        }
492    }
493
494    /// Sign `message` with ECDSA/P-256/SHA-256, returning the fixed-width `r || s` form RFC 7518
495    /// section 3.4 mandates for `ES256` (64 bytes; NOT the DER form OpenSSL emits by default).
496    fn sign_es256(&self, message: &[u8]) -> Result<[u8; 64], JwtError> {
497        let signature: Signature = self
498            .signing
499            .try_sign(message)
500            .map_err(|_| JwtError("ECDSA signing failed".into()))?;
501        let bytes = signature.to_bytes();
502        let mut out = [0u8; 64];
503        out.copy_from_slice(&bytes);
504        Ok(out)
505    }
506}
507
508#[cfg(feature = "jwt-p256")]
509impl fmt::Debug for EcdsaP256Key {
510    /// Redacted on purpose: `ServerConfig` derives `Debug`, and a host that logs its config must
511    /// not thereby log its signing key.
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        f.debug_struct("EcdsaP256Key")
514            .field("kid", &self.kid)
515            .field("private_key", &"<redacted>")
516            .finish()
517    }
518}
519
520#[cfg(feature = "jwt-p256")]
521impl PartialEq for EcdsaP256Key {
522    /// Equality over the PUBLIC identity only (kid plus public point). Two handles to the same key
523    /// compare equal without any comparison touching the secret scalar.
524    fn eq(&self, other: &Self) -> bool {
525        self.kid == other.kid
526            && self
527                .signing
528                .verifying_key()
529                .to_encoded_point(false)
530                .as_bytes()
531                == other
532                    .signing
533                    .verifying_key()
534                    .to_encoded_point(false)
535                    .as_bytes()
536    }
537}
538
539#[cfg(feature = "jwt-p256")]
540impl Eq for EcdsaP256Key {}
541
542/// The built-in backend's signing half. `sign` is async by the trait and does no I/O here: the key
543/// is in this process, so the future is ready on its first poll and there is no suspension point
544/// for the token path to pay for.
545#[cfg(feature = "jwt-p256")]
546impl Es256Signer for EcdsaP256Key {
547    fn sign(
548        &self,
549        signing_input: &[u8],
550    ) -> impl Future<Output = Result<[u8; 64], SignerError>> + Send {
551        // Computed BEFORE the async block, so nothing borrows `signing_input` across a suspension
552        // point that does not exist. The future this returns owns a `Result` and nothing else.
553        let signed = self.sign_es256(signing_input).map_err(|e| SignerError(e.0));
554        async move { signed }
555    }
556
557    fn public_jwk(&self) -> Jwk {
558        EcdsaP256Key::public_jwk(self)
559    }
560}
561
562/// The built-in backend's verifying half: ES256 (ECDSA/P-256/SHA-256) over `p256`.
563///
564/// A unit struct rather than a function so it can be INSTALLED, which is what makes a host's own
565/// verifier able to replace it. `AuthorizationServer` falls back to this one when the host installs
566/// none and `jwt-p256` is compiled in, which is why enabling that feature reproduces exactly the
567/// behaviour every consumer had before the seam existed.
568#[cfg(feature = "jwt-p256")]
569#[cfg_attr(docsrs, doc(cfg(feature = "jwt-p256")))]
570#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
571pub struct P256Verifier;
572
573#[cfg(feature = "jwt-p256")]
574impl Es256Verifier for P256Verifier {
575    fn verify(&self, key: &PublicJwk, signing_input: &[u8], signature: &[u8]) -> bool {
576        verify_es256(key, signing_input, signature)
577    }
578}
579
580/// One RFC 7517 JWK: the PUBLIC parameters of an EC P-256 signing key and nothing else.
581///
582/// The fields are the complete set this crate ever emits. There is deliberately no `d`
583/// (RFC 7517 section 6.2.2.1, the private key parameter) and no way to add one.
584///
585/// THE FIELDS ARE PUBLIC, unlike [`PublicJwk`]'s, and the difference is what each type is for: this
586/// one is what a HOST FILLS IN. [`Es256Signer::public_jwk`] returns it, so every host implementing
587/// that seam over a KMS or a PKCS#11 token has to build one by hand, and sealing it would mean
588/// shipping a fallible constructor for a value the host already knows is correct.
589///
590/// What that costs is worth stating where the literal gets written: [`Jwk::to_public_jwk`] does not
591/// revalidate, so a `Jwk` literal whose `x` and `y` are not 32-byte base64url produces a
592/// [`PublicJwk`] that [`PublicJwk::from_json`] would have refused. It fails CLOSED — nothing
593/// verifies under such a key, so the effect is a signer whose signatures never check out and, on the
594/// DPoP path, a token bound to a thumbprint nobody can present — but it fails at verification time
595/// rather than here. [`crate::signer_conformance`] is the check that catches it: it verifies a real
596/// signature against the key the signer publishes, which is exactly the mismatch this shape allows.
597/// Run it against any signer before a deployment trusts it.
598#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
599pub struct Jwk {
600    /// Key type; always `EC` (RFC 7518 section 6.2).
601    pub kty: &'static str,
602    /// Curve; always `P-256`.
603    pub crv: &'static str,
604    /// Base64url (unpadded) x coordinate, fixed 32 byte width.
605    pub x: String,
606    /// Base64url (unpadded) y coordinate, fixed 32 byte width.
607    pub y: String,
608    /// The key identifier, matching the `kid` of every token signed with it.
609    pub kid: String,
610    /// Public key use; always `sig` (RFC 7517 section 4.2).
611    #[serde(rename = "use")]
612    pub use_: &'static str,
613    /// The algorithm this key is for; always `ES256` (RFC 7517 section 4.4).
614    pub alg: &'static str,
615}
616
617impl Jwk {
618    /// The same key in the VERIFYING shape.
619    ///
620    /// [`Jwk`] exists to be SERIALIZED, so every member this crate fixes is a `&'static str`;
621    /// [`PublicJwk`] is parsed from attacker-controlled JSON and is therefore a different type on
622    /// purpose (see its own docs). This is the one direction that is always safe, because these
623    /// parameters were produced here rather than received: a `Jwk` a signer published is by
624    /// construction `EC` / `P-256` with 32-byte coordinates.
625    ///
626    /// Used by [`crate::signer_conformance`] to check a signer's output against the key that
627    /// signer publishes, which is the one check that catches a `public_jwk()` belonging to some
628    /// other key.
629    ///
630    /// It REVALIDATES NOTHING, and cannot usefully: it is infallible, so there is no channel for a
631    /// refusal, and making it fallible would push a `Result` onto every caller for a value they
632    /// produced themselves. "Produced here" is doing the work, and [`Jwk`]'s own docs say plainly
633    /// what it means for a host that hand-builds one with coordinates that are not 32 bytes: this
634    /// hands back a [`PublicJwk`] that [`PublicJwk::from_json`] would have refused, which fails
635    /// closed at verification rather than being caught here.
636    pub fn to_public_jwk(&self) -> PublicJwk {
637        PublicJwk {
638            kty: self.kty.to_string(),
639            crv: self.crv.to_string(),
640            x: self.x.clone(),
641            y: self.y.clone(),
642            kid: Some(self.kid.clone()),
643        }
644    }
645}
646
647/// An RFC 7517 section 5 JWK Set: what the host serves at its `jwks_uri`.
648#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
649pub struct Jwks {
650    /// The advertised keys.
651    pub keys: Vec<Jwk>,
652}
653
654/// The `aud` claim, which RFC 9068 section 2.2 requires and RFC 7519 section 4.1.3 allows to be
655/// either a single string or an array of strings. Serialized untagged so one audience is a plain
656/// string, which is what most resource servers expect.
657#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
658#[serde(untagged)]
659pub enum Audience {
660    /// Exactly one audience.
661    One(String),
662    /// Several audiences.
663    Many(Vec<String>),
664}
665
666impl Audience {
667    /// Whether this actually names somebody.
668    ///
669    /// AN EMPTY `aud` IS NOT A HARMLESS ONE. `Many(vec![])` serializes untagged as the literal
670    /// `"aud": []`, and a resource server whose check reads "if `aud` is present and non-empty it
671    /// must contain me" treats that as NO RESTRICTION: the fail-open reading of the one claim the
672    /// authorization server believed it was constraining. `One(String::new())` is the degenerate
673    /// form and fails the other way, a token valid nowhere, which is an outage an operator cannot
674    /// see in their configuration. An empty ELEMENT of a list is the first case wearing the second
675    /// one's clothes, so it counts against the whole value.
676    ///
677    /// This is what makes [`AccessTokenClaims`]'s "a missing required claim should be impossible to
678    /// express" true rather than aspirational: the type could always express it, so the check has
679    /// to stand where the bytes are produced. See `JwtConfig::signing_input` (crate-private: it is
680    /// the step [`JwtConfig::sign_access_token`] runs before it hands anything to a signer).
681    pub fn names_a_resource_server(&self) -> bool {
682        match self {
683            Audience::One(one) => !one.is_empty(),
684            Audience::Many(many) => !many.is_empty() && many.iter().all(|a| !a.is_empty()),
685        }
686    }
687}
688
689/// The RFC 9068 section 2.2 claim set. Every field here except `scope` is REQUIRED by the RFC, so
690/// they are not `Option`: a missing required claim should be impossible to express, not merely
691/// discouraged.
692///
693/// TYPES CANNOT CARRY THAT ALONE, and `aud` is where it showed. Dropping the `Option` stops a claim
694/// being ABSENT; it does not stop it being EMPTY, and `Audience::Many(vec![])` serialized untagged
695/// as the literal `"aud": []`, which a resource server that checks `aud` only when it is non-empty
696/// reads as no restriction at all. So the promise above is kept by a check as well as by a shape:
697/// `JwtConfig::signing_input`, the crate-private step behind
698/// [`JwtConfig::sign_access_token`], refuses to sign a claim set whose audience names nobody. See
699/// [`Audience::names_a_resource_server`].
700#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
701/// `#[non_exhaustive]`: `rar` adds `authorization_details`, either sender-constraining feature
702/// adds `cnf`, `token-exchange` adds `act` and `consent` adds `auth_time` and `acr`, and the `cnf`
703/// doc below records what it cost to get that gate wrong once already.
704/// A host DOES construct this, because [`JwtConfig::sign_access_token`] takes one, so
705/// [`AccessTokenClaims::new`] takes the claims RFC 9068 section 2.2 makes REQUIRED and leaves the
706/// conditional ones as public fields, which is the same split the paragraph above describes.
707#[non_exhaustive]
708pub struct AccessTokenClaims {
709    /// The authorization server's issuer identifier.
710    pub iss: String,
711    /// Expiry, seconds since the Unix epoch.
712    pub exp: u64,
713    /// The resource server(s) this token is for.
714    pub aud: Audience,
715    /// The subject. For a token with no resource owner, RFC 9068 section 2.2 directs the AS to use
716    /// the `client_id` here.
717    pub sub: String,
718    /// The client the token was issued to (RFC 8693 section 4.3 claim, required by RFC 9068).
719    pub client_id: String,
720    /// Issuance instant, seconds since the Unix epoch.
721    pub iat: u64,
722    /// A unique identifier for this token; also the AS-side record key.
723    pub jti: String,
724    /// Space-delimited granted scope, omitted when empty (RFC 9068 section 2.2.3 makes it
725    /// conditional, not required).
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub scope: Option<String>,
728    /// RFC 9396 section 9.1: the authorization details this token carries, as a top-level
729    /// claim, so a resource server holding the JWT can read what the token authorizes
730    /// without calling introspection for it.
731    ///
732    /// Omitted rather than sent empty when the grant carried none, exactly as `scope` is: a
733    /// claim present and empty is a statement about the token, and the truth here is that
734    /// there is nothing to state.
735    #[cfg(feature = "rar")]
736    #[serde(
737        default,
738        skip_serializing_if = "crate::rar::AuthorizationDetails::is_empty"
739    )]
740    pub authorization_details: crate::rar::AuthorizationDetails,
741    /// RFC 9470 section 6.1 with RFC 9068 section 2.2.1: when the resource owner behind this token
742    /// authenticated, as seconds since the Unix epoch (OpenID Connect Core section 2 `auth_time`).
743    ///
744    /// This is the claim an offline resource server measures a `max_age` against. Section 6 of RFC
745    /// 9470 has exactly two subsections because a token reaches a resource server in exactly two
746    /// ways: 6.2 is RFC 7662 introspection, which is all an OPAQUE token has, and 6.1 is this,
747    /// which is all a resource server verifying signatures locally ever sees. Reporting the
748    /// authentication only through introspection left the deployment step-up is aimed at — the
749    /// resource server that sent the section 3 challenge and validates the answer offline — with
750    /// nothing to check but the client's word.
751    ///
752    /// Present exactly when the host REPORTED an authentication for the grant (see
753    /// [`crate::consent::Authentication`]), and omitted rather than sent as `null` when it did
754    /// not, for the reason `cnf` below is: a member present and null reads to a careless resource
755    /// server as a freshness it has already checked.
756    ///
757    /// Answered from the SAME stored report, through the same conversion, that RFC 7662
758    /// introspection answers from, so the two channels cannot state different things about one
759    /// token.
760    #[cfg(feature = "consent")]
761    #[cfg_attr(docsrs, doc(cfg(feature = "consent")))]
762    #[serde(skip_serializing_if = "Option::is_none")]
763    pub auth_time: Option<u64>,
764    /// RFC 9470 section 6.1 with RFC 9068 section 2.2.1: the authentication context class the host
765    /// reported for the grant (OpenID Connect Core section 2 `acr`). Opaque to this crate; see
766    /// [`crate::consent::Authentication::acr`].
767    ///
768    /// Absent when the host reported an authentication but no class, which is a different
769    /// statement from reporting a class of `""`: the first is "we did not say", and only the
770    /// second would claim a class was satisfied.
771    #[cfg(feature = "consent")]
772    #[cfg_attr(docsrs, doc(cfg(feature = "consent")))]
773    #[serde(skip_serializing_if = "Option::is_none")]
774    pub acr: Option<String>,
775    /// RFC 7800 `cnf`, which RFC 9068 section 2.2.1 lists as the claim carrying how a token is
776    /// sender constrained. RFC 9449 section 6.1 puts the DPoP key thumbprint here as `jkt` and
777    /// RFC 8705 section 3.1 puts the certificate thumbprint here as `x5t#S256`.
778    ///
779    /// Gated on EITHER mechanism, and this is load bearing rather than tidiness. RFC 9449
780    /// section 6 requires that a resource server be able to "reliably identify whether an access
781    /// token is DPoP-bound"; for a signed token verified locally, this claim is the only thing
782    /// that says so. Gated on `mtls` alone, a `jwt` + `dpop` build (which is the deployment DPoP
783    /// exists for: resource servers verifying signatures rather than calling introspection) issued
784    /// tokens whose binding was invisible, so a leaked token was accepted as a plain bearer token
785    /// by the servers least able to notice.
786    ///
787    /// Absent for an ordinary bearer token, and absent from the claim set entirely in a build with
788    /// neither mechanism.
789    #[cfg(any(feature = "dpop", feature = "mtls"))]
790    #[serde(skip_serializing_if = "Option::is_none")]
791    pub cnf: Option<crate::token::Confirmation>,
792    /// RFC 8693 section 4.1 `act`: who authority was delegated TO, present exactly when this token
793    /// came out of a DELEGATION token exchange.
794    ///
795    /// RFC 9068 section 2.2.3 allows claims beyond the required set, and section 4.1 of RFC 8693
796    /// defines this one as a claim IN the issued token, which is what makes it belong here rather
797    /// than only on the stored record.
798    ///
799    /// Both routes are needed and the reason is the two token formats, not belt and braces. A JWT
800    /// is typically validated OFFLINE by a resource server that never calls introspection, so a
801    /// delegation recorded only on this server's record is invisible to it; an OPAQUE token is the
802    /// mirror image, carrying nothing itself and reachable only through RFC 7662. Persisting the
803    /// claim without also putting it here would have moved the deficiency from one deployment
804    /// shape to the other. See [`crate::token_exchange`]'s module docs.
805    ///
806    /// Omitted rather than sent as `null`, like `cnf` above: a member that is present and null
807    /// invites a careless reader to treat it as answered.
808    #[cfg(feature = "token-exchange")]
809    #[cfg_attr(docsrs, doc(cfg(feature = "token-exchange")))]
810    #[serde(skip_serializing_if = "Option::is_none")]
811    pub act: Option<crate::token_exchange::ActClaim>,
812}
813
814impl AccessTokenClaims {
815    /// The seven claims RFC 9068 section 2.2 makes REQUIRED, in the order the section lists them,
816    /// and nothing else.
817    ///
818    /// `scope` is section 2.2.3 CONDITIONAL and the other two are feature-gated extensions, so all
819    /// three are public fields set on the returned value. That is the same distinction the struct
820    /// doc draws between a claim that cannot be missing and one that can: a required claim is an
821    /// argument the caller cannot forget, and a conditional one is a decision the caller makes.
822    #[allow(clippy::too_many_arguments)]
823    pub fn new(
824        iss: impl Into<String>,
825        exp: u64,
826        aud: Audience,
827        sub: impl Into<String>,
828        client_id: impl Into<String>,
829        iat: u64,
830        jti: impl Into<String>,
831    ) -> Self {
832        AccessTokenClaims {
833            iss: iss.into(),
834            exp,
835            aud,
836            sub: sub.into(),
837            client_id: client_id.into(),
838            iat,
839            jti: jti.into(),
840            scope: None,
841            #[cfg(feature = "rar")]
842            authorization_details: crate::rar::AuthorizationDetails::none(),
843            // The required-set constructor: what the host reported about the login is not one of
844            // the seven, and RFC 9470 s6.1 has this server state it only when it has one.
845            #[cfg(feature = "consent")]
846            auth_time: None,
847            #[cfg(feature = "consent")]
848            acr: None,
849            #[cfg(any(feature = "dpop", feature = "mtls"))]
850            cnf: None,
851            // The required-set constructor: a delegation is not one of the seven.
852            #[cfg(feature = "token-exchange")]
853            act: None,
854        }
855    }
856}
857
858/// Everything needed to issue RFC 9068 access tokens: the ACTIVE signing key, any RETIRED keys
859/// still being published so tokens already signed under them keep verifying, the audience, and the
860/// URL the host serves the key set from.
861///
862/// # Rotation
863///
864/// Signing always uses the active key, and its `kid` goes on every token (RFC 7515 section 4.1.4).
865/// [`JwtConfig::rotate_to`] promotes a new key and RETIRES the previous one: the retired key's
866/// PUBLIC half stays in [`JwtConfig::jwks`], so a resource server that fetches the key set can
867/// still select and verify a token minted a minute before the swap. Without that, rotation would
868/// invalidate every live access token at the instant of the swap, which is why an AS that can hold
869/// only one key has no rotation story at all, scheduled or on compromise.
870///
871/// Retired keys are dropped by the host, explicitly, with
872/// [`JwtConfig::forget_retired_key_breaking_its_live_tokens`]. There is deliberately NO timer here:
873/// this crate has no background tasks by design (see the crate doc's "Zero cost until enabled"),
874/// and the host is the only party that knows its own [`crate::ServerConfig::access_token_ttl`],
875/// which is the number that decides when dropping is safe.
876///
877/// # Rotating a key that lives in a KMS
878///
879/// [`JwtConfig::rotate_to`] is the ONLY thing that rotates. Rotating in the KMS alone leaves this
880/// process advertising a cached public half that no longer signs, and every token the deployment
881/// issues then fails verification against its own published JWKS, silently. See
882/// [`Es256Signer::public_jwk`].
883///
884/// `Clone` shares the signer rather than duplicating it (`Arc`), and `PartialEq` compares the
885/// PUBLISHED IDENTITY: the active and retired JWKs, the audience and the `jwks_uri`. There is no
886/// private scalar left to compare once the key may be outside this process, and comparing handles
887/// would make two configurations over one KMS key unequal for no reason a host could act on.
888#[derive(Clone)]
889pub struct JwtConfig {
890    /// The host's ES256 backend, which may be a key in this process or a handle to one in a KMS.
891    ///
892    /// `Arc<dyn _>` and not a generic parameter. Making [`JwtConfig`] generic would put a THIRD
893    /// monomorphization axis on `AuthorizationServer`, and the second one is MEASURED at 53,548
894    /// bytes per additional `(Storage, Clock)` pair, 27% of this crate's whole default binary
895    /// surface. One indirect call against a signing operation that may be a network round trip is
896    /// not measurable; that is.
897    signer: Arc<dyn DynEs256Signer>,
898    /// The ACTIVE key's public half, read from the signer ONCE, here.
899    ///
900    /// Cached rather than re-asked per call, and that is the other half of the contract
901    /// [`Es256Signer::public_jwk`] states: the JWKS document is a public, unauthenticated,
902    /// cacheable thing any client may poll, and a signer that reaches a KMS to answer would put a
903    /// network call behind it. It also keeps [`JwtConfig::kid`] able to return a `&str`.
904    active: Jwk,
905    /// The PUBLIC halves of previously active keys, most recently retired first.
906    ///
907    /// Public halves, not signers, and that is the point: a retired key must never sign again, and
908    /// dropping the SIGNER at retirement makes that structural rather than a promise the code
909    /// merely keeps today. With the private half possibly in a KMS this matters more, not less:
910    /// the handle is what could still be called, and there is no handle left. It is also the
911    /// cheaper representation, which matters because [`JwtConfig`] sits behind the box in
912    /// [`AccessTokenFormat::Jwt`] precisely to keep key material out of every
913    /// [`crate::ServerConfig`].
914    retired: Vec<Jwk>,
915    audience: Audience,
916    jwks_uri: Option<String>,
917    /// The base64url form of the JOSE protected header, PRECOMPUTED.
918    ///
919    /// It is a function of the active key's `kid` and two constants, so it is fixed for the life of
920    /// a `JwtConfig` and changes only at [`JwtConfig::rotate_to`]. Building it per token cost a
921    /// `serde_json::to_vec` and a base64 `String` on every access token this server signs, to
922    /// produce the same bytes every time. MEASURED on one `client_credentials` issuance under
923    /// `--features jwt`: 28 allocations / 4767 bytes before, 25 / 4560 after.
924    encoded_header: Box<str>,
925}
926
927/// The base64url form of the RFC 7515 s4.1 protected header for `kid`.
928///
929/// Built by hand rather than through `serde_json`, and that is not an optimisation: it is what
930/// makes precomputing this INFALLIBLE. `serde_json::to_vec` returns a `Result`, which would make
931/// [`JwtConfig::new`] and [`JwtConfig::rotate_to`] fallible (or force an `expect` into a library
932/// that must not panic on a host's input) for an error that cannot occur. The header has exactly
933/// three members, two of them constants, and the third is a string; the only work is escaping it.
934fn encoded_jose_header(kid: &str) -> Box<str> {
935    // RFC 9068 s2.1 fixes `typ`; `alg` is a constant here, so no code path in this crate can emit
936    // an unsigned access token. Member order matches what `JoseHeader`'s derive produced, so the
937    // bytes on the wire are unchanged by this precomputation.
938    let mut json = String::with_capacity(40 + kid.len());
939    json.push_str(r#"{"alg":"ES256","typ":"at+jwt","kid":""#);
940    // RFC 8259 s7: a JSON string escapes the quote, the backslash, and everything below 0x20.
941    // Nothing else needs escaping, and in particular a `kid` is not required to be ASCII.
942    for c in kid.chars() {
943        match c {
944            '"' => json.push_str("\\\""),
945            '\\' => json.push_str("\\\\"),
946            '\n' => json.push_str("\\n"),
947            '\r' => json.push_str("\\r"),
948            '\t' => json.push_str("\\t"),
949            '\u{8}' => json.push_str("\\b"),
950            '\u{c}' => json.push_str("\\f"),
951            c if (c as u32) < 0x20 => json.push_str(&format!("\\u{:04x}", c as u32)),
952            c => json.push(c),
953        }
954    }
955    json.push_str(r#""}"#);
956    URL_SAFE_NO_PAD.encode(json).into_boxed_str()
957}
958
959impl JwtConfig {
960    /// Configure signing for one audience. The audience is REQUIRED (RFC 9068 section 2.2) and has
961    /// no default: only the deployment knows which resource server a token is meant for, and a
962    /// guessed `aud` is a token that is valid somewhere nobody intended.
963    /// `signer` is anything implementing [`Es256Signer`]: [`EcdsaP256Key`] under `jwt-p256`, an
964    /// `Arc` of one shared with another configuration, or the host's own KMS-backed type. Its
965    /// public half is read HERE, once, and never again; see [`Es256Signer::public_jwk`] for what
966    /// that requires of an implementor and for why rotation must come back through
967    /// [`JwtConfig::rotate_to`].
968    pub fn new(signer: impl Es256Signer + 'static, audience: impl Into<String>) -> Self {
969        let active = signer.public_jwk();
970        JwtConfig {
971            encoded_header: encoded_jose_header(&active.kid),
972            active,
973            signer: Arc::new(signer),
974            // A brand new configuration has retired nothing. The single-key deployment, which is
975            // most of them, never touches anything below and keeps exactly the API it had.
976            retired: Vec::new(),
977            audience: Audience::One(audience.into()),
978            jwks_uri: None,
979        }
980    }
981
982    /// Promote `new_active` to the signing key and RETIRE the current one.
983    ///
984    /// After this call: new tokens are signed under `new_active`'s `kid`, and the previous key's
985    /// public half is still published by [`JwtConfig::jwks`], so tokens signed under it keep
986    /// verifying until the host drops it. That is the whole mechanism RFC 7517 section 4.5 and RFC
987    /// 7515 section 4.1.4 exist to enable: the token names its key, so a verifier selects rather
988    /// than trials, and two generations of key can be live at once.
989    ///
990    /// Rotating to a `kid` that is already published REPLACES that entry rather than publishing
991    /// the name twice, because two JWKs sharing a `kid` make selection ambiguous, which is the one
992    /// thing `kid` exists to prevent. A host that reuses a `kid` for a genuinely different key is
993    /// making a mistake this crate cannot detect, and the RFC's advice is simply to not do that.
994    ///
995    /// THE SIGNER IS DROPPED, not stored: what is retained of the outgoing key is its public half
996    /// and nothing else, so a retired key cannot sign again by construction. For a KMS-backed
997    /// signer this is also the ONLY correct way to rotate; rotating in the KMS while this process
998    /// holds the old cached public half is silent breakage (see [`Es256Signer::public_jwk`]).
999    pub fn rotate_to(mut self, new_active: impl Es256Signer + 'static) -> Self {
1000        let retiring = std::mem::replace(&mut self.active, new_active.public_jwk());
1001        // The previous signer is dropped by this assignment. There is deliberately nowhere else it
1002        // is written down.
1003        self.signer = Arc::new(new_active);
1004        // The header names the ACTIVE key, so it is rebuilt exactly here and nowhere else.
1005        self.encoded_header = encoded_jose_header(&self.active.kid);
1006        let active_kid = self.active.kid.as_str();
1007        // A kid appears at most once in the published set: any older entry sharing a name with the
1008        // key just retired, or with the new active key, goes.
1009        self.retired
1010            .retain(|jwk| jwk.kid != retiring.kid && jwk.kid != active_kid);
1011        if retiring.kid != active_kid {
1012            // Most recently retired FIRST: it is the one with the most tokens still alive, so it
1013            // is the one a verifier is most likely to need after the active key itself.
1014            self.retired.insert(0, retiring);
1015        }
1016        self
1017    }
1018
1019    /// The `kid`s of the retired keys still being published, most recently retired first.
1020    ///
1021    /// This is what a host consults to decide what it may drop: a key retired longer ago than
1022    /// [`crate::ServerConfig::access_token_ttl`] has no live tokens left.
1023    pub fn retired_kids(&self) -> impl Iterator<Item = &str> {
1024        self.retired.iter().map(|jwk| jwk.kid.as_str())
1025    }
1026
1027    /// Stop publishing the retired key named `kid`. THIS BREAKS EVERY UNEXPIRED TOKEN SIGNED UNDER
1028    /// IT: once the key leaves the JWKS, a resource server has nothing to verify those tokens
1029    /// with, and the client sees them fail mid-session rather than at a renewal boundary.
1030    ///
1031    /// The rule: keep a key retired for AT LEAST [`crate::ServerConfig::access_token_ttl`] after
1032    /// the [`JwtConfig::rotate_to`] that retired it, plus whatever the deployment's resource
1033    /// servers cache the JWKS for, since a cached copy is not refetched the moment this changes.
1034    /// Only after that is every token signed under it certain to have expired on its own.
1035    ///
1036    /// The one time to call this SOONER is a key compromise, where the point is exactly to
1037    /// invalidate those tokens, and the breakage is the goal rather than the cost.
1038    ///
1039    /// Naming a `kid` that is not retired (including the ACTIVE `kid`) does nothing. Letting a host
1040    /// drop its own signing key by naming it would leave an AS signing with a key it does not
1041    /// publish: no token it issues would verify anywhere, which is strictly worse than the state
1042    /// the host was trying to leave.
1043    // NO `#[must_use]`, and that is a decision about the whole crate rather than about this one
1044    // method: see `tests/host_api_shape.rs`. Every builder here CONSUMES its receiver, so dropping
1045    // the result moves the configuration away and the borrow checker refuses the next use of it.
1046    // The attribute would add only the case where the entire expression is discarded, which is
1047    // dead code rather than a misconfiguration. It sat here alone, on one of twenty-nine such
1048    // builders, which taught a reader a rule the other twenty-eight did not follow.
1049    pub fn forget_retired_key_breaking_its_live_tokens(mut self, kid: &str) -> Self {
1050        self.retired.retain(|jwk| jwk.kid != kid);
1051        self
1052    }
1053
1054    /// Configure signing for several audiences (RFC 7519 section 4.1.3 array form).
1055    ///
1056    /// FALLIBLE, unlike every other builder here, and the one thing it refuses is an audience that
1057    /// names nobody: an empty list, or a list with an empty member. It used to accept both and mint
1058    /// `"aud": []` on every token the configuration signed, which
1059    /// [`Audience::names_a_resource_server`] explains is the FAIL-OPEN reading of the claim to a
1060    /// resource server that checks `aud` only when it is non-empty. A `Result` costs a deployment
1061    /// nothing because this is called once, at construction, on a value the operator wrote down.
1062    ///
1063    /// [`JwtConfig::new`] stays infallible and takes one audience, so the same mistake in its
1064    /// degenerate form (an empty string) is caught at signing time instead; see
1065    /// [`Audience::names_a_resource_server`] for why both doors need closing.
1066    pub fn with_audiences(mut self, audiences: Vec<String>) -> Result<Self, JwtError> {
1067        let audience = Audience::Many(audiences);
1068        if !audience.names_a_resource_server() {
1069            return Err(JwtError(
1070                "aud must name at least one resource server, and no member may be empty".into(),
1071            ));
1072        }
1073        self.audience = audience;
1074        Ok(self)
1075    }
1076
1077    /// The URL at which the host serves [`JwtConfig::jwks`]. This crate does not fetch or serve
1078    /// it; it exists so the RFC 8414 metadata document can advertise `jwks_uri` exactly when
1079    /// tokens are actually signed, and never when they are opaque.
1080    pub fn with_jwks_uri(mut self, uri: impl Into<String>) -> Self {
1081        self.jwks_uri = Some(uri.into());
1082        self
1083    }
1084
1085    /// The configured `jwks_uri`, if the host set one.
1086    pub fn jwks_uri(&self) -> Option<&str> {
1087        self.jwks_uri.as_deref()
1088    }
1089
1090    /// The signing key's identifier.
1091    pub fn kid(&self) -> &str {
1092        &self.active.kid
1093    }
1094
1095    /// The RFC 7517 key set to serve: public parameters only, ACTIVE key first, then every retired
1096    /// key most recently retired first.
1097    ///
1098    /// Publishing the retired keys is what makes rotation non-destructive: a resource server that
1099    /// fetched this document after the swap can still select, by `kid`, the key a token minted
1100    /// before the swap was signed under (RFC 7515 section 4.1.4).
1101    ///
1102    /// RFC 7517 section 5 places no ordering requirement on `keys`, so the order here is chosen
1103    /// rather than mandated: active first means a verifier that ignores `kid` and takes the first
1104    /// `alg`-compatible key is right for the tokens it will mostly be handed. Such a verifier is
1105    /// wrong in general, which is why `kid` exists, but the ordering costs nothing and the failure
1106    /// mode it avoids is real.
1107    pub fn jwks(&self) -> Jwks {
1108        let mut keys = Vec::with_capacity(1 + self.retired.len());
1109        keys.push(self.active.clone());
1110        keys.extend(self.retired.iter().cloned());
1111        Jwks { keys }
1112    }
1113
1114    /// The `aud` value tokens from this config carry.
1115    pub fn audience(&self) -> &Audience {
1116        &self.audience
1117    }
1118
1119    /// Serialize and sign one access token into RFC 7515 section 3.1 compact form.
1120    ///
1121    /// ASYNC because [`Es256Signer::sign`] is, which is because the key may not be in this
1122    /// process. With the in-process [`EcdsaP256Key`] backend the future is ready on its first poll
1123    /// and there is no suspension point.
1124    pub async fn sign_access_token(&self, claims: &AccessTokenClaims) -> Result<String, JwtError> {
1125        self.finish_signing(self.signing_input(claims)?).await
1126    }
1127
1128    /// The SYNC half: everything up to and including `BASE64URL(header) "." BASE64URL(payload)`.
1129    ///
1130    /// Split from the await deliberately, and the split is what keeps the token endpoint's future
1131    /// small. [`AccessTokenClaims`] is eight owned fields; if it were still live across the
1132    /// signature's suspension point it would join the coroutine frame, and that frame is held
1133    /// under tokio's 2048-byte debug boxing threshold by `tests/allocation.rs`. Built this way, all
1134    /// that crosses the await is this `String` and a borrow of `self`.
1135    pub(crate) fn signing_input(&self, claims: &AccessTokenClaims) -> Result<String, JwtError> {
1136        // The header is PRECOMPUTED (see `JwtConfig::encoded_header`): it is fixed for the life of
1137        // this configuration, so serializing and encoding it per token produced identical bytes at
1138        // a cost paid on every token issued.
1139        let header = &self.encoded_header;
1140
1141        // THE LAST DOOR ON AN `aud` THAT NAMES NOBODY, and the only one that closes all of them.
1142        // `AccessTokenClaims`'s doc says a missing required claim "should be impossible to express",
1143        // and RFC 9068 section 2.2 makes `aud` required, but `Audience` is a public enum with public
1144        // variants and the claim set is built by the caller, so the type has never actually made it
1145        // impossible: `Audience::Many(vec![])` serializes untagged as the literal `"aud": []`, and
1146        // `JwtConfig::new(signer, "")` yields `"aud": ""`. `with_audiences` refuses its half, but it
1147        // is a builder and not a chokepoint. This is the chokepoint. See
1148        // `Audience::names_a_resource_server` for why an empty array is the FAIL-OPEN one of the
1149        // two and therefore the one worth a refusal rather than a warning.
1150        //
1151        // Refusing HERE rather than panicking or minting anyway is what `JwtError`'s doc already
1152        // prescribes for every other way signing can fail: mint no token, answer RFC 6749 section
1153        // 5.2 `server_error`. A misconfiguration is a server error; a token valid at a resource
1154        // server nobody intended is not recoverable at all.
1155        if !claims.aud.names_a_resource_server() {
1156            return Err(JwtError(
1157                "aud must name at least one resource server, and no member may be empty".into(),
1158            ));
1159        }
1160
1161        let claims_json = serde_json::to_vec(claims)
1162            .map_err(|e| JwtError(format!("claims serialization: {e}")))?;
1163
1164        // ONE buffer for the whole token, and the JWS Signing Input is a PREFIX of it rather than
1165        // a string of its own (RFC 7515 section 5.1 steps 5 and 7: the signing input is the ASCII
1166        // of "header.payload", and the compact serialization is that followed by ".signature").
1167        // Built with `format!` this was three intermediate `String`s and two full copies of a
1168        // token that is close to a kilobyte: one to build the signing input, one to build the
1169        // result from it. Appending instead means the bytes are written once.
1170        //
1171        // The capacity is EXACT, not an estimate, so the buffer is allocated once and never grown:
1172        // base64url without padding is ceil(n * 4 / 3) characters, and an ES256 signature is a
1173        // fixed 64 bytes, which is 86.
1174        let mut compact =
1175            String::with_capacity(header.len() + 1 + base64_len(claims_json.len()) + 1 + 86);
1176        compact.push_str(header);
1177        compact.push('.');
1178        URL_SAFE_NO_PAD.encode_string(&claims_json, &mut compact);
1179        Ok(compact)
1180    }
1181
1182    /// The ASYNC half: the signature over what [`JwtConfig::signing_input`] built, appended in
1183    /// place so the token's bytes are still written exactly once.
1184    pub(crate) async fn finish_signing(&self, mut compact: String) -> Result<String, JwtError> {
1185        let signature = self
1186            .signer
1187            .dyn_sign(compact.as_bytes())
1188            .await
1189            // The host's own detail is DISCARDED here rather than wrapped: the host wrote the
1190            // signer, so it already has the real error on its own channel, and `server.rs` maps
1191            // this onto RFC 6749 s5.2 `server_error` without echoing anything about the key.
1192            .map_err(|_| JwtError("the ES256 signer could not sign".into()))?;
1193        compact.push('.');
1194        URL_SAFE_NO_PAD.encode_string(signature, &mut compact);
1195        Ok(compact)
1196    }
1197}
1198
1199impl fmt::Debug for JwtConfig {
1200    /// [`crate::ServerConfig`] derives `Debug`, so a host that logs its configuration logs this. It
1201    /// prints the PUBLISHED identity, which is public by definition, and says only that a signer is
1202    /// present: what the signer is, and what it holds, is the host's and may be a live KMS
1203    /// credential.
1204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1205        f.debug_struct("JwtConfig")
1206            .field("signer", &"<redacted>")
1207            .field("active", &self.active)
1208            .field("retired", &self.retired)
1209            .field("audience", &self.audience)
1210            .field("jwks_uri", &self.jwks_uri)
1211            .finish()
1212    }
1213}
1214
1215impl PartialEq for JwtConfig {
1216    /// Over the PUBLISHED IDENTITY only. There is no private scalar to compare once the key may be
1217    /// a handle to something in another process, and comparing handles would make two
1218    /// configurations over one KMS key unequal for no reason a host could act on. The encoded
1219    /// header is a pure function of `active.kid`, so it is not compared separately.
1220    fn eq(&self, other: &Self) -> bool {
1221        self.active == other.active
1222            && self.retired == other.retired
1223            && self.audience == other.audience
1224            && self.jwks_uri == other.jwks_uri
1225    }
1226}
1227
1228impl Eq for JwtConfig {}
1229
1230/// What the client receives as its `access_token`.
1231///
1232/// [`AccessTokenFormat::Opaque`] is the DEFAULT and is what this crate did before the `jwt`
1233/// feature existed: a 256-bit random string that means nothing without asking the AS. It is the
1234/// right default because it leaks nothing, is revocable in the only sense that matters (the AS
1235/// stops honouring it immediately), and costs one introspection call per protected request.
1236/// Since 0.9.2 a registered resource server can make that call itself
1237/// ([`crate::ServerConfig::resource_servers`]), so opaque is a real choice for a deployment with
1238/// resource servers rather than a client-only one. A deployment whose resource servers must
1239/// validate WITHOUT talking to the AS at all still wants [`AccessTokenFormat::Jwt`].
1240#[derive(Debug, Clone, PartialEq, Eq, Default)]
1241pub enum AccessTokenFormat {
1242    /// Opaque random access tokens (RFC 7662 introspection reads them, for the token's own client
1243    /// and for a resource server the token is addressed to).
1244    #[default]
1245    Opaque,
1246    /// RFC 9068 `at+jwt` access tokens, signed with ES256. The record is still persisted, so
1247    /// introspection and revocation continue to work on the exact string the client presents.
1248    ///
1249    /// BOXED deliberately. [`JwtConfig`] carries a signing key, an audience and a `jwks_uri`, and
1250    /// inlining that here put all of it in every [`crate::server::ServerConfig`], which grew
1251    /// `AuthorizationServer` from 656 to 856 bytes and tripped the size gate in
1252    /// `tests/allocation.rs`. The box costs ONE allocation per server at construction, never per
1253    /// request, and keeps the struct the same size for the opaque-token majority who pay for a
1254    /// feature they did not enable otherwise. The gate caught this; raising the budget instead
1255    /// would have made the gate meaningless.
1256    Jwt(Box<JwtConfig>),
1257}
1258
1259/// Seconds since the Unix epoch, the only representation RFC 7519 section 2 `NumericDate` allows
1260/// for `iat`/`exp`.
1261pub(crate) fn unix_seconds(t: SystemTime) -> Result<u64, JwtError> {
1262    t.duration_since(UNIX_EPOCH)
1263        .map(|d| d.as_secs())
1264        .map_err(|_| JwtError("clock is before the Unix epoch".into()))
1265}
1266
1267#[cfg(test)]
1268#[path = "tests/jwt.rs"]
1269mod tests;
1270
1271// =============================================================================================
1272// VERIFICATION.
1273//
1274// Everything above this line SIGNS; everything below it VERIFIES, which is a different and much
1275// more dangerous job because the input is attacker controlled. The three rules that boundary is
1276// built on are in this module's `//!` docs, where a reader on docs.rs can see them without
1277// opening this file; they are not repeated here so that there is only one copy to keep true.
1278// =============================================================================================
1279
1280/// A JWS could not be parsed, or did not verify.
1281///
1282/// The message is deliberately coarse and never names which check failed in a way a client could
1283/// use to probe a key: callers map this onto one RFC 6749 section 5.2 error code and the detail
1284/// stays on the host's own audit channel. It never contains key material.
1285#[derive(Debug, Clone, PartialEq, Eq)]
1286pub struct VerifyError(String);
1287
1288impl VerifyError {
1289    pub(crate) fn new(msg: impl Into<String>) -> Self {
1290        VerifyError(msg.into())
1291    }
1292}
1293
1294impl fmt::Display for VerifyError {
1295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1296        write!(f, "JWS verification error: {}", self.0)
1297    }
1298}
1299
1300impl std::error::Error for VerifyError {}
1301
1302/// The JWK members that carry PRIVATE or SYMMETRIC key material, in the RFC 7518 section 6
1303/// spellings: `d` (the EC/RSA private value, sections 6.2.2.1 and 6.3.2.1), the RSA CRT
1304/// parameters, and `k` (the octets of a symmetric key, section 6.4.1).
1305///
1306/// RFC 9449 section 4.3 makes rejecting a proof whose `jwk` contains any of these a REQUIREMENT,
1307/// and the reason generalises past DPoP: a JWK carrying a private parameter is either a client
1308/// that has just leaked its own key to us, or an attacker trying to get a key it controls adopted
1309/// where only a public half was expected. Neither is a request worth serving.
1310const PRIVATE_JWK_MEMBERS: &[&str] = &["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
1311
1312/// The PUBLIC parameters of one EC P-256 key, as received from a client.
1313///
1314/// This is the VERIFYING counterpart of [`Jwk`], which exists to be SERIALIZED and therefore holds
1315/// `&'static str` for every member this crate fixes. This one is parsed from attacker-controlled
1316/// JSON, so it is a separate type rather than a relaxation of that one: making [`Jwk`]'s members
1317/// owned so it could be deserialized would also make it possible to SERVE a `kty` this crate never
1318/// signs with.
1319///
1320/// Deserialization goes through [`PublicJwk::from_json`], including through `serde`, so there is no
1321/// route from JSON into this type that skips the private-parameter rejection. The two other
1322/// constructors take no JSON at all: [`PublicJwk::from_coordinates`] takes the two coordinates and
1323/// runs the same width check, and [`Jwk::to_public_jwk`] converts a key this crate published, which
1324/// has no private half to reject (see that method on what it does not revalidate).
1325///
1326/// The FIELDS ARE SEALED, which is what makes the sentence above true. They were public through
1327/// 0.9, and a struct literal was exactly such a route: an `AssertionKeys::PublicKeys` built by hand
1328/// could carry a `kty` this crate never verifies, or coordinates of any width, and
1329/// [`PublicJwk::thumbprint`] would then hand back a `cnf.jkt` over it. Verification revalidates and
1330/// so fails closed, but a thumbprint is a value a host WRITES DOWN, and a token bound to a key
1331/// nobody can present is a token nobody can use. Read them with the accessors; build them with
1332/// [`PublicJwk::from_json`] or [`PublicJwk::from_coordinates`].
1333#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1334pub struct PublicJwk {
1335    /// Key type; only `EC` is accepted.
1336    kty: String,
1337    /// Curve; only `P-256` is accepted.
1338    crv: String,
1339    /// Base64url (unpadded) x coordinate, exactly 32 bytes decoded.
1340    x: String,
1341    /// Base64url (unpadded) y coordinate, exactly 32 bytes decoded.
1342    y: String,
1343    /// The optional key identifier (RFC 7517 section 4.5).
1344    #[serde(skip_serializing_if = "Option::is_none")]
1345    kid: Option<String>,
1346}
1347
1348impl<'de> Deserialize<'de> for PublicJwk {
1349    /// Routed through [`PublicJwk::from_json`] rather than derived, so that a JWK loaded from the
1350    /// host's own client store is held to exactly the same rules as one arriving in a DPoP proof
1351    /// header. A derived impl would IGNORE an unknown `d` member rather than reject it, and a
1352    /// registration silently carrying a client's private key is precisely the state this type
1353    /// exists to make unrepresentable.
1354    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1355        let value = serde_json::Value::deserialize(d)?;
1356        PublicJwk::from_json(&value).map_err(serde::de::Error::custom)
1357    }
1358}
1359
1360impl PublicJwk {
1361    /// Parse and validate one JWK.
1362    ///
1363    /// Rejects, in this order: a non-object, any member of `PRIVATE_JWK_MEMBERS`, a `kty` other
1364    /// than `EC`, a `crv` other than `P-256`, and coordinates that are not exactly 32 bytes of
1365    /// base64url. The width check is not pedantry: RFC 7518 section 6.2.1.2 fixes the octet length
1366    /// at the curve's field size and requires leading zeros to be KEPT, so a trimmed coordinate is
1367    /// a different point, and accepting it is the classic JWK interoperability bug.
1368    pub fn from_json(value: &serde_json::Value) -> Result<Self, VerifyError> {
1369        let object = value
1370            .as_object()
1371            .ok_or_else(|| VerifyError::new("a JWK must be a JSON object"))?;
1372        for member in PRIVATE_JWK_MEMBERS {
1373            if object.contains_key(*member) {
1374                return Err(VerifyError::new(
1375                    "the JWK carries a private or symmetric key parameter",
1376                ));
1377            }
1378        }
1379        let string = |name: &str| -> Result<String, VerifyError> {
1380            object
1381                .get(name)
1382                .and_then(|v| v.as_str())
1383                .map(str::to_string)
1384                .ok_or_else(|| VerifyError::new("the JWK is missing a required member"))
1385        };
1386        let kty = string("kty")?;
1387        if kty != "EC" {
1388            return Err(VerifyError::new("only EC keys are supported"));
1389        }
1390        let crv = string("crv")?;
1391        if crv != "P-256" {
1392            return Err(VerifyError::new("only the P-256 curve is supported"));
1393        }
1394        let x = string("x")?;
1395        let y = string("y")?;
1396        let coordinate = |b64: &str| -> Result<(), VerifyError> {
1397            match URL_SAFE_NO_PAD.decode(b64) {
1398                Ok(bytes) if bytes.len() == 32 => Ok(()),
1399                _ => Err(VerifyError::new(
1400                    "a P-256 coordinate is exactly 32 base64url-encoded bytes",
1401                )),
1402            }
1403        };
1404        coordinate(&x)?;
1405        coordinate(&y)?;
1406        Ok(PublicJwk {
1407            kty,
1408            crv,
1409            x,
1410            y,
1411            kid: object
1412                .get("kid")
1413                .and_then(|v| v.as_str())
1414                .map(str::to_string),
1415        })
1416    }
1417
1418    /// One P-256 public key from its two RFC 7518 section 6.2.1.2 coordinates, exactly as they
1419    /// appear in a JWK: base64url, unpadded, 32 bytes each.
1420    ///
1421    /// The constructor for a host that holds the coordinates rather than a JSON document, and the
1422    /// reason the sealed fields cost nobody anything. `kty` and `crv` are not arguments because
1423    /// there is exactly one pair this crate verifies with, so admitting others would only admit a
1424    /// key that cannot be used. The same width check [`PublicJwk::from_json`] performs runs here:
1425    /// a constructor that skipped it would be the hole the fields were sealed to close.
1426    pub fn from_coordinates(x: &str, y: &str) -> Result<Self, VerifyError> {
1427        let coordinate = |b64: &str| -> Result<(), VerifyError> {
1428            match URL_SAFE_NO_PAD.decode(b64) {
1429                Ok(bytes) if bytes.len() == 32 => Ok(()),
1430                _ => Err(VerifyError::new(
1431                    "a P-256 coordinate is exactly 32 base64url-encoded bytes",
1432                )),
1433            }
1434        };
1435        coordinate(x)?;
1436        coordinate(y)?;
1437        Ok(PublicJwk {
1438            kty: "EC".to_string(),
1439            crv: "P-256".to_string(),
1440            x: x.to_string(),
1441            y: y.to_string(),
1442            kid: None,
1443        })
1444    }
1445
1446    /// Name this key, with the RFC 7517 section 4.5 `kid` a client publishes it under.
1447    ///
1448    /// Deliberately NOT part of the thumbprint: see [`PublicJwk::thumbprint`] on why relabelling a
1449    /// key must not change what a token is bound to.
1450    pub fn with_kid(mut self, kid: &str) -> Self {
1451        self.kid = Some(kid.to_string());
1452        self
1453    }
1454
1455    /// Key type. Always `EC`: nothing else parses.
1456    pub fn kty(&self) -> &str {
1457        &self.kty
1458    }
1459
1460    /// Curve. Always `P-256`: nothing else parses.
1461    pub fn crv(&self) -> &str {
1462        &self.crv
1463    }
1464
1465    /// The base64url x coordinate.
1466    pub fn x(&self) -> &str {
1467        &self.x
1468    }
1469
1470    /// The base64url y coordinate.
1471    pub fn y(&self) -> &str {
1472        &self.y
1473    }
1474
1475    /// The RFC 7517 section 4.5 `kid`, if the key carries one.
1476    pub fn kid(&self) -> Option<&str> {
1477        self.kid.as_deref()
1478    }
1479
1480    /// The RFC 7638 section 3 JWK Thumbprint of this key: SHA-256, base64url without padding.
1481    ///
1482    /// This is the value RFC 9449 section 6.1 puts in `cnf.jkt` to bind a token to the key a client
1483    /// proved possession of. The construction is exact and every part of it is load bearing
1484    /// (sections 3.1 through 3.3): ONLY the members required to identify the key type, in
1485    /// LEXICOGRAPHIC order, with no whitespace and no other member. `kid`, `use` and `alg` are
1486    /// deliberately excluded, which is what makes the thumbprint a property of the KEY rather than
1487    /// of one description of it; including any of them would let the same key produce two
1488    /// thumbprints and so two tokens a resource server could not tell were bound to one client.
1489    pub fn thumbprint(&self) -> String {
1490        // Built by hand rather than through `serde_json`, because a serializer's member order is a
1491        // property of a struct declaration and this order is a property of the RFC. For `EC` the
1492        // required set is `crv`, `kty`, `x`, `y`, which is already lexicographic.
1493        let mut json = String::with_capacity(40 + self.crv.len() + self.x.len() + self.y.len());
1494        json.push_str("{\"crv\":\"");
1495        json.push_str(&self.crv);
1496        json.push_str("\",\"kty\":\"");
1497        json.push_str(&self.kty);
1498        json.push_str("\",\"x\":\"");
1499        json.push_str(&self.x);
1500        json.push_str("\",\"y\":\"");
1501        json.push_str(&self.y);
1502        json.push_str("\"}");
1503        URL_SAFE_NO_PAD.encode(Sha256::digest(json.as_bytes()))
1504    }
1505}
1506
1507/// One RFC 7515 section 3.1 compact JWS, split and decoded but NOT yet verified.
1508///
1509/// Holding the unverified form as its own value is deliberate: it makes "parsed" and "verified"
1510/// two different things a caller cannot confuse, and it keeps [`CompactJws::signing_input`]
1511/// borrowing the received bytes, so that verification happens over what actually arrived.
1512/// `Debug` is HAND-WRITTEN (below). `signing_input` is `header.payload` verbatim and `signature`
1513/// is the decoded octets, so a derived one reconstructs the whole token from its parts: printing a
1514/// parsed RFC 7523 client assertion or RFC 9449 DPoP proof yields everything needed to rebuild a
1515/// bearer credential that is live until its `exp`. `jti` single-use bounds a proof that was
1516/// ACCEPTED; one that was refused and then logged is still replayable elsewhere.
1517///
1518/// The decoded `header` and `payload` DO print. They are what a host debugging a refused assertion
1519/// actually needs -- which `alg`, which `iss`, which `aud` -- and neither carries key material;
1520/// what makes the token spendable is the signature over the exact input bytes, and that is what is
1521/// withheld.
1522pub struct CompactJws<'a> {
1523    /// `BASE64URL(header) "." BASE64URL(payload)`: the JWS Signing Input of RFC 7515 section 5.1
1524    /// step 5, borrowed from the input.
1525    pub signing_input: &'a str,
1526    /// The decoded JOSE protected header.
1527    pub header: serde_json::Map<String, serde_json::Value>,
1528    /// The decoded payload (the JWT claims set).
1529    pub payload: serde_json::Map<String, serde_json::Value>,
1530    /// The decoded signature octets.
1531    pub signature: Vec<u8>,
1532}
1533
1534impl fmt::Debug for CompactJws<'_> {
1535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1536        f.debug_struct("CompactJws")
1537            .field("signing_input", &"[redacted]")
1538            .field("header", &self.header)
1539            .field("payload", &self.payload)
1540            .field("signature", &"[redacted]")
1541            .finish()
1542    }
1543}
1544
1545impl<'a> CompactJws<'a> {
1546    /// Split and decode `token`.
1547    ///
1548    /// Rejects anything that is not exactly three base64url segments over two dots. A FIVE segment
1549    /// token (the RFC 7516 JWE compact serialization) is therefore refused here rather than
1550    /// silently read as a JWS with odd contents, and a two segment token (the unsecured form of
1551    /// RFC 7515 appendix A.5, whose signature is the empty string) is refused because it has no
1552    /// third segment at all.
1553    pub fn parse(token: &'a str) -> Result<Self, VerifyError> {
1554        let malformed = || VerifyError::new("not a compact JWS of exactly three segments");
1555        let mut parts = token.split('.');
1556        let header_b64 = parts.next().ok_or_else(malformed)?;
1557        let payload_b64 = parts.next().ok_or_else(malformed)?;
1558        let signature_b64 = parts.next().ok_or_else(malformed)?;
1559        if parts.next().is_some() {
1560            return Err(malformed());
1561        }
1562        // Borrowed rather than rebuilt with `format!`: the signature must cover the bytes that
1563        // arrived, and a re-joined string is a second chance to get that wrong.
1564        let signing_input = &token[..header_b64.len() + 1 + payload_b64.len()];
1565        let object =
1566            |b64: &str| -> Result<serde_json::Map<String, serde_json::Value>, VerifyError> {
1567                let bytes = URL_SAFE_NO_PAD
1568                    .decode(b64)
1569                    .map_err(|_| VerifyError::new("a JWS segment is not unpadded base64url"))?;
1570                match serde_json::from_slice::<serde_json::Value>(&bytes) {
1571                    Ok(serde_json::Value::Object(map)) => Ok(map),
1572                    _ => Err(VerifyError::new("a JWS segment is not a JSON object")),
1573                }
1574            };
1575        Ok(CompactJws {
1576            header: object(header_b64)?,
1577            payload: object(payload_b64)?,
1578            signature: URL_SAFE_NO_PAD
1579                .decode(signature_b64)
1580                .map_err(|_| VerifyError::new("the signature is not unpadded base64url"))?,
1581            signing_input,
1582        })
1583    }
1584
1585    /// A string-valued member of the protected header, or `None` when absent or not a string.
1586    pub fn header_str(&self, name: &str) -> Option<&str> {
1587        self.header.get(name).and_then(|v| v.as_str())
1588    }
1589
1590    /// RFC 7515 section 4.1.11 `crit`: refuse a JWS whose header names an extension this server
1591    /// does not implement.
1592    ///
1593    /// It is UNCONDITIONAL, and that is the point of the member: the producer is stating that
1594    /// understanding the named parameters is required to process the JWS correctly, so ignoring
1595    /// one is not a lenient reading, it is processing a different message from the one that was
1596    /// signed. RFC 8725 section 3.10 names this as an attack surface. This verifier implements NO
1597    /// JWS extensions, so any `crit` at all is a refusal, and an EMPTY array is separately
1598    /// forbidden by 4.1.11 itself.
1599    ///
1600    /// ON `CompactJws` RATHER THAN AT ONE CALL SITE, deliberately. Until 0.9.1's audit this rule
1601    /// was implemented once, in `par.rs`, for request objects — while client assertions and DPoP
1602    /// proofs, which are also attacker-supplied JWS parsed by this same type, checked `typ` and
1603    /// `alg` and nothing else. One hardened reader and two unhardened ones is the shape that
1604    /// produced this crate's earlier `claim_time` defect, where the hand-rolled copy was the one
1605    /// that failed open. Every verifier now asks the same question of the same parser.
1606    pub fn reject_unknown_crit(&self) -> Result<(), VerifyError> {
1607        match self.header.get("crit") {
1608            None => Ok(()),
1609            Some(serde_json::Value::Array(names)) if names.is_empty() => Err(VerifyError::new(
1610                "the header has an empty crit, which RFC 7515 s4.1.11 forbids",
1611            )),
1612            Some(serde_json::Value::Array(_)) => Err(VerifyError::new(
1613                "the header's crit names an extension this server does not implement",
1614            )),
1615            Some(_) => Err(VerifyError::new("the header's crit is not an array")),
1616        }
1617    }
1618
1619    /// A string-valued claim, or `None` when absent or not a string.
1620    pub fn claim_str(&self, name: &str) -> Option<&str> {
1621        self.payload.get(name).and_then(|v| v.as_str())
1622    }
1623
1624    /// A `NumericDate` claim (RFC 7519 section 2), or `None` when absent or not a non-negative
1625    /// integer.
1626    ///
1627    /// A negative or fractional value is read as ABSENT rather than truncated: `exp: -1` truncated
1628    /// towards zero would read as the epoch, and a claim this crate cannot represent exactly must
1629    /// not be silently reinterpreted as one it can.
1630    pub fn claim_time(&self, name: &str) -> Option<u64> {
1631        self.payload.get(name).and_then(|v| v.as_u64())
1632    }
1633}
1634
1635/// Verify an `ES256` signature (RFC 7518 section 3.4) over `signing_input` with a public JWK.
1636///
1637/// `false` for every failure, including a malformed key or a signature of the wrong length: a
1638/// caller has exactly one safe reaction to any of them, so distinguishing them would only invite
1639/// somebody to treat one as recoverable.
1640///
1641/// THIS IS THE BUILT-IN BACKEND'S BODY, and it is the crate's ONE implementation of ES256
1642/// verification. Everything inside the crate reaches it through [`P256Verifier`] and the
1643/// [`Es256Verifier`] seam; it stays public because a host writing the resource-server half of RFC
1644/// 9449 in the same tree needs it directly, and because it is what every existing consumer calls.
1645#[cfg(feature = "jwt-p256")]
1646#[cfg_attr(docsrs, doc(cfg(feature = "jwt-p256")))]
1647pub fn verify_es256(jwk: &PublicJwk, signing_input: &[u8], signature: &[u8]) -> bool {
1648    // RFC 7518 section 3.4 fixes the ES256 signature as the fixed-width `r || s` concatenation, 64
1649    // bytes. The DER form OpenSSL emits by default is NOT this, and accepting both would give one
1650    // signature two encodings.
1651    if signature.len() != 64 {
1652        return false;
1653    }
1654    let (Ok(x), Ok(y)) = (
1655        URL_SAFE_NO_PAD.decode(&jwk.x),
1656        URL_SAFE_NO_PAD.decode(&jwk.y),
1657    ) else {
1658        return false;
1659    };
1660    if x.len() != 32 || y.len() != 32 {
1661        return false;
1662    }
1663    // Uncompressed SEC 1 point: 0x04 || X || Y. `from_sec1_bytes` is what rejects a coordinate pair
1664    // that is not actually on the curve, which is the check an invalid-curve attack needs to find
1665    // missing.
1666    let mut sec1 = [0u8; 65];
1667    sec1[0] = 0x04;
1668    sec1[1..33].copy_from_slice(&x);
1669    sec1[33..].copy_from_slice(&y);
1670    let Ok(key) = VerifyingKey::from_sec1_bytes(&sec1) else {
1671        return false;
1672    };
1673    let Ok(signature) = Signature::from_slice(signature) else {
1674        return false;
1675    };
1676    key.verify(signing_input, &signature).is_ok()
1677}
1678
1679/// HMAC-SHA-256 (RFC 2104), the primitive `HS256` is (RFC 7518 section 3.2).
1680///
1681/// Hand written rather than pulled in. `hmac` is already in this crate's dependency GRAPH, through
1682/// `p256`'s RFC 6979 deterministic nonce, but it is not a direct dependency, and taking one on to
1683/// express twenty lines of fully specified construction would widen a surface this crate promises
1684/// to keep tiny. The construction has published test vectors, which `src/tests/client_assertion.rs`
1685/// checks against, so "we wrote it ourselves" is a checkable claim rather than an assertion.
1686pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
1687    // RFC 2104: a key longer than the block size is replaced by its own digest; a shorter one is
1688    // zero padded to the block size. SHA-256's block size is 64 bytes.
1689    let mut block = [0u8; 64];
1690    if key.len() > 64 {
1691        block[..32].copy_from_slice(&Sha256::digest(key));
1692    } else {
1693        block[..key.len()].copy_from_slice(key);
1694    }
1695    let mut ipad = [0x36u8; 64];
1696    let mut opad = [0x5cu8; 64];
1697    for i in 0..64 {
1698        ipad[i] ^= block[i];
1699        opad[i] ^= block[i];
1700    }
1701    let inner = Sha256::new()
1702        .chain_update(ipad)
1703        .chain_update(message)
1704        .finalize();
1705    Sha256::new()
1706        .chain_update(opad)
1707        .chain_update(inner)
1708        .finalize()
1709        .into()
1710}
1711
1712/// Verify an `HS256` signature (RFC 7518 section 3.2) over `signing_input` with a shared secret.
1713///
1714/// The comparison is CONSTANT TIME with respect to the presented tag. A byte-by-byte compare that
1715/// exits at the first difference lets a network attacker build a valid tag one byte at a time
1716/// without ever learning the secret, which is the classic MAC verification timing attack; the
1717/// length here is fixed at 32 bytes by SHA-256, so unlike `client::constant_time_eq` there is no
1718/// length channel to close as well.
1719pub fn verify_hs256(secret: &[u8], signing_input: &[u8], signature: &[u8]) -> bool {
1720    if signature.len() != 32 {
1721        return false;
1722    }
1723    let expected = hmac_sha256(secret, signing_input);
1724    let mut acc = 0u8;
1725    for i in 0..32 {
1726        acc |= expected[i] ^ signature[i];
1727    }
1728    acc == 0
1729}
1730
1731/// Assemble one RFC 7515 section 3.1 compact JWS from an already-serialized header and payload.
1732///
1733/// This crate builds client assertions and DPoP proofs for nobody, so this exists for the OTHER
1734/// side of the seam: a host writing the CLIENT half of RFC 7523 or RFC 9449, and this crate's own
1735/// tests, which have to be able to produce a WRONG token (a foreign key, a bad `alg`, a stale
1736/// `iat`) to demonstrate that the verifier refuses it. A test suite that can only build correct
1737/// inputs cannot demonstrate an attack, and this crate's rule is that a security check is not
1738/// trusted until the attack it stops has been watched succeeding without it.
1739pub fn compact_jws(header: &[u8], payload: &[u8], sign: impl FnOnce(&str) -> Vec<u8>) -> String {
1740    // ONE buffer, for the reason `sign_access_token` gives: the JWS Signing Input is a PREFIX of
1741    // the compact serialization (RFC 7515 section 5.1 steps 5 and 7), so it does not need a string
1742    // of its own and the result does not need to be copied out of one.
1743    let mut compact =
1744        String::with_capacity(base64_len(header.len()) + 1 + base64_len(payload.len()) + 1 + 86);
1745    URL_SAFE_NO_PAD.encode_string(header, &mut compact);
1746    compact.push('.');
1747    URL_SAFE_NO_PAD.encode_string(payload, &mut compact);
1748    let signature = sign(&compact);
1749    compact.push('.');
1750    URL_SAFE_NO_PAD.encode_string(signature, &mut compact);
1751    compact
1752}
1753
1754/// How many characters `n` bytes take in base64url WITHOUT padding: four per three bytes, rounded
1755/// up. Exact, so a caller sizing a buffer with it allocates once and never grows.
1756fn base64_len(n: usize) -> usize {
1757    // `div_ceil` is 1.73, comfortably under this crate's measured 1.75 floor.
1758    (n * 4).div_ceil(3)
1759}
1760
1761#[cfg(feature = "jwt-p256")]
1762impl EcdsaP256Key {
1763    /// Sign an arbitrary JWS Signing Input with `ES256`: the counterpart of [`verify_es256`], and
1764    /// the signing half [`compact_jws`] is usually handed.
1765    pub fn sign_signing_input(&self, signing_input: &str) -> Result<Vec<u8>, JwtError> {
1766        self.sign_es256(signing_input.as_bytes())
1767            .map(|s| s.to_vec())
1768    }
1769
1770    /// The public half in the VERIFYING shape, for a host registering this key as a client's
1771    /// `private_key_jwt` key. Same parameters as [`EcdsaP256Key::public_jwk`], which produces the
1772    /// SERVING shape; there is still no method anywhere in this crate that emits `d`.
1773    pub fn to_public_jwk(&self) -> PublicJwk {
1774        Jwk::to_public_jwk(&self.public_jwk())
1775    }
1776}