oauth_as/mtls.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! RFC 8705 mutual-TLS client authentication (`tls_client_auth`, `self_signed_tls_client_auth`)
5//! and certificate-bound access tokens. Compiled ONLY under the off-by-default `mtls` cargo
6//! feature; with the feature off this module does not exist, no type here appears in the public
7//! API, and the crate's dependency set and runtime cost are unchanged.
8//!
9//! # THE TRUST BOUNDARY. Read this before wiring anything up.
10//!
11//! **This library never sees a socket, so it cannot validate a certificate chain it did not
12//! negotiate.** TLS termination is the HOST's job in this design (see the crate docs), which means
13//! the host, and only the host, is the party that can know whether the certificate on the
14//! connection was actually presented, actually matched a private key the client proved possession
15//! of during the handshake, and actually chained to a trust anchor the deployment accepts.
16//!
17//! Everything in this module runs AFTER that. A [`ClientCertificate`] handed to this crate is
18//! taken as an established fact, exactly the way a `client_secret` read out of a request body is
19//! taken as a presented string. **A host that constructs a [`ClientCertificate`] from an
20//! unverified source has authenticated nobody**, and the comparisons below then compare attacker
21//! chosen values against registered ones, which is a check that passes whenever the attacker wants
22//! it to. Two ways to get that wrong, both of which have shipped in real deployments:
23//!
24//! - Reading a certificate out of a request HEADER (`X-Client-Cert`, `X-SSL-Client-S-DN`, and
25//! friends) without stripping that header on the way in. If a client can set the header, the
26//! client can set its own subject DN. The header is only trustworthy when the TLS terminator
27//! overwrites it unconditionally on every request AND the terminator is the only route to the
28//! application.
29//! - Configuring the terminator to REQUEST a client certificate but not to REQUIRE and verify one.
30//! An unverified certificate is a public document; anybody can replay somebody else's.
31//!
32//! RFC 8705 section 2 is explicit that the authorization server validates the certificate chain
33//! for the PKI method. In this crate that sentence is addressed to the host, and this module can
34//! neither perform nor check that validation. It is stated here, rather than only in a changelog,
35//! because it is the one thing an integrator can get wrong in a way that produces a working
36//! deployment which authenticates nothing.
37//!
38//! # Why this module does not parse X.509
39//!
40//! It does not need to, and the crate's dependency policy (see `Cargo.toml`) does not admit an
41//! ASN.1 parser for a job the host has already done. The host terminated the TLS connection, so it
42//! already holds the parsed certificate: every TLS stack exposes the subject and the subjectAltName
43//! entries, and every reverse proxy exposes them as strings. This module therefore takes the FACTS
44//! (a subject DN, the SAN entries, the DER bytes) and derives the one value RFC 8705 section 3.1
45//! actually defines arithmetic on: the SHA-256 thumbprint, which is `sha2` over bytes and
46//! `base64`, both of which this crate already depends on for RFC 7636. Adding an X.509 parser
47//! would add attack surface (a parser fed attacker-controlled DER) to buy a result the host must
48//! compute anyway to have completed the handshake at all.
49
50use std::fmt;
51
52use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
53use base64::Engine as _;
54use serde::de::Error as _;
55use serde::{Deserialize, Deserializer, Serialize, Serializer};
56use sha2::{Digest, Sha256};
57
58use crate::client::{Client, ClientAuth};
59use crate::events::ClientAuthFailure;
60use crate::server::ClientCredential;
61use crate::token::Confirmation;
62
63/// The RFC 8705 section 2.1.1 `token_endpoint_auth_method` value for the PKI method: the client is
64/// identified by a certificate issued by a CA the deployment trusts, matched against ONE registered
65/// expected subject value.
66pub const TLS_CLIENT_AUTH: &str = "tls_client_auth";
67
68/// The RFC 8705 section 2.2.1 `token_endpoint_auth_method` value for the self-signed method: the
69/// client is identified by a certificate it registered itself, matched by thumbprint.
70pub const SELF_SIGNED_TLS_CLIENT_AUTH: &str = "self_signed_tls_client_auth";
71
72/// RFC 8705 section 2.1.1 client metadata: the expected subject distinguished name, in the RFC 4514
73/// string representation.
74pub const TLS_CLIENT_AUTH_SUBJECT_DN: &str = "tls_client_auth_subject_dn";
75/// RFC 8705 section 2.1.1 client metadata: the expected `dNSName` subjectAltName entry.
76pub const TLS_CLIENT_AUTH_SAN_DNS: &str = "tls_client_auth_san_dns";
77/// RFC 8705 section 2.1.1 client metadata: the expected `uniformResourceIdentifier` SAN entry.
78pub const TLS_CLIENT_AUTH_SAN_URI: &str = "tls_client_auth_san_uri";
79/// RFC 8705 section 2.1.1 client metadata: the expected `iPAddress` SAN entry.
80pub const TLS_CLIENT_AUTH_SAN_IP: &str = "tls_client_auth_san_ip";
81/// RFC 8705 section 2.1.1 client metadata: the expected `rfc822Name` SAN entry.
82pub const TLS_CLIENT_AUTH_SAN_EMAIL: &str = "tls_client_auth_san_email";
83
84/// The RFC 8705 section 3.1 `x5t#S256` value: the SHA-256 hash of the DER encoding of an X.509
85/// certificate.
86///
87/// Held as the 32 RAW bytes rather than as the base64url text, and the first reason is the one
88/// that decides it: comparison is then a fixed 32-byte compare that cannot be confused by an
89/// encoding difference (padded against unpadded, standard alphabet against URL-safe), which is the
90/// classic way two implementations agree about a certificate and disagree about a string. A
91/// `String` would make an equality test a question about text that has two legal spellings.
92///
93/// It is also `Copy` and allocates nothing, where the 43-character base64url text would be a heap
94/// allocation per value. That was originally argued from this type appearing inside
95/// [`crate::token::IssuedToken`], "which is cloned out of the host's store on every
96/// introspection"; that premise is GONE, because [`crate::store::Storage::get_token`] hands back
97/// an `Arc<IssuedToken>` and introspection clones nothing. What survives is the write side and the
98/// size gate: the token record is built on every issuance and `tests/allocation.rs` holds its
99/// `size_of` to a budget, and the binding is stored as `Option<Box<CertificateThumbprint>>` there,
100/// so an unbound token pays 8 bytes and no allocation while a bound one pays 32 bytes rather than
101/// a 43-byte string. The base64url form is produced only where it is actually needed: on the wire.
102#[derive(Clone, Copy, PartialEq, Eq, Hash)]
103pub struct CertificateThumbprint([u8; 32]);
104
105impl CertificateThumbprint {
106 /// Compute the thumbprint of a DER-encoded X.509 certificate (RFC 8705 section 3.1).
107 ///
108 /// `der` is the certificate itself, NOT a PEM block: base64 text with `-----BEGIN
109 /// CERTIFICATE-----` around it hashes to something that matches nothing. See
110 /// [`CertificateThumbprint::from_pem`] for that form.
111 pub fn from_der(der: &[u8]) -> Self {
112 CertificateThumbprint(Sha256::digest(der).into())
113 }
114
115 /// Compute the thumbprint of a PEM-encoded certificate, ignoring the armour lines and all
116 /// whitespace. Present because a host's certificate almost always arrives as PEM (from a file,
117 /// from a proxy header, from a KMS), and re-deriving the DER by hand is precisely where the
118 /// "hashed the wrong bytes" mistake happens.
119 ///
120 /// Only the FIRST certificate in the file is used: RFC 8705 binds to the end-entity
121 /// certificate, and a PEM bundle carries its issuers after it.
122 pub fn from_pem(pem: &str) -> Result<Self, MtlsRegistrationError> {
123 let body = match pem.find("-----BEGIN CERTIFICATE-----") {
124 Some(start) => {
125 let after = &pem[start + "-----BEGIN CERTIFICATE-----".len()..];
126 match after.find("-----END CERTIFICATE-----") {
127 Some(end) => &after[..end],
128 None => return Err(MtlsRegistrationError::MalformedCertificate),
129 }
130 }
131 None => return Err(MtlsRegistrationError::MalformedCertificate),
132 };
133 let compact: String = body.chars().filter(|c| !c.is_ascii_whitespace()).collect();
134 let der = STANDARD
135 .decode(compact.as_bytes())
136 .map_err(|_| MtlsRegistrationError::MalformedCertificate)?;
137 Ok(CertificateThumbprint::from_der(&der))
138 }
139
140 /// The raw 32 byte hash.
141 pub fn as_bytes(&self) -> &[u8; 32] {
142 &self.0
143 }
144
145 /// The RFC 8705 section 3.1 wire form: base64url, no padding, 43 characters.
146 pub fn to_base64url(&self) -> String {
147 URL_SAFE_NO_PAD.encode(self.0)
148 }
149
150 /// Parse the wire form. Rejects anything that is not exactly a 32 byte hash: an `x5t#S256`
151 /// value of some other length was never produced by SHA-256, so accepting it would store a
152 /// binding that can never match a certificate and would fail at token-presentation time
153 /// instead of at configuration time.
154 pub fn from_base64url(text: &str) -> Result<Self, MtlsRegistrationError> {
155 let bytes = URL_SAFE_NO_PAD
156 .decode(text.as_bytes())
157 .map_err(|_| MtlsRegistrationError::MalformedThumbprint)?;
158 let fixed: [u8; 32] = bytes
159 .try_into()
160 .map_err(|_| MtlsRegistrationError::MalformedThumbprint)?;
161 Ok(CertificateThumbprint(fixed))
162 }
163}
164
165/// The base64url form, which is what RFC 8705 section 3.1 puts on the wire and what an operator
166/// reading a log needs to compare against a certificate fingerprint.
167impl fmt::Display for CertificateThumbprint {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.write_str(&self.to_base64url())
170 }
171}
172
173/// NOT redacted, unlike every other hand-written `Debug` in this crate. A certificate thumbprint is
174/// a hash of a PUBLIC document: it authenticates nobody, it is published inside every bound access
175/// token, and an operator diagnosing "this token is bound to a certificate the client is not
176/// presenting" needs to be able to see both values.
177impl fmt::Debug for CertificateThumbprint {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 write!(f, "CertificateThumbprint({})", self.to_base64url())
180 }
181}
182
183/// Serialized as the RFC 8705 section 3.1 base64url text, not as an array of 32 numbers.
184///
185/// This is what a host's store persists and what [`crate::token::IntrospectionResponse`] emits, so
186/// the two are the same string and a host can grep for one and find the other.
187impl Serialize for CertificateThumbprint {
188 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
189 serializer.serialize_str(&self.to_base64url())
190 }
191}
192
193impl<'de> Deserialize<'de> for CertificateThumbprint {
194 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
195 let text = String::deserialize(deserializer)?;
196 CertificateThumbprint::from_base64url(&text).map_err(D::Error::custom)
197 }
198}
199
200/// A client certificate the HOST has already verified, decomposed into the facts RFC 8705 matches
201/// on.
202///
203/// Read the module docs on the trust boundary before constructing one. Nothing in this type is a
204/// secret and nothing here is redacted in `Debug`: a certificate is a public document, and what
205/// authenticates the client is possession of the private key, which the TLS handshake proved to the
206/// HOST and which this crate never sees.
207///
208/// Everything borrows, so building one allocates nothing but the thumbprint's hash, and passing it
209/// into the token endpoint costs one pointer.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct ClientCertificate<'a> {
212 thumbprint: CertificateThumbprint,
213 subject_dn: Option<&'a str>,
214 san_dns: &'a [&'a str],
215 san_uri: &'a [&'a str],
216 san_ip: &'a [&'a str],
217 san_email: &'a [&'a str],
218}
219
220impl<'a> ClientCertificate<'a> {
221 /// From the DER encoding of the VERIFIED certificate: this crate cannot check the chain and
222 /// takes the bytes as an established fact, so read this module's trust boundary section before
223 /// choosing where they come from. Bytes off an unstripped `X-Client-Cert` header authenticate
224 /// nobody.
225 ///
226 /// The thumbprint (RFC 8705 section 3.1) is
227 /// computed here, once, so no caller has to decide which bytes to hash or which base64 alphabet
228 /// to use.
229 ///
230 /// The subject DN and the SAN entries default to absent; add whichever the deployment registers
231 /// clients by. A certificate with no facts attached can still be BOUND to a token (section 3),
232 /// which is why they are optional rather than required: section 4 makes certificate binding
233 /// available to a client that does not authenticate with mutual TLS at all.
234 pub fn from_der(der: &[u8]) -> Self {
235 ClientCertificate::from_thumbprint(CertificateThumbprint::from_der(der))
236 }
237
238 /// From a thumbprint the host computed itself, for a deployment whose TLS terminator forwards a
239 /// fingerprint rather than the certificate (nginx's `$ssl_client_fingerprint`, an ALB's
240 /// header). The host is then responsible for the encoding, which is why
241 /// [`ClientCertificate::from_der`] is the constructor to prefer where the DER is available.
242 pub fn from_thumbprint(thumbprint: CertificateThumbprint) -> Self {
243 ClientCertificate {
244 thumbprint,
245 subject_dn: None,
246 san_dns: &[],
247 san_uri: &[],
248 san_ip: &[],
249 san_email: &[],
250 }
251 }
252
253 /// The subject distinguished name, in the RFC 4514 string representation.
254 ///
255 /// The comparison this crate performs is EXACT STRING EQUALITY against the registered value
256 /// (see [`ExpectedSubject::SubjectDn`]), so the host must produce the same spelling its
257 /// registrations use. RFC 8705 section 2.1 allows a server to implement a more sophisticated
258 /// DN comparison; this crate deliberately does not, because a partial DN parser that gets
259 /// attribute ordering, escaping or case folding subtly wrong is a way to make two different
260 /// subjects compare equal, and that is an authentication bypass rather than an inconvenience.
261 pub fn with_subject_dn(mut self, dn: &'a str) -> Self {
262 self.subject_dn = Some(dn);
263 self
264 }
265
266 /// The `dNSName` subjectAltName entries.
267 pub fn with_san_dns(mut self, entries: &'a [&'a str]) -> Self {
268 self.san_dns = entries;
269 self
270 }
271
272 /// The `uniformResourceIdentifier` subjectAltName entries.
273 pub fn with_san_uri(mut self, entries: &'a [&'a str]) -> Self {
274 self.san_uri = entries;
275 self
276 }
277
278 /// The `iPAddress` subjectAltName entries, in their textual form.
279 pub fn with_san_ip(mut self, entries: &'a [&'a str]) -> Self {
280 self.san_ip = entries;
281 self
282 }
283
284 /// The `rfc822Name` subjectAltName entries.
285 pub fn with_san_email(mut self, entries: &'a [&'a str]) -> Self {
286 self.san_email = entries;
287 self
288 }
289
290 /// This certificate's RFC 8705 section 3.1 thumbprint, which is what a token gets bound to.
291 pub fn thumbprint(&self) -> &CertificateThumbprint {
292 &self.thumbprint
293 }
294
295 /// Whether this certificate satisfies ONE registered expected subject value (RFC 8705 section
296 /// 2.1).
297 ///
298 /// Every comparison is exact. No wildcard is honoured (a `*.example.com` SAN matches only a
299 /// registration whose value is literally `*.example.com`), no case folding is applied even for
300 /// DNS names where the protocol would allow it, and no normalisation is applied to IP address
301 /// or URI text. Exactness is the whole security property here: each relaxation is a way for a
302 /// certificate issued to one subject to authenticate as another, and a host that needs a
303 /// normalised form can register the normalised form.
304 fn satisfies(&self, expected: &ExpectedSubject) -> bool {
305 match expected {
306 ExpectedSubject::SubjectDn(dn) => self.subject_dn == Some(dn.as_str()),
307 ExpectedSubject::SanDns(v) => self.san_dns.contains(&v.as_str()),
308 ExpectedSubject::SanUri(v) => self.san_uri.contains(&v.as_str()),
309 ExpectedSubject::SanIp(v) => self.san_ip.contains(&v.as_str()),
310 ExpectedSubject::SanEmail(v) => self.san_email.contains(&v.as_str()),
311 }
312 }
313}
314
315/// The ONE registered value a `tls_client_auth` client is identified by (RFC 8705 section 2.1.1).
316///
317/// Section 2.1.2 requires that exactly one of the five metadata parameters is registered, and this
318/// enum is how that requirement is enforced: with one variant per parameter and no way to hold two
319/// at once, a registration carrying both a subject DN and a SAN is not a state this crate can
320/// represent, rather than a state it checks for and hopefully rejects. The wire-facing check, for a
321/// registration document that CAN spell two, is
322/// [`ExpectedSubject::from_registration_parameters`].
323///
324/// Why the rule matters: the five parameters are alternatives, not a conjunction, so a server that
325/// accepted two would have to decide whether to require both or either. "Either" is a strictly
326/// weaker credential than the operator asked for, and a server that quietly picks it turns a
327/// registration mistake into an authentication bypass.
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub enum ExpectedSubject {
330 /// `tls_client_auth_subject_dn`: the expected subject DN, RFC 4514 string form.
331 SubjectDn(String),
332 /// `tls_client_auth_san_dns`: the expected `dNSName` SAN entry.
333 SanDns(String),
334 /// `tls_client_auth_san_uri`: the expected `uniformResourceIdentifier` SAN entry.
335 SanUri(String),
336 /// `tls_client_auth_san_ip`: the expected `iPAddress` SAN entry.
337 SanIp(String),
338 /// `tls_client_auth_san_email`: the expected `rfc822Name` SAN entry.
339 SanEmail(String),
340}
341
342impl ExpectedSubject {
343 /// Build from the RFC 8705 section 2.1.1 client metadata parameters, enforcing section 2.1.2.
344 ///
345 /// This is the seam for a registration document (RFC 7591 dynamic registration, or a host's own
346 /// admin API) where "two parameters were sent" is expressible. Exactly one recognised parameter
347 /// must be present with a non-empty value; zero and two are both refused, and refused BEFORE a
348 /// client exists rather than at the first token request.
349 ///
350 /// An empty value is refused for the same reason a malformed thumbprint is: it can never match
351 /// a real certificate, so accepting it registers a client that can never authenticate and
352 /// reports the problem at the worst possible moment.
353 pub fn from_registration_parameters<'a, I>(parameters: I) -> Result<Self, MtlsRegistrationError>
354 where
355 I: IntoIterator<Item = (&'a str, &'a str)>,
356 {
357 let mut found: Option<ExpectedSubject> = None;
358 for (name, value) in parameters {
359 let candidate = match name {
360 TLS_CLIENT_AUTH_SUBJECT_DN => ExpectedSubject::SubjectDn(value.to_string()),
361 TLS_CLIENT_AUTH_SAN_DNS => ExpectedSubject::SanDns(value.to_string()),
362 TLS_CLIENT_AUTH_SAN_URI => ExpectedSubject::SanUri(value.to_string()),
363 TLS_CLIENT_AUTH_SAN_IP => ExpectedSubject::SanIp(value.to_string()),
364 TLS_CLIENT_AUTH_SAN_EMAIL => ExpectedSubject::SanEmail(value.to_string()),
365 // Not one of the five; a registration document carries plenty of other members.
366 _ => continue,
367 };
368 if value.is_empty() {
369 return Err(MtlsRegistrationError::EmptySubjectValue);
370 }
371 // Section 2.1.2. Refused rather than resolved: there is no correct way to pick, and
372 // every way of picking is weaker than what the operator wrote down. Note this fires on
373 // the SECOND parameter whichever order they arrived in, so the answer does not depend
374 // on how the host happened to iterate its own registration document.
375 if found.is_some() {
376 return Err(MtlsRegistrationError::MoreThanOneSubjectValue);
377 }
378 found = Some(candidate);
379 }
380 found.ok_or(MtlsRegistrationError::NoSubjectValue)
381 }
382
383 /// The registered parameter name this value came from.
384 pub fn parameter_name(&self) -> &'static str {
385 match self {
386 ExpectedSubject::SubjectDn(_) => TLS_CLIENT_AUTH_SUBJECT_DN,
387 ExpectedSubject::SanDns(_) => TLS_CLIENT_AUTH_SAN_DNS,
388 ExpectedSubject::SanUri(_) => TLS_CLIENT_AUTH_SAN_URI,
389 ExpectedSubject::SanIp(_) => TLS_CLIENT_AUTH_SAN_IP,
390 ExpectedSubject::SanEmail(_) => TLS_CLIENT_AUTH_SAN_EMAIL,
391 }
392 }
393
394 /// The registered value.
395 pub fn value(&self) -> &str {
396 match self {
397 ExpectedSubject::SubjectDn(v)
398 | ExpectedSubject::SanDns(v)
399 | ExpectedSubject::SanUri(v)
400 | ExpectedSubject::SanIp(v)
401 | ExpectedSubject::SanEmail(v) => v,
402 }
403 }
404}
405
406/// The certificates a `self_signed_tls_client_auth` client registered, as RFC 8705 section 3.1
407/// thumbprints (RFC 8705 section 2.2).
408///
409/// Section 2.2 has the client register its certificates in a JWK Set, and has the server compare
410/// the presented certificate against them. This crate stores the comparison in its cheapest exact
411/// form, the SHA-256 thumbprint of the DER, rather than the certificates themselves: the
412/// comparison section 2.2 asks for is "is this the same certificate", the thumbprint answers
413/// exactly that question in 32 bytes, and keeping whole certificates in the client record would put
414/// kilobytes into a value this crate clones on every token request.
415///
416/// It is deliberately a LIST. A client re-keying has to be able to present either the old or the
417/// new certificate for the overlap window, and a deployment with no way to express that gets a
418/// flag day instead of a rotation.
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub struct RegisteredCertificates(Vec<CertificateThumbprint>);
421
422impl RegisteredCertificates {
423 /// From thumbprints already computed.
424 ///
425 /// An EMPTY list is refused, exactly as [`RegisteredCertificates::from_jwks`] refuses a key set
426 /// that yields no certificate, and for the identical reason: a registration with nothing to
427 /// compare against can never authenticate anybody, so it is a configuration mistake and not a
428 /// permissive setting. Accepting it here and refusing it there would have made the outcome of
429 /// one mistake depend on which constructor the host happened to reach for.
430 pub fn from_thumbprints(
431 thumbprints: Vec<CertificateThumbprint>,
432 ) -> Result<Self, MtlsRegistrationError> {
433 if thumbprints.is_empty() {
434 return Err(MtlsRegistrationError::NoCertificates);
435 }
436 Ok(RegisteredCertificates(thumbprints))
437 }
438
439 /// From the DER encodings of the registered certificates. An empty iterator is refused, for the
440 /// reason [`RegisteredCertificates::from_thumbprints`] gives.
441 pub fn from_der_certificates<'a, I>(certificates: I) -> Result<Self, MtlsRegistrationError>
442 where
443 I: IntoIterator<Item = &'a [u8]>,
444 {
445 RegisteredCertificates::from_thumbprints(
446 certificates
447 .into_iter()
448 .map(CertificateThumbprint::from_der)
449 .collect(),
450 )
451 }
452
453 /// From the client's registered RFC 7517 JWK Set, which is the form RFC 8705 section 2.2
454 /// actually defines the registration in: each key's `x5c` member (RFC 7517 section 4.7) is a
455 /// chain whose FIRST entry is the certificate holding that key, base64 encoded with the
456 /// standard alphabet and padding (NOT base64url: section 4.7 is explicit, and it is the
457 /// difference that makes a hand-rolled version of this function hash the wrong bytes).
458 ///
459 /// Provided rather than left to the host because this is the one place a self-signed
460 /// registration goes wrong silently: a host that decodes with the wrong alphabet, or that
461 /// hashes the second chain entry, produces a registration that simply never authenticates.
462 ///
463 /// A key with no `x5c` is SKIPPED, not an error: a JWK Set may legitimately carry keys for
464 /// other purposes (RFC 9101 request object signing, say). A set that yields no certificate at
465 /// all IS an error, because that registration can never authenticate anybody.
466 pub fn from_jwks(jwks: &str) -> Result<Self, MtlsRegistrationError> {
467 let document: serde_json::Value =
468 serde_json::from_str(jwks).map_err(|_| MtlsRegistrationError::MalformedJwks)?;
469 let keys = document
470 .get("keys")
471 .and_then(|k| k.as_array())
472 .ok_or(MtlsRegistrationError::MalformedJwks)?;
473 // Sized from the key count: at most one certificate per key, and this runs at
474 // registration time where the count is already in hand.
475 let mut thumbprints = Vec::with_capacity(keys.len());
476 for key in keys {
477 let chain = match key.get("x5c").and_then(|c| c.as_array()) {
478 Some(chain) => chain,
479 None => continue,
480 };
481 let leaf = chain
482 .first()
483 .and_then(|c| c.as_str())
484 .ok_or(MtlsRegistrationError::MalformedJwks)?;
485 let der = STANDARD
486 .decode(leaf.as_bytes())
487 .map_err(|_| MtlsRegistrationError::MalformedCertificate)?;
488 thumbprints.push(CertificateThumbprint::from_der(&der));
489 }
490 if thumbprints.is_empty() {
491 return Err(MtlsRegistrationError::NoCertificateInJwks);
492 }
493 Ok(RegisteredCertificates(thumbprints))
494 }
495
496 /// The registered thumbprints.
497 pub fn thumbprints(&self) -> &[CertificateThumbprint] {
498 &self.0
499 }
500
501 /// Whether `thumbprint` is one of them.
502 ///
503 /// A plain comparison, not a constant-time one, and that is not an oversight. Both sides are
504 /// hashes of PUBLIC documents: the presented certificate travels in the clear in the TLS
505 /// handshake and the registered one is whatever the client published. There is no secret here
506 /// for a timing side channel to leak, and what actually authenticates the client is possession
507 /// of the private key, which the handshake proved to the host. Compare with
508 /// [`crate::client::ClientAuth::verify_with`], where the value IS a secret and the comparison
509 /// is constant time for that reason.
510 pub fn contains(&self, thumbprint: &CertificateThumbprint) -> bool {
511 self.0.contains(thumbprint)
512 }
513}
514
515/// How a mutual-TLS client is recognised: the RFC 8705 section 2.1 PKI method or the section 2.2
516/// self-signed method.
517///
518/// Held INLINE in [`crate::client::ClientAuth`] rather than boxed, and that is measured rather than
519/// assumed: the largest variant here is one `String` plus a discriminant (40 bytes) against the
520/// existing `ConfidentialSecretHash` variant's two `String`s (48 bytes), so a client registered for
521/// mutual TLS makes `ClientAuth`, and therefore `Client`, exactly as big as it already was. A box
522/// would have bought nothing and cost an allocation on every `Client` clone, which happens on every
523/// token request.
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525pub enum MtlsClientRegistration {
526 /// RFC 8705 section 2.1 (`tls_client_auth`): a CA-issued certificate, matched against exactly
527 /// one registered subject value. The DEPLOYMENT's trust anchors decide which CAs count, and
528 /// that decision is made by the host's TLS terminator, not here.
529 TlsClientAuth(ExpectedSubject),
530 /// RFC 8705 section 2.2 (`self_signed_tls_client_auth`): a certificate the client registered
531 /// itself, matched by thumbprint. No CA is involved and none is needed: the registration IS the
532 /// trust anchor.
533 SelfSignedTlsClientAuth(RegisteredCertificates),
534}
535
536impl MtlsClientRegistration {
537 /// The RFC 8705 `token_endpoint_auth_method` value this registration corresponds to, which is
538 /// also what the RFC 8414 metadata document advertises.
539 pub fn method_name(&self) -> &'static str {
540 match self {
541 MtlsClientRegistration::TlsClientAuth(_) => TLS_CLIENT_AUTH,
542 MtlsClientRegistration::SelfSignedTlsClientAuth(_) => SELF_SIGNED_TLS_CLIENT_AUTH,
543 }
544 }
545
546 /// Whether `certificate` authenticates this client.
547 ///
548 /// This answer is only ever as good as the certificate handed in. READ this module's trust
549 /// boundary section: a `true` here means "the presented certificate matches what was
550 /// registered" and NOTHING about whether it was presented on a TLS connection whose handshake
551 /// proved possession of the private key. That part is the host's, it happened before this call,
552 /// and a [`ClientCertificate`] built from an unverified header makes this function return
553 /// whatever the caller wanted it to.
554 pub fn accepts(&self, certificate: &ClientCertificate<'_>) -> bool {
555 match self {
556 MtlsClientRegistration::TlsClientAuth(expected) => certificate.satisfies(expected),
557 MtlsClientRegistration::SelfSignedTlsClientAuth(registered) => {
558 registered.contains(certificate.thumbprint())
559 }
560 }
561 }
562}
563
564/// A registration this crate refuses to build, because the result could never authenticate anybody
565/// or could authenticate the wrong body.
566///
567/// These are CONFIGURATION errors, reported to whoever is registering a client, and none of them
568/// reaches a token-endpoint response: RFC 6749 section 5.2 collapses every client authentication
569/// failure into `invalid_client` precisely so that a caller cannot probe a registration.
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571#[non_exhaustive]
572pub enum MtlsRegistrationError {
573 /// None of the five RFC 8705 section 2.1.1 subject parameters was present.
574 NoSubjectValue,
575 /// More than one was present, which RFC 8705 section 2.1.2 forbids.
576 MoreThanOneSubjectValue,
577 /// A subject parameter was present with an empty value, which no certificate can match.
578 EmptySubjectValue,
579 /// A JWK Set that could not be parsed, or that has no `keys` array.
580 MalformedJwks,
581 /// A JWK Set that parsed but carries no certificate for this crate to match against.
582 NoCertificateInJwks,
583 /// A certificate registration built from an empty list, which can never match anything.
584 NoCertificates,
585 /// A certificate that is not the DER (or PEM-wrapped DER) this crate can hash.
586 MalformedCertificate,
587 /// An `x5t#S256` value that is not the base64url encoding of a 32 byte hash.
588 MalformedThumbprint,
589}
590
591impl fmt::Display for MtlsRegistrationError {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 let text = match self {
594 MtlsRegistrationError::NoSubjectValue => {
595 "tls_client_auth requires one of the RFC 8705 s2.1.1 subject parameters"
596 }
597 MtlsRegistrationError::MoreThanOneSubjectValue => {
598 "RFC 8705 s2.1.2 permits exactly one tls_client_auth subject parameter"
599 }
600 MtlsRegistrationError::EmptySubjectValue => {
601 "a tls_client_auth subject parameter was empty, which no certificate can match"
602 }
603 MtlsRegistrationError::MalformedJwks => "the JWK Set could not be parsed",
604 MtlsRegistrationError::NoCertificateInJwks => {
605 "the JWK Set carries no x5c certificate to match against"
606 }
607 MtlsRegistrationError::NoCertificates => {
608 "a certificate registration must name at least one certificate"
609 }
610 MtlsRegistrationError::MalformedCertificate => {
611 "the certificate is not DER or PEM-wrapped DER"
612 }
613 MtlsRegistrationError::MalformedThumbprint => {
614 "an x5t#S256 value is base64url of exactly 32 bytes"
615 }
616 };
617 f.write_str(text)
618 }
619}
620
621impl std::error::Error for MtlsRegistrationError {}
622
623impl Confirmation {
624 /// RESOURCE SERVER side of RFC 8705 section 3: whether the certificate on the connection the
625 /// token was presented over is the one the token is bound to.
626 ///
627 /// A resource server that has introspected a token (section 3.2 — a channel this server opens
628 /// to registered resource servers since 0.9.2; see [`crate::ServerConfig::resource_servers`])
629 /// or verified a JWT (section
630 /// 3.1) calls this with the DER of the client certificate ITS OWN TLS layer verified. The two
631 /// halves are equally load bearing, and this method can only do the second one: a certificate
632 /// the resource server did not verify proves nothing, exactly as set out in this module's docs.
633 ///
634 /// Answers `false` for a token that carries no certificate binding at all. That is the safe
635 /// direction and the only one this method can take: a resource server calling it is asking "is
636 /// this token bound to my caller", and an unbound token is not. A resource server that ACCEPTS
637 /// unbound tokens (a mixed deployment, mid-migration) must ask that question separately, with
638 /// [`Confirmation::certificate_thumbprint`], rather than reading a `false` here as permission.
639 pub fn confirms_certificate(&self, der: &[u8]) -> bool {
640 match self.certificate_thumbprint() {
641 Some(bound) => *bound == CertificateThumbprint::from_der(der),
642 None => false,
643 }
644 }
645
646 /// The RFC 8705 section 3.1 `x5t#S256` this token is bound to, or `None` for an unbound token.
647 pub fn certificate_thumbprint(&self) -> Option<&CertificateThumbprint> {
648 self.x5t_s256.as_ref()
649 }
650
651 /// The confirmation an access token issued over `certificate` carries (RFC 8705 section 3.1).
652 ///
653 /// The RFC 9449 `jkt` member, when that feature is also compiled in, is left absent rather
654 /// than overwritten: a token can be bound by both mechanisms at once and neither owns the
655 /// object. See [`Confirmation`].
656 pub fn for_certificate(certificate: &ClientCertificate<'_>) -> Self {
657 Confirmation {
658 #[cfg(feature = "dpop")]
659 jkt: None,
660 x5t_s256: Some(*certificate.thumbprint()),
661 }
662 }
663}
664
665/// Whether the certificate on this request authenticates `client`, for a registration that
666/// authenticates BY certificate.
667///
668/// Reached from `AuthorizationServer::authenticate_client`, which dispatches on the REGISTRATION
669/// rather than on what the request happened to present. That direction is the security argument:
670///
671/// - a [`ClientAuth::Mtls`] registration is decided here and ONLY here. It has no secret, so there
672/// is no string that could be the right one, and it never reaches the secret comparison:
673/// [`ClientAuth::verify_with`] answers `false` for the variant, so no presented secret can
674/// authenticate an mTLS client by that route either.
675/// - every other registration is decided exactly as it was before, and a certificate presented
676/// alongside is NOT an authentication credential there. It is used only for RFC 8705 section 3
677/// token binding, which section 4 makes available to clients that authenticate some other way,
678/// and to public clients, which authenticate not at all.
679pub(crate) fn verify_certificate(
680 client: &Client,
681 cred: &ClientCredential<'_>,
682) -> Result<(), ClientAuthFailure> {
683 let registration = match &client.auth {
684 ClientAuth::Mtls { registration } => registration,
685 // Not reachable through `authenticate_client`. A direct caller that lands here has asked
686 // whether a certificate authenticates a client that does not authenticate by certificate,
687 // and the answer to that is no rather than "try something else".
688 _ => return Err(ClientAuthFailure::SecretMismatch),
689 };
690 // RFC 6749 s2.3, and OAuth 2.1 s2.4 in the same words: a client uses exactly ONE
691 // authentication method per request. A request carrying a secret as well has not said which
692 // credential it is relying on, and a server that picks one behaves differently from the next
693 // server, which is the ambiguity an intermediary exploits.
694 if cred.client_secret.is_some() {
695 return Err(ClientAuthFailure::SecretMismatch);
696 }
697 let certificate = cred
698 .certificate
699 .ok_or(ClientAuthFailure::NoCertificatePresented)?;
700 // The registration decides, and it is the ONLY thing that decides. "The host verified this
701 // certificate" says the chain is good, not that it belongs to this client: every deployment
702 // that trusts a CA for client certificates has more than one certificate under it, and for the
703 // section 2.2 self-signed method a certificate is something the caller can mint for themselves
704 // in a second.
705 if registration.accepts(certificate) {
706 Ok(())
707 } else {
708 Err(ClientAuthFailure::CertificateMismatch)
709 }
710}
711
712#[cfg(test)]
713#[path = "tests/mtls.rs"]
714mod tests;