Skip to main content

moq_native/
tls.rs

1use crate::crypto;
2use rustls::pki_types::pem::PemObject;
3use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::{fs, io};
7
8#[cfg(any(feature = "quinn", feature = "noq"))]
9use rustls::pki_types::PrivatePkcs8KeyDer;
10#[cfg(any(feature = "quinn", feature = "noq"))]
11use std::sync::RwLock;
12
13/// Errors loading or generating TLS certificates and keys.
14///
15/// Shared by the client TLS config and the quinn/noq servers so each backend's
16/// error type can compose it via `#[from]`.
17#[derive(Debug, thiserror::Error)]
18#[non_exhaustive]
19pub enum Error {
20	#[error("failed to open certificate file")]
21	Open(#[source] std::io::Error),
22
23	#[error("failed to read file")]
24	ReadFile(#[source] std::io::Error),
25
26	#[error("failed to read certificates")]
27	Read(#[source] rustls::pki_types::pem::Error),
28
29	#[error("failed to parse private key")]
30	Key(#[source] rustls::pki_types::pem::Error),
31
32	#[error("no certificates found")]
33	Empty,
34
35	#[error("no roots found in {}", .0.display())]
36	EmptyRoots(PathBuf),
37
38	#[error(
39		"no trusted roots: provide --tls-root, enable --tls-system-roots, or use --tls-fingerprint / --tls-disable-verify"
40	)]
41	NoRoots,
42
43	#[error("invalid TLS fingerprint (expected hex-encoded SHA-256)")]
44	Fingerprint(#[source] hex::FromHexError),
45
46	#[error("invalid TLS fingerprint length: expected 32 bytes (SHA-256), got {0}")]
47	FingerprintLength(usize),
48
49	#[error("failed to add root certificate")]
50	AddRoot(#[source] rustls::Error),
51
52	#[error("failed to configure client certificate")]
53	ClientAuth(#[source] rustls::Error),
54
55	#[error("both --client-tls-cert and --client-tls-key must be provided")]
56	IncompleteClientAuth,
57
58	#[error("must provide both cert and key")]
59	CertKeyCountMismatch,
60
61	#[error("must provide at least one cert/key pair or generate entry")]
62	NoCertSource,
63
64	#[error("private key {} doesn't match certificate {}", key.display(), cert.display())]
65	KeyMismatch {
66		key: PathBuf,
67		cert: PathBuf,
68		#[source]
69		source: rustls::Error,
70	},
71
72	#[error(transparent)]
73	Rustls(#[from] rustls::Error),
74
75	#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
76	#[error(transparent)]
77	Rcgen(#[from] rcgen::Error),
78
79	#[error("no crypto provider available; enable aws-lc-rs or ring feature")]
80	NoCryptoProvider,
81}
82
83/// Convenience alias for results produced by this module.
84pub type Result<T> = std::result::Result<T, Error>;
85
86/// Read a PEM file into its list of certificates.
87pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
88	let file = fs::File::open(path).map_err(Error::Open)?;
89	let mut reader = io::BufReader::new(file);
90	CertificateDer::pem_reader_iter(&mut reader)
91		.collect::<std::result::Result<_, _>>()
92		.map_err(Error::Read)
93}
94
95// ── Client ──────────────────────────────────────────────────────────
96
97/// TLS configuration for the client.
98#[serde_with::serde_as]
99#[derive(Clone, Default, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
100#[serde(default, deny_unknown_fields)]
101#[group(id = "tls-client")]
102#[non_exhaustive]
103pub struct Client {
104	/// Trust the TLS root at this path, encoded as PEM.
105	///
106	/// This value can be provided multiple times for multiple roots.
107	/// In config files, accepts either a single string or a TOML array.
108	///
109	/// These roots are added on top of the system roots. By default the system
110	/// roots are only loaded when no custom root is given, so passing a root
111	/// replaces them; set `--tls-system-roots` to trust both (e.g. to reach a
112	/// local relay with a private CA and a remote one with a public CA).
113	#[serde(skip_serializing_if = "Vec::is_empty")]
114	#[arg(id = "tls-root", long = "tls-root", env = "MOQ_CLIENT_TLS_ROOT")]
115	#[serde_as(as = "serde_with::OneOrMany<_>")]
116	pub root: Vec<PathBuf>,
117
118	/// Also trust the platform's native root certificates.
119	///
120	/// Defaults to enabled only when no `--tls-root` is given. Set it explicitly
121	/// to trust the system roots alongside any custom roots, or set it to false
122	/// to trust only the custom roots. Trusting neither (no custom root and
123	/// system roots disabled) is rejected, since verification could never pass.
124	#[serde(skip_serializing_if = "Option::is_none")]
125	#[arg(
126		id = "tls-system-roots",
127		long = "tls-system-roots",
128		env = "MOQ_CLIENT_TLS_SYSTEM_ROOTS",
129		default_missing_value = "true",
130		num_args = 0..=1,
131		require_equals = true,
132		value_parser = clap::value_parser!(bool),
133	)]
134	pub system_roots: Option<bool>,
135
136	/// Pin the peer to a certificate with one of these SHA-256 fingerprints, encoded as hex.
137	///
138	/// This is the native equivalent of the browser's WebTransport `serverCertificateHashes`,
139	/// and accepts the same values a server reports via its certificate fingerprints. Use it to
140	/// trust a self-signed certificate without disabling verification or fetching the hash over
141	/// an insecure `http://` request. When set, the normal CA/root chain is bypassed: only the
142	/// leaf certificate's fingerprint is checked.
143	///
144	/// This value can be provided multiple times to accept any of several fingerprints (e.g.
145	/// across a certificate rotation). In config files, accepts either a single string or a TOML array.
146	#[serde(skip_serializing_if = "Vec::is_empty")]
147	#[arg(id = "tls-fingerprint", long = "tls-fingerprint", env = "MOQ_CLIENT_TLS_FINGERPRINT")]
148	#[serde_as(as = "serde_with::OneOrMany<_>")]
149	pub fingerprint: Vec<String>,
150
151	/// PEM file containing the client certificate chain for mTLS.
152	///
153	/// Only certificates are extracted; any private keys in the file are ignored.
154	/// Must be paired with `--client-tls-key`.
155	#[serde(skip_serializing_if = "Option::is_none")]
156	#[arg(id = "client-tls-cert", long = "client-tls-cert", env = "MOQ_CLIENT_TLS_CERT")]
157	pub cert: Option<PathBuf>,
158
159	/// PEM file containing the private key for mTLS.
160	///
161	/// Only the private key is extracted; any certificates in the file are ignored.
162	/// Must be paired with `--client-tls-cert`.
163	#[serde(skip_serializing_if = "Option::is_none")]
164	#[arg(id = "client-tls-key", long = "client-tls-key", env = "MOQ_CLIENT_TLS_KEY")]
165	pub key: Option<PathBuf>,
166
167	/// Danger: Disable TLS certificate verification.
168	///
169	/// Fine for local development and between relays, but should be used in caution in production.
170	#[serde(skip_serializing_if = "Option::is_none")]
171	#[arg(
172		id = "tls-disable-verify",
173		long = "tls-disable-verify",
174		env = "MOQ_CLIENT_TLS_DISABLE_VERIFY",
175		default_missing_value = "true",
176		num_args = 0..=1,
177		require_equals = true,
178		value_parser = clap::value_parser!(bool),
179	)]
180	pub disable_verify: Option<bool>,
181}
182
183impl Client {
184	/// Build a [`rustls::ClientConfig`] from this configuration.
185	///
186	/// Trusts the configured roots plus the platform's native roots (the latter
187	/// gated by `system_roots`), optionally attaches a client identity for mTLS,
188	/// and swaps in fingerprint pinning or disabled verification when requested.
189	pub fn build(&self) -> Result<rustls::ClientConfig> {
190		let provider = crypto::provider();
191
192		// Default to system roots only when no custom root is given, so passing a
193		// root replaces them unless the system roots are explicitly re-enabled.
194		let system_roots = self.system_roots.unwrap_or(self.root.is_empty());
195
196		// fingerprint pinning and disable_verify swap in their own verifier below,
197		// so an empty root store is fine in those cases. Otherwise WebPKI needs at
198		// least one trusted root to ever succeed, so fail fast instead of producing
199		// confusing handshake errors later.
200		let custom_verifier = self.disable_verify.unwrap_or_default() || !self.fingerprint.is_empty();
201		if !system_roots && self.root.is_empty() && !custom_verifier {
202			return Err(Error::NoRoots);
203		}
204
205		let mut roots = rustls::RootCertStore::empty();
206		if system_roots {
207			let native = rustls_native_certs::load_native_certs();
208			for err in native.errors {
209				tracing::warn!(%err, "failed to load root cert");
210			}
211			for cert in native.certs {
212				roots.add(cert).map_err(Error::AddRoot)?;
213			}
214		}
215		for root in &self.root {
216			let certs = read_certs(root)?;
217			if certs.is_empty() {
218				return Err(Error::EmptyRoots(root.clone()));
219			}
220			for cert in certs {
221				roots.add(cert).map_err(Error::AddRoot)?;
222			}
223		}
224
225		// Allow TLS 1.2 in addition to 1.3 for WebSocket compatibility.
226		// QUIC always negotiates TLS 1.3 regardless of this setting.
227		let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
228			.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
229			.with_root_certificates(roots);
230
231		let mut tls = match (&self.cert, &self.key) {
232			(Some(cert_path), Some(key_path)) => {
233				let cert_pem = fs::read(cert_path).map_err(Error::ReadFile)?;
234				let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
235					.collect::<std::result::Result<_, _>>()
236					.map_err(Error::Read)?;
237				if chain.is_empty() {
238					return Err(Error::Empty);
239				}
240				let key_pem = fs::read(key_path).map_err(Error::ReadFile)?;
241				let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(Error::Key)?;
242				builder.with_client_auth_cert(chain, key).map_err(Error::ClientAuth)?
243			}
244			(None, None) => builder.with_no_client_auth(),
245			_ => return Err(Error::IncompleteClientAuth),
246		};
247
248		if self.disable_verify.unwrap_or_default() {
249			tracing::warn!("TLS server certificate verification is disabled; A man-in-the-middle attack is possible.");
250			let noop = NoCertificateVerification(provider);
251			tls.dangerous().set_certificate_verifier(Arc::new(noop));
252		} else if !self.fingerprint.is_empty() {
253			let fingerprints = self
254				.fingerprint
255				.iter()
256				.map(|fp| {
257					let bytes = hex::decode(fp.trim()).map_err(Error::Fingerprint)?;
258					match bytes.len() {
259						32 => Ok(bytes),
260						len => Err(Error::FingerprintLength(len)),
261					}
262				})
263				.collect::<Result<Vec<_>>>()?;
264
265			let verifier = FingerprintVerifier::new(provider, fingerprints);
266			tls.dangerous().set_certificate_verifier(Arc::new(verifier));
267		}
268
269		Ok(tls)
270	}
271}
272
273// ── Server ──────────────────────────────────────────────────────────
274
275/// TLS configuration for the server.
276///
277/// Certificate and keys must currently be files on disk.
278/// Alternatively, you can generate a self-signed certificate given a list of hostnames.
279///
280/// In config files, each list field accepts either a single string or a TOML array.
281#[serde_with::serde_as]
282#[derive(clap::Args, Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
283#[serde(deny_unknown_fields)]
284#[group(id = "tls-server")]
285#[non_exhaustive]
286pub struct Server {
287	/// Load the given certificate from disk.
288	#[arg(long = "tls-cert", id = "tls-cert", env = "MOQ_SERVER_TLS_CERT")]
289	#[serde(default, skip_serializing_if = "Vec::is_empty")]
290	#[serde_as(as = "serde_with::OneOrMany<_>")]
291	pub cert: Vec<PathBuf>,
292
293	/// Load the given key from disk.
294	#[arg(long = "tls-key", id = "tls-key", env = "MOQ_SERVER_TLS_KEY")]
295	#[serde(default, skip_serializing_if = "Vec::is_empty")]
296	#[serde_as(as = "serde_with::OneOrMany<_>")]
297	pub key: Vec<PathBuf>,
298
299	/// Or generate a new certificate and key with the given hostnames.
300	/// This won't be valid unless the client uses the fingerprint or disables verification.
301	#[arg(
302		long = "tls-generate",
303		id = "tls-generate",
304		value_delimiter = ',',
305		env = "MOQ_SERVER_TLS_GENERATE"
306	)]
307	#[serde(default, skip_serializing_if = "Vec::is_empty")]
308	#[serde_as(as = "serde_with::OneOrMany<_>")]
309	pub generate: Vec<String>,
310
311	/// PEM file(s) of root CAs for validating optional client certificates (mTLS).
312	///
313	/// When set, clients *may* present a certificate during the TLS handshake.
314	/// Valid presentations are reported via [`crate::Request::peer_identity`]
315	/// and can be used by the application to grant elevated access. Clients that
316	/// do not present a certificate are unaffected.
317	///
318	/// Only supported by the Quinn and noq backends.
319	#[arg(
320		long = "server-tls-root",
321		id = "server-tls-root",
322		value_delimiter = ',',
323		env = "MOQ_SERVER_TLS_ROOT"
324	)]
325	#[serde(default, skip_serializing_if = "Vec::is_empty")]
326	#[serde_as(as = "serde_with::OneOrMany<_>")]
327	pub root: Vec<PathBuf>,
328}
329
330impl Server {
331	/// Load all configured root CAs into a [`rustls::RootCertStore`].
332	pub fn load_roots(&self) -> Result<rustls::RootCertStore> {
333		let mut roots = rustls::RootCertStore::empty();
334		for path in &self.root {
335			let certs = read_certs(path)?;
336			if certs.is_empty() {
337				return Err(Error::Empty);
338			}
339			for cert in certs {
340				roots.add(cert).map_err(Error::AddRoot)?;
341			}
342		}
343		Ok(roots)
344	}
345}
346
347/// A peer's validated client-certificate chain from the mTLS handshake.
348///
349/// Returned by [`crate::Request::peer_identity`] when the peer presented a
350/// certificate that chained to a configured [`Server::root`]. Owns the chain
351/// (leaf first) so callers can inspect it, e.g. [`expiry`](Self::expiry),
352/// without re-parsing the type-erased QUIC identity.
353pub struct PeerIdentity {
354	chain: Vec<CertificateDer<'static>>,
355}
356
357impl PeerIdentity {
358	/// Wrap the type-erased identity from `quinn::Connection::peer_identity`.
359	/// Returns `None` if the peer presented no certificate or the identity is
360	/// not a certificate chain.
361	#[cfg(any(feature = "quinn", feature = "noq"))]
362	pub(crate) fn from_any(identity: Option<Box<dyn std::any::Any>>) -> Option<Self> {
363		let chain = identity?.downcast::<Vec<CertificateDer<'static>>>().ok()?;
364		Some(Self { chain: *chain })
365	}
366
367	/// The validated certificate chain, leaf first.
368	///
369	/// Exposes [`rustls::pki_types::CertificateDer`] directly (already part of
370	/// this crate's public API via the `rustls` re-export), so a major `rustls`
371	/// bump is a breaking change for consumers of this method.
372	pub fn chain(&self) -> &[CertificateDer<'static>] {
373		&self.chain
374	}
375
376	/// The leaf certificate's `notAfter`, if it parses. A `notAfter` before the
377	/// Unix epoch is reported as `None`.
378	pub fn expiry(&self) -> Option<std::time::SystemTime> {
379		use std::time::{Duration, UNIX_EPOCH};
380
381		let leaf = self.chain.first()?;
382		let (_, cert) = x509_parser::parse_x509_certificate(leaf).ok()?;
383		let secs = u64::try_from(cert.validity().not_after.timestamp()).ok()?;
384		Some(UNIX_EPOCH + Duration::from_secs(secs))
385	}
386}
387
388/// TLS certificate information including fingerprints.
389#[derive(Debug)]
390pub struct Info {
391	#[cfg(any(feature = "noq", feature = "quinn"))]
392	pub(crate) certs: Vec<Arc<rustls::sign::CertifiedKey>>,
393	pub fingerprints: Vec<String>,
394}
395
396// ── NoCertificateVerification ───────────────────────────────────────
397
398#[derive(Debug)]
399struct NoCertificateVerification(crypto::Provider);
400
401impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
402	fn verify_server_cert(
403		&self,
404		_end_entity: &CertificateDer<'_>,
405		_intermediates: &[CertificateDer<'_>],
406		_server_name: &ServerName<'_>,
407		_ocsp: &[u8],
408		_now: UnixTime,
409	) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
410		Ok(rustls::client::danger::ServerCertVerified::assertion())
411	}
412
413	fn verify_tls12_signature(
414		&self,
415		message: &[u8],
416		cert: &CertificateDer<'_>,
417		dss: &rustls::DigitallySignedStruct,
418	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
419		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
420	}
421
422	fn verify_tls13_signature(
423		&self,
424		message: &[u8],
425		cert: &CertificateDer<'_>,
426		dss: &rustls::DigitallySignedStruct,
427	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
428		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
429	}
430
431	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
432		self.0.signature_verification_algorithms.supported_schemes()
433	}
434}
435
436// ── FingerprintVerifier ─────────────────────────────────────────────
437
438#[derive(Debug)]
439pub(crate) struct FingerprintVerifier {
440	provider: crypto::Provider,
441	fingerprints: Vec<Vec<u8>>,
442}
443
444impl FingerprintVerifier {
445	pub fn new(provider: crypto::Provider, fingerprints: Vec<Vec<u8>>) -> Self {
446		Self { provider, fingerprints }
447	}
448}
449
450impl rustls::client::danger::ServerCertVerifier for FingerprintVerifier {
451	fn verify_server_cert(
452		&self,
453		end_entity: &CertificateDer<'_>,
454		_intermediates: &[CertificateDer<'_>],
455		_server_name: &ServerName<'_>,
456		_ocsp: &[u8],
457		_now: UnixTime,
458	) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
459		let fingerprint = crypto::sha256(&self.provider, end_entity);
460		if self.fingerprints.iter().any(|fp| fingerprint.as_ref() == fp.as_slice()) {
461			Ok(rustls::client::danger::ServerCertVerified::assertion())
462		} else {
463			Err(rustls::Error::General("fingerprint mismatch".into()))
464		}
465	}
466
467	fn verify_tls12_signature(
468		&self,
469		message: &[u8],
470		cert: &CertificateDer<'_>,
471		dss: &rustls::DigitallySignedStruct,
472	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
473		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
474	}
475
476	fn verify_tls13_signature(
477		&self,
478		message: &[u8],
479		cert: &CertificateDer<'_>,
480		dss: &rustls::DigitallySignedStruct,
481	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
482		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
483	}
484
485	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
486		self.provider.signature_verification_algorithms.supported_schemes()
487	}
488}
489
490#[cfg(test)]
491#[cfg(all(any(feature = "quinn", feature = "noq", feature = "quiche"), feature = "aws-lc-rs"))]
492mod tests {
493	use super::*;
494	use rustls::client::danger::ServerCertVerifier;
495	use rustls::pki_types::ServerName;
496
497	fn self_signed() -> CertificateDer<'static> {
498		let key = rcgen::KeyPair::generate().unwrap();
499		let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
500		params.self_signed(&key).unwrap().into()
501	}
502
503	#[cfg(any(feature = "quinn", feature = "noq"))]
504	#[test]
505	fn peer_identity_expiry_reads_not_after() {
506		// notAfter at a whole second so the round-trip is exact.
507		let not_after = ::time::OffsetDateTime::from_unix_timestamp(2_000_000_000).unwrap();
508
509		let key = rcgen::KeyPair::generate().unwrap();
510		let mut params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
511		params.not_after = not_after;
512		let cert: CertificateDer<'static> = params.self_signed(&key).unwrap().into();
513
514		// quinn/noq hand back the chain as a boxed Vec<CertificateDer>.
515		let identity: Box<dyn std::any::Any> = Box::new(vec![cert]);
516		let parsed = PeerIdentity::from_any(Some(identity)).expect("chain parsed");
517		let expiry = parsed.expiry().expect("expiry parsed");
518		assert_eq!(
519			expiry.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
520			2_000_000_000
521		);
522	}
523
524	#[cfg(any(feature = "quinn", feature = "noq"))]
525	#[test]
526	fn peer_identity_none_without_chain() {
527		assert!(PeerIdentity::from_any(None).is_none());
528		// A wrong downcast type (not a cert chain) yields None rather than panicking.
529		let bogus: Box<dyn std::any::Any> = Box::new(42u32);
530		assert!(PeerIdentity::from_any(Some(bogus)).is_none());
531	}
532
533	#[test]
534	fn fingerprint_verifier_matches_and_rejects() {
535		let provider = crypto::provider();
536		let cert = self_signed();
537		let fingerprint = crypto::sha256(&provider, cert.as_ref()).as_ref().to_vec();
538
539		let name = ServerName::try_from("localhost").unwrap();
540		let now = UnixTime::now();
541
542		let verifier = FingerprintVerifier::new(provider.clone(), vec![fingerprint]);
543		assert!(verifier.verify_server_cert(&cert, &[], &name, &[], now).is_ok());
544
545		// A different leaf certificate must not satisfy the pin.
546		let other = self_signed();
547		assert!(verifier.verify_server_cert(&other, &[], &name, &[], now).is_err());
548	}
549
550	#[test]
551	fn build_installs_fingerprint_verifier() {
552		let cert = self_signed();
553		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
554
555		// A bogus hash still builds; verification happens at handshake time.
556		let config = Client {
557			fingerprint: vec![fingerprint],
558			..Default::default()
559		};
560		assert!(config.build().is_ok());
561	}
562
563	#[test]
564	fn build_rejects_invalid_fingerprint_hex() {
565		let config = Client {
566			fingerprint: vec!["not-hex".to_string()],
567			..Default::default()
568		};
569		assert!(matches!(config.build(), Err(Error::Fingerprint(_))));
570	}
571
572	#[test]
573	fn build_rejects_wrong_length_fingerprint() {
574		// Valid hex, but only 2 bytes instead of 32.
575		let config = Client {
576			fingerprint: vec!["abcd".to_string()],
577			..Default::default()
578		};
579		assert!(matches!(config.build(), Err(Error::FingerprintLength(2))));
580	}
581
582	#[test]
583	fn build_rejects_no_roots() {
584		// System roots disabled with no custom root and no alternate verifier:
585		// nothing could ever verify, so reject up front.
586		let config = Client {
587			system_roots: Some(false),
588			..Default::default()
589		};
590		assert!(matches!(config.build(), Err(Error::NoRoots)));
591	}
592
593	#[test]
594	fn build_allows_no_roots_when_verification_overridden() {
595		// disable_verify swaps in its own verifier, so an empty store is fine.
596		let config = Client {
597			system_roots: Some(false),
598			disable_verify: Some(true),
599			..Default::default()
600		};
601		assert!(config.build().is_ok());
602
603		// Same for fingerprint pinning.
604		let cert = self_signed();
605		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
606		let config = Client {
607			system_roots: Some(false),
608			fingerprint: vec![fingerprint],
609			..Default::default()
610		};
611		assert!(config.build().is_ok());
612	}
613}
614
615// ── ServeCerts ──────────────────────────────────────────────────────
616
617#[cfg(any(feature = "quinn", feature = "noq"))]
618#[derive(Debug)]
619pub(crate) struct ServeCerts {
620	pub info: Arc<RwLock<Info>>,
621	provider: crypto::Provider,
622}
623
624#[cfg(any(feature = "quinn", feature = "noq"))]
625impl ServeCerts {
626	pub fn new(provider: crypto::Provider) -> Self {
627		Self {
628			info: Arc::new(RwLock::new(Info {
629				certs: Vec::new(),
630				fingerprints: Vec::new(),
631			})),
632			provider,
633		}
634	}
635
636	pub fn load_certs(&self, config: &Server) -> Result<()> {
637		if config.cert.len() != config.key.len() {
638			return Err(Error::CertKeyCountMismatch);
639		}
640		if config.cert.is_empty() && config.generate.is_empty() {
641			return Err(Error::NoCertSource);
642		}
643
644		let mut certs = Vec::new();
645
646		// Load the certificate and key files based on their index.
647		for (cert, key) in config.cert.iter().zip(config.key.iter()) {
648			certs.push(Arc::new(self.load(cert, key)?));
649		}
650
651		// Generate a new certificate if requested.
652		if !config.generate.is_empty() {
653			certs.push(Arc::new(self.generate(&config.generate)?));
654		}
655
656		self.set_certs(certs);
657		Ok(())
658	}
659
660	// Load a certificate and corresponding key from a file, but don't add it to the certs
661	fn load(&self, chain_path: &Path, key_path: &Path) -> Result<rustls::sign::CertifiedKey> {
662		let chain = read_certs(chain_path)?;
663		if chain.is_empty() {
664			return Err(Error::Empty);
665		}
666
667		// Read the PEM private key
668		let key = PrivateKeyDer::from_pem_file(key_path).map_err(Error::Key)?;
669		let key = self.provider.key_provider.load_private_key(key)?;
670
671		let certified_key = rustls::sign::CertifiedKey::new(chain, key);
672
673		certified_key.keys_match().map_err(|source| Error::KeyMismatch {
674			key: key_path.to_path_buf(),
675			cert: chain_path.to_path_buf(),
676			source,
677		})?;
678
679		Ok(certified_key)
680	}
681
682	#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
683	fn generate(&self, hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
684		let key_pair = rcgen::KeyPair::generate()?;
685
686		let mut params = rcgen::CertificateParams::new(hostnames)?;
687
688		// Make the certificate valid for two weeks, starting yesterday (in case of clock drift).
689		// WebTransport certificates MUST be valid for two weeks at most.
690		params.not_before = ::time::OffsetDateTime::now_utc() - ::time::Duration::days(1);
691		params.not_after = params.not_before + ::time::Duration::days(14);
692
693		// Generate the certificate
694		let cert = params.self_signed(&key_pair)?;
695
696		// Convert the rcgen type to the rustls type.
697		let key_der = key_pair.serialized_der().to_vec();
698		let key_der = PrivatePkcs8KeyDer::from(key_der);
699		let key = self.provider.key_provider.load_private_key(key_der.into())?;
700
701		// Create a rustls::sign::CertifiedKey
702		Ok(rustls::sign::CertifiedKey::new(vec![cert.into()], key))
703	}
704
705	#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
706	fn generate(&self, _hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
707		Err(Error::NoCryptoProvider)
708	}
709
710	// Replace the certificates
711	pub fn set_certs(&self, certs: Vec<Arc<rustls::sign::CertifiedKey>>) {
712		let fingerprints = certs
713			.iter()
714			.map(|ck| {
715				let fingerprint = crate::crypto::sha256(&self.provider, ck.cert[0].as_ref());
716				hex::encode(fingerprint)
717			})
718			.collect();
719
720		let mut info = self.info.write().expect("info write lock poisoned");
721		info.certs = certs;
722		info.fingerprints = fingerprints;
723	}
724
725	// Return the best certificate for the given ClientHello.
726	fn best_certificate(
727		&self,
728		client_hello: &rustls::server::ClientHello<'_>,
729	) -> Option<Arc<rustls::sign::CertifiedKey>> {
730		let server_name = client_hello.server_name()?;
731		let dns_name = rustls::pki_types::ServerName::try_from(server_name).ok()?;
732
733		for ck in self.info.read().expect("info read lock poisoned").certs.iter() {
734			let leaf: webpki::EndEntityCert = ck
735				.end_entity_cert()
736				.expect("missing certificate")
737				.try_into()
738				.expect("failed to parse certificate");
739
740			if leaf.verify_is_valid_for_subject_name(&dns_name).is_ok() {
741				return Some(ck.clone());
742			}
743		}
744
745		None
746	}
747}
748
749#[cfg(any(feature = "quinn", feature = "noq"))]
750impl rustls::server::ResolvesServerCert for ServeCerts {
751	fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<rustls::sign::CertifiedKey>> {
752		if let Some(cert) = self.best_certificate(&client_hello) {
753			return Some(cert);
754		}
755
756		// If this happens, it means the client was trying to connect to an unknown hostname.
757		// We do our best and return the first certificate.
758		tracing::warn!(server_name = ?client_hello.server_name(), "no SNI certificate found");
759
760		self.info
761			.read()
762			.expect("info read lock poisoned")
763			.certs
764			.first()
765			.cloned()
766	}
767}
768
769// ── reload_certs ────────────────────────────────────────────────────
770
771/// Watch the on-disk cert/key files and reload them whenever they change.
772///
773/// Reacting to the filesystem means cert-manager, Kubernetes secret mounts, and
774/// `mv`-into-place rotate certs with no external signal. Returns immediately when
775/// only generated certs are configured: there's nothing on disk to watch.
776#[cfg(any(feature = "quinn", feature = "noq"))]
777pub(crate) async fn reload_certs(certs: Arc<ServeCerts>, tls_config: Server) {
778	let paths: Vec<PathBuf> = tls_config.cert.iter().chain(tls_config.key.iter()).cloned().collect();
779	if paths.is_empty() {
780		return;
781	}
782
783	let mut watcher = match crate::watch::FileWatcher::new(&paths) {
784		Ok(watcher) => watcher,
785		Err(err) => {
786			tracing::error!(%err, "failed to watch certificate files; hot reload disabled");
787			return;
788		}
789	};
790
791	loop {
792		watcher.changed().await;
793		tracing::info!("reloading server certificates");
794
795		if let Err(err) = certs.load_certs(&tls_config) {
796			tracing::warn!(%err, "failed to reload server certificates");
797		}
798	}
799}