Skip to main content

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;