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}