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