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}