Skip to main content

moq_native/
tls.rs

1//! TLS trust, certificates, and keys, split by role.
2//!
3//! [`Client`] (`--client-tls-*`) picks who to trust: system roots, custom roots,
4//! a pinned SHA-256 fingerprint, or nothing at all. [`Server`] (`--server-tls-*`)
5//! supplies the certificate chain to serve, loaded from disk or self-signed on
6//! startup, and optionally the roots that authenticate mTLS clients.
7//!
8//! Certificates loaded from disk are watched and hot reloaded, so rotating them
9//! needs no restart. [`Certificates`] reads the current set back out.
10
11use crate::crypto;
12use rustls::pki_types::pem::PemObject;
13use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use std::{fs, io};
17
18#[cfg(all(
19	any(feature = "quinn", feature = "noq", feature = "quiche"),
20	any(feature = "aws-lc-rs", feature = "ring")
21))]
22use rustls::pki_types::PrivatePkcs8KeyDer;
23#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
24use std::sync::RwLock;
25
26/// Errors loading or generating TLS certificates and keys.
27///
28/// Shared by the client TLS config and the quinn/noq servers so each backend's
29/// error type can compose it via `#[from]`.
30#[derive(Debug, thiserror::Error)]
31#[non_exhaustive]
32pub enum Error {
33	/// A certificate file couldn't be opened, usually a bad path or permissions.
34	#[error("failed to open certificate file")]
35	Open(#[source] std::io::Error),
36
37	/// A certificate or key file was opened but couldn't be read to the end.
38	#[error("failed to read file")]
39	ReadFile(#[source] std::io::Error),
40
41	/// A file's contents aren't valid PEM certificates.
42	#[error("failed to read certificates")]
43	Read(#[source] rustls::pki_types::pem::Error),
44
45	/// A file's contents aren't a valid PEM private key.
46	#[error("failed to parse private key")]
47	Key(#[source] rustls::pki_types::pem::Error),
48
49	/// A PEM file parsed cleanly but held no certificates.
50	#[error("no certificates found")]
51	Empty,
52
53	/// A root PEM file parsed cleanly but held no certificates, so it would trust nothing.
54	#[error("no roots found in {}", .0.display())]
55	EmptyRoots(PathBuf),
56
57	/// Nothing is configured that could ever verify a server certificate.
58	#[error(
59		"no trusted roots: provide --client-tls-root, enable --client-tls-system-roots, or use --client-tls-fingerprint / --client-tls-disable-verify"
60	)]
61	NoRoots,
62
63	/// A configured fingerprint isn't valid hex.
64	#[error("invalid TLS fingerprint (expected hex-encoded SHA-256)")]
65	Fingerprint(#[source] hex::FromHexError),
66
67	/// A configured fingerprint is valid hex but the wrong size for a SHA-256 digest.
68	#[error("invalid TLS fingerprint length: expected 32 bytes (SHA-256), got {0}")]
69	FingerprintLength(usize),
70
71	/// Fingerprint pinning was combined with CA roots. Pinning bypasses the chain, so one of
72	/// the two would be silently ignored.
73	#[error(
74		"--client-tls-fingerprint cannot be combined with --client-tls-root or --client-tls-system-roots: fingerprint pinning bypasses CA verification"
75	)]
76	FingerprintWithRoots,
77
78	/// Trust material was configured alongside the flag that ignores all of it.
79	#[error(
80		"--client-tls-disable-verify cannot be combined with --client-tls-fingerprint, --client-tls-root or --client-tls-system-roots: it accepts every certificate, so the trust material would be ignored"
81	)]
82	DisableVerifyWithTrust,
83
84	/// A root certificate parsed as PEM but rustls rejected it as a trust anchor.
85	#[error("failed to add root certificate")]
86	AddRoot(#[source] rustls::Error),
87
88	/// The JNI call in [`init_android`] failed, so the platform verifier is unavailable.
89	#[cfg(target_os = "android")]
90	#[error("failed to initialize the Android platform verifier")]
91	AndroidInit(#[source] jni::errors::Error),
92
93	/// rustls rejected the mTLS client certificate and key, e.g. they don't match.
94	#[error("failed to configure client certificate")]
95	ClientAuth(#[source] rustls::Error),
96
97	/// Only one half of the mTLS client identity was given; it needs both a cert and a key.
98	#[error("both --client-tls-cert and --client-tls-key must be provided")]
99	IncompleteClientAuth,
100
101	/// The server was given a different number of certificates than keys. They pair by index.
102	#[error("must provide both cert and key")]
103	CertKeyCountMismatch,
104
105	/// The server has no certificate to serve: no cert/key pair and no hostnames to generate one for.
106	#[error("must provide at least one cert/key pair or generate entry")]
107	NoCertSource,
108
109	/// A server cert/key pair was paired up by index but the key isn't the certificate's.
110	#[error("private key {} doesn't match certificate {}", key.display(), cert.display())]
111	KeyMismatch {
112		/// Path of the private key file.
113		key: PathBuf,
114		/// Path of the certificate file it was paired with.
115		cert: PathBuf,
116		/// Why rustls says the two don't match.
117		#[source]
118		source: rustls::Error,
119	},
120
121	/// A rustls error with no more specific context, e.g. building a config.
122	#[error(transparent)]
123	Rustls(#[from] rustls::Error),
124
125	/// The mTLS client-certificate verifier couldn't be built from the configured roots.
126	#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
127	#[error("failed to build client certificate verifier")]
128	ClientVerifier(#[source] rustls::server::VerifierBuilderError),
129
130	/// Generating a self-signed certificate failed.
131	#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
132	#[error(transparent)]
133	Rcgen(#[from] rcgen::Error),
134
135	/// The crate was built without a crypto provider, so no TLS is possible.
136	#[error("no crypto provider available; enable aws-lc-rs or ring feature")]
137	NoCryptoProvider,
138}
139
140/// Convenience alias for results produced by this module.
141pub type Result<T> = std::result::Result<T, Error>;
142
143/// Parse a hex-encoded SHA-256 certificate fingerprint.
144pub fn parse_fingerprint(value: &str) -> Result<[u8; 32]> {
145	let bytes = hex::decode(value.trim()).map_err(Error::Fingerprint)?;
146	bytes.try_into().map_err(|v: Vec<u8>| Error::FingerprintLength(v.len()))
147}
148
149/// Read a PEM file into its list of certificates.
150pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
151	let file = fs::File::open(path).map_err(Error::Open)?;
152	let mut reader = io::BufReader::new(file);
153	CertificateDer::pem_reader_iter(&mut reader)
154		.collect::<std::result::Result<_, _>>()
155		.map_err(Error::Read)
156}
157
158// ── Client ──────────────────────────────────────────────────────────
159
160/// TLS configuration for the client.
161#[serde_with::serde_as]
162#[derive(Clone, Default, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
163#[serde(default, deny_unknown_fields)]
164#[group(id = "tls-client")]
165#[non_exhaustive]
166pub struct Client {
167	/// Trust the TLS root at this path, encoded as PEM.
168	///
169	/// This value can be provided multiple times for multiple roots.
170	/// In config files, accepts either a single string or a TOML array.
171	///
172	/// These roots are added on top of the system roots. By default the system
173	/// roots are only loaded when no custom root is given, so passing a root
174	/// replaces them; set `--client-tls-system-roots` to trust both (e.g. to reach a
175	/// local relay with a private CA and a remote one with a public CA).
176	#[serde(skip_serializing_if = "Vec::is_empty")]
177	#[arg(id = "client-tls-root", long = "client-tls-root", env = "MOQ_CLIENT_TLS_ROOT")]
178	#[serde_as(as = "serde_with::OneOrMany<_>")]
179	pub root: Vec<PathBuf>,
180
181	/// Also trust the platform's native root certificates.
182	///
183	/// Defaults to enabled only when no `--client-tls-root` is given. Set it
184	/// explicitly to trust the system roots alongside any custom roots, or set it
185	/// to false to trust only the custom roots. Trusting neither (no custom root
186	/// and system roots disabled) is rejected, since verification could never pass.
187	#[serde(skip_serializing_if = "Option::is_none")]
188	#[arg(
189		id = "client-tls-system-roots",
190		long = "client-tls-system-roots",
191		env = "MOQ_CLIENT_TLS_SYSTEM_ROOTS",
192		default_missing_value = "true",
193		num_args = 0..=1,
194		require_equals = true,
195		value_parser = clap::value_parser!(bool),
196	)]
197	pub system_roots: Option<bool>,
198
199	/// Pin the peer to a certificate with one of these SHA-256 fingerprints, encoded as hex.
200	///
201	/// This is the native equivalent of the browser's WebTransport `serverCertificateHashes`,
202	/// and accepts the same values a server reports via its certificate fingerprints. Use it to
203	/// trust a self-signed certificate without disabling verification or fetching the hash over
204	/// an insecure `http://` request. When set, the normal CA/root chain is bypassed: only the
205	/// leaf certificate's fingerprint is checked.
206	///
207	/// This value can be provided multiple times to accept any of several fingerprints (e.g.
208	/// across a certificate rotation). In config files, accepts either a single string or a TOML array.
209	#[serde(skip_serializing_if = "Vec::is_empty")]
210	#[arg(
211		id = "client-tls-fingerprint",
212		long = "client-tls-fingerprint",
213		env = "MOQ_CLIENT_TLS_FINGERPRINT"
214	)]
215	#[serde_as(as = "serde_with::OneOrMany<_>")]
216	pub fingerprint: Vec<String>,
217
218	/// PEM file containing the client certificate chain for mTLS.
219	///
220	/// Only certificates are extracted; any private keys in the file are ignored.
221	/// Must be paired with `--client-tls-key`.
222	#[serde(skip_serializing_if = "Option::is_none")]
223	#[arg(id = "client-tls-cert", long = "client-tls-cert", env = "MOQ_CLIENT_TLS_CERT")]
224	pub cert: Option<PathBuf>,
225
226	/// PEM file containing the private key for mTLS.
227	///
228	/// Only the private key is extracted; any certificates in the file are ignored.
229	/// Must be paired with `--client-tls-cert`.
230	#[serde(skip_serializing_if = "Option::is_none")]
231	#[arg(id = "client-tls-key", long = "client-tls-key", env = "MOQ_CLIENT_TLS_KEY")]
232	pub key: Option<PathBuf>,
233
234	/// Danger: Disable TLS certificate verification.
235	///
236	/// Fine for local development and between relays, but should be used in caution in production.
237	#[serde(skip_serializing_if = "Option::is_none")]
238	#[arg(
239		id = "client-tls-disable-verify",
240		long = "client-tls-disable-verify",
241		env = "MOQ_CLIENT_TLS_DISABLE_VERIFY",
242		default_missing_value = "true",
243		num_args = 0..=1,
244		require_equals = true,
245		value_parser = clap::value_parser!(bool),
246	)]
247	pub disable_verify: Option<bool>,
248
249	/// Override the TLS SNI and certificate verification hostname for outbound connections.
250	///
251	/// When unset, the connect URL's host is used (default behavior). Useful when dialing a
252	/// raw IP address but needing to present/verify a DNS name the server certificate covers.
253	#[serde(skip_serializing_if = "Option::is_none")]
254	#[arg(
255		id = "client-tls-host-name",
256		long = "client-tls-host-name",
257		env = "MOQ_CLIENT_TLS_HOST_NAME"
258	)]
259	pub host_name: Option<String>,
260
261	/// Deprecated `--tls-*` spellings, folded into the canonical fields above with
262	/// a warning. Private and hidden so they stay off the public surface; not a
263	/// TOML field (config files use the canonical names).
264	#[command(flatten)]
265	#[serde(skip)]
266	deprecated: Deprecated,
267}
268
269/// Holds the deprecated bare `--tls-*` flag spellings (renamed to `--client-tls-*`).
270/// Flattened into [`Client`] so they keep parsing; folded into the canonical
271/// fields by [`Client::build`] with a deprecation warning. No env (the env names
272/// were never renamed) and no TOML.
273#[derive(Clone, Default, Debug, clap::Args)]
274struct Deprecated {
275	#[arg(long = "tls-root", hide = true)]
276	root: Vec<PathBuf>,
277
278	#[arg(
279		long = "tls-system-roots",
280		hide = true,
281		default_missing_value = "true",
282		num_args = 0..=1,
283		require_equals = true,
284		value_parser = clap::value_parser!(bool),
285	)]
286	system_roots: Option<bool>,
287
288	#[arg(long = "tls-fingerprint", hide = true)]
289	fingerprint: Vec<String>,
290
291	#[arg(
292		long = "tls-disable-verify",
293		hide = true,
294		default_missing_value = "true",
295		num_args = 0..=1,
296		require_equals = true,
297		value_parser = clap::value_parser!(bool),
298	)]
299	disable_verify: Option<bool>,
300}
301
302/// The resolved server-certificate verification policy.
303///
304/// Computed once by [Client::verification] and shared by every backend (the
305/// rustls-based quinn/noq via [Client::build], and quiche directly) so they
306/// agree on precedence, the system-roots default, and which flag combinations
307/// are valid.
308#[derive(Clone)]
309pub(crate) enum Verification {
310	/// No verification at all. Insecure; only via `--client-tls-disable-verify`.
311	Disabled,
312
313	/// Pin the leaf certificate by SHA-256. The CA chain is not consulted, so
314	/// this is mutually exclusive with any roots.
315	Fingerprints(Vec<[u8; 32]>),
316
317	/// Standard CA verification. When `system` is set the platform/default trust
318	/// store is trusted too; each backend resolves that its own way (the rustls
319	/// backends use the OS platform verifier, quiche loads the native roots).
320	/// `custom` are extra PEM roots trusted in addition.
321	Roots {
322		custom: Vec<CertificateDer<'static>>,
323		system: bool,
324	},
325}
326
327impl Client {
328	/// Log a warning for each deprecated `--tls-*` flag in use. Called once from
329	/// [`Self::verification`], which every backend runs, so a deprecated flag warns once.
330	pub(crate) fn warn_deprecated(&self) {
331		if !self.deprecated.root.is_empty() {
332			tracing::warn!("--tls-root is deprecated; use --client-tls-root");
333		}
334		if self.deprecated.system_roots.is_some() {
335			tracing::warn!("--tls-system-roots is deprecated; use --client-tls-system-roots");
336		}
337		if !self.deprecated.fingerprint.is_empty() {
338			tracing::warn!("--tls-fingerprint is deprecated; use --client-tls-fingerprint");
339		}
340		if self.deprecated.disable_verify.is_some() {
341			tracing::warn!("--tls-disable-verify is deprecated; use --client-tls-disable-verify");
342		}
343	}
344
345	/// Roots from the canonical field plus the deprecated `--tls-root` spelling.
346	pub(crate) fn effective_root(&self) -> Vec<PathBuf> {
347		let mut root = self.root.clone();
348		root.extend(self.deprecated.root.iter().cloned());
349		root
350	}
351
352	/// Fingerprints from the canonical field plus the deprecated `--tls-fingerprint`.
353	pub(crate) fn effective_fingerprint(&self) -> Vec<String> {
354		let mut fp = self.fingerprint.clone();
355		fp.extend(self.deprecated.fingerprint.iter().cloned());
356		fp
357	}
358
359	/// `system_roots`, preferring the canonical flag over the deprecated alias.
360	pub(crate) fn effective_system_roots(&self) -> Option<bool> {
361		self.system_roots.or(self.deprecated.system_roots)
362	}
363
364	/// `disable_verify`, preferring the canonical flag over the deprecated alias.
365	pub(crate) fn effective_disable_verify(&self) -> Option<bool> {
366		self.disable_verify.or(self.deprecated.disable_verify)
367	}
368
369	/// Resolve the verification policy from the configured flags.
370	///
371	/// Precedence and rules (shared by all backends):
372	/// - `--client-tls-disable-verify` disables verification, and combining it with any
373	///   trust material is rejected rather than silently ignoring that material.
374	/// - `--client-tls-fingerprint` pins the leaf and bypasses the CA chain; combining
375	///   it with `--client-tls-root` or `--client-tls-system-roots` is rejected rather than
376	///   silently ignoring one of them.
377	/// - Otherwise, verify against the system roots (default) plus any custom
378	///   roots. The system roots are dropped once a custom root is given unless
379	///   `--client-tls-system-roots` re-enables them.
380	///
381	/// Every combination that would quietly drop one setting is an error. Silently
382	/// weakening trust is the worst outcome here: someone moving off
383	/// `disable_verify` by adding a fingerprint would otherwise still accept every
384	/// certificate, with the UI showing the pin as configured.
385	pub(crate) fn verification(&self) -> Result<Verification> {
386		self.warn_deprecated();
387
388		let fingerprints = self.fingerprints()?;
389		let roots = self.effective_root();
390		let system_roots = self.effective_system_roots();
391
392		if self.effective_disable_verify().unwrap_or_default() {
393			if !fingerprints.is_empty() || !roots.is_empty() || system_roots == Some(true) {
394				return Err(Error::DisableVerifyWithTrust);
395			}
396			return Ok(Verification::Disabled);
397		}
398
399		if !fingerprints.is_empty() {
400			if !roots.is_empty() || system_roots == Some(true) {
401				return Err(Error::FingerprintWithRoots);
402			}
403			return Ok(Verification::Fingerprints(fingerprints));
404		}
405
406		// Default to system roots only when no custom root is given, so passing a
407		// root replaces them unless the system roots are explicitly re-enabled.
408		let system = system_roots.unwrap_or(roots.is_empty());
409
410		let mut custom = Vec::new();
411		for root in &roots {
412			let certs = read_certs(root)?;
413			if certs.is_empty() {
414				return Err(Error::EmptyRoots(root.clone()));
415			}
416			custom.extend(certs);
417		}
418
419		// WebPKI needs at least one trusted root to ever succeed, so fail fast
420		// instead of producing confusing handshake errors later. With system
421		// trust enabled the verifier supplies its own roots, so custom roots are
422		// optional.
423		if !system && custom.is_empty() {
424			return Err(Error::NoRoots);
425		}
426
427		Ok(Verification::Roots { custom, system })
428	}
429
430	/// Whether an insecure `http://` certificate-fingerprint bootstrap may be
431	/// honored for a connection.
432	///
433	/// Only when no stronger verification is configured: an explicit
434	/// `--client-tls-fingerprint` must never be weakened by an attacker-controlled
435	/// plaintext fetch, and there is nothing to bootstrap when verification is
436	/// disabled. With CA roots (the default), `http://` is the deliberate
437	/// per-connection way to pin a self-signed relay, so it is allowed.
438	#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
439	pub(crate) fn allows_http_bootstrap(&self) -> bool {
440		self.effective_fingerprint().is_empty() && !self.effective_disable_verify().unwrap_or_default()
441	}
442
443	/// Parse the configured fingerprints into fixed-size SHA-256 digests.
444	fn fingerprints(&self) -> Result<Vec<[u8; 32]>> {
445		self.effective_fingerprint()
446			.iter()
447			.map(|fp| parse_fingerprint(fp))
448			.collect()
449	}
450
451	/// Build a [`rustls::ClientConfig`] from this configuration.
452	///
453	/// Resolves the verification policy, optionally attaches a client identity
454	/// for mTLS, and installs the matching verifier.
455	pub fn build(&self) -> Result<rustls::ClientConfig> {
456		let provider = crypto::provider();
457		let verification = self.verification()?;
458
459		// Allow TLS 1.2 in addition to 1.3 for WebSocket compatibility.
460		// QUIC always negotiates TLS 1.3 regardless of this setting.
461		let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
462			.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?;
463
464		// Install the server-certificate verifier. Disabled/Fingerprints get a
465		// placeholder empty store here and swap in their own verifier below.
466		let builder = match &verification {
467			Verification::Roots { custom, system: true } => Self::system_verifier(builder, custom, &provider)?,
468			Verification::Roots { custom, system: false } => builder.with_root_certificates(root_store(custom)?),
469			Verification::Disabled | Verification::Fingerprints(_) => {
470				builder.with_root_certificates(rustls::RootCertStore::empty())
471			}
472		};
473
474		let mut tls = self.with_client_auth(builder)?;
475
476		match verification {
477			Verification::Disabled => {
478				tracing::warn!(
479					"TLS server certificate verification is disabled; A man-in-the-middle attack is possible."
480				);
481				tls.dangerous()
482					.set_certificate_verifier(Arc::new(NoCertificateVerification(provider)));
483			}
484			Verification::Fingerprints(fingerprints) => {
485				let fingerprints = fingerprints.into_iter().map(|fp| fp.to_vec()).collect();
486				let verifier = FingerprintVerifier::new(provider, fingerprints);
487				tls.dangerous().set_certificate_verifier(Arc::new(verifier));
488			}
489			// The verifier was installed by the builder above.
490			Verification::Roots { .. } => {}
491		}
492
493		Ok(tls)
494	}
495
496	/// Build the verifier for system/default trust on the rustls backends.
497	///
498	/// Uses the OS-native platform verifier (Keychain/SecTrust, Windows
499	/// CryptoAPI, or the native store on Linux) everywhere it works, optionally
500	/// extended with `custom` PEM roots. Android's platform verifier needs JNI
501	/// setup (see [`init_android`]); until that has run we trust the bundled
502	/// Mozilla roots so verification still works out of the box.
503	fn system_verifier(
504		builder: rustls::ConfigBuilder<rustls::ClientConfig, rustls::WantsVerifier>,
505		custom: &[CertificateDer<'static>],
506		provider: &crypto::Provider,
507	) -> Result<rustls::ConfigBuilder<rustls::ClientConfig, rustls::client::WantsClientCert>> {
508		// Android's platform verifier needs JNI init (see `init_android`) and,
509		// unlike the other platforms, can't be extended with custom roots. So use
510		// it only once initialized and with no custom roots; otherwise trust the
511		// bundled Mozilla roots (plus any custom roots) so verification still works.
512		#[cfg(target_os = "android")]
513		{
514			if ANDROID_INITIALIZED.load(std::sync::atomic::Ordering::Acquire) && custom.is_empty() {
515				let verifier = rustls_platform_verifier::Verifier::new(provider.clone())?;
516				return Ok(builder.dangerous().with_custom_certificate_verifier(Arc::new(verifier)));
517			}
518
519			let mut roots = rustls::RootCertStore::empty();
520			roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
521			for cert in custom {
522				roots.add(cert.clone()).map_err(Error::AddRoot)?;
523			}
524			Ok(builder.with_root_certificates(roots))
525		}
526
527		#[cfg(not(target_os = "android"))]
528		{
529			let verifier = if custom.is_empty() {
530				rustls_platform_verifier::Verifier::new(provider.clone())?
531			} else {
532				rustls_platform_verifier::Verifier::new_with_extra_roots(custom.iter().cloned(), provider.clone())?
533			};
534			Ok(builder.dangerous().with_custom_certificate_verifier(Arc::new(verifier)))
535		}
536	}
537
538	/// Attach the optional mTLS client identity, finishing the rustls builder.
539	fn with_client_auth(
540		&self,
541		builder: rustls::ConfigBuilder<rustls::ClientConfig, rustls::client::WantsClientCert>,
542	) -> Result<rustls::ClientConfig> {
543		Ok(match (&self.cert, &self.key) {
544			(Some(cert_path), Some(key_path)) => {
545				let cert_pem = fs::read(cert_path).map_err(Error::ReadFile)?;
546				let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
547					.collect::<std::result::Result<_, _>>()
548					.map_err(Error::Read)?;
549				if chain.is_empty() {
550					return Err(Error::Empty);
551				}
552				let key_pem = fs::read(key_path).map_err(Error::ReadFile)?;
553				let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(Error::Key)?;
554				builder.with_client_auth_cert(chain, key).map_err(Error::ClientAuth)?
555			}
556			(None, None) => builder.with_no_client_auth(),
557			_ => return Err(Error::IncompleteClientAuth),
558		})
559	}
560}
561
562/// Build a [`rustls::RootCertStore`] from a list of custom PEM roots.
563fn root_store(custom: &[CertificateDer<'static>]) -> Result<rustls::RootCertStore> {
564	let mut roots = rustls::RootCertStore::empty();
565	for cert in custom {
566		roots.add(cert.clone()).map_err(Error::AddRoot)?;
567	}
568	Ok(roots)
569}
570
571/// Whether [`init_android`] has successfully wired up the platform verifier.
572#[cfg(target_os = "android")]
573static ANDROID_INITIALIZED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
574
575/// Initialize Android platform certificate verification.
576///
577/// On Android the OS trust store is only reachable through the JVM, so the
578/// platform verifier needs a JNI handle to the application `Context` before it
579/// can be used. Call this once at startup (e.g. from `JNI_OnLoad`) with an
580/// attached [`jni::Env`] for the calling thread and the application `Context`.
581/// The `moq-ffi` bindings call it automatically, so most consumers never touch
582/// this directly.
583///
584/// Until it succeeds, clients fall back to the bundled Mozilla roots, so a
585/// missing or failed init degrades to webpki verification rather than failing.
586#[cfg(target_os = "android")]
587pub fn init_android(env: &mut jni::Env, context: jni::objects::JObject) -> Result<()> {
588	rustls_platform_verifier::android::init_with_env(env, context).map_err(Error::AndroidInit)?;
589	ANDROID_INITIALIZED.store(true, std::sync::atomic::Ordering::Release);
590	Ok(())
591}
592
593// ── Server ──────────────────────────────────────────────────────────
594
595/// TLS configuration for the server.
596///
597/// Certificate and keys must currently be files on disk.
598/// Alternatively, you can generate a self-signed certificate given a list of hostnames.
599///
600/// In config files, each list field accepts either a single string or a TOML array.
601#[serde_with::serde_as]
602#[derive(clap::Args, Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
603#[serde(deny_unknown_fields)]
604#[group(id = "tls-server")]
605#[non_exhaustive]
606pub struct Server {
607	/// Load the given certificate from disk.
608	#[arg(long = "tls-cert", id = "tls-cert", env = "MOQ_SERVER_TLS_CERT")]
609	#[serde(default, skip_serializing_if = "Vec::is_empty")]
610	#[serde_as(as = "serde_with::OneOrMany<_>")]
611	pub cert: Vec<PathBuf>,
612
613	/// Load the given key from disk.
614	#[arg(long = "tls-key", id = "tls-key", env = "MOQ_SERVER_TLS_KEY")]
615	#[serde(default, skip_serializing_if = "Vec::is_empty")]
616	#[serde_as(as = "serde_with::OneOrMany<_>")]
617	pub key: Vec<PathBuf>,
618
619	/// Or generate a new certificate and key with the given hostnames.
620	/// This won't be valid unless the client uses the fingerprint or disables verification.
621	#[arg(
622		long = "tls-generate",
623		id = "tls-generate",
624		value_delimiter = ',',
625		env = "MOQ_SERVER_TLS_GENERATE"
626	)]
627	#[serde(default, skip_serializing_if = "Vec::is_empty")]
628	#[serde_as(as = "serde_with::OneOrMany<_>")]
629	pub generate: Vec<String>,
630
631	/// PEM file(s) of root CAs for validating optional client certificates (mTLS).
632	///
633	/// When set, clients *may* present a certificate during the TLS handshake.
634	/// Valid presentations are reported via [`crate::Request::peer_identity`]
635	/// and can be used by the application to grant elevated access. Clients that
636	/// do not present a certificate are unaffected.
637	///
638	/// Plain-TLS listeners built via [`Self::server_config`] also use these roots
639	/// for optional mTLS.
640	#[arg(
641		long = "server-tls-root",
642		id = "server-tls-root",
643		value_delimiter = ',',
644		env = "MOQ_SERVER_TLS_ROOT"
645	)]
646	#[serde(default, skip_serializing_if = "Vec::is_empty")]
647	#[serde_as(as = "serde_with::OneOrMany<_>")]
648	pub root: Vec<PathBuf>,
649}
650
651impl Server {
652	/// Load all configured root CAs into a [`rustls::RootCertStore`].
653	pub fn load_roots(&self) -> Result<rustls::RootCertStore> {
654		let mut roots = rustls::RootCertStore::empty();
655		for path in &self.root {
656			let certs = read_certs(path)?;
657			if certs.is_empty() {
658				return Err(Error::Empty);
659			}
660			for cert in certs {
661				roots.add(cert).map_err(Error::AddRoot)?;
662			}
663		}
664		Ok(roots)
665	}
666
667	/// Build a [`rustls::ServerConfig`] for a plain-TLS (non-QUIC) server, e.g. an
668	/// RTMPS or HTTPS listener fronting the QUIC endpoint, reusing the QUIC
669	/// backend's certificate handling: on-disk `cert`/`key` pairs, `generate`
670	/// self-signed certs, and optional mTLS `root` client CAs.
671	///
672	/// `alpn` sets the advertised ALPN protocols (e.g.
673	/// `vec![b"h2".to_vec(), b"http/1.1".to_vec()]`); pass an empty list for a
674	/// protocol like RTMPS that doesn't use ALPN.
675	#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
676	pub fn server_config(&self, alpn: Vec<Vec<u8>>) -> Result<Arc<rustls::ServerConfig>> {
677		server_config(self, alpn)
678	}
679}
680
681/// Build a [`rustls::ServerConfig`] from a [`Server`] for a plain-TLS listener.
682#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
683fn server_config(config: &Server, alpn: Vec<Vec<u8>>) -> Result<Arc<rustls::ServerConfig>> {
684	let provider = crypto::provider();
685
686	let certs = ServeCerts::new(provider.clone());
687	certs.load_certs(config)?;
688	let certs = Arc::new(certs);
689
690	// TCP can negotiate TLS 1.2 as well as 1.3, unlike QUIC which is 1.3-only.
691	let builder =
692		rustls::ServerConfig::builder_with_provider(provider.clone()).with_safe_default_protocol_versions()?;
693
694	let mut tls = if config.root.is_empty() {
695		builder.with_no_client_auth().with_cert_resolver(certs)
696	} else {
697		let roots = config.load_roots()?;
698		let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
699			.allow_unauthenticated()
700			.build()
701			.map_err(Error::ClientVerifier)?;
702		builder.with_client_cert_verifier(verifier).with_cert_resolver(certs)
703	};
704
705	tls.alpn_protocols = alpn;
706	Ok(Arc::new(tls))
707}
708
709/// A peer's validated client-certificate chain from the mTLS handshake.
710///
711/// Returned by [`crate::Request::peer_identity`] when the peer presented a
712/// certificate that chained to a configured [`Server::root`]. Owns the chain
713/// (leaf first) so callers can inspect it, e.g. [`expiry`](Self::expiry),
714/// without re-parsing the type-erased QUIC identity.
715#[derive(Clone)]
716pub struct PeerIdentity {
717	chain: Vec<CertificateDer<'static>>,
718}
719
720impl PeerIdentity {
721	/// Wrap the type-erased identity from `quinn::Connection::peer_identity`.
722	/// Returns `None` if the peer presented no certificate or the identity is
723	/// not a certificate chain.
724	#[cfg(any(feature = "quinn", feature = "noq"))]
725	pub(crate) fn from_any(identity: Option<Box<dyn std::any::Any>>) -> Option<Self> {
726		let chain = identity?.downcast::<Vec<CertificateDer<'static>>>().ok()?;
727		Some(Self { chain: *chain })
728	}
729
730	/// Wrap a certificate chain already exposed by a QUIC backend.
731	#[cfg(feature = "quiche")]
732	pub(crate) fn from_chain(chain: Vec<CertificateDer<'static>>) -> Self {
733		Self { chain }
734	}
735
736	/// The validated certificate chain, leaf first.
737	///
738	/// Exposes [`rustls::pki_types::CertificateDer`] directly (already part of
739	/// this crate's public API via the `rustls` re-export), so a major `rustls`
740	/// bump is a breaking change for consumers of this method.
741	pub fn chain(&self) -> &[CertificateDer<'static>] {
742		&self.chain
743	}
744
745	/// The leaf certificate's `notAfter`, if it parses. A `notAfter` before the
746	/// Unix epoch is reported as `None`.
747	pub fn expiry(&self) -> Option<std::time::SystemTime> {
748		use std::time::{Duration, UNIX_EPOCH};
749
750		let leaf = self.chain.first()?;
751		let (_, cert) = x509_parser::parse_x509_certificate(leaf).ok()?;
752		let secs = u64::try_from(cert.validity().not_after.timestamp()).ok()?;
753		Some(UNIX_EPOCH + Duration::from_secs(secs))
754	}
755}
756
757/// The certificates a server is currently serving.
758///
759/// Only a QUIC backend serves TLS of its own, so nothing else populates this.
760#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
761#[derive(Debug, Default)]
762pub(crate) struct Info {
763	pub(crate) certs: Vec<Arc<rustls::sign::CertifiedKey>>,
764	pub(crate) fingerprints: Vec<String>,
765}
766
767/// A live handle to the certificates a [`crate::Server`] is serving.
768///
769/// Cheap to clone, and every read reflects the latest hot reload of the files on
770/// disk, so a caller can build one at startup and hold it for the process
771/// lifetime. Obtained from [`crate::Server::certificates`].
772#[derive(Clone, Debug)]
773pub struct Certificates {
774	#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
775	info: Arc<RwLock<Info>>,
776}
777
778impl Certificates {
779	#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
780	pub(crate) fn new(info: Arc<RwLock<Info>>) -> Self {
781		Self { info }
782	}
783
784	/// An empty set, used when no TLS-bearing backend is configured.
785	pub(crate) fn empty() -> Self {
786		Self {
787			#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
788			info: Arc::new(RwLock::new(Info::default())),
789		}
790	}
791
792	/// The SHA-256 fingerprints of the certificates being served right now, hex
793	/// encoded, one per certificate and in configuration order.
794	///
795	/// Empty when the server has no TLS-bearing backend. Re-read this per use
796	/// rather than caching it: a cert rotation on disk changes the values.
797	pub fn fingerprints(&self) -> Vec<String> {
798		#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
799		{
800			// A panicking writer can't leave the cert list half-updated (it is
801			// replaced wholesale), so a poisoned lock is still safe to read.
802			let info = self.info.read().unwrap_or_else(std::sync::PoisonError::into_inner);
803			info.fingerprints.clone()
804		}
805		#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche")))]
806		Vec::new()
807	}
808}
809
810// ── NoCertificateVerification ───────────────────────────────────────
811
812#[derive(Debug)]
813struct NoCertificateVerification(crypto::Provider);
814
815impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
816	fn verify_server_cert(
817		&self,
818		_end_entity: &CertificateDer<'_>,
819		_intermediates: &[CertificateDer<'_>],
820		_server_name: &ServerName<'_>,
821		_ocsp: &[u8],
822		_now: UnixTime,
823	) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
824		Ok(rustls::client::danger::ServerCertVerified::assertion())
825	}
826
827	fn verify_tls12_signature(
828		&self,
829		message: &[u8],
830		cert: &CertificateDer<'_>,
831		dss: &rustls::DigitallySignedStruct,
832	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
833		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
834	}
835
836	fn verify_tls13_signature(
837		&self,
838		message: &[u8],
839		cert: &CertificateDer<'_>,
840		dss: &rustls::DigitallySignedStruct,
841	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
842		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
843	}
844
845	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
846		self.0.signature_verification_algorithms.supported_schemes()
847	}
848}
849
850// ── FingerprintVerifier ─────────────────────────────────────────────
851
852#[derive(Debug)]
853pub(crate) struct FingerprintVerifier {
854	provider: crypto::Provider,
855	fingerprints: Vec<Vec<u8>>,
856}
857
858impl FingerprintVerifier {
859	pub fn new(provider: crypto::Provider, fingerprints: Vec<Vec<u8>>) -> Self {
860		Self { provider, fingerprints }
861	}
862}
863
864impl rustls::client::danger::ServerCertVerifier for FingerprintVerifier {
865	fn verify_server_cert(
866		&self,
867		end_entity: &CertificateDer<'_>,
868		_intermediates: &[CertificateDer<'_>],
869		_server_name: &ServerName<'_>,
870		_ocsp: &[u8],
871		_now: UnixTime,
872	) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
873		let fingerprint = crypto::sha256(&self.provider, end_entity);
874		if self.fingerprints.iter().any(|fp| fingerprint.as_ref() == fp.as_slice()) {
875			Ok(rustls::client::danger::ServerCertVerified::assertion())
876		} else {
877			Err(rustls::Error::General("fingerprint mismatch".into()))
878		}
879	}
880
881	fn verify_tls12_signature(
882		&self,
883		message: &[u8],
884		cert: &CertificateDer<'_>,
885		dss: &rustls::DigitallySignedStruct,
886	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
887		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
888	}
889
890	fn verify_tls13_signature(
891		&self,
892		message: &[u8],
893		cert: &CertificateDer<'_>,
894		dss: &rustls::DigitallySignedStruct,
895	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
896		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
897	}
898
899	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
900		self.provider.signature_verification_algorithms.supported_schemes()
901	}
902}
903
904#[cfg(test)]
905#[cfg(all(any(feature = "quinn", feature = "noq", feature = "quiche"), feature = "aws-lc-rs"))]
906mod tests {
907	/// Disabling verification cannot be combined with trust material that it would
908	/// otherwise ignore.
909	#[test]
910	fn disable_verify_rejects_trust_material() {
911		let insecure = Client {
912			disable_verify: Some(true),
913			..Default::default()
914		};
915		assert!(matches!(insecure.verification(), Ok(Verification::Disabled)));
916
917		let with_fingerprint = Client {
918			disable_verify: Some(true),
919			fingerprint: vec!["ab".repeat(32)],
920			..Default::default()
921		};
922		assert!(matches!(
923			with_fingerprint.verification(),
924			Err(Error::DisableVerifyWithTrust)
925		));
926
927		let with_root = Client {
928			disable_verify: Some(true),
929			root: vec!["/tmp/root.pem".into()],
930			..Default::default()
931		};
932		assert!(matches!(with_root.verification(), Err(Error::DisableVerifyWithTrust)));
933
934		let with_system_roots = Client {
935			disable_verify: Some(true),
936			system_roots: Some(true),
937			..Default::default()
938		};
939		assert!(matches!(
940			with_system_roots.verification(),
941			Err(Error::DisableVerifyWithTrust)
942		));
943
944		let without_system_roots = Client {
945			disable_verify: Some(true),
946			system_roots: Some(false),
947			..Default::default()
948		};
949		assert!(matches!(
950			without_system_roots.verification(),
951			Ok(Verification::Disabled)
952		));
953	}
954
955	use super::*;
956	use rustls::client::danger::ServerCertVerifier;
957	use rustls::pki_types::ServerName;
958
959	fn self_signed() -> CertificateDer<'static> {
960		let key = rcgen::KeyPair::generate().unwrap();
961		let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
962		params.self_signed(&key).unwrap().into()
963	}
964
965	#[cfg(any(feature = "quinn", feature = "noq"))]
966	#[test]
967	fn peer_identity_expiry_reads_not_after() {
968		// notAfter at a whole second so the round-trip is exact.
969		let not_after = ::time::OffsetDateTime::from_unix_timestamp(2_000_000_000).unwrap();
970
971		let key = rcgen::KeyPair::generate().unwrap();
972		let mut params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
973		params.not_after = not_after;
974		let cert: CertificateDer<'static> = params.self_signed(&key).unwrap().into();
975
976		// quinn/noq hand back the chain as a boxed Vec<CertificateDer>.
977		let identity: Box<dyn std::any::Any> = Box::new(vec![cert]);
978		let parsed = PeerIdentity::from_any(Some(identity)).expect("chain parsed");
979		let expiry = parsed.expiry().expect("expiry parsed");
980		assert_eq!(
981			expiry.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
982			2_000_000_000
983		);
984	}
985
986	#[cfg(any(feature = "quinn", feature = "noq"))]
987	#[test]
988	fn peer_identity_none_without_chain() {
989		assert!(PeerIdentity::from_any(None).is_none());
990		// A wrong downcast type (not a cert chain) yields None rather than panicking.
991		let bogus: Box<dyn std::any::Any> = Box::new(42u32);
992		assert!(PeerIdentity::from_any(Some(bogus)).is_none());
993	}
994
995	#[test]
996	fn fingerprint_verifier_matches_and_rejects() {
997		let provider = crypto::provider();
998		let cert = self_signed();
999		let fingerprint = crypto::sha256(&provider, cert.as_ref()).as_ref().to_vec();
1000
1001		let name = ServerName::try_from("localhost").unwrap();
1002		let now = UnixTime::now();
1003
1004		let verifier = FingerprintVerifier::new(provider.clone(), vec![fingerprint]);
1005		assert!(verifier.verify_server_cert(&cert, &[], &name, &[], now).is_ok());
1006
1007		// A different leaf certificate must not satisfy the pin.
1008		let other = self_signed();
1009		assert!(verifier.verify_server_cert(&other, &[], &name, &[], now).is_err());
1010	}
1011
1012	#[test]
1013	fn build_installs_fingerprint_verifier() {
1014		let cert = self_signed();
1015		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
1016
1017		// A bogus hash still builds; verification happens at handshake time.
1018		let config = Client {
1019			fingerprint: vec![fingerprint],
1020			..Default::default()
1021		};
1022		assert!(config.build().is_ok());
1023	}
1024
1025	#[test]
1026	fn build_rejects_invalid_fingerprint_hex() {
1027		let config = Client {
1028			fingerprint: vec!["not-hex".to_string()],
1029			..Default::default()
1030		};
1031		assert!(matches!(config.build(), Err(Error::Fingerprint(_))));
1032	}
1033
1034	#[test]
1035	fn build_rejects_wrong_length_fingerprint() {
1036		// Valid hex, but only 2 bytes instead of 32.
1037		let config = Client {
1038			fingerprint: vec!["abcd".to_string()],
1039			..Default::default()
1040		};
1041		assert!(matches!(config.build(), Err(Error::FingerprintLength(2))));
1042	}
1043
1044	#[test]
1045	fn build_rejects_no_roots() {
1046		// System roots disabled with no custom root and no alternate verifier:
1047		// nothing could ever verify, so reject up front.
1048		let config = Client {
1049			system_roots: Some(false),
1050			..Default::default()
1051		};
1052		assert!(matches!(config.build(), Err(Error::NoRoots)));
1053	}
1054
1055	#[test]
1056	fn build_allows_no_roots_when_verification_overridden() {
1057		// disable_verify swaps in its own verifier, so an empty store is fine.
1058		let config = Client {
1059			system_roots: Some(false),
1060			disable_verify: Some(true),
1061			..Default::default()
1062		};
1063		assert!(config.build().is_ok());
1064
1065		// Same for fingerprint pinning.
1066		let cert = self_signed();
1067		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
1068		let config = Client {
1069			system_roots: Some(false),
1070			fingerprint: vec![fingerprint],
1071			..Default::default()
1072		};
1073		assert!(config.build().is_ok());
1074	}
1075
1076	#[test]
1077	fn build_rejects_fingerprint_with_roots() {
1078		let cert = self_signed();
1079		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
1080
1081		// Fingerprint pinning bypasses the CA chain, so combining it with roots
1082		// is rejected rather than silently ignoring one of them.
1083		let with_system = Client {
1084			fingerprint: vec![fingerprint.clone()],
1085			system_roots: Some(true),
1086			..Default::default()
1087		};
1088		assert!(matches!(with_system.build(), Err(Error::FingerprintWithRoots)));
1089
1090		// The conflict is detected before any root file is read, so the path
1091		// need not exist.
1092		let with_custom = Client {
1093			fingerprint: vec![fingerprint],
1094			root: vec![PathBuf::from("/does-not-exist.pem")],
1095			..Default::default()
1096		};
1097		assert!(matches!(with_custom.build(), Err(Error::FingerprintWithRoots)));
1098	}
1099
1100	/// Write a self-signed cert to a temp PEM file, returning the keep-alive
1101	/// handle alongside its path.
1102	fn self_signed_root() -> (tempfile::NamedTempFile, PathBuf) {
1103		use std::io::Write;
1104		let key = rcgen::KeyPair::generate().unwrap();
1105		let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
1106		let cert = params.self_signed(&key).unwrap();
1107		let mut file = tempfile::NamedTempFile::new().unwrap();
1108		file.write_all(cert.pem().as_bytes()).unwrap();
1109		let path = file.path().to_path_buf();
1110		(file, path)
1111	}
1112
1113	#[test]
1114	fn build_uses_platform_verifier_by_default() {
1115		// No custom roots, system trust on: resolves to the OS platform verifier
1116		// (bundled Mozilla roots on Android) and must build cleanly everywhere.
1117		assert!(Client::default().build().is_ok());
1118	}
1119
1120	#[test]
1121	fn build_with_custom_roots_only() {
1122		// A custom root with system trust left at its default disables the system
1123		// roots, verifying against the custom PEM alone.
1124		let (_keep, path) = self_signed_root();
1125		let config = Client {
1126			root: vec![path],
1127			..Default::default()
1128		};
1129		assert!(config.build().is_ok());
1130	}
1131
1132	#[test]
1133	fn build_with_custom_and_system_roots() {
1134		// Custom roots layered on top of system trust: exercises the platform
1135		// verifier's extra-roots path (or the bundled roots plus custom on Android).
1136		let (_keep, path) = self_signed_root();
1137		let config = Client {
1138			root: vec![path],
1139			system_roots: Some(true),
1140			..Default::default()
1141		};
1142		assert!(config.build().is_ok());
1143	}
1144}
1145
1146// ── ServeCerts ──────────────────────────────────────────────────────
1147
1148#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
1149#[derive(Debug)]
1150pub(crate) struct ServeCerts {
1151	pub info: Arc<RwLock<Info>>,
1152	provider: crypto::Provider,
1153}
1154
1155#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
1156impl ServeCerts {
1157	pub fn new(provider: crypto::Provider) -> Self {
1158		Self {
1159			info: Arc::new(RwLock::new(Info::default())),
1160			provider,
1161		}
1162	}
1163
1164	pub fn load_certs(&self, config: &Server) -> Result<()> {
1165		if config.cert.len() != config.key.len() {
1166			return Err(Error::CertKeyCountMismatch);
1167		}
1168		if config.cert.is_empty() && config.generate.is_empty() {
1169			return Err(Error::NoCertSource);
1170		}
1171
1172		let mut certs = Vec::new();
1173
1174		// Load the certificate and key files based on their index.
1175		for (cert, key) in config.cert.iter().zip(config.key.iter()) {
1176			certs.push(Arc::new(self.load(cert, key)?));
1177		}
1178
1179		// Generate a new certificate if requested.
1180		if !config.generate.is_empty() {
1181			certs.push(Arc::new(self.generate(&config.generate)?));
1182		}
1183
1184		self.set_certs(certs);
1185		Ok(())
1186	}
1187
1188	// Load a certificate and corresponding key from a file, but don't add it to the certs
1189	fn load(&self, chain_path: &Path, key_path: &Path) -> Result<rustls::sign::CertifiedKey> {
1190		let chain = read_certs(chain_path)?;
1191		if chain.is_empty() {
1192			return Err(Error::Empty);
1193		}
1194
1195		// Read the PEM private key
1196		let key = PrivateKeyDer::from_pem_file(key_path).map_err(Error::Key)?;
1197		let key = self.provider.key_provider.load_private_key(key)?;
1198
1199		let certified_key = rustls::sign::CertifiedKey::new(chain, key);
1200
1201		certified_key.keys_match().map_err(|source| Error::KeyMismatch {
1202			key: key_path.to_path_buf(),
1203			cert: chain_path.to_path_buf(),
1204			source,
1205		})?;
1206
1207		Ok(certified_key)
1208	}
1209
1210	#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
1211	fn generate(&self, hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
1212		let key_pair = rcgen::KeyPair::generate()?;
1213
1214		let mut params = rcgen::CertificateParams::new(hostnames)?;
1215
1216		// Make the certificate valid for two weeks, starting yesterday (in case of clock drift).
1217		// WebTransport certificates MUST be valid for two weeks at most.
1218		params.not_before = ::time::OffsetDateTime::now_utc() - ::time::Duration::days(1);
1219		params.not_after = params.not_before + ::time::Duration::days(14);
1220
1221		// Generate the certificate
1222		let cert = params.self_signed(&key_pair)?;
1223
1224		// Convert the rcgen type to the rustls type.
1225		let key_der = key_pair.serialized_der().to_vec();
1226		let key_der = PrivatePkcs8KeyDer::from(key_der);
1227		let key = self.provider.key_provider.load_private_key(key_der.into())?;
1228
1229		// Create a rustls::sign::CertifiedKey
1230		Ok(rustls::sign::CertifiedKey::new(vec![cert.into()], key))
1231	}
1232
1233	#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
1234	fn generate(&self, _hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
1235		Err(Error::NoCryptoProvider)
1236	}
1237
1238	// Replace the certificates
1239	pub fn set_certs(&self, certs: Vec<Arc<rustls::sign::CertifiedKey>>) {
1240		let fingerprints = certs
1241			.iter()
1242			.map(|ck| {
1243				let fingerprint = crate::crypto::sha256(&self.provider, ck.cert[0].as_ref());
1244				hex::encode(fingerprint)
1245			})
1246			.collect();
1247
1248		let mut info = self.info.write().expect("info write lock poisoned");
1249		info.certs = certs;
1250		info.fingerprints = fingerprints;
1251	}
1252
1253	// Return the best certificate for the given ClientHello.
1254	fn best_certificate(
1255		&self,
1256		client_hello: &rustls::server::ClientHello<'_>,
1257	) -> Option<Arc<rustls::sign::CertifiedKey>> {
1258		let server_name = client_hello.server_name()?;
1259		let dns_name = rustls::pki_types::ServerName::try_from(server_name).ok()?;
1260
1261		for ck in self.info.read().expect("info read lock poisoned").certs.iter() {
1262			let leaf: webpki::EndEntityCert = ck
1263				.end_entity_cert()
1264				.expect("missing certificate")
1265				.try_into()
1266				.expect("failed to parse certificate");
1267
1268			if leaf.verify_is_valid_for_subject_name(&dns_name).is_ok() {
1269				return Some(ck.clone());
1270			}
1271		}
1272
1273		None
1274	}
1275}
1276
1277#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
1278impl rustls::server::ResolvesServerCert for ServeCerts {
1279	fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<rustls::sign::CertifiedKey>> {
1280		if let Some(cert) = self.best_certificate(&client_hello) {
1281			return Some(cert);
1282		}
1283
1284		// If this happens, it means the client was trying to connect to an unknown hostname.
1285		// We do our best and return the first certificate.
1286		tracing::warn!(server_name = ?client_hello.server_name(), "no SNI certificate found");
1287
1288		self.info
1289			.read()
1290			.expect("info read lock poisoned")
1291			.certs
1292			.first()
1293			.cloned()
1294	}
1295}
1296
1297// ── reload_certs ────────────────────────────────────────────────────
1298
1299/// Watch the on-disk cert/key files and reload them whenever they change.
1300///
1301/// Reacting to the filesystem means cert-manager, Kubernetes secret mounts, and
1302/// `mv`-into-place rotate certs with no external signal. Returns immediately when
1303/// only generated certs are configured: there's nothing on disk to watch.
1304#[cfg(any(feature = "quinn", feature = "noq"))]
1305pub(crate) async fn reload_certs(certs: Arc<ServeCerts>, tls_config: Server) {
1306	let paths: Vec<PathBuf> = tls_config.cert.iter().chain(tls_config.key.iter()).cloned().collect();
1307	if paths.is_empty() {
1308		return;
1309	}
1310
1311	let mut watcher = match crate::watch::FileWatcher::new(&paths) {
1312		Ok(watcher) => watcher,
1313		Err(err) => {
1314			tracing::error!(%err, "failed to watch certificate files; hot reload disabled");
1315			return;
1316		}
1317	};
1318
1319	loop {
1320		watcher.changed().await;
1321		tracing::info!("reloading server certificates");
1322
1323		if let Err(err) = certs.load_certs(&tls_config) {
1324			tracing::warn!(%err, "failed to reload server certificates");
1325		}
1326	}
1327}