Skip to main content

autumn_web/
tls.rs

1//! Inbound (server-side) TLS support (issue #1603).
2//!
3//! This module lets an Autumn app terminate HTTPS in-process, without a
4//! sidecar reverse proxy, when `[server.tls]` names a certificate + key.
5//! It provides five things:
6//!
7//! (`lib.rs` carries an outer doc comment on `pub mod tls`, which is merged
8//! with this block, so every link below is fully qualified — a bare item name
9//! would resolve in the crate root instead of here.)
10//!
11//! 1. **Fail-fast loading** — [`load_certified_key`](crate::tls::load_certified_key)
12//!    reads the PEM cert chain and private key from disk, verifies they parse
13//!    and that the private key matches the leaf certificate (rustls'
14//!    [`CertifiedKey::from_der`](rustls::sign::CertifiedKey::from_der) compares
15//!    `SubjectPublicKeyInfo`), and rejects an already-expired leaf certificate.
16//!    Every error names the offending path so a misconfiguration is actionable.
17//! 2. **A reloadable resolver** —
18//!    [`ReloadableCertResolver`](crate::tls::ReloadableCertResolver) holds the
19//!    current [`CertifiedKey`](rustls::sign::CertifiedKey) behind an `RwLock`
20//!    and implements [`ResolvesServerCert`](rustls::server::ResolvesServerCert),
21//!    so the certificate can be swapped atomically at runtime (e.g. after an
22//!    ACME/`certbot` renewal) without dropping the listener or restarting the
23//!    process.
24//! 3. **Expiry inspection** — [`inspect_leaf`](crate::tls::inspect_leaf)
25//!    returns the leaf certificate's `notAfter` so `autumn doctor` can warn on
26//!    near-expiry and fail on an expired certificate, offline (no server boot,
27//!    no network).
28//! 4. **The listener** — [`TlsListener`](crate::tls::TlsListener) is the
29//!    `axum::serve::Listener` the app binds when `[server.tls]` is set: it
30//!    drives each rustls handshake off the accept loop, under a per-connection
31//!    timeout, so neither a failed nor a stalled handshake can wedge the
32//!    server.
33//! 5. **Renewal** — [`CertReloader`](crate::tls::CertReloader) polls the
34//!    cert/key mtimes and swaps the resolver's certificate when they change, so
35//!    a `certbot`/ACME renewal is picked up without a restart.
36//!
37//! The crypto backend is `ring`, the SAME backend the outbound Postgres TLS
38//! path already uses — the workspace deliberately forbids a second TLS backend
39//! (no aws-lc-rs / native-tls / openssl).
40
41// autumn-determinism-gate: production code in this module must read time and
42// mint identifiers through the framework's injected seams (ClockSource /
43// Entropy), never `Instant::now()` / `Utc::now()` / `SystemTime::now()` /
44// `Uuid::new_v4()` directly. See CONTRIBUTING.md "Determinism seam gate"
45// (issue #1797). The one exception is `now_unix` below: certificate validity
46// is judged against real wall time by design, and it carries a per-site
47// #[allow(clippy::disallowed_methods, reason = "…")].
48#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
49
50use std::path::{Path, PathBuf};
51use std::sync::{Arc, RwLock};
52
53use rustls::crypto::CryptoProvider;
54use rustls::server::{ClientHello, ResolvesServerCert};
55use rustls::sign::CertifiedKey;
56use rustls_pki_types::pem::PemObject as _;
57use rustls_pki_types::{CertificateDer, PrivateKeyDer};
58
59/// Default cert/key reload poll interval, in seconds.
60///
61/// The running server polls the cert and key file modification times this often
62/// to detect an external renewal. 60s is frequent enough to pick up a
63/// `certbot`/ACME renewal promptly while imposing a negligible
64/// two-`stat`-per-minute cost.
65pub const DEFAULT_RELOAD_INTERVAL_SECS: u64 = 60;
66
67/// Something went wrong loading, validating, or inspecting the configured TLS
68/// material. Every variant names the offending path so the operator can act on
69/// it without guesswork.
70#[derive(Debug, thiserror::Error)]
71pub enum TlsError {
72    /// The certificate file could not be read.
73    #[error("failed to read TLS certificate file `{path}`: {source}")]
74    ReadCert {
75        /// Path that failed to read.
76        path: PathBuf,
77        /// Underlying I/O error.
78        source: std::io::Error,
79    },
80    /// The private key file could not be read.
81    #[error("failed to read TLS private key file `{path}`: {source}")]
82    ReadKey {
83        /// Path that failed to read.
84        path: PathBuf,
85        /// Underlying I/O error.
86        source: std::io::Error,
87    },
88    /// The certificate PEM could not be parsed.
89    #[error("failed to parse a PEM certificate in `{path}`: {source}")]
90    ParseCert {
91        /// Path whose PEM failed to parse.
92        path: PathBuf,
93        /// Underlying PEM parse error.
94        source: rustls_pki_types::pem::Error,
95    },
96    /// No `CERTIFICATE` PEM block was present in the certificate file.
97    #[error("no certificates found in `{path}` (expected at least one PEM CERTIFICATE block)")]
98    NoCertificates {
99        /// Path that contained no certificate.
100        path: PathBuf,
101    },
102    /// The private key PEM could not be parsed.
103    #[error("failed to parse a PEM private key in `{path}`: {source}")]
104    ParseKey {
105        /// Path whose PEM failed to parse.
106        path: PathBuf,
107        /// Underlying PEM parse error.
108        source: rustls_pki_types::pem::Error,
109    },
110    /// The private key is unusable, or it does not match the leaf certificate.
111    #[error(
112        "the TLS private key `{key}` is invalid or does not match the leaf certificate `{cert}`: \
113         {source}"
114    )]
115    InvalidKeyPair {
116        /// Certificate path.
117        cert: PathBuf,
118        /// Key path.
119        key: PathBuf,
120        /// Underlying rustls error.
121        source: Box<rustls::Error>,
122    },
123    /// The leaf certificate DER could not be parsed for expiry inspection.
124    #[error("failed to parse the leaf certificate in `{path}` for expiry inspection: {detail}")]
125    ParseLeaf {
126        /// Certificate path.
127        path: PathBuf,
128        /// Human-readable parse detail.
129        detail: String,
130    },
131    /// A certificate in the chain (at 1-based `position`) is not valid DER.
132    ///
133    /// The PEM block decoded, but the bytes are not a parseable X.509
134    /// certificate — a malformed intermediate that rustls would store unparsed
135    /// and serve to clients that reject the chain.
136    #[error("certificate #{position} in the chain in `{path}` is malformed: {detail}")]
137    ParseChainCert {
138        /// Certificate path.
139        path: PathBuf,
140        /// 1-based position of the offending certificate in the chain.
141        position: usize,
142        /// Human-readable parse detail.
143        detail: String,
144    },
145    /// The leaf certificate has already expired.
146    #[error("the leaf certificate in `{path}` expired at {not_after} (UNIX {not_after_unix})")]
147    Expired {
148        /// Certificate path.
149        path: PathBuf,
150        /// RFC 2822-ish rendering of `notAfter`.
151        not_after: String,
152        /// `notAfter` as a UNIX timestamp.
153        not_after_unix: i64,
154    },
155    /// The leaf certificate is not yet valid — its `notBefore` is in the future.
156    #[error(
157        "the leaf certificate in `{path}` is not valid until {not_before} (UNIX {not_before_unix})"
158    )]
159    NotYetValid {
160        /// Certificate path.
161        path: PathBuf,
162        /// RFC 2822-ish rendering of `notBefore`.
163        not_before: String,
164        /// `notBefore` as a UNIX timestamp.
165        not_before_unix: i64,
166    },
167    /// A non-leaf certificate in the chain (at 1-based `position`) has already
168    /// expired. Normal TLS clients reject the whole chain during path
169    /// validation, so refuse it at load/inspection time rather than boot a
170    /// listener that serves it. Symmetric to the leaf's [`Self::Expired`].
171    #[error(
172        "certificate #{position} in the chain in `{path}` expired at {not_after} \
173         (UNIX {not_after_unix})"
174    )]
175    ExpiredChainCert {
176        /// Certificate path.
177        path: PathBuf,
178        /// 1-based position of the offending certificate in the chain.
179        position: usize,
180        /// RFC 2822-ish rendering of `notAfter`.
181        not_after: String,
182        /// `notAfter` as a UNIX timestamp.
183        not_after_unix: i64,
184    },
185    /// A non-leaf certificate in the chain (at 1-based `position`) is not yet
186    /// valid — its `notBefore` is in the future. Symmetric to the leaf's
187    /// [`Self::NotYetValid`].
188    #[error(
189        "certificate #{position} in the chain in `{path}` is not yet valid until {not_before} \
190         (UNIX {not_before_unix})"
191    )]
192    NotYetValidChainCert {
193        /// Certificate path.
194        path: PathBuf,
195        /// 1-based position of the offending certificate in the chain.
196        position: usize,
197        /// RFC 2822-ish rendering of `notBefore`.
198        not_before: String,
199        /// `notBefore` as a UNIX timestamp.
200        not_before_unix: i64,
201    },
202    /// Building the rustls `ServerConfig` failed.
203    #[error("failed to build the rustls server configuration: {source}")]
204    BuildConfig {
205        /// Underlying rustls error.
206        source: rustls::Error,
207    },
208    /// The mTLS client-CA bundle could not be read (issue #1640).
209    #[error("failed to read the mTLS client CA bundle `{path}`: {source}")]
210    ReadClientCa {
211        /// Path that failed to read.
212        path: PathBuf,
213        /// Underlying I/O error.
214        source: std::io::Error,
215    },
216    /// The mTLS client-CA bundle is not parseable PEM.
217    #[error("failed to parse a PEM certificate in the mTLS client CA bundle `{path}`: {source}")]
218    ParseClientCa {
219        /// Path whose PEM failed to parse.
220        path: PathBuf,
221        /// Underlying PEM parse error.
222        source: rustls_pki_types::pem::Error,
223    },
224    /// The mTLS client-CA bundle contains no certificate.
225    #[error(
226        "no CAs found in the mTLS client CA bundle `{path}` (expected at least one PEM \
227         CERTIFICATE block)"
228    )]
229    NoClientCas {
230        /// Path that contained no certificate.
231        path: PathBuf,
232    },
233    /// A certificate in the bundle (at 1-based `position`) is not usable as a
234    /// trust anchor.
235    #[error(
236        "CA #{position} in the mTLS client CA bundle `{path}` is not a valid trust anchor: {source}"
237    )]
238    InvalidClientCa {
239        /// Bundle path.
240        path: PathBuf,
241        /// 1-based position of the offending certificate in the bundle.
242        position: usize,
243        /// Underlying rustls error.
244        source: Box<rustls::Error>,
245    },
246    /// The mTLS revocation list could not be read.
247    #[error("failed to read the mTLS revocation list `{path}`: {source}")]
248    ReadCrl {
249        /// Path that failed to read.
250        path: PathBuf,
251        /// Underlying I/O error.
252        source: std::io::Error,
253    },
254    /// The mTLS revocation list is not parseable PEM.
255    #[error("failed to parse a PEM block in the mTLS revocation list `{path}`: {source}")]
256    ParseCrl {
257        /// Path whose PEM failed to parse.
258        path: PathBuf,
259        /// Underlying PEM parse error.
260        source: rustls_pki_types::pem::Error,
261    },
262    /// The mTLS revocation list contains no CRL.
263    #[error("no revocation list found in `{path}` (expected at least one PEM X509 CRL block)")]
264    NoCrls {
265        /// Path that contained no CRL.
266        path: PathBuf,
267    },
268    /// A CRL in the file (at 1-based `position`) is not valid DER.
269    #[error("CRL #{position} in `{path}` is malformed: {detail}")]
270    ParseCrlDer {
271        /// Revocation-list path.
272        path: PathBuf,
273        /// 1-based position of the offending CRL in the file.
274        position: usize,
275        /// Human-readable parse detail.
276        detail: String,
277    },
278    /// The mTLS revocation list does not cover every CA in the client-CA
279    /// bundle: no CRL in the file is issued by one or more of the bundle's
280    /// CAs (issue #2706).
281    ///
282    /// Once any CRL is configured, rustls denies handshakes whose revocation
283    /// status is *unknown*, so clients presenting certificates issued by an
284    /// uncovered CA are refused even though their CA is trusted — the
285    /// availability trap in the CA rotation `docs/guide/tls.md` documents
286    /// (old + new CA in one bundle, CRL published for the old one first).
287    /// Publish a CRL for each CA in the bundle (or remove the uncovered CA)
288    /// before starting with a CRL configured.
289    #[error(
290        "the mTLS revocation list `{crl_path}` has no CRL issued by {uncovered:?} \
291         from the client CA bundle `{ca_bundle_path}`; once any CRL is configured, \
292         rustls refuses handshakes whose revocation status is unknown, so these \
293         CAs' clients would be rejected — publish a CRL for each CA in the \
294         bundle or remove the uncovered CA"
295    )]
296    CrlCoverageGap {
297        /// CA bundle path.
298        ca_bundle_path: PathBuf,
299        /// CRL path.
300        crl_path: PathBuf,
301        /// Subject DNs of the CAs no CRL in the file is issued by.
302        uncovered: Vec<String>,
303    },
304    /// Building the rustls client-certificate verifier failed.
305    #[error("failed to build the mTLS client certificate verifier: {source}")]
306    BuildClientVerifier {
307        /// Underlying rustls error.
308        source: rustls::server::VerifierBuilderError,
309    },
310    /// A verified peer certificate could not be parsed into an identity.
311    #[error("failed to parse the verified client certificate: {detail}")]
312    ParsePeerCert {
313        /// Human-readable parse detail.
314        detail: String,
315    },
316}
317
318/// Mutual-TLS client-certificate verification (issue #1640).
319pub mod client_auth;
320
321/// The `ring` crypto provider used for all inbound TLS. Built once per call;
322/// callers that build many configs should cache the returned `Arc`.
323#[must_use]
324pub fn crypto_provider() -> Arc<CryptoProvider> {
325    Arc::new(rustls::crypto::ring::default_provider())
326}
327
328/// Read and PEM-decode the certificate chain at `cert_path`.
329fn read_cert_chain(cert_path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError> {
330    let pem = std::fs::read(cert_path).map_err(|source| TlsError::ReadCert {
331        path: cert_path.to_path_buf(),
332        source,
333    })?;
334    let mut chain = Vec::new();
335    for cert in CertificateDer::pem_slice_iter(&pem) {
336        let cert = cert.map_err(|source| TlsError::ParseCert {
337            path: cert_path.to_path_buf(),
338            source,
339        })?;
340        chain.push(cert);
341    }
342    if chain.is_empty() {
343        return Err(TlsError::NoCertificates {
344            path: cert_path.to_path_buf(),
345        });
346    }
347    Ok(chain)
348}
349
350/// Read and PEM-decode the private key at `key_path`.
351fn read_private_key(key_path: &Path) -> Result<PrivateKeyDer<'static>, TlsError> {
352    let pem = std::fs::read(key_path).map_err(|source| TlsError::ReadKey {
353        path: key_path.to_path_buf(),
354        source,
355    })?;
356    PrivateKeyDer::from_pem_slice(&pem).map_err(|source| TlsError::ParseKey {
357        path: key_path.to_path_buf(),
358        source,
359    })
360}
361
362/// The `notBefore` and `notAfter` of the leaf certificate, as UNIX timestamps
363/// (seconds), returned as `(not_before, not_after)`.
364///
365/// Parses just enough of the DER to read the validity window; a parse failure
366/// is surfaced rather than silently ignored so a corrupt certificate is caught
367/// at load time.
368fn leaf_validity_unix(cert_path: &Path, leaf: &CertificateDer<'_>) -> Result<(i64, i64), TlsError> {
369    use x509_parser::prelude::FromDer as _;
370
371    let (_, parsed) =
372        x509_parser::certificate::X509Certificate::from_der(leaf.as_ref()).map_err(|e| {
373            TlsError::ParseLeaf {
374                path: cert_path.to_path_buf(),
375                detail: e.to_string(),
376            }
377        })?;
378    let validity = parsed.validity();
379    Ok((
380        validity.not_before.timestamp(),
381        validity.not_after.timestamp(),
382    ))
383}
384
385/// Parse and lifetime-check every NON-leaf certificate DER in `chain`, failing
386/// fast (naming the 1-based chain position) if any block is not a parseable
387/// X.509 certificate OR is outside its validity window at `now_unix`.
388///
389/// [`read_cert_chain`] only PEM-decodes each block, and rustls'
390/// [`CertifiedKey::from_der`] validates only the leaf (`chain[0]`). A malformed
391/// — or an expired / not-yet-valid — INTERMEDIATE would therefore be stored
392/// unchecked and served, so startup and `autumn doctor` pass while normal TLS
393/// clients reject the chain during path validation. Parsing and lifetime-
394/// checking each intermediate here turns that into an actionable fail-fast at
395/// load/inspection time. The leaf (`chain[0]`) is parsed and lifetime-checked
396/// separately (by [`leaf_validity_unix`] and its callers).
397///
398/// `now_unix` is the current UNIX time; it is a parameter (rather than read
399/// internally) so tests can pin "now" deterministically.
400fn validate_chain_certs(
401    cert_path: &Path,
402    chain: &[CertificateDer<'_>],
403    now_unix: i64,
404) -> Result<(), TlsError> {
405    use x509_parser::prelude::FromDer as _;
406
407    // Skip index 0 (the leaf); its DER and validity window are checked by the
408    // callers via `leaf_validity_unix`.
409    for (idx, cert) in chain.iter().enumerate().skip(1) {
410        // 1-based, matching how operators count "certificate #2" in a bundle.
411        let position = idx + 1;
412        let (_, parsed) = x509_parser::certificate::X509Certificate::from_der(cert.as_ref())
413            .map_err(|e| TlsError::ParseChainCert {
414                path: cert_path.to_path_buf(),
415                position,
416                detail: e.to_string(),
417            })?;
418
419        // Reject an intermediate outside its validity window, mirroring the
420        // leaf's not-yet-valid / expired handling: `notBefore` in the future is
421        // as fatal as a past `notAfter`, since either makes clients reject the
422        // served chain.
423        let validity = parsed.validity();
424        let not_before = validity.not_before.timestamp();
425        let not_after = validity.not_after.timestamp();
426        if now_unix < not_before {
427            return Err(TlsError::NotYetValidChainCert {
428                path: cert_path.to_path_buf(),
429                position,
430                not_before: render_unix(not_before),
431                not_before_unix: not_before,
432            });
433        }
434        if not_after < now_unix {
435            return Err(TlsError::ExpiredChainCert {
436                path: cert_path.to_path_buf(),
437                position,
438                not_after: render_unix(not_after),
439                not_after_unix: not_after,
440            });
441        }
442    }
443    Ok(())
444}
445
446/// Load, validate, and return the certificate + key as a rustls [`CertifiedKey`].
447///
448/// Fails fast on any of: missing/unreadable file, unparseable PEM, an empty
449/// certificate file, a private key that does not match the leaf certificate, or
450/// an already-expired leaf certificate.
451///
452/// `now_unix` is the current UNIX time; it is a parameter (rather than read
453/// internally) so tests can pin "now" deterministically.
454///
455/// # Errors
456///
457/// Returns a [`TlsError`] describing the first problem encountered.
458pub fn load_certified_key(
459    cert_path: &Path,
460    key_path: &Path,
461    provider: &CryptoProvider,
462    now_unix: i64,
463) -> Result<Arc<CertifiedKey>, TlsError> {
464    let chain = read_cert_chain(cert_path)?;
465    let key = read_private_key(key_path)?;
466
467    // Reject a leaf outside its validity window before we even build the key:
468    // an expired OR not-yet-valid certificate fails every client handshake, so
469    // refuse at startup with a clear message rather than booting into a broken
470    // listener. Symmetric bounds: `notBefore` in the future is as fatal as a
471    // past `notAfter`.
472    let (not_before, not_after) = leaf_validity_unix(cert_path, &chain[0])?;
473    if now_unix < not_before {
474        return Err(TlsError::NotYetValid {
475            path: cert_path.to_path_buf(),
476            not_before: render_unix(not_before),
477            not_before_unix: not_before,
478        });
479    }
480    if not_after < now_unix {
481        return Err(TlsError::Expired {
482            path: cert_path.to_path_buf(),
483            not_after: render_unix(not_after),
484            not_after_unix: not_after,
485        });
486    }
487
488    // Parse and lifetime-check every intermediate before accepting the chain:
489    // rustls validates only the leaf, so a malformed — or expired / not-yet-valid
490    // — intermediate would otherwise boot a listener that serves a chain normal
491    // clients reject during path validation.
492    validate_chain_certs(cert_path, &chain, now_unix)?;
493
494    // `from_der` loads the key with the crypto provider (rejecting an invalid
495    // key) and compares the key's SubjectPublicKeyInfo against the leaf
496    // certificate's, so a cert/key mismatch is caught here.
497    let certified = CertifiedKey::from_der(chain, key, provider).map_err(|source| {
498        TlsError::InvalidKeyPair {
499            cert: cert_path.to_path_buf(),
500            key: key_path.to_path_buf(),
501            source: Box::new(source),
502        }
503    })?;
504
505    Ok(Arc::new(certified))
506}
507
508/// Render a UNIX timestamp as a UTC string for error messages. Falls back to
509/// the raw timestamp if it is out of range for the formatter.
510fn render_unix(secs: i64) -> String {
511    x509_parser::time::ASN1Time::from_timestamp(secs)
512        .ok()
513        .map_or_else(|| format!("UNIX {secs}"), |t| t.to_string())
514}
515
516/// A [`ResolvesServerCert`] whose certificate can be swapped at runtime.
517///
518/// Every TLS handshake takes a short read lock to clone the current
519/// `Arc<CertifiedKey>`; a reload swaps in a new `Arc` under a brief write lock.
520/// Readers never block each other, and a reload never interrupts an in-flight
521/// handshake — it only affects handshakes that start after the swap.
522#[derive(Debug)]
523pub struct ReloadableCertResolver {
524    current: RwLock<Arc<CertifiedKey>>,
525}
526
527impl ReloadableCertResolver {
528    /// Create a resolver serving `initial`.
529    #[must_use]
530    pub const fn new(initial: Arc<CertifiedKey>) -> Self {
531        Self {
532            current: RwLock::new(initial),
533        }
534    }
535
536    /// Atomically replace the served certificate.
537    pub fn store(&self, next: Arc<CertifiedKey>) {
538        let mut guard = self
539            .current
540            .write()
541            .unwrap_or_else(std::sync::PoisonError::into_inner);
542        *guard = next;
543    }
544
545    /// Snapshot the currently served certificate.
546    #[must_use]
547    pub fn current(&self) -> Arc<CertifiedKey> {
548        let guard = self
549            .current
550            .read()
551            .unwrap_or_else(std::sync::PoisonError::into_inner);
552        Arc::clone(&guard)
553    }
554}
555
556impl ResolvesServerCert for ReloadableCertResolver {
557    fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
558        Some(self.current())
559    }
560}
561
562/// Build the rustls [`ServerConfig`](rustls::ServerConfig) that terminates
563/// inbound TLS, backed by `resolver` so the certificate stays swappable.
564///
565/// Advertises ALPN `[b"h2", b"http/1.1"]` (#2321) so browsers negotiate
566/// HTTP/2; ALPN-less and http/1.1-only clients are unaffected.
567///
568/// # Errors
569///
570/// Returns [`TlsError::BuildConfig`] if rustls rejects the chosen protocol
571/// versions for the provider.
572pub fn build_server_config(
573    provider: Arc<CryptoProvider>,
574    resolver: Arc<ReloadableCertResolver>,
575) -> Result<Arc<rustls::ServerConfig>, TlsError> {
576    build_server_config_with_resolver(provider, resolver)
577}
578
579/// [`build_server_config`], for any [`ResolvesServerCert`].
580///
581/// The custom-domain path (#1635) serves a per-SNI resolver rather than the
582/// single swappable certificate, so the listener takes the resolver as a trait
583/// object; everything else about the config is identical — including the
584/// ALPN `[b"h2", b"http/1.1"]` advertisement (#2321).
585///
586/// # Errors
587///
588/// Returns [`TlsError::BuildConfig`] if rustls rejects the chosen protocol
589/// versions for the provider.
590pub fn build_server_config_with_resolver(
591    provider: Arc<CryptoProvider>,
592    resolver: Arc<dyn ResolvesServerCert>,
593) -> Result<Arc<rustls::ServerConfig>, TlsError> {
594    build_server_config_with_client_auth(provider, resolver, None)
595}
596
597/// [`build_server_config_with_resolver`], additionally verifying client
598/// certificates against `client_verifier` (issue #1640).
599///
600/// `None` takes the identical `with_no_client_auth()` path as before, so a
601/// deployment with no `[server.tls.client_auth]` section handshakes exactly as
602/// it did under #1603 — apart from the ALPN `[b"h2", b"http/1.1"]`
603/// advertisement (#2321), which both arms set identically.
604///
605/// # Errors
606///
607/// Returns [`TlsError::BuildConfig`] if rustls rejects the chosen protocol
608/// versions for the provider.
609pub fn build_server_config_with_client_auth(
610    provider: Arc<CryptoProvider>,
611    resolver: Arc<dyn ResolvesServerCert>,
612    client_verifier: Option<Arc<dyn rustls::server::danger::ClientCertVerifier>>,
613) -> Result<Arc<rustls::ServerConfig>, TlsError> {
614    let builder = rustls::ServerConfig::builder_with_provider(provider)
615        .with_safe_default_protocol_versions()
616        .map_err(|source| TlsError::BuildConfig { source })?;
617    let mut config = match client_verifier {
618        Some(verifier) => {
619            let mut config = builder
620                .with_client_cert_verifier(verifier)
621                .with_cert_resolver(resolver);
622            // Turn OFF session resumption for a client-authenticating listener.
623            //
624            // rustls restores a resumed connection's `peer_certificates` from
625            // the stored session and never calls the verifier again
626            // (`server/tls13.rs`, `server/tls12.rs`). So a client whose CA was
627            // rotated out — or whose certificate was just added to the CRL —
628            // would keep reconnecting on a resumed session until it expired,
629            // and would keep presenting a verified-looking identity to
630            // handlers. That is the one hole a swap-the-verifier design cannot
631            // close by swapping, because the check it swaps is not run.
632            //
633            // The cost is a full handshake per connection, which is the right
634            // trade for a listener whose whole purpose is deciding who may
635            // connect. Server-only TLS keeps resumption untouched.
636            config.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
637            config.send_tls13_tickets = 0;
638            config
639        }
640        None => builder.with_no_client_auth().with_cert_resolver(resolver),
641    };
642    // Advertise ALPN (#2321). Without an `alpn_protocols` list rustls
643    // completes the handshake with no protocol selected, so a browser (or
644    // `curl --http2`) never sends the HTTP/2 preface and the serve path's
645    // h2 half — `hyper_util::server::conn::auto` already speaks it — stays
646    // dead code in practice. `h2` first, then `http/1.1`: ALPN-less and
647    // http/1.1-only clients are unaffected, and a client offering only `h2`
648    // negotiates it. Both TLS modes funnel through here (static
649    // `[server.tls]` and the ACME path, #1608), as do both client-auth arms
650    // above, so the advertisement is identical everywhere.
651    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
652    Ok(Arc::new(config))
653}
654
655/// Connection info for a request served over the mTLS-capable HTTPS listener
656/// (issue #1640).
657///
658/// axum's `into_make_service_with_connect_info::<C>` requires `C:
659/// Connected<IncomingStream>`; this carries the peer `SocketAddr` — so the rest
660/// of the serve stack behaves exactly as on plain TCP — plus the verified
661/// client identity, when the handshake produced one.
662///
663/// The identity is parsed once per *connection*, not per request.
664#[derive(Clone, Debug)]
665pub struct TlsConnectInfo {
666    /// The peer's TCP address, identical to the plain-TCP path's connect info.
667    pub peer: std::net::SocketAddr,
668    /// The verified client identity, when the peer presented a certificate that
669    /// passed verification. Always `None` on a listener with client auth off.
670    pub client: Option<Arc<client_auth::ClientIdentity>>,
671}
672
673impl axum::extract::connect_info::Connected<axum::serve::IncomingStream<'_, TlsListener>>
674    for TlsConnectInfo
675{
676    fn connect_info(stream: axum::serve::IncomingStream<'_, TlsListener>) -> Self {
677        Self::from_stream(stream.io(), *stream.remote_addr())
678    }
679}
680
681/// The HTTPS serve path wraps its listener in `StopAcceptingOnShutdown` (see
682/// `accept_drain`), so the connect info must be available for the wrapper too.
683impl
684    axum::extract::connect_info::Connected<
685        axum::serve::IncomingStream<'_, crate::accept_drain::StopAcceptingOnShutdown<TlsListener>>,
686    > for TlsConnectInfo
687{
688    fn connect_info(
689        stream: axum::serve::IncomingStream<
690            '_,
691            crate::accept_drain::StopAcceptingOnShutdown<TlsListener>,
692        >,
693    ) -> Self {
694        Self::from_stream(stream.io(), *stream.remote_addr())
695    }
696}
697
698impl
699    axum::extract::connect_info::Connected<
700        crate::http_server::IncomingStream<
701            '_,
702            crate::accept_drain::StopAcceptingOnShutdown<TlsListener>,
703        >,
704    > for TlsConnectInfo
705{
706    fn connect_info(
707        stream: crate::http_server::IncomingStream<
708            '_,
709            crate::accept_drain::StopAcceptingOnShutdown<TlsListener>,
710        >,
711    ) -> Self {
712        Self::from_stream(stream.io(), *stream.remote_addr())
713    }
714}
715
716impl TlsConnectInfo {
717    fn from_stream(
718        io: &tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
719        peer: std::net::SocketAddr,
720    ) -> Self {
721        // rustls exposes the peer chain only after a successful handshake, so
722        // anything here has already passed the configured verifier — the parse
723        // turns a verified certificate into a usable identity, it does not
724        // decide trust.
725        let client = io
726            .get_ref()
727            .1
728            .peer_certificates()
729            .and_then(<[rustls_pki_types::CertificateDer<'_>]>::first)
730            .and_then(|leaf| match client_auth::ClientIdentity::from_der(leaf) {
731                Ok(identity) => Some(Arc::new(identity)),
732                Err(e) => {
733                    // Verified but unparseable: drop the identity rather than
734                    // fabricate one. Routes that require mTLS then reject.
735                    tracing::warn!(peer = %peer, error = %e, "could not parse the verified client certificate");
736                    None
737                }
738            });
739        Self { peer, client }
740    }
741}
742
743/// Upper bound on TLS handshakes running concurrently at any instant.
744///
745/// The background acceptor task acquires a permit from a semaphore of this size
746/// before spawning each handshake, so a flood of connecting clients can never
747/// spawn an unbounded number of handshake tasks: once this many are in flight
748/// the acceptor parks on the next permit (still draining the kernel accept
749/// queue's backlog as permits free up). 256 is a generous default for a single
750/// listener; it could be made configurable later if a deployment needs to tune
751/// the in-flight-handshake ceiling.
752const MAX_CONCURRENT_HANDSHAKES: usize = 256;
753
754/// A completed TLS stream, its peer, and its connection slot (when capped).
755type ReadyConn = (
756    tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
757    std::net::SocketAddr,
758    Option<crate::http_server::Slot>,
759);
760
761/// Capacity of the channel carrying completed TLS streams to `accept`.
762///
763/// Bounded so a burst of successful handshakes that outruns axum's consumption
764/// applies backpressure (a completed-handshake task parks on `send` while still
765/// holding its semaphore permit) rather than buffering without limit.
766const READY_CONN_CHANNEL_CAPACITY: usize = 1024;
767
768/// A TLS-terminating [`axum::serve::Listener`] wrapping a
769/// [`tokio::net::TcpListener`].
770///
771/// [`accept`](axum::serve::Listener::accept) yields a decrypted
772/// [`TlsStream`](tokio_rustls::server::TlsStream) plus the peer's
773/// [`SocketAddr`](std::net::SocketAddr). Because the peer address is a real TCP
774/// `SocketAddr`, the rest of the serve stack — connect-info, trusted-proxy
775/// resolution, graceful shutdown, SSE/WebSocket streaming — is identical to the
776/// plain-TCP path; the only difference is the handshake performed here.
777///
778/// TCP-accept and the rustls handshake are **decoupled**: a background acceptor
779/// task drains `tcp.accept()` and, for each connection, spawns a bounded
780/// handshake task (bounded by this module's `MAX_CONCURRENT_HANDSHAKES`, which
781/// is private) that performs the rustls
782/// handshake and forwards the finished stream over a channel to `accept`. This
783/// means a flood of silent or stalled clients cannot serialize the accept loop:
784/// a client that opens TCP but never completes (or even starts) the handshake
785/// occupies only its own handshake task (bounded by `handshake_timeout`), never
786/// head-of-line-blocking the acceptance of the next connection. A failed
787/// handshake (a plaintext or malformed client, an unsupported cipher, a dropped
788/// connection) is logged at debug and dropped; it never affects the acceptor.
789pub struct TlsListener {
790    /// Completed streams from the background handshake tasks.
791    rx: tokio::sync::mpsc::Receiver<ReadyConn>,
792    /// Starts the acceptor, with the `server.http.max_connections` slots
793    /// when set. Until then no TCP connection is accepted, so none can
794    /// escape a cap installed after `new` (issue #3065). `None` once sent.
795    start: Option<tokio::sync::oneshot::Sender<Option<Arc<tokio::sync::Semaphore>>>>,
796    /// The `max_connections` slots `limit_connections` installed. The first
797    /// `accept` starts the acceptor with them.
798    cap: Option<Arc<tokio::sync::Semaphore>>,
799    /// Where `accept` puts the slot of the stream it returns.
800    handoff: Option<crate::http_server::SlotHandoff>,
801    /// The bound address, captured before `tcp` moved into the acceptor task.
802    local_addr: std::net::SocketAddr,
803    /// Shutdown signal; also used to park `accept` once the acceptor has ended
804    /// (channel closed) so axum's own graceful-shutdown future drives teardown.
805    shutdown: tokio_util::sync::CancellationToken,
806    #[cfg(test)]
807    handshakes: Arc<tokio::sync::Semaphore>,
808}
809
810impl TlsListener {
811    /// Wrap `tcp` so accepted connections are TLS-terminated with `config`,
812    /// bounding each handshake by `handshake_timeout` (a stalled or silent
813    /// client is dropped rather than starving other clients). `shutdown` ties
814    /// the background acceptor task's lifetime to server shutdown.
815    ///
816    /// The acceptor starts at the first [`accept`](axum::serve::Listener::accept).
817    /// Until then clients wait in the kernel backlog.
818    ///
819    /// # Panics
820    ///
821    /// Panics if `tcp.local_addr()` fails — the listener is already bound, so
822    /// this is not expected in practice.
823    #[must_use]
824    pub fn new(
825        tcp: tokio::net::TcpListener,
826        config: Arc<rustls::ServerConfig>,
827        handshake_timeout: std::time::Duration,
828        shutdown: tokio_util::sync::CancellationToken,
829    ) -> Self {
830        let local_addr = tcp
831            .local_addr()
832            .expect("bound TLS listener must have a local address");
833        let acceptor = tokio_rustls::TlsAcceptor::from(config);
834        let (tx, rx) = tokio::sync::mpsc::channel(READY_CONN_CHANNEL_CAPACITY);
835        let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_HANDSHAKES));
836        let (start, started) = tokio::sync::oneshot::channel();
837        let acceptor_shutdown = shutdown.clone();
838        #[cfg(test)]
839        let handshakes = Arc::clone(&semaphore);
840
841        tokio::spawn(async move {
842            let cap = tokio::select! {
843                () = acceptor_shutdown.cancelled() => return,
844                cap = started => match cap {
845                    Ok(cap) => cap,
846                    // The listener was dropped before it accepted.
847                    Err(_) => return,
848                },
849            };
850            run_acceptor(
851                tcp,
852                acceptor,
853                handshake_timeout,
854                semaphore,
855                cap,
856                tx,
857                acceptor_shutdown,
858            )
859            .await;
860        });
861
862        Self {
863            rx,
864            start: Some(start),
865            cap: None,
866            handoff: None,
867            local_addr,
868            shutdown,
869            #[cfg(test)]
870            handshakes,
871        }
872    }
873
874    /// Count at most `max` connections, from TCP accept to close. The
875    /// acceptor still starts at the first `accept`, so an upgrade successor
876    /// takes no connection before its server runs. Returns where `accept`
877    /// puts the slot of each stream, for the serve loop. `None` when the
878    /// acceptor already started: the cap can no longer cover every
879    /// connection, so the serve loop keeps its own.
880    pub(crate) fn limit_connections(
881        &mut self,
882        max: usize,
883    ) -> Option<crate::http_server::SlotHandoff> {
884        if self.start.is_none() || self.handoff.is_some() {
885            return None;
886        }
887        let max = max.min(tokio::sync::Semaphore::MAX_PERMITS);
888        self.cap = Some(Arc::new(tokio::sync::Semaphore::new(max)));
889        let handoff = crate::http_server::SlotHandoff::default();
890        self.handoff = Some(Arc::clone(&handoff));
891        Some(handoff)
892    }
893
894    #[cfg(test)]
895    fn handshakes_in_flight(&self) -> usize {
896        MAX_CONCURRENT_HANDSHAKES - self.handshakes.available_permits()
897    }
898}
899
900/// Take a connection slot, when the cap is set. `None` on shutdown.
901async fn take_slot(
902    cap: Option<&Arc<tokio::sync::Semaphore>>,
903    shutdown: &tokio_util::sync::CancellationToken,
904) -> Option<Option<crate::http_server::Slot>> {
905    let Some(slots) = cap else {
906        return Some(None);
907    };
908    tokio::select! {
909        () = shutdown.cancelled() => None,
910        permit = Arc::clone(slots).acquire_owned() => {
911            permit.ok().map(|permit| Some(Box::new(permit) as crate::http_server::Slot))
912        }
913    }
914}
915
916/// Background accept loop: drain `tcp`, and for each connection spawn a bounded
917/// handshake task that forwards the completed TLS stream over `tx`. Breaks (and
918/// drops `tx`) on `shutdown`.
919async fn run_acceptor(
920    tcp: tokio::net::TcpListener,
921    acceptor: tokio_rustls::TlsAcceptor,
922    handshake_timeout: std::time::Duration,
923    semaphore: Arc<tokio::sync::Semaphore>,
924    cap: Option<Arc<tokio::sync::Semaphore>>,
925    tx: tokio::sync::mpsc::Sender<ReadyConn>,
926    shutdown: tokio_util::sync::CancellationToken,
927) {
928    loop {
929        // At the connection cap, wait for a free slot before TCP accept.
930        let Some(slot) = take_slot(cap.as_ref(), &shutdown).await else {
931            break;
932        };
933        let (stream, peer) = tokio::select! {
934            () = shutdown.cancelled() => break,
935            result = tcp.accept() => match result {
936                Ok(pair) => pair,
937                Err(e) => {
938                    // Never break the loop on a per-connection error — that
939                    // would tear down the whole listener. Retry transient
940                    // per-connection errors immediately; back off briefly on
941                    // anything else so we do not spin (e.g. on the process's
942                    // open-file limit).
943                    if is_transient_connection_error(&e) {
944                        continue;
945                    }
946                    tracing::error!(error = %e, "TLS listener accept error");
947                    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
948                    continue;
949                }
950            },
951        };
952
953        // Acquire a permit BEFORE spawning so in-flight handshakes are bounded:
954        // once `MAX_CONCURRENT_HANDSHAKES` are running, this parks (applying
955        // backpressure to accept) until one finishes. The only error is a closed
956        // semaphore, which we never close, so the `else` is unreachable in
957        // practice; stop the loop defensively if it ever happens.
958        let Ok(permit) = Arc::clone(&semaphore).acquire_owned().await else {
959            break;
960        };
961        let acceptor = acceptor.clone();
962        let tx = tx.clone();
963        tokio::spawn(async move {
964            // The permit is released when this task ends (handshake done,
965            // failed, or timed out), freeing a slot for the next connection.
966            let _permit = permit;
967            // Bound the handshake so a client that connects but never sends a
968            // ClientHello (or stalls mid-handshake) releases its permit instead
969            // of holding it for the process lifetime.
970            match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
971                Ok(Ok(tls)) => {
972                    // A closed receiver means the listener is gone; drop.
973                    let _ = tx.send((tls, peer, slot)).await;
974                }
975                Ok(Err(e)) => {
976                    // An mTLS client-certificate rejection is an operator-facing
977                    // event: counted by reason and logged at warn, rate-limited
978                    // (#1640). Everything else keeps #1603's quiet debug line.
979                    // The client sees only the standard TLS alert either way.
980                    if !client_auth::record_handshake_rejection(&e, peer) {
981                        tracing::debug!(
982                            peer = %peer,
983                            error = %e,
984                            "TLS handshake failed; dropping connection"
985                        );
986                    }
987                }
988                Err(_elapsed) => {
989                    tracing::debug!(peer = %peer, "TLS handshake timed out");
990                }
991            }
992        });
993    }
994}
995
996/// Whether an accept error is a per-connection condition (the client went away
997/// between the kernel accept and ours) rather than a listener-wide one. These
998/// are retried immediately; other errors (e.g. `EMFILE`) get a short backoff.
999/// Mirrors axum's own built-in `TcpListener` accept behavior.
1000fn is_transient_connection_error(e: &std::io::Error) -> bool {
1001    matches!(
1002        e.kind(),
1003        std::io::ErrorKind::ConnectionRefused
1004            | std::io::ErrorKind::ConnectionAborted
1005            | std::io::ErrorKind::ConnectionReset
1006    )
1007}
1008
1009impl axum::serve::Listener for TlsListener {
1010    type Io = tokio_rustls::server::TlsStream<tokio::net::TcpStream>;
1011    type Addr = std::net::SocketAddr;
1012
1013    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
1014        if let Some(start) = self.start.take() {
1015            // Start the acceptor, with the cap when one was installed.
1016            let _ = start.send(self.cap.take());
1017        }
1018        if let Some((tls, peer, slot)) = self.rx.recv().await {
1019            if let Some(handoff) = &self.handoff {
1020                *handoff
1021                    .lock()
1022                    .unwrap_or_else(std::sync::PoisonError::into_inner) = slot;
1023            }
1024            return (tls, peer);
1025        }
1026        // The acceptor task has ended and the channel is drained (shutdown, or
1027        // an unrecoverable acceptor exit). `Listener::accept` returns no
1028        // `Result` and axum loops on it, so we must neither panic nor busy-loop:
1029        // park here forever (awaiting the shutdown token, then a never-resolving
1030        // future) so axum's own graceful-shutdown path drives teardown instead
1031        // of us spinning.
1032        self.shutdown.cancelled().await;
1033        std::future::pending().await
1034    }
1035
1036    fn local_addr(&self) -> std::io::Result<Self::Addr> {
1037        Ok(self.local_addr)
1038    }
1039}
1040
1041/// The outcome of inspecting a configured certificate + key pair, offline.
1042#[derive(Debug, Clone, Copy)]
1043pub struct LeafInspection {
1044    /// The leaf certificate's `notBefore`, as a UNIX timestamp (seconds).
1045    pub not_before_unix: i64,
1046    /// The leaf certificate's `notAfter`, as a UNIX timestamp (seconds).
1047    pub not_after_unix: i64,
1048}
1049
1050impl LeafInspection {
1051    /// Whether the leaf certificate is not yet valid at `now_unix` — its
1052    /// `notBefore` lies in the future. Such a certificate fails every handshake
1053    /// until its validity window opens, so the runtime rejects it at startup and
1054    /// doctor grades it a failure, symmetric to [`Self::is_expired`].
1055    #[must_use]
1056    pub const fn is_not_yet_valid(&self, now_unix: i64) -> bool {
1057        now_unix < self.not_before_unix
1058    }
1059
1060    /// Whole days from `now_unix` until `notAfter`. Negative once expired.
1061    ///
1062    /// Note: this truncates toward zero, so a certificate that expired less
1063    /// than 24h ago still reports `0` days. Callers grading expiry must use
1064    /// [`Self::is_expired`] for the pass/fail decision rather than relying on
1065    /// this day bucket.
1066    #[must_use]
1067    pub const fn days_until_expiry(&self, now_unix: i64) -> i64 {
1068        (self.not_after_unix - now_unix) / 86_400
1069    }
1070
1071    /// Whether the leaf certificate has already expired at `now_unix`.
1072    ///
1073    /// Decided from the raw `notAfter` timestamp (not the truncated day
1074    /// bucket), so a certificate expired even seconds ago reports `true` —
1075    /// matching the runtime, which rejects such a pair at startup.
1076    #[must_use]
1077    pub const fn is_expired(&self, now_unix: i64) -> bool {
1078        self.not_after_unix <= now_unix
1079    }
1080}
1081
1082/// Validate the configured certificate + key and report the leaf's expiry,
1083/// WITHOUT booting a server or touching the network. Used by `autumn doctor`.
1084///
1085/// This performs the same parsing and cert/key-match validation as
1086/// [`load_certified_key`] (so a broken pair is reported), but tolerates a LEAF
1087/// outside its validity window — already-expired OR not-yet-valid — leaving the
1088/// caller to decide how to grade it, by still returning the `notBefore`/
1089/// `notAfter` bounds. Non-leaf (intermediate) certificates are NOT tolerated:
1090/// an expired or not-yet-valid intermediate is rejected here (mirroring the
1091/// runtime), since clients reject the served chain during path validation.
1092///
1093/// `now_unix` is the current UNIX time; it is a parameter (rather than read
1094/// internally) so tests can pin "now" deterministically. It is used only to
1095/// lifetime-check the intermediates — the leaf's window is returned ungraded.
1096///
1097/// # Errors
1098///
1099/// Returns a [`TlsError`] for a missing/unreadable file, unparseable PEM, an
1100/// empty certificate file, a key that does not match the leaf certificate, or a
1101/// malformed / expired / not-yet-valid intermediate certificate.
1102pub fn inspect_leaf(
1103    cert_path: &Path,
1104    key_path: &Path,
1105    now_unix: i64,
1106) -> Result<LeafInspection, TlsError> {
1107    let chain = read_cert_chain(cert_path)?;
1108    let key = read_private_key(key_path)?;
1109    let (not_before, not_after) = leaf_validity_unix(cert_path, &chain[0])?;
1110
1111    // Parse and lifetime-check every intermediate too, so doctor fails a
1112    // malformed — or expired / not-yet-valid — intermediate chain instead of
1113    // greenlighting one the runtime would boot but clients reject (rustls
1114    // validates only the leaf).
1115    validate_chain_certs(cert_path, &chain, now_unix)?;
1116
1117    // Validate the key matches the leaf even though we do not need the key
1118    // material — a mismatched pair would fail every handshake at runtime, so
1119    // doctor should surface it too.
1120    let provider = crypto_provider();
1121    CertifiedKey::from_der(chain, key, &provider).map_err(|source| TlsError::InvalidKeyPair {
1122        cert: cert_path.to_path_buf(),
1123        key: key_path.to_path_buf(),
1124        source: Box::new(source),
1125    })?;
1126
1127    Ok(LeafInspection {
1128        not_before_unix: not_before,
1129        not_after_unix: not_after,
1130    })
1131}
1132
1133/// Build a rustls [`CertifiedKey`] from an in-memory PEM certificate chain and
1134/// private key, WITHOUT touching the filesystem.
1135///
1136/// Used by the ACME path (issue #1608) to hot-swap a freshly issued certificate
1137/// into a [`ReloadableCertResolver`] without a round-trip through disk, and by
1138/// the custom-domain SNI cache (issue #1635) to parse a per-tenant certificate
1139/// read back from the store. Like
1140/// [`load_certified_key`], `from_der` validates the key and checks it matches
1141/// the leaf; unlike it, this does not reject an expired leaf (the caller — the
1142/// renewal task — decides how to react to a stale cert, and the self-signed
1143/// placeholder it also loads is deliberately short-lived).
1144///
1145/// # Errors
1146///
1147/// Returns a human-readable message if the PEM cannot be parsed or the key does
1148/// not match the leaf certificate.
1149pub fn certified_key_from_pem(
1150    chain_pem: &[u8],
1151    key_pem: &[u8],
1152    provider: &CryptoProvider,
1153) -> Result<Arc<CertifiedKey>, String> {
1154    let mut chain = Vec::new();
1155    for cert in CertificateDer::pem_slice_iter(chain_pem) {
1156        chain.push(cert.map_err(|e| format!("failed to parse ACME certificate PEM: {e}"))?);
1157    }
1158    if chain.is_empty() {
1159        return Err("ACME certificate chain contained no PEM CERTIFICATE blocks".to_owned());
1160    }
1161    let key = PrivateKeyDer::from_pem_slice(key_pem)
1162        .map_err(|e| format!("failed to parse ACME private key PEM: {e}"))?;
1163    let certified = CertifiedKey::from_der(chain, key, provider)
1164        .map_err(|e| format!("ACME private key does not match the issued certificate: {e}"))?;
1165    Ok(Arc::new(certified))
1166}
1167
1168/// The leaf certificate's `notAfter` (UNIX seconds) from an in-memory PEM chain.
1169///
1170/// Used by the ACME renewal loop and health indicator — and by the per-domain
1171/// custom-domain renewal scheduler (#1635) — to decide when a stored
1172/// certificate is due for renewal. Returns an error message if the PEM has no
1173/// certificate or the leaf cannot be parsed.
1174///
1175/// # Errors
1176///
1177/// Returns a human-readable message on a missing or unparseable leaf.
1178pub fn leaf_not_after_from_pem(chain_pem: &[u8]) -> Result<i64, String> {
1179    let leaf = CertificateDer::pem_slice_iter(chain_pem)
1180        .next()
1181        .ok_or_else(|| "certificate chain contained no PEM CERTIFICATE blocks".to_owned())?
1182        .map_err(|e| format!("failed to parse certificate PEM: {e}"))?;
1183    leaf_validity_unix(Path::new("<memory>"), &leaf)
1184        .map(|(_, not_after)| not_after)
1185        .map_err(|e| e.to_string())
1186}
1187
1188/// Wall-clock seconds since the epoch — the reference instant certificate
1189/// validity is judged against, and the one deliberate real-time read in this
1190/// module.
1191///
1192/// Deliberately **real** time, not the injected clock: a certificate's
1193/// `notBefore`/`notAfter` are facts about the real world, so a test or
1194/// simulation clock pinned to the sim epoch must never be able to declare a
1195/// live certificate not-yet-valid (or an expired one fine).
1196#[allow(
1197    clippy::disallowed_methods,
1198    reason = "TLS certificate validity is judged against real wall time by \
1199              design — see this function's doc comment. Injecting a virtual \
1200              clock here would let a simulation misjudge a real certificate."
1201)]
1202pub(crate) fn now_unix() -> i64 {
1203    std::time::SystemTime::now()
1204        .duration_since(std::time::UNIX_EPOCH)
1205        .map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
1206}
1207
1208/// Modification times of the cert and key files, `None` for a file that could
1209/// not be stat'd. Reloads trigger on any change to this pair.
1210fn file_mtimes(
1211    cert: &Path,
1212    key: &Path,
1213) -> (Option<std::time::SystemTime>, Option<std::time::SystemTime>) {
1214    let mtime = |p: &Path| std::fs::metadata(p).and_then(|m| m.modified()).ok();
1215    (mtime(cert), mtime(key))
1216}
1217
1218/// The background certificate hot-reloader (issue #1603).
1219///
1220/// Polls the cert/key file mtimes and swaps the served certificate in place
1221/// when either changes, so a `certbot` / ACME renewal is picked up **without a
1222/// restart and without dropping the site**.
1223///
1224/// Never breaks the listener: a failed reload logs an error, keeps the
1225/// previously loaded certificate, and retries on the next tick. The baseline
1226/// mtimes only advance on a *successful* load, so a partial write observed
1227/// mid-renewal is retried rather than skipped.
1228///
1229/// `app.rs` constructs one of these when `[server.tls]` names a static cert and
1230/// spawns [`run`](Self::run) as a child of the server's shutdown token.
1231pub struct CertReloader {
1232    resolver: Arc<ReloadableCertResolver>,
1233    provider: Arc<CryptoProvider>,
1234    cert_path: PathBuf,
1235    key_path: PathBuf,
1236    interval: std::time::Duration,
1237    /// Mtimes of the cert/key as of just *before* the load whose certificate
1238    /// the resolver serves (see [`load`](Self::load)). Captured there rather
1239    /// than on the loop's first tick so a renewal landing anywhere between the
1240    /// load and the first poll is still seen as a change.
1241    baseline: (Option<std::time::SystemTime>, Option<std::time::SystemTime>),
1242}
1243
1244impl CertReloader {
1245    /// Load the certificate and key, build the resolver that will serve them,
1246    /// and build the reloader that watches them — deliberately one operation,
1247    /// in that order.
1248    ///
1249    /// The baseline mtimes are stat'd **before** the load. Any other ordering
1250    /// loses a renewal: stat after loading and a renewal that lands in the gap
1251    /// is recorded as the baseline while the resolver still holds the
1252    /// superseded certificate, so every later poll sees "no change" and the old
1253    /// certificate is served until the *next* renewal. Stat-first can only ever
1254    /// cost a redundant reload of a certificate already in hand.
1255    ///
1256    /// `interval` is the mtime poll period (see
1257    /// [`DEFAULT_RELOAD_INTERVAL_SECS`]). A zero interval would busy-loop, so
1258    /// [`run`](Self::run) substitutes the default for it rather than spinning.
1259    ///
1260    /// # Errors
1261    ///
1262    /// Returns the same [`TlsError`] as
1263    /// [`load_certified_key`](crate::tls::load_certified_key): a missing or
1264    /// unreadable file, unparseable or empty PEM, a key that does not match the
1265    /// leaf, or an expired / not-yet-valid certificate in the chain.
1266    pub fn load(
1267        cert_path: PathBuf,
1268        key_path: PathBuf,
1269        provider: Arc<CryptoProvider>,
1270        now_unix_secs: i64,
1271        interval: std::time::Duration,
1272    ) -> Result<(Arc<ReloadableCertResolver>, Self), TlsError> {
1273        let baseline = file_mtimes(&cert_path, &key_path);
1274        let certified = load_certified_key(&cert_path, &key_path, &provider, now_unix_secs)?;
1275        let resolver = Arc::new(ReloadableCertResolver::new(certified));
1276        let reloader = Self {
1277            resolver: Arc::clone(&resolver),
1278            provider,
1279            cert_path,
1280            key_path,
1281            interval,
1282            baseline,
1283        };
1284        Ok((resolver, reloader))
1285    }
1286
1287    /// Run the poll loop until `shutdown` is cancelled.
1288    pub async fn run(self, shutdown: tokio_util::sync::CancellationToken) {
1289        // A zero interval would spin the loop (and its two `spawn_blocking`
1290        // stats per tick) as fast as the runtime allows. `app.rs` clamps
1291        // `reload_interval_secs` before constructing this, but the type is
1292        // public, so enforce the invariant where it belongs instead of trusting
1293        // every caller to know it.
1294        let interval = if self.interval.is_zero() {
1295            std::time::Duration::from_secs(DEFAULT_RELOAD_INTERVAL_SECS)
1296        } else {
1297            self.interval
1298        };
1299        // Stat and PEM-read the cert/key on a blocking thread — both touch the
1300        // filesystem and must not run on a tokio worker. On a `JoinError` (the
1301        // blocking pool shutting down) just skip the tick and retry next time.
1302        let stat_mtimes = |cert: PathBuf, key: PathBuf| {
1303            crate::time::spawn_blocking(move || file_mtimes(&cert, &key))
1304        };
1305
1306        // The baseline was taken when the served certificate was loaded, so a
1307        // renewal between then and the first tick below is a change, not the
1308        // status quo.
1309        let mut last = self.baseline;
1310        loop {
1311            tokio::select! {
1312                () = tokio::time::sleep(interval) => {}
1313                () = shutdown.cancelled() => break,
1314            }
1315
1316            let current = match stat_mtimes(self.cert_path.clone(), self.key_path.clone()).await {
1317                Ok(mtimes) => mtimes,
1318                Err(e) => {
1319                    tracing::warn!(error = %e, "TLS reload: mtime read task failed; skipping tick");
1320                    continue;
1321                }
1322            };
1323            if current == last {
1324                continue;
1325            }
1326
1327            let cert_path = self.cert_path.clone();
1328            let key_path = self.key_path.clone();
1329            let provider = Arc::clone(&self.provider);
1330            let loaded = crate::time::spawn_blocking(move || {
1331                load_certified_key(&cert_path, &key_path, &provider, now_unix())
1332            })
1333            .await;
1334            let loaded = match loaded {
1335                Ok(result) => result,
1336                Err(e) => {
1337                    tracing::warn!(error = %e, "TLS reload: load task failed; skipping tick");
1338                    continue;
1339                }
1340            };
1341
1342            match loaded {
1343                Ok(next) => {
1344                    self.resolver.store(next);
1345                    // Only advance the baseline on a successful load, so a
1346                    // partial write observed mid-renewal is retried on the
1347                    // next tick.
1348                    last = current;
1349                    tracing::info!(
1350                        cert = %self.cert_path.display(),
1351                        "Reloaded TLS certificate after detecting a change on disk"
1352                    );
1353                }
1354                Err(e) => {
1355                    tracing::error!(
1356                        error = %e,
1357                        cert = %self.cert_path.display(),
1358                        "TLS certificate reload failed; keeping the previously loaded certificate"
1359                    );
1360                }
1361            }
1362        }
1363    }
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368    use super::*;
1369    use std::io::Write as _;
1370
1371    // Self-signed `CN=localhost`, valid until 2126. Test fixture only — the key
1372    // pair is intentionally public (mirrors `tests/integration/pg_tls.rs`).
1373    const CERT_PEM: &str = include_str!("../tests/fixtures/tls/localhost.cert.pem");
1374    const KEY_PEM: &str = include_str!("../tests/fixtures/tls/localhost.key.pem");
1375    // A second, unrelated self-signed key (does not match `CERT_PEM`).
1376    const MISMATCHED_KEY_PEM: &str = include_str!("../tests/fixtures/tls/other.key.pem");
1377    // Self-signed `CN=localhost` that expired in 2021.
1378    const EXPIRED_CERT_PEM: &str = include_str!("../tests/fixtures/tls/expired.cert.pem");
1379    const EXPIRED_KEY_PEM: &str = include_str!("../tests/fixtures/tls/expired.key.pem");
1380
1381    // A well-formed PEM CERTIFICATE block whose body is valid base64 but NOT a
1382    // parseable X.509 DER (all-zero bytes). PEM-decodes fine, so it survives
1383    // `read_cert_chain`, but `X509Certificate::from_der` rejects it — standing in
1384    // for a malformed intermediate in a chain.
1385    const MALFORMED_CERT_PEM: &str = "\
1386-----BEGIN CERTIFICATE-----
1387AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1388-----END CERTIFICATE-----
1389";
1390
1391    fn write_temp(dir: &Path, name: &str, contents: &str) -> PathBuf {
1392        let path = dir.join(name);
1393        let mut f = std::fs::File::create(&path).unwrap();
1394        f.write_all(contents.as_bytes()).unwrap();
1395        path
1396    }
1397
1398    fn now() -> i64 {
1399        std::time::SystemTime::now()
1400            .duration_since(std::time::UNIX_EPOCH)
1401            .unwrap()
1402            .as_secs()
1403            .try_into()
1404            .unwrap()
1405    }
1406
1407    #[test]
1408    fn load_valid_pair_succeeds() {
1409        let dir = tempfile::tempdir().unwrap();
1410        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1411        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1412        let provider = crypto_provider();
1413        let ck = load_certified_key(&cert, &key, &provider, now()).expect("valid pair loads");
1414        assert!(!ck.cert.is_empty());
1415    }
1416
1417    // Regression (#1603, Codex): rustls validates only the leaf, so a malformed
1418    // INTERMEDIATE would boot a listener serving a chain normal clients reject.
1419    // `load_certified_key` must parse every cert in the chain and fail fast,
1420    // naming the offending 1-based position.
1421    #[test]
1422    fn malformed_intermediate_in_chain_is_rejected() {
1423        let dir = tempfile::tempdir().unwrap();
1424        // [valid leaf, malformed intermediate].
1425        let chain_pem = format!("{CERT_PEM}\n{MALFORMED_CERT_PEM}");
1426        let cert = write_temp(dir.path(), "chain.pem", &chain_pem);
1427        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1428        let provider = crypto_provider();
1429        let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
1430        assert!(
1431            matches!(err, TlsError::ParseChainCert { position: 2, .. }),
1432            "a malformed intermediate must fail fast naming its chain position, got {err:?}"
1433        );
1434        assert!(
1435            err.to_string().contains("#2"),
1436            "error must name the offending position, got: {err}"
1437        );
1438    }
1439
1440    // A chain of [valid leaf, well-formed valid intermediate] still loads.
1441    // Chain-cert validation checks DER parseability and the validity window
1442    // (not chain-of-trust), so a second currently-valid certificate stands in
1443    // for a real intermediate here.
1444    #[test]
1445    fn valid_leaf_and_intermediate_chain_loads() {
1446        let dir = tempfile::tempdir().unwrap();
1447        // Both blocks are the far-future `CERT_PEM` fixture, so the intermediate
1448        // is within its validity window (an expired intermediate is now rejected).
1449        let chain_pem = format!("{CERT_PEM}\n{CERT_PEM}");
1450        let cert = write_temp(dir.path(), "chain.pem", &chain_pem);
1451        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1452        let provider = crypto_provider();
1453        let ck = load_certified_key(&cert, &key, &provider, now())
1454            .expect("a valid leaf + well-formed intermediate must load");
1455        assert_eq!(ck.cert.len(), 2, "both chain certificates are retained");
1456    }
1457
1458    // Regression (#1603, Codex): an EXPIRED intermediate is rejected by clients
1459    // during path validation, so `validate_chain_certs` must fail fast at
1460    // load/inspection time, naming the 1-based position and that it expired.
1461    #[test]
1462    fn expired_intermediate_in_chain_is_rejected() {
1463        let dir = tempfile::tempdir().unwrap();
1464        // [valid leaf (far-future), expired intermediate (2021)].
1465        let chain_pem = format!("{CERT_PEM}\n{EXPIRED_CERT_PEM}");
1466        let path = write_temp(dir.path(), "chain.pem", &chain_pem);
1467        let chain = read_cert_chain(&path).unwrap();
1468        // "Now" is inside the leaf's window but well past the intermediate's
1469        // 2021 notAfter.
1470        let err = validate_chain_certs(&path, &chain, now()).unwrap_err();
1471        assert!(
1472            matches!(err, TlsError::ExpiredChainCert { position: 2, .. }),
1473            "an expired intermediate must fail fast at position 2, got {err:?}"
1474        );
1475        assert!(
1476            err.to_string().contains("#2"),
1477            "error must name the offending position, got: {err}"
1478        );
1479        assert!(
1480            err.to_string().contains("expired"),
1481            "error must report expiry, got: {err}"
1482        );
1483    }
1484
1485    // Regression (#1603, Codex): a NOT-YET-VALID intermediate is likewise
1486    // rejected by clients, so it must fail fast at position 2. `now` is pinned
1487    // before the intermediate's notBefore; the leaf (`chain[0]`) is skipped by
1488    // `validate_chain_certs`, so only the intermediate is graded.
1489    #[test]
1490    fn not_yet_valid_intermediate_in_chain_is_rejected() {
1491        let dir = tempfile::tempdir().unwrap();
1492        let leaf = write_temp(dir.path(), "leaf.pem", CERT_PEM);
1493        let leaf_key = write_temp(dir.path(), "leaf.key.pem", KEY_PEM);
1494        // The intermediate is a second copy of `CERT_PEM`; pin "now" one hour
1495        // before its notBefore so its validity window has not opened yet.
1496        let not_before = inspect_leaf(&leaf, &leaf_key, now())
1497            .unwrap()
1498            .not_before_unix;
1499        let chain_pem = format!("{CERT_PEM}\n{CERT_PEM}");
1500        let path = write_temp(dir.path(), "chain.pem", &chain_pem);
1501        let chain = read_cert_chain(&path).unwrap();
1502        let err = validate_chain_certs(&path, &chain, not_before - 3600).unwrap_err();
1503        assert!(
1504            matches!(err, TlsError::NotYetValidChainCert { position: 2, .. }),
1505            "a not-yet-valid intermediate must fail fast at position 2, got {err:?}"
1506        );
1507        assert!(
1508            err.to_string().contains("#2"),
1509            "error must name the offending position, got: {err}"
1510        );
1511        assert!(
1512            err.to_string().contains("not yet valid"),
1513            "error must report the not-yet-valid window, got: {err}"
1514        );
1515    }
1516
1517    // A chain whose intermediate is currently within its validity window passes.
1518    #[test]
1519    fn valid_intermediate_in_chain_is_accepted() {
1520        let dir = tempfile::tempdir().unwrap();
1521        let chain_pem = format!("{CERT_PEM}\n{CERT_PEM}");
1522        let path = write_temp(dir.path(), "chain.pem", &chain_pem);
1523        let chain = read_cert_chain(&path).unwrap();
1524        validate_chain_certs(&path, &chain, now())
1525            .expect("a valid, in-window intermediate must be accepted");
1526    }
1527
1528    #[test]
1529    fn missing_cert_file_names_the_path() {
1530        let dir = tempfile::tempdir().unwrap();
1531        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1532        let missing = dir.path().join("does-not-exist.pem");
1533        let provider = crypto_provider();
1534        let err = load_certified_key(&missing, &key, &provider, now()).unwrap_err();
1535        assert!(matches!(err, TlsError::ReadCert { .. }));
1536        assert!(err.to_string().contains("does-not-exist.pem"));
1537    }
1538
1539    #[test]
1540    fn unparseable_cert_pem_is_reported() {
1541        let dir = tempfile::tempdir().unwrap();
1542        let cert = write_temp(dir.path(), "c.pem", "not a pem file");
1543        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1544        let provider = crypto_provider();
1545        let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
1546        // An input with no PEM blocks yields "no certificates"; a malformed
1547        // block yields a parse error. Either is an actionable, path-named error.
1548        assert!(matches!(
1549            err,
1550            TlsError::NoCertificates { .. } | TlsError::ParseCert { .. }
1551        ));
1552    }
1553
1554    #[test]
1555    fn mismatched_key_is_rejected() {
1556        let dir = tempfile::tempdir().unwrap();
1557        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1558        let key = write_temp(dir.path(), "k.pem", MISMATCHED_KEY_PEM);
1559        let provider = crypto_provider();
1560        let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
1561        assert!(
1562            matches!(err, TlsError::InvalidKeyPair { .. }),
1563            "got {err:?}"
1564        );
1565    }
1566
1567    #[test]
1568    fn expired_leaf_is_rejected() {
1569        let dir = tempfile::tempdir().unwrap();
1570        let cert = write_temp(dir.path(), "c.pem", EXPIRED_CERT_PEM);
1571        let key = write_temp(dir.path(), "k.pem", EXPIRED_KEY_PEM);
1572        let provider = crypto_provider();
1573        let err = load_certified_key(&cert, &key, &provider, now()).unwrap_err();
1574        assert!(matches!(err, TlsError::Expired { .. }), "got {err:?}");
1575    }
1576
1577    #[test]
1578    fn not_yet_valid_leaf_is_rejected() {
1579        let dir = tempfile::tempdir().unwrap();
1580        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1581        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1582        let provider = crypto_provider();
1583
1584        // Pin "now" to one hour BEFORE the fixture's notBefore (equivalent to a
1585        // cert whose notBefore is now + 3600): the leaf is not yet valid, so
1586        // load must fail fast — symmetric to the expired-leaf rejection.
1587        let not_before = inspect_leaf(&cert, &key, now()).unwrap().not_before_unix;
1588        let err = load_certified_key(&cert, &key, &provider, not_before - 3600).unwrap_err();
1589        assert!(matches!(err, TlsError::NotYetValid { .. }), "got {err:?}");
1590
1591        // A currently-valid cert is unaffected: one second into the validity
1592        // window (and well before notAfter) the same pair loads cleanly.
1593        load_certified_key(&cert, &key, &provider, not_before + 1)
1594            .expect("cert loads once notBefore has passed");
1595    }
1596
1597    #[test]
1598    fn inspect_reports_not_yet_valid_window() {
1599        let dir = tempfile::tempdir().unwrap();
1600        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1601        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1602        // inspect_leaf tolerates a not-yet-valid leaf (doctor grades it) but
1603        // returns notBefore so the caller can see the window has not opened.
1604        let inspection = inspect_leaf(&cert, &key, now()).expect("pair inspects");
1605        assert!(inspection.is_not_yet_valid(inspection.not_before_unix - 1));
1606        assert!(!inspection.is_not_yet_valid(inspection.not_before_unix));
1607        assert!(!inspection.is_not_yet_valid(inspection.not_after_unix));
1608    }
1609
1610    #[test]
1611    fn inspect_reports_future_expiry() {
1612        let dir = tempfile::tempdir().unwrap();
1613        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1614        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1615        let inspection = inspect_leaf(&cert, &key, now()).expect("valid pair inspects");
1616        assert!(
1617            inspection.days_until_expiry(now()) > 30,
1618            "fixture should be valid far into the future"
1619        );
1620    }
1621
1622    #[test]
1623    fn inspect_still_reports_expiry_for_expired_cert() {
1624        let dir = tempfile::tempdir().unwrap();
1625        let cert = write_temp(dir.path(), "c.pem", EXPIRED_CERT_PEM);
1626        let key = write_temp(dir.path(), "k.pem", EXPIRED_KEY_PEM);
1627        // inspect_leaf tolerates expiry (doctor grades it) but still returns
1628        // the notAfter so the caller can see it is in the past.
1629        let inspection = inspect_leaf(&cert, &key, now()).expect("expired pair still inspects");
1630        assert!(inspection.days_until_expiry(now()) < 0);
1631    }
1632
1633    #[test]
1634    fn reloadable_resolver_swaps_certificate() {
1635        let dir = tempfile::tempdir().unwrap();
1636        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1637        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1638        let provider = crypto_provider();
1639        let first = load_certified_key(&cert, &key, &provider, now()).unwrap();
1640        let resolver = ReloadableCertResolver::new(Arc::clone(&first));
1641        assert!(Arc::ptr_eq(&resolver.current(), &first));
1642
1643        let second = load_certified_key(&cert, &key, &provider, now()).unwrap();
1644        resolver.store(Arc::clone(&second));
1645        assert!(Arc::ptr_eq(&resolver.current(), &second));
1646    }
1647
1648    // Regression (#2321): `build_server_config` never set `alpn_protocols`,
1649    // so rustls completed the handshake with no protocol selected and every
1650    // browser silently fell back to HTTP/1.1 — even though the serve path's
1651    // `hyper_util::server::conn::auto` already speaks h2 once the client
1652    // sends the preface. The fix advertises `h2` first, then `http/1.1`:
1653    // ALPN-less and http/1.1-only clients are unaffected.
1654    #[tokio::test(flavor = "multi_thread")]
1655    async fn no_connection_is_accepted_before_the_cap_is_installed() {
1656        let dir = tempfile::tempdir().unwrap();
1657        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1658        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1659        let provider = crypto_provider();
1660        let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
1661        let config =
1662            build_server_config(provider, Arc::new(ReloadableCertResolver::new(certified)))
1663                .unwrap();
1664        let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1665        let addr = tcp.local_addr().unwrap();
1666        let shutdown = tokio_util::sync::CancellationToken::new();
1667        let mut listener = TlsListener::new(
1668            tcp,
1669            config,
1670            std::time::Duration::from_secs(30),
1671            shutdown.clone(),
1672        );
1673        // Two silent clients wait in the kernel backlog before the serve arm
1674        // installs the cap (for example during an upgrade's startup hooks).
1675        let _first = tokio::net::TcpStream::connect(addr).await.unwrap();
1676        let _second = tokio::net::TcpStream::connect(addr).await.unwrap();
1677        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1678        assert_eq!(listener.handshakes_in_flight(), 0, "nothing starts early");
1679        assert!(listener.limit_connections(1).is_some());
1680        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1681        // Installing the cap does not start the acceptor: an upgrade successor
1682        // installs it before its startup hooks run, and must not take
1683        // connections from its predecessor until the server is polled.
1684        assert_eq!(
1685            listener.handshakes_in_flight(),
1686            0,
1687            "the cap alone starts nothing"
1688        );
1689        let handshakes = Arc::clone(&listener.handshakes);
1690        let serving = tokio::spawn(async move {
1691            let _ = axum::serve::Listener::accept(&mut listener).await;
1692        });
1693        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1694        assert_eq!(
1695            MAX_CONCURRENT_HANDSHAKES - handshakes.available_permits(),
1696            1,
1697            "with max_connections = 1, the second client waits before its handshake"
1698        );
1699        shutdown.cancel();
1700        serving.abort();
1701    }
1702
1703    #[tokio::test(flavor = "multi_thread")]
1704    async fn the_public_serve_caps_tls_connections_before_the_handshake() {
1705        let dir = tempfile::tempdir().unwrap();
1706        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1707        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1708        let provider = crypto_provider();
1709        let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
1710        let config =
1711            build_server_config(provider, Arc::new(ReloadableCertResolver::new(certified)))
1712                .unwrap();
1713        let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1714        let addr = tcp.local_addr().unwrap();
1715        let shutdown = tokio_util::sync::CancellationToken::new();
1716        let listener = TlsListener::new(
1717            tcp,
1718            config,
1719            std::time::Duration::from_secs(30),
1720            shutdown.clone(),
1721        );
1722        let handshakes = Arc::clone(&listener.handshakes);
1723        let limits = crate::http_server::HttpLimits {
1724            max_connections: Some(1),
1725            ..crate::http_server::HttpLimits::default()
1726        };
1727        let signal = shutdown.clone();
1728        tokio::spawn(crate::http_server::serve(
1729            listener,
1730            axum::Router::new().into_make_service(),
1731            limits,
1732            async move { signal.cancelled().await },
1733        ));
1734        // Two clients that never send a ClientHello.
1735        let _first = tokio::net::TcpStream::connect(addr).await.unwrap();
1736        let _second = tokio::net::TcpStream::connect(addr).await.unwrap();
1737        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1738        assert_eq!(
1739            MAX_CONCURRENT_HANDSHAKES - handshakes.available_permits(),
1740            1,
1741            "with max_connections = 1, the public serve must not start a second handshake"
1742        );
1743        shutdown.cancel();
1744    }
1745
1746    #[tokio::test]
1747    async fn the_connection_cap_applies_before_the_tls_handshake() {
1748        let dir = tempfile::tempdir().unwrap();
1749        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1750        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1751        let provider = crypto_provider();
1752        let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
1753        let config =
1754            build_server_config(provider, Arc::new(ReloadableCertResolver::new(certified)))
1755                .unwrap();
1756        let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1757        let addr = tcp.local_addr().unwrap();
1758        let handshakes = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_HANDSHAKES));
1759        let cap = Some(Arc::new(tokio::sync::Semaphore::new(1)));
1760        let (tx, _rx) = tokio::sync::mpsc::channel(READY_CONN_CHANNEL_CAPACITY);
1761        let shutdown = tokio_util::sync::CancellationToken::new();
1762        tokio::spawn(run_acceptor(
1763            tcp,
1764            tokio_rustls::TlsAcceptor::from(config),
1765            std::time::Duration::from_secs(30),
1766            Arc::clone(&handshakes),
1767            cap,
1768            tx,
1769            shutdown.clone(),
1770        ));
1771        // Two clients that never send a ClientHello.
1772        let _first = tokio::net::TcpStream::connect(addr).await.unwrap();
1773        let _second = tokio::net::TcpStream::connect(addr).await.unwrap();
1774        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1775        assert_eq!(
1776            MAX_CONCURRENT_HANDSHAKES - handshakes.available_permits(),
1777            1,
1778            "with max_connections = 1, the second client waits before its handshake"
1779        );
1780        shutdown.cancel();
1781    }
1782
1783    #[test]
1784    fn server_config_advertises_h2_then_http11_alpn() {
1785        let dir = tempfile::tempdir().unwrap();
1786        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1787        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1788        let provider = crypto_provider();
1789        let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
1790        let config =
1791            build_server_config(provider, Arc::new(ReloadableCertResolver::new(certified)))
1792                .expect("server config builds");
1793        assert_eq!(
1794            config.alpn_protocols,
1795            vec![b"h2".to_vec(), b"http/1.1".to_vec()],
1796            "the static [server.tls] path must advertise h2 first, then http/1.1"
1797        );
1798    }
1799
1800    // The ACME path (#1608) funnels through `build_server_config_with_resolver`
1801    // and the client-auth path (#1640) takes a different `match` arm, so pin
1802    // the ALPN on both entry points rather than assuming the shared funnel.
1803    #[test]
1804    fn server_config_with_resolver_and_client_auth_advertise_the_same_alpn() {
1805        let dir = tempfile::tempdir().unwrap();
1806        let cert = write_temp(dir.path(), "c.pem", CERT_PEM);
1807        let key = write_temp(dir.path(), "k.pem", KEY_PEM);
1808        let provider = crypto_provider();
1809        let certified = load_certified_key(&cert, &key, &provider, now()).unwrap();
1810        let expected = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1811
1812        let via_resolver = build_server_config_with_resolver(
1813            Arc::clone(&provider),
1814            Arc::new(ReloadableCertResolver::new(Arc::clone(&certified)))
1815                as Arc<dyn ResolvesServerCert>,
1816        )
1817        .expect("resolver config builds");
1818        assert_eq!(
1819            via_resolver.alpn_protocols, expected,
1820            "the resolver (ACME) entry point must advertise the same ALPN"
1821        );
1822
1823        // rustls refuses to build a client verifier with zero trust anchors
1824        // (`NoRootAnchors`) even in `Optional` mode, so load the client CA
1825        // fixture instead of an empty store — the anchors are irrelevant to
1826        // the ALPN assertion below; only the `Some(verifier)` arm matters.
1827        let ca = write_temp(
1828            dir.path(),
1829            "ca.pem",
1830            include_str!("../tests/fixtures/tls/client/ca.cert.pem"),
1831        );
1832        let verifier = client_auth::build_client_verifier(
1833            client_auth::load_client_roots(&ca).expect("client roots load"),
1834            vec![],
1835            crate::config::ClientAuthMode::Optional,
1836            Arc::clone(&provider),
1837        )
1838        .expect("client verifier builds");
1839        let via_client_auth = build_server_config_with_client_auth(
1840            provider,
1841            Arc::new(ReloadableCertResolver::new(certified)) as Arc<dyn ResolvesServerCert>,
1842            Some(verifier),
1843        )
1844        .expect("client-auth config builds");
1845        assert_eq!(
1846            via_client_auth.alpn_protocols, expected,
1847            "the client-auth arm must advertise the same ALPN"
1848        );
1849    }
1850
1851    // Regression (Codex P1 on PR #2780): advertising `h2` in ALPN is only
1852    // safe while the serve stack can speak HTTP/2. The serve loop
1853    // (`crate::http_server`) runs every connection through
1854    // `hyper_util::server::conn::auto::Builder`, whose H2 arm is compiled out
1855    // unless hyper-util's `http2` feature is on. `Builder::http2()` exists
1856    // only under that feature, so this test does not compile without it.
1857    #[test]
1858    fn serve_stack_speaks_http2() {
1859        let mut builder =
1860            hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
1861        let _ = builder.http2();
1862    }
1863}