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(all(
9	any(feature = "quinn", feature = "noq", feature = "quiche"),
10	any(feature = "aws-lc-rs", feature = "ring")
11))]
12use rustls::pki_types::PrivatePkcs8KeyDer;
13#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
14use std::sync::RwLock;
15
16/// Errors loading or generating TLS certificates and keys.
17///
18/// Shared by the client TLS config and the quinn/noq servers so each backend's
19/// error type can compose it via `#[from]`.
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum Error {
23	#[error("failed to open certificate file")]
24	Open(#[source] std::io::Error),
25
26	#[error("failed to read file")]
27	ReadFile(#[source] std::io::Error),
28
29	#[error("failed to read certificates")]
30	Read(#[source] rustls::pki_types::pem::Error),
31
32	#[error("failed to parse private key")]
33	Key(#[source] rustls::pki_types::pem::Error),
34
35	#[error("no certificates found")]
36	Empty,
37
38	#[error("no roots found in {}", .0.display())]
39	EmptyRoots(PathBuf),
40
41	#[error(
42		"no trusted roots: provide --client-tls-root, enable --client-tls-system-roots, or use --client-tls-fingerprint / --client-tls-disable-verify"
43	)]
44	NoRoots,
45
46	#[error("invalid TLS fingerprint (expected hex-encoded SHA-256)")]
47	Fingerprint(#[source] hex::FromHexError),
48
49	#[error("invalid TLS fingerprint length: expected 32 bytes (SHA-256), got {0}")]
50	FingerprintLength(usize),
51
52	#[error(
53		"--client-tls-fingerprint cannot be combined with --client-tls-root or --client-tls-system-roots: fingerprint pinning bypasses CA verification"
54	)]
55	FingerprintWithRoots,
56
57	#[error("failed to add root certificate")]
58	AddRoot(#[source] rustls::Error),
59
60	#[error("failed to configure client certificate")]
61	ClientAuth(#[source] rustls::Error),
62
63	#[error("both --client-tls-cert and --client-tls-key must be provided")]
64	IncompleteClientAuth,
65
66	#[error("must provide both cert and key")]
67	CertKeyCountMismatch,
68
69	#[error("must provide at least one cert/key pair or generate entry")]
70	NoCertSource,
71
72	#[error("private key {} doesn't match certificate {}", key.display(), cert.display())]
73	KeyMismatch {
74		key: PathBuf,
75		cert: PathBuf,
76		#[source]
77		source: rustls::Error,
78	},
79
80	#[error(transparent)]
81	Rustls(#[from] rustls::Error),
82
83	#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
84	#[error("failed to build client certificate verifier")]
85	ClientVerifier(#[source] rustls::server::VerifierBuilderError),
86
87	#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
88	#[error(transparent)]
89	Rcgen(#[from] rcgen::Error),
90
91	#[error("no crypto provider available; enable aws-lc-rs or ring feature")]
92	NoCryptoProvider,
93}
94
95/// Convenience alias for results produced by this module.
96pub type Result<T> = std::result::Result<T, Error>;
97
98/// Read a PEM file into its list of certificates.
99pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
100	let file = fs::File::open(path).map_err(Error::Open)?;
101	let mut reader = io::BufReader::new(file);
102	CertificateDer::pem_reader_iter(&mut reader)
103		.collect::<std::result::Result<_, _>>()
104		.map_err(Error::Read)
105}
106
107// ── Client ──────────────────────────────────────────────────────────
108
109/// TLS configuration for the client.
110#[serde_with::serde_as]
111#[derive(Clone, Default, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
112#[serde(default, deny_unknown_fields)]
113#[group(id = "tls-client")]
114#[non_exhaustive]
115pub struct Client {
116	/// Trust the TLS root at this path, encoded as PEM.
117	///
118	/// This value can be provided multiple times for multiple roots.
119	/// In config files, accepts either a single string or a TOML array.
120	///
121	/// These roots are added on top of the system roots. By default the system
122	/// roots are only loaded when no custom root is given, so passing a root
123	/// replaces them; set `--client-tls-system-roots` to trust both (e.g. to reach a
124	/// local relay with a private CA and a remote one with a public CA).
125	#[serde(skip_serializing_if = "Vec::is_empty")]
126	#[arg(id = "client-tls-root", long = "client-tls-root", env = "MOQ_CLIENT_TLS_ROOT")]
127	#[serde_as(as = "serde_with::OneOrMany<_>")]
128	pub root: Vec<PathBuf>,
129
130	/// Also trust the platform's native root certificates.
131	///
132	/// Defaults to enabled only when no `--client-tls-root` is given. Set it
133	/// explicitly to trust the system roots alongside any custom roots, or set it
134	/// to false to trust only the custom roots. Trusting neither (no custom root
135	/// and system roots disabled) is rejected, since verification could never pass.
136	#[serde(skip_serializing_if = "Option::is_none")]
137	#[arg(
138		id = "client-tls-system-roots",
139		long = "client-tls-system-roots",
140		env = "MOQ_CLIENT_TLS_SYSTEM_ROOTS",
141		default_missing_value = "true",
142		num_args = 0..=1,
143		require_equals = true,
144		value_parser = clap::value_parser!(bool),
145	)]
146	pub system_roots: Option<bool>,
147
148	/// Pin the peer to a certificate with one of these SHA-256 fingerprints, encoded as hex.
149	///
150	/// This is the native equivalent of the browser's WebTransport `serverCertificateHashes`,
151	/// and accepts the same values a server reports via its certificate fingerprints. Use it to
152	/// trust a self-signed certificate without disabling verification or fetching the hash over
153	/// an insecure `http://` request. When set, the normal CA/root chain is bypassed: only the
154	/// leaf certificate's fingerprint is checked.
155	///
156	/// This value can be provided multiple times to accept any of several fingerprints (e.g.
157	/// across a certificate rotation). In config files, accepts either a single string or a TOML array.
158	#[serde(skip_serializing_if = "Vec::is_empty")]
159	#[arg(
160		id = "client-tls-fingerprint",
161		long = "client-tls-fingerprint",
162		env = "MOQ_CLIENT_TLS_FINGERPRINT"
163	)]
164	#[serde_as(as = "serde_with::OneOrMany<_>")]
165	pub fingerprint: Vec<String>,
166
167	/// PEM file containing the client certificate chain for mTLS.
168	///
169	/// Only certificates are extracted; any private keys in the file are ignored.
170	/// Must be paired with `--client-tls-key`.
171	#[serde(skip_serializing_if = "Option::is_none")]
172	#[arg(id = "client-tls-cert", long = "client-tls-cert", env = "MOQ_CLIENT_TLS_CERT")]
173	pub cert: Option<PathBuf>,
174
175	/// PEM file containing the private key for mTLS.
176	///
177	/// Only the private key is extracted; any certificates in the file are ignored.
178	/// Must be paired with `--client-tls-cert`.
179	#[serde(skip_serializing_if = "Option::is_none")]
180	#[arg(id = "client-tls-key", long = "client-tls-key", env = "MOQ_CLIENT_TLS_KEY")]
181	pub key: Option<PathBuf>,
182
183	/// Danger: Disable TLS certificate verification.
184	///
185	/// Fine for local development and between relays, but should be used in caution in production.
186	#[serde(skip_serializing_if = "Option::is_none")]
187	#[arg(
188		id = "client-tls-disable-verify",
189		long = "client-tls-disable-verify",
190		env = "MOQ_CLIENT_TLS_DISABLE_VERIFY",
191		default_missing_value = "true",
192		num_args = 0..=1,
193		require_equals = true,
194		value_parser = clap::value_parser!(bool),
195	)]
196	pub disable_verify: Option<bool>,
197
198	/// Deprecated `--tls-*` spellings, folded into the canonical fields above with
199	/// a warning. Private and hidden so they stay off the public surface; not a
200	/// TOML field (config files use the canonical names).
201	#[command(flatten)]
202	#[serde(skip)]
203	deprecated: Deprecated,
204}
205
206/// Holds the deprecated bare `--tls-*` flag spellings (renamed to `--client-tls-*`).
207/// Flattened into [`Client`] so they keep parsing; folded into the canonical
208/// fields by [`Client::build`] with a deprecation warning. No env (the env names
209/// were never renamed) and no TOML.
210#[derive(Clone, Default, Debug, clap::Args)]
211struct Deprecated {
212	#[arg(long = "tls-root", hide = true)]
213	root: Vec<PathBuf>,
214
215	#[arg(
216		long = "tls-system-roots",
217		hide = true,
218		default_missing_value = "true",
219		num_args = 0..=1,
220		require_equals = true,
221		value_parser = clap::value_parser!(bool),
222	)]
223	system_roots: Option<bool>,
224
225	#[arg(long = "tls-fingerprint", hide = true)]
226	fingerprint: Vec<String>,
227
228	#[arg(
229		long = "tls-disable-verify",
230		hide = true,
231		default_missing_value = "true",
232		num_args = 0..=1,
233		require_equals = true,
234		value_parser = clap::value_parser!(bool),
235	)]
236	disable_verify: Option<bool>,
237}
238
239/// The resolved server-certificate verification policy.
240///
241/// Computed once by [Client::verification] and shared by every backend (the
242/// rustls-based quinn/noq via [Client::build], and quiche directly) so they
243/// agree on precedence, the system-roots default, and which flag combinations
244/// are valid.
245#[derive(Clone)]
246pub(crate) enum Verification {
247	/// No verification at all. Insecure; only via `--client-tls-disable-verify`.
248	Disabled,
249
250	/// Pin the leaf certificate by SHA-256. The CA chain is not consulted, so
251	/// this is mutually exclusive with any roots.
252	Fingerprints(Vec<[u8; 32]>),
253
254	/// Standard verification against these roots (system and/or custom, already
255	/// resolved). The two sets are additive.
256	Roots(Vec<CertificateDer<'static>>),
257}
258
259impl Client {
260	/// Log a warning for each deprecated `--tls-*` flag in use. Called once from
261	/// [`Self::verification`], which every backend runs, so a deprecated flag warns once.
262	pub(crate) fn warn_deprecated(&self) {
263		if !self.deprecated.root.is_empty() {
264			tracing::warn!("--tls-root is deprecated; use --client-tls-root");
265		}
266		if self.deprecated.system_roots.is_some() {
267			tracing::warn!("--tls-system-roots is deprecated; use --client-tls-system-roots");
268		}
269		if !self.deprecated.fingerprint.is_empty() {
270			tracing::warn!("--tls-fingerprint is deprecated; use --client-tls-fingerprint");
271		}
272		if self.deprecated.disable_verify.is_some() {
273			tracing::warn!("--tls-disable-verify is deprecated; use --client-tls-disable-verify");
274		}
275	}
276
277	/// Roots from the canonical field plus the deprecated `--tls-root` spelling.
278	pub(crate) fn effective_root(&self) -> Vec<PathBuf> {
279		let mut root = self.root.clone();
280		root.extend(self.deprecated.root.iter().cloned());
281		root
282	}
283
284	/// Fingerprints from the canonical field plus the deprecated `--tls-fingerprint`.
285	pub(crate) fn effective_fingerprint(&self) -> Vec<String> {
286		let mut fp = self.fingerprint.clone();
287		fp.extend(self.deprecated.fingerprint.iter().cloned());
288		fp
289	}
290
291	/// `system_roots`, preferring the canonical flag over the deprecated alias.
292	pub(crate) fn effective_system_roots(&self) -> Option<bool> {
293		self.system_roots.or(self.deprecated.system_roots)
294	}
295
296	/// `disable_verify`, preferring the canonical flag over the deprecated alias.
297	pub(crate) fn effective_disable_verify(&self) -> Option<bool> {
298		self.disable_verify.or(self.deprecated.disable_verify)
299	}
300
301	/// Resolve the verification policy from the configured flags.
302	///
303	/// Precedence and rules (shared by all backends):
304	/// - `--client-tls-disable-verify` wins and disables verification.
305	/// - `--client-tls-fingerprint` pins the leaf and bypasses the CA chain; combining
306	///   it with `--client-tls-root` or `--client-tls-system-roots` is rejected rather than
307	///   silently ignoring one of them.
308	/// - Otherwise, verify against the system roots (default) plus any custom
309	///   roots. The system roots are dropped once a custom root is given unless
310	///   `--client-tls-system-roots` re-enables them.
311	pub(crate) fn verification(&self) -> Result<Verification> {
312		self.warn_deprecated();
313
314		if self.effective_disable_verify().unwrap_or_default() {
315			return Ok(Verification::Disabled);
316		}
317
318		let fingerprints = self.fingerprints()?;
319		if !fingerprints.is_empty() {
320			if !self.effective_root().is_empty() || self.effective_system_roots() == Some(true) {
321				return Err(Error::FingerprintWithRoots);
322			}
323			return Ok(Verification::Fingerprints(fingerprints));
324		}
325
326		let root = self.effective_root();
327		// Default to system roots only when no custom root is given, so passing a
328		// root replaces them unless the system roots are explicitly re-enabled.
329		let system_roots = self.effective_system_roots().unwrap_or(root.is_empty());
330
331		let mut roots = Vec::new();
332		if system_roots {
333			let native = rustls_native_certs::load_native_certs();
334			for err in native.errors {
335				tracing::warn!(%err, "failed to load root cert");
336			}
337			roots.extend(native.certs);
338		}
339		for root in &root {
340			let certs = read_certs(root)?;
341			if certs.is_empty() {
342				return Err(Error::EmptyRoots(root.clone()));
343			}
344			roots.extend(certs);
345		}
346
347		// WebPKI needs at least one trusted root to ever succeed, so fail fast
348		// instead of producing confusing handshake errors later.
349		if roots.is_empty() {
350			return Err(Error::NoRoots);
351		}
352
353		Ok(Verification::Roots(roots))
354	}
355
356	/// Whether an insecure `http://` certificate-fingerprint bootstrap may be
357	/// honored for a connection.
358	///
359	/// Only when no stronger verification is configured: an explicit
360	/// `--client-tls-fingerprint` must never be weakened by an attacker-controlled
361	/// plaintext fetch, and there is nothing to bootstrap when verification is
362	/// disabled. With CA roots (the default), `http://` is the deliberate
363	/// per-connection way to pin a self-signed relay, so it is allowed.
364	pub(crate) fn allows_http_bootstrap(&self) -> bool {
365		self.effective_fingerprint().is_empty() && !self.effective_disable_verify().unwrap_or_default()
366	}
367
368	/// Parse the configured fingerprints into fixed-size SHA-256 digests.
369	fn fingerprints(&self) -> Result<Vec<[u8; 32]>> {
370		self.effective_fingerprint()
371			.iter()
372			.map(|fp| {
373				let bytes = hex::decode(fp.trim()).map_err(Error::Fingerprint)?;
374				bytes.try_into().map_err(|v: Vec<u8>| Error::FingerprintLength(v.len()))
375			})
376			.collect()
377	}
378
379	/// Build a [`rustls::ClientConfig`] from this configuration.
380	///
381	/// Resolves the verification policy, optionally attaches a client identity
382	/// for mTLS, and installs the matching verifier.
383	pub fn build(&self) -> Result<rustls::ClientConfig> {
384		let provider = crypto::provider();
385		let verification = self.verification()?;
386
387		let mut roots = rustls::RootCertStore::empty();
388		if let Verification::Roots(certs) = &verification {
389			for cert in certs {
390				roots.add(cert.clone()).map_err(Error::AddRoot)?;
391			}
392		}
393
394		// Allow TLS 1.2 in addition to 1.3 for WebSocket compatibility.
395		// QUIC always negotiates TLS 1.3 regardless of this setting.
396		let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
397			.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
398			.with_root_certificates(roots);
399
400		let mut tls = match (&self.cert, &self.key) {
401			(Some(cert_path), Some(key_path)) => {
402				let cert_pem = fs::read(cert_path).map_err(Error::ReadFile)?;
403				let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
404					.collect::<std::result::Result<_, _>>()
405					.map_err(Error::Read)?;
406				if chain.is_empty() {
407					return Err(Error::Empty);
408				}
409				let key_pem = fs::read(key_path).map_err(Error::ReadFile)?;
410				let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(Error::Key)?;
411				builder.with_client_auth_cert(chain, key).map_err(Error::ClientAuth)?
412			}
413			(None, None) => builder.with_no_client_auth(),
414			_ => return Err(Error::IncompleteClientAuth),
415		};
416
417		match verification {
418			Verification::Disabled => {
419				tracing::warn!(
420					"TLS server certificate verification is disabled; A man-in-the-middle attack is possible."
421				);
422				tls.dangerous()
423					.set_certificate_verifier(Arc::new(NoCertificateVerification(provider)));
424			}
425			Verification::Fingerprints(fingerprints) => {
426				let fingerprints = fingerprints.into_iter().map(|fp| fp.to_vec()).collect();
427				let verifier = FingerprintVerifier::new(provider, fingerprints);
428				tls.dangerous().set_certificate_verifier(Arc::new(verifier));
429			}
430			// Roots are already in the store above; use the default WebPKI verifier.
431			Verification::Roots(_) => {}
432		}
433
434		Ok(tls)
435	}
436}
437
438// ── Server ──────────────────────────────────────────────────────────
439
440/// TLS configuration for the server.
441///
442/// Certificate and keys must currently be files on disk.
443/// Alternatively, you can generate a self-signed certificate given a list of hostnames.
444///
445/// In config files, each list field accepts either a single string or a TOML array.
446#[serde_with::serde_as]
447#[derive(clap::Args, Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
448#[serde(deny_unknown_fields)]
449#[group(id = "tls-server")]
450#[non_exhaustive]
451pub struct Server {
452	/// Load the given certificate from disk.
453	#[arg(long = "tls-cert", id = "tls-cert", env = "MOQ_SERVER_TLS_CERT")]
454	#[serde(default, skip_serializing_if = "Vec::is_empty")]
455	#[serde_as(as = "serde_with::OneOrMany<_>")]
456	pub cert: Vec<PathBuf>,
457
458	/// Load the given key from disk.
459	#[arg(long = "tls-key", id = "tls-key", env = "MOQ_SERVER_TLS_KEY")]
460	#[serde(default, skip_serializing_if = "Vec::is_empty")]
461	#[serde_as(as = "serde_with::OneOrMany<_>")]
462	pub key: Vec<PathBuf>,
463
464	/// Or generate a new certificate and key with the given hostnames.
465	/// This won't be valid unless the client uses the fingerprint or disables verification.
466	#[arg(
467		long = "tls-generate",
468		id = "tls-generate",
469		value_delimiter = ',',
470		env = "MOQ_SERVER_TLS_GENERATE"
471	)]
472	#[serde(default, skip_serializing_if = "Vec::is_empty")]
473	#[serde_as(as = "serde_with::OneOrMany<_>")]
474	pub generate: Vec<String>,
475
476	/// PEM file(s) of root CAs for validating optional client certificates (mTLS).
477	///
478	/// When set, clients *may* present a certificate during the TLS handshake.
479	/// Valid presentations are reported via [`crate::Request::peer_identity`]
480	/// and can be used by the application to grant elevated access. Clients that
481	/// do not present a certificate are unaffected.
482	///
483	/// Client certificate reporting is only supported by the Quinn and noq QUIC
484	/// backends. Plain-TLS listeners built via [`Self::server_config`] also use
485	/// these roots for optional mTLS when the feature set includes quinn, noq, or
486	/// quiche.
487	#[arg(
488		long = "server-tls-root",
489		id = "server-tls-root",
490		value_delimiter = ',',
491		env = "MOQ_SERVER_TLS_ROOT"
492	)]
493	#[serde(default, skip_serializing_if = "Vec::is_empty")]
494	#[serde_as(as = "serde_with::OneOrMany<_>")]
495	pub root: Vec<PathBuf>,
496}
497
498impl Server {
499	/// Load all configured root CAs into a [`rustls::RootCertStore`].
500	pub fn load_roots(&self) -> Result<rustls::RootCertStore> {
501		let mut roots = rustls::RootCertStore::empty();
502		for path in &self.root {
503			let certs = read_certs(path)?;
504			if certs.is_empty() {
505				return Err(Error::Empty);
506			}
507			for cert in certs {
508				roots.add(cert).map_err(Error::AddRoot)?;
509			}
510		}
511		Ok(roots)
512	}
513
514	/// Build a [`rustls::ServerConfig`] for a plain-TLS (non-QUIC) server, e.g. an
515	/// RTMPS or HTTPS listener fronting the QUIC endpoint, reusing the QUIC
516	/// backend's certificate handling: on-disk `cert`/`key` pairs, `generate`
517	/// self-signed certs, and optional mTLS `root` client CAs.
518	///
519	/// `alpn` sets the advertised ALPN protocols (e.g.
520	/// `vec![b"h2".to_vec(), b"http/1.1".to_vec()]`); pass an empty list for a
521	/// protocol like RTMPS that doesn't use ALPN.
522	#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
523	pub fn server_config(&self, alpn: Vec<Vec<u8>>) -> Result<Arc<rustls::ServerConfig>> {
524		server_config(self, alpn)
525	}
526}
527
528/// Build a [`rustls::ServerConfig`] from a [`Server`] for a plain-TLS listener.
529#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
530fn server_config(config: &Server, alpn: Vec<Vec<u8>>) -> Result<Arc<rustls::ServerConfig>> {
531	let provider = crypto::provider();
532
533	let certs = ServeCerts::new(provider.clone());
534	certs.load_certs(config)?;
535	let certs = Arc::new(certs);
536
537	// TCP can negotiate TLS 1.2 as well as 1.3, unlike QUIC which is 1.3-only.
538	let builder =
539		rustls::ServerConfig::builder_with_provider(provider.clone()).with_safe_default_protocol_versions()?;
540
541	let mut tls = if config.root.is_empty() {
542		builder.with_no_client_auth().with_cert_resolver(certs)
543	} else {
544		let roots = config.load_roots()?;
545		let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
546			.allow_unauthenticated()
547			.build()
548			.map_err(Error::ClientVerifier)?;
549		builder.with_client_cert_verifier(verifier).with_cert_resolver(certs)
550	};
551
552	tls.alpn_protocols = alpn;
553	Ok(Arc::new(tls))
554}
555
556/// A peer's validated client-certificate chain from the mTLS handshake.
557///
558/// Returned by [`crate::Request::peer_identity`] when the peer presented a
559/// certificate that chained to a configured [`Server::root`]. Owns the chain
560/// (leaf first) so callers can inspect it, e.g. [`expiry`](Self::expiry),
561/// without re-parsing the type-erased QUIC identity.
562pub struct PeerIdentity {
563	chain: Vec<CertificateDer<'static>>,
564}
565
566impl PeerIdentity {
567	/// Wrap the type-erased identity from `quinn::Connection::peer_identity`.
568	/// Returns `None` if the peer presented no certificate or the identity is
569	/// not a certificate chain.
570	#[cfg(any(feature = "quinn", feature = "noq"))]
571	pub(crate) fn from_any(identity: Option<Box<dyn std::any::Any>>) -> Option<Self> {
572		let chain = identity?.downcast::<Vec<CertificateDer<'static>>>().ok()?;
573		Some(Self { chain: *chain })
574	}
575
576	/// The validated certificate chain, leaf first.
577	///
578	/// Exposes [`rustls::pki_types::CertificateDer`] directly (already part of
579	/// this crate's public API via the `rustls` re-export), so a major `rustls`
580	/// bump is a breaking change for consumers of this method.
581	pub fn chain(&self) -> &[CertificateDer<'static>] {
582		&self.chain
583	}
584
585	/// The leaf certificate's `notAfter`, if it parses. A `notAfter` before the
586	/// Unix epoch is reported as `None`.
587	pub fn expiry(&self) -> Option<std::time::SystemTime> {
588		use std::time::{Duration, UNIX_EPOCH};
589
590		let leaf = self.chain.first()?;
591		let (_, cert) = x509_parser::parse_x509_certificate(leaf).ok()?;
592		let secs = u64::try_from(cert.validity().not_after.timestamp()).ok()?;
593		Some(UNIX_EPOCH + Duration::from_secs(secs))
594	}
595}
596
597/// TLS certificate information including fingerprints.
598#[derive(Debug)]
599pub struct Info {
600	#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
601	pub(crate) certs: Vec<Arc<rustls::sign::CertifiedKey>>,
602	pub fingerprints: Vec<String>,
603}
604
605// ── NoCertificateVerification ───────────────────────────────────────
606
607#[derive(Debug)]
608struct NoCertificateVerification(crypto::Provider);
609
610impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
611	fn verify_server_cert(
612		&self,
613		_end_entity: &CertificateDer<'_>,
614		_intermediates: &[CertificateDer<'_>],
615		_server_name: &ServerName<'_>,
616		_ocsp: &[u8],
617		_now: UnixTime,
618	) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
619		Ok(rustls::client::danger::ServerCertVerified::assertion())
620	}
621
622	fn verify_tls12_signature(
623		&self,
624		message: &[u8],
625		cert: &CertificateDer<'_>,
626		dss: &rustls::DigitallySignedStruct,
627	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
628		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
629	}
630
631	fn verify_tls13_signature(
632		&self,
633		message: &[u8],
634		cert: &CertificateDer<'_>,
635		dss: &rustls::DigitallySignedStruct,
636	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
637		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
638	}
639
640	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
641		self.0.signature_verification_algorithms.supported_schemes()
642	}
643}
644
645// ── FingerprintVerifier ─────────────────────────────────────────────
646
647#[derive(Debug)]
648pub(crate) struct FingerprintVerifier {
649	provider: crypto::Provider,
650	fingerprints: Vec<Vec<u8>>,
651}
652
653impl FingerprintVerifier {
654	pub fn new(provider: crypto::Provider, fingerprints: Vec<Vec<u8>>) -> Self {
655		Self { provider, fingerprints }
656	}
657}
658
659impl rustls::client::danger::ServerCertVerifier for FingerprintVerifier {
660	fn verify_server_cert(
661		&self,
662		end_entity: &CertificateDer<'_>,
663		_intermediates: &[CertificateDer<'_>],
664		_server_name: &ServerName<'_>,
665		_ocsp: &[u8],
666		_now: UnixTime,
667	) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
668		let fingerprint = crypto::sha256(&self.provider, end_entity);
669		if self.fingerprints.iter().any(|fp| fingerprint.as_ref() == fp.as_slice()) {
670			Ok(rustls::client::danger::ServerCertVerified::assertion())
671		} else {
672			Err(rustls::Error::General("fingerprint mismatch".into()))
673		}
674	}
675
676	fn verify_tls12_signature(
677		&self,
678		message: &[u8],
679		cert: &CertificateDer<'_>,
680		dss: &rustls::DigitallySignedStruct,
681	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
682		rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
683	}
684
685	fn verify_tls13_signature(
686		&self,
687		message: &[u8],
688		cert: &CertificateDer<'_>,
689		dss: &rustls::DigitallySignedStruct,
690	) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
691		rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
692	}
693
694	fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
695		self.provider.signature_verification_algorithms.supported_schemes()
696	}
697}
698
699#[cfg(test)]
700#[cfg(all(any(feature = "quinn", feature = "noq", feature = "quiche"), feature = "aws-lc-rs"))]
701mod tests {
702	use super::*;
703	use rustls::client::danger::ServerCertVerifier;
704	use rustls::pki_types::ServerName;
705
706	fn self_signed() -> CertificateDer<'static> {
707		let key = rcgen::KeyPair::generate().unwrap();
708		let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
709		params.self_signed(&key).unwrap().into()
710	}
711
712	#[cfg(any(feature = "quinn", feature = "noq"))]
713	#[test]
714	fn peer_identity_expiry_reads_not_after() {
715		// notAfter at a whole second so the round-trip is exact.
716		let not_after = ::time::OffsetDateTime::from_unix_timestamp(2_000_000_000).unwrap();
717
718		let key = rcgen::KeyPair::generate().unwrap();
719		let mut params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
720		params.not_after = not_after;
721		let cert: CertificateDer<'static> = params.self_signed(&key).unwrap().into();
722
723		// quinn/noq hand back the chain as a boxed Vec<CertificateDer>.
724		let identity: Box<dyn std::any::Any> = Box::new(vec![cert]);
725		let parsed = PeerIdentity::from_any(Some(identity)).expect("chain parsed");
726		let expiry = parsed.expiry().expect("expiry parsed");
727		assert_eq!(
728			expiry.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
729			2_000_000_000
730		);
731	}
732
733	#[cfg(any(feature = "quinn", feature = "noq"))]
734	#[test]
735	fn peer_identity_none_without_chain() {
736		assert!(PeerIdentity::from_any(None).is_none());
737		// A wrong downcast type (not a cert chain) yields None rather than panicking.
738		let bogus: Box<dyn std::any::Any> = Box::new(42u32);
739		assert!(PeerIdentity::from_any(Some(bogus)).is_none());
740	}
741
742	#[test]
743	fn fingerprint_verifier_matches_and_rejects() {
744		let provider = crypto::provider();
745		let cert = self_signed();
746		let fingerprint = crypto::sha256(&provider, cert.as_ref()).as_ref().to_vec();
747
748		let name = ServerName::try_from("localhost").unwrap();
749		let now = UnixTime::now();
750
751		let verifier = FingerprintVerifier::new(provider.clone(), vec![fingerprint]);
752		assert!(verifier.verify_server_cert(&cert, &[], &name, &[], now).is_ok());
753
754		// A different leaf certificate must not satisfy the pin.
755		let other = self_signed();
756		assert!(verifier.verify_server_cert(&other, &[], &name, &[], now).is_err());
757	}
758
759	#[test]
760	fn build_installs_fingerprint_verifier() {
761		let cert = self_signed();
762		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
763
764		// A bogus hash still builds; verification happens at handshake time.
765		let config = Client {
766			fingerprint: vec![fingerprint],
767			..Default::default()
768		};
769		assert!(config.build().is_ok());
770	}
771
772	#[test]
773	fn build_rejects_invalid_fingerprint_hex() {
774		let config = Client {
775			fingerprint: vec!["not-hex".to_string()],
776			..Default::default()
777		};
778		assert!(matches!(config.build(), Err(Error::Fingerprint(_))));
779	}
780
781	#[test]
782	fn build_rejects_wrong_length_fingerprint() {
783		// Valid hex, but only 2 bytes instead of 32.
784		let config = Client {
785			fingerprint: vec!["abcd".to_string()],
786			..Default::default()
787		};
788		assert!(matches!(config.build(), Err(Error::FingerprintLength(2))));
789	}
790
791	#[test]
792	fn build_rejects_no_roots() {
793		// System roots disabled with no custom root and no alternate verifier:
794		// nothing could ever verify, so reject up front.
795		let config = Client {
796			system_roots: Some(false),
797			..Default::default()
798		};
799		assert!(matches!(config.build(), Err(Error::NoRoots)));
800	}
801
802	#[test]
803	fn build_allows_no_roots_when_verification_overridden() {
804		// disable_verify swaps in its own verifier, so an empty store is fine.
805		let config = Client {
806			system_roots: Some(false),
807			disable_verify: Some(true),
808			..Default::default()
809		};
810		assert!(config.build().is_ok());
811
812		// Same for fingerprint pinning.
813		let cert = self_signed();
814		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
815		let config = Client {
816			system_roots: Some(false),
817			fingerprint: vec![fingerprint],
818			..Default::default()
819		};
820		assert!(config.build().is_ok());
821	}
822
823	#[test]
824	fn build_rejects_fingerprint_with_roots() {
825		let cert = self_signed();
826		let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
827
828		// Fingerprint pinning bypasses the CA chain, so combining it with roots
829		// is rejected rather than silently ignoring one of them.
830		let with_system = Client {
831			fingerprint: vec![fingerprint.clone()],
832			system_roots: Some(true),
833			..Default::default()
834		};
835		assert!(matches!(with_system.build(), Err(Error::FingerprintWithRoots)));
836
837		// The conflict is detected before any root file is read, so the path
838		// need not exist.
839		let with_custom = Client {
840			fingerprint: vec![fingerprint],
841			root: vec![PathBuf::from("/does-not-exist.pem")],
842			..Default::default()
843		};
844		assert!(matches!(with_custom.build(), Err(Error::FingerprintWithRoots)));
845	}
846}
847
848// ── ServeCerts ──────────────────────────────────────────────────────
849
850#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
851#[derive(Debug)]
852pub(crate) struct ServeCerts {
853	pub info: Arc<RwLock<Info>>,
854	provider: crypto::Provider,
855}
856
857#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
858impl ServeCerts {
859	pub fn new(provider: crypto::Provider) -> Self {
860		Self {
861			info: Arc::new(RwLock::new(Info {
862				certs: Vec::new(),
863				fingerprints: Vec::new(),
864			})),
865			provider,
866		}
867	}
868
869	pub fn load_certs(&self, config: &Server) -> Result<()> {
870		if config.cert.len() != config.key.len() {
871			return Err(Error::CertKeyCountMismatch);
872		}
873		if config.cert.is_empty() && config.generate.is_empty() {
874			return Err(Error::NoCertSource);
875		}
876
877		let mut certs = Vec::new();
878
879		// Load the certificate and key files based on their index.
880		for (cert, key) in config.cert.iter().zip(config.key.iter()) {
881			certs.push(Arc::new(self.load(cert, key)?));
882		}
883
884		// Generate a new certificate if requested.
885		if !config.generate.is_empty() {
886			certs.push(Arc::new(self.generate(&config.generate)?));
887		}
888
889		self.set_certs(certs);
890		Ok(())
891	}
892
893	// Load a certificate and corresponding key from a file, but don't add it to the certs
894	fn load(&self, chain_path: &Path, key_path: &Path) -> Result<rustls::sign::CertifiedKey> {
895		let chain = read_certs(chain_path)?;
896		if chain.is_empty() {
897			return Err(Error::Empty);
898		}
899
900		// Read the PEM private key
901		let key = PrivateKeyDer::from_pem_file(key_path).map_err(Error::Key)?;
902		let key = self.provider.key_provider.load_private_key(key)?;
903
904		let certified_key = rustls::sign::CertifiedKey::new(chain, key);
905
906		certified_key.keys_match().map_err(|source| Error::KeyMismatch {
907			key: key_path.to_path_buf(),
908			cert: chain_path.to_path_buf(),
909			source,
910		})?;
911
912		Ok(certified_key)
913	}
914
915	#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
916	fn generate(&self, hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
917		let key_pair = rcgen::KeyPair::generate()?;
918
919		let mut params = rcgen::CertificateParams::new(hostnames)?;
920
921		// Make the certificate valid for two weeks, starting yesterday (in case of clock drift).
922		// WebTransport certificates MUST be valid for two weeks at most.
923		params.not_before = ::time::OffsetDateTime::now_utc() - ::time::Duration::days(1);
924		params.not_after = params.not_before + ::time::Duration::days(14);
925
926		// Generate the certificate
927		let cert = params.self_signed(&key_pair)?;
928
929		// Convert the rcgen type to the rustls type.
930		let key_der = key_pair.serialized_der().to_vec();
931		let key_der = PrivatePkcs8KeyDer::from(key_der);
932		let key = self.provider.key_provider.load_private_key(key_der.into())?;
933
934		// Create a rustls::sign::CertifiedKey
935		Ok(rustls::sign::CertifiedKey::new(vec![cert.into()], key))
936	}
937
938	#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
939	fn generate(&self, _hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
940		Err(Error::NoCryptoProvider)
941	}
942
943	// Replace the certificates
944	pub fn set_certs(&self, certs: Vec<Arc<rustls::sign::CertifiedKey>>) {
945		let fingerprints = certs
946			.iter()
947			.map(|ck| {
948				let fingerprint = crate::crypto::sha256(&self.provider, ck.cert[0].as_ref());
949				hex::encode(fingerprint)
950			})
951			.collect();
952
953		let mut info = self.info.write().expect("info write lock poisoned");
954		info.certs = certs;
955		info.fingerprints = fingerprints;
956	}
957
958	// Return the best certificate for the given ClientHello.
959	fn best_certificate(
960		&self,
961		client_hello: &rustls::server::ClientHello<'_>,
962	) -> Option<Arc<rustls::sign::CertifiedKey>> {
963		let server_name = client_hello.server_name()?;
964		let dns_name = rustls::pki_types::ServerName::try_from(server_name).ok()?;
965
966		for ck in self.info.read().expect("info read lock poisoned").certs.iter() {
967			let leaf: webpki::EndEntityCert = ck
968				.end_entity_cert()
969				.expect("missing certificate")
970				.try_into()
971				.expect("failed to parse certificate");
972
973			if leaf.verify_is_valid_for_subject_name(&dns_name).is_ok() {
974				return Some(ck.clone());
975			}
976		}
977
978		None
979	}
980}
981
982#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
983impl rustls::server::ResolvesServerCert for ServeCerts {
984	fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<rustls::sign::CertifiedKey>> {
985		if let Some(cert) = self.best_certificate(&client_hello) {
986			return Some(cert);
987		}
988
989		// If this happens, it means the client was trying to connect to an unknown hostname.
990		// We do our best and return the first certificate.
991		tracing::warn!(server_name = ?client_hello.server_name(), "no SNI certificate found");
992
993		self.info
994			.read()
995			.expect("info read lock poisoned")
996			.certs
997			.first()
998			.cloned()
999	}
1000}
1001
1002// ── reload_certs ────────────────────────────────────────────────────
1003
1004/// Watch the on-disk cert/key files and reload them whenever they change.
1005///
1006/// Reacting to the filesystem means cert-manager, Kubernetes secret mounts, and
1007/// `mv`-into-place rotate certs with no external signal. Returns immediately when
1008/// only generated certs are configured: there's nothing on disk to watch.
1009#[cfg(any(feature = "quinn", feature = "noq"))]
1010pub(crate) async fn reload_certs(certs: Arc<ServeCerts>, tls_config: Server) {
1011	let paths: Vec<PathBuf> = tls_config.cert.iter().chain(tls_config.key.iter()).cloned().collect();
1012	if paths.is_empty() {
1013		return;
1014	}
1015
1016	let mut watcher = match crate::watch::FileWatcher::new(&paths) {
1017		Ok(watcher) => watcher,
1018		Err(err) => {
1019			tracing::error!(%err, "failed to watch certificate files; hot reload disabled");
1020			return;
1021		}
1022	};
1023
1024	loop {
1025		watcher.changed().await;
1026		tracing::info!("reloading server certificates");
1027
1028		if let Err(err) = certs.load_certs(&tls_config) {
1029			tracing::warn!(%err, "failed to reload server certificates");
1030		}
1031	}
1032}