Skip to main content

rustls_crl_refresh/
verifier.rs

1//! Wrapper certificate verifiers that build a fresh
2//! `WebPkiClientVerifier` / `WebPkiServerVerifier` per handshake from
3//! the latest CRL snapshot held in [`crate::CrlCache`]. This is the
4//! mechanism that satisfies the `Arc<ClientConfig>` /
5//! `Arc<ServerConfig>` stability invariant: refreshing CRL bytes does
6//! not invalidate cached configs, and the wrapper just sees the new
7//! bytes the next time it consults the cache.
8
9use std::sync::Arc;
10
11use arc_swap::ArcSwapOption;
12use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
13use rustls::pki_types::{CertificateDer, CertificateRevocationListDer, ServerName, UnixTime};
14use rustls::server::WebPkiClientVerifier;
15use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
16use rustls::{DigitallySignedStruct, DistinguishedName, RootCertStore, SignatureScheme};
17
18use crate::cache::{CrlCache, CrlSourceId};
19
20/// Identity fingerprint of a CRL snapshot. We hash by the
21/// `Arc::as_ptr` of each `CertificateRevocationListDer` rather than by
22/// the bytes themselves: [`CrlCache`] guarantees that the inner Arc
23/// changes iff the bytes change, so the pointer-tuple is a cheap and
24/// correct "did this snapshot move?" key. Heavy CRLs that arrive every
25/// few hours therefore don't pay a per-handshake byte compare.
26type CrlFingerprint = Vec<usize>;
27
28fn fingerprint(crls: &[Arc<CertificateRevocationListDer<'static>>]) -> CrlFingerprint {
29	crls.iter().map(|arc| Arc::as_ptr(arc) as usize).collect()
30}
31
32fn build_owned(
33	crls: &[Arc<CertificateRevocationListDer<'static>>],
34) -> Vec<CertificateRevocationListDer<'static>> {
35	crls.iter().map(|arc| (**arc).clone()).collect()
36}
37
38struct CachedClient {
39	fingerprint: CrlFingerprint,
40	verifier: Arc<dyn ClientCertVerifier>,
41}
42
43struct CachedServer {
44	fingerprint: CrlFingerprint,
45	verifier: Arc<dyn ServerCertVerifier>,
46}
47
48/// Listener-side wrapper that defers to a `WebPkiClientVerifier`
49/// rebuilt only when the cached CRL snapshot's Arc identity changes,
50/// against the latest CRL bytes pulled from the cache.
51pub struct RefreshableClientCertVerifier {
52	cache: Arc<CrlCache>,
53	sources: Vec<CrlSourceId>,
54	cas: Arc<RootCertStore>,
55	allow_unauthenticated: bool,
56	root_hint_subjects: Vec<DistinguishedName>,
57	cached: ArcSwapOption<CachedClient>,
58}
59
60impl std::fmt::Debug for RefreshableClientCertVerifier {
61	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62		f.debug_struct("RefreshableClientCertVerifier")
63			.field("sources", &self.sources)
64			.field("allow_unauthenticated", &self.allow_unauthenticated)
65			.finish_non_exhaustive()
66	}
67}
68
69impl RefreshableClientCertVerifier {
70	#[must_use]
71	pub fn new(
72		cache: Arc<CrlCache>,
73		sources: Vec<CrlSourceId>,
74		cas: Arc<RootCertStore>,
75		allow_unauthenticated: bool,
76	) -> Arc<Self> {
77		let root_hint_subjects = cas.subjects();
78		Arc::new(Self {
79			cache,
80			sources,
81			cas,
82			allow_unauthenticated,
83			root_hint_subjects,
84			cached: ArcSwapOption::from(None),
85		})
86	}
87
88	fn build_inner(&self) -> Result<Arc<dyn ClientCertVerifier>, rustls::Error> {
89		let crls = self
90			.cache
91			.snapshot(&self.sources)
92			.map_err(|e| rustls::Error::General(format!("crl unavailable: {e}")))?;
93		let fp = fingerprint(&crls);
94		if let Some(hit) = self.cached.load_full()
95			&& hit.fingerprint == fp
96		{
97			return Ok(Arc::clone(&hit.verifier));
98		}
99		let mut builder = WebPkiClientVerifier::builder(Arc::clone(&self.cas));
100		if !crls.is_empty() {
101			builder = builder.with_crls(build_owned(&crls));
102		}
103		if self.allow_unauthenticated {
104			builder = builder.allow_unauthenticated();
105		}
106		let verifier =
107			builder.build().map_err(|e| rustls::Error::General(format!("verifier build: {e}")))?;
108		let verifier: Arc<dyn ClientCertVerifier> = verifier;
109		self
110			.cached
111			.store(Some(Arc::new(CachedClient { fingerprint: fp, verifier: Arc::clone(&verifier) })));
112		Ok(verifier)
113	}
114}
115
116impl ClientCertVerifier for RefreshableClientCertVerifier {
117	fn offer_client_auth(&self) -> bool {
118		true
119	}
120
121	fn client_auth_mandatory(&self) -> bool {
122		!self.allow_unauthenticated
123	}
124
125	fn root_hint_subjects(&self) -> &[DistinguishedName] {
126		&self.root_hint_subjects
127	}
128
129	fn verify_client_cert(
130		&self,
131		end_entity: &CertificateDer<'_>,
132		intermediates: &[CertificateDer<'_>],
133		now: UnixTime,
134	) -> Result<ClientCertVerified, rustls::Error> {
135		self.build_inner()?.verify_client_cert(end_entity, intermediates, now)
136	}
137
138	fn verify_tls12_signature(
139		&self,
140		message: &[u8],
141		cert: &CertificateDer<'_>,
142		dss: &DigitallySignedStruct,
143	) -> Result<HandshakeSignatureValid, rustls::Error> {
144		// Defer to the process-wide rustls crypto provider — must be
145		// installed by the host before any handshake runs.
146		rustls::crypto::verify_tls12_signature(
147			message,
148			cert,
149			dss,
150			&rustls::crypto::CryptoProvider::get_default()
151				.expect("rustls crypto provider installed at boot")
152				.signature_verification_algorithms,
153		)
154	}
155
156	fn verify_tls13_signature(
157		&self,
158		message: &[u8],
159		cert: &CertificateDer<'_>,
160		dss: &DigitallySignedStruct,
161	) -> Result<HandshakeSignatureValid, rustls::Error> {
162		rustls::crypto::verify_tls13_signature(
163			message,
164			cert,
165			dss,
166			&rustls::crypto::CryptoProvider::get_default()
167				.expect("rustls crypto provider installed at boot")
168				.signature_verification_algorithms,
169		)
170	}
171
172	fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
173		rustls::crypto::CryptoProvider::get_default()
174			.expect("rustls crypto provider installed at boot")
175			.signature_verification_algorithms
176			.supported_schemes()
177	}
178}
179
180/// Upstream-side counterpart. Reuses a cached `WebPkiServerVerifier`
181/// across handshakes when the CRL snapshot's Arc identity is
182/// unchanged; rebuilds only after a refresh swaps the underlying
183/// bytes.
184pub struct RefreshableServerCertVerifier {
185	cache: Arc<CrlCache>,
186	sources: Vec<CrlSourceId>,
187	cas: Arc<RootCertStore>,
188	cached: ArcSwapOption<CachedServer>,
189}
190
191impl std::fmt::Debug for RefreshableServerCertVerifier {
192	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193		f.debug_struct("RefreshableServerCertVerifier")
194			.field("sources", &self.sources)
195			.finish_non_exhaustive()
196	}
197}
198
199impl RefreshableServerCertVerifier {
200	#[must_use]
201	pub fn new(
202		cache: Arc<CrlCache>,
203		sources: Vec<CrlSourceId>,
204		cas: Arc<RootCertStore>,
205	) -> Arc<Self> {
206		Arc::new(Self { cache, sources, cas, cached: ArcSwapOption::from(None) })
207	}
208
209	fn build_inner(&self) -> Result<Arc<dyn ServerCertVerifier>, rustls::Error> {
210		let crls = self
211			.cache
212			.snapshot(&self.sources)
213			.map_err(|e| rustls::Error::General(format!("crl unavailable: {e}")))?;
214		let fp = fingerprint(&crls);
215		if let Some(hit) = self.cached.load_full()
216			&& hit.fingerprint == fp
217		{
218			return Ok(Arc::clone(&hit.verifier));
219		}
220		let mut builder = rustls::client::WebPkiServerVerifier::builder(Arc::clone(&self.cas));
221		if !crls.is_empty() {
222			builder = builder.with_crls(build_owned(&crls));
223		}
224		let inner =
225			builder.build().map_err(|e| rustls::Error::General(format!("verifier build: {e}")))?;
226		let verifier: Arc<dyn ServerCertVerifier> = inner;
227		self
228			.cached
229			.store(Some(Arc::new(CachedServer { fingerprint: fp, verifier: Arc::clone(&verifier) })));
230		Ok(verifier)
231	}
232}
233
234impl ServerCertVerifier for RefreshableServerCertVerifier {
235	fn verify_server_cert(
236		&self,
237		end_entity: &CertificateDer<'_>,
238		intermediates: &[CertificateDer<'_>],
239		server_name: &ServerName<'_>,
240		ocsp_response: &[u8],
241		now: UnixTime,
242	) -> Result<ServerCertVerified, rustls::Error> {
243		self.build_inner()?.verify_server_cert(
244			end_entity,
245			intermediates,
246			server_name,
247			ocsp_response,
248			now,
249		)
250	}
251
252	fn verify_tls12_signature(
253		&self,
254		message: &[u8],
255		cert: &CertificateDer<'_>,
256		dss: &DigitallySignedStruct,
257	) -> Result<HandshakeSignatureValid, rustls::Error> {
258		rustls::crypto::verify_tls12_signature(
259			message,
260			cert,
261			dss,
262			&rustls::crypto::CryptoProvider::get_default()
263				.expect("rustls crypto provider installed at boot")
264				.signature_verification_algorithms,
265		)
266	}
267
268	fn verify_tls13_signature(
269		&self,
270		message: &[u8],
271		cert: &CertificateDer<'_>,
272		dss: &DigitallySignedStruct,
273	) -> Result<HandshakeSignatureValid, rustls::Error> {
274		rustls::crypto::verify_tls13_signature(
275			message,
276			cert,
277			dss,
278			&rustls::crypto::CryptoProvider::get_default()
279				.expect("rustls crypto provider installed at boot")
280				.signature_verification_algorithms,
281		)
282	}
283
284	fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
285		rustls::crypto::CryptoProvider::get_default()
286			.expect("rustls crypto provider installed at boot")
287			.signature_verification_algorithms
288			.supported_schemes()
289	}
290}
291
292#[cfg(test)]
293mod tests {
294	use std::sync::Arc;
295	use std::sync::atomic::{AtomicUsize, Ordering};
296
297	use async_trait::async_trait;
298	use rustls::RootCertStore;
299
300	use super::*;
301	use crate::cache::{CrlCache, CrlFetchFailure, CrlFetcher, CrlSourceId};
302
303	struct StaticFetcher {
304		bytes: Vec<u8>,
305		count: AtomicUsize,
306	}
307
308	#[async_trait]
309	impl CrlFetcher for StaticFetcher {
310		async fn fetch(&self, _src: &CrlSourceId) -> Result<Vec<u8>, String> {
311			self.count.fetch_add(1, Ordering::SeqCst);
312			Ok(self.bytes.clone())
313		}
314	}
315
316	struct FailingFetcher;
317
318	#[async_trait]
319	impl CrlFetcher for FailingFetcher {
320		async fn fetch(&self, _src: &CrlSourceId) -> Result<Vec<u8>, String> {
321			Err("test failure".into())
322		}
323	}
324
325	fn install_crypto_once() {
326		// Idempotent — `install_default` is best-effort and other tests
327		// in this crate also call it. Ignore the result.
328		let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
329	}
330
331	fn ca_only_root_store() -> (Arc<RootCertStore>, rcgen::Issuer<'static, rcgen::KeyPair>) {
332		use rcgen::{BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair, KeyUsagePurpose};
333		let mut params = CertificateParams::new(vec!["test ca".into()]).expect("ca params");
334		params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
335		params.key_usages = vec![
336			KeyUsagePurpose::KeyCertSign,
337			KeyUsagePurpose::DigitalSignature,
338			KeyUsagePurpose::CrlSign,
339		];
340		let key = KeyPair::generate().expect("ca key");
341		let cert = params.clone().self_signed(&key).expect("self-sign ca");
342		let cert_der = cert.der().clone();
343		let issuer = Issuer::new(params, key);
344		let mut store = RootCertStore::empty();
345		store.add(cert_der).expect("add ca");
346		(Arc::new(store), issuer)
347	}
348
349	fn fixture_crl(issuer: &rcgen::Issuer<'_, rcgen::KeyPair>, revoked: &[u64]) -> Vec<u8> {
350		use rcgen::{
351			CertificateRevocationListParams, KeyIdMethod, RevocationReason, RevokedCertParams,
352			SerialNumber,
353		};
354		let now = time::OffsetDateTime::now_utc();
355		let params = CertificateRevocationListParams {
356			this_update: now,
357			next_update: now + time::Duration::hours(24),
358			crl_number: SerialNumber::from(1u64),
359			issuing_distribution_point: None,
360			revoked_certs: revoked
361				.iter()
362				.map(|s| RevokedCertParams {
363					serial_number: SerialNumber::from(*s),
364					revocation_time: now,
365					reason_code: Some(RevocationReason::KeyCompromise),
366					invalidity_date: None,
367				})
368				.collect(),
369			key_identifier_method: KeyIdMethod::Sha256,
370		};
371		params.signed_by(issuer).expect("sign crl").der().as_ref().to_vec()
372	}
373
374	#[tokio::test(flavor = "multi_thread")]
375	async fn client_verifier_builds_with_empty_cache_when_no_sources() {
376		install_crypto_once();
377		let (cas, _issuer) = ca_only_root_store();
378		let fetcher = Arc::new(StaticFetcher { bytes: vec![], count: AtomicUsize::new(0) });
379		let cache = CrlCache::new(fetcher);
380		let v = RefreshableClientCertVerifier::new(cache, Vec::new(), cas, false);
381		assert!(v.build_inner().is_ok());
382		assert!(v.client_auth_mandatory());
383	}
384
385	#[tokio::test(flavor = "multi_thread")]
386	async fn client_verifier_propagates_reject_unavailable() {
387		install_crypto_once();
388		let (cas, _issuer) = ca_only_root_store();
389		let cache = CrlCache::new(Arc::new(FailingFetcher));
390		let src = CrlSourceId::Url("https://crl.example/down".into());
391		let _ = cache.ensure_loaded(&[(src.clone(), CrlFetchFailure::Reject)]);
392		let v = RefreshableClientCertVerifier::new(cache, vec![src], cas, false);
393		let err = v.build_inner().expect_err("reject unavailable must fail");
394		match err {
395			rustls::Error::General(msg) => assert!(msg.contains("crl unavailable"), "{msg}"),
396			other => panic!("unexpected: {other:?}"),
397		}
398	}
399
400	#[tokio::test(flavor = "multi_thread")]
401	async fn server_verifier_builds_with_real_crl_bytes() {
402		install_crypto_once();
403		let (cas, issuer) = ca_only_root_store();
404		let bytes = fixture_crl(&issuer, &[42]);
405		let fetcher = Arc::new(StaticFetcher { bytes, count: AtomicUsize::new(0) });
406		let cache = CrlCache::new(fetcher);
407		let src = CrlSourceId::Url("https://crl.example/with-revoke".into());
408		cache.ensure_loaded(&[(src.clone(), CrlFetchFailure::Tolerate)]).expect("load");
409		let v = RefreshableServerCertVerifier::new(cache, vec![src], cas);
410		assert!(v.build_inner().is_ok());
411	}
412
413	// Test-only fetcher that serves a different byte string on the
414	// second fetch so we can simulate a real CRL rotation without
415	// rebuilding the cache from scratch.
416	struct SwapFetcher {
417		calls: AtomicUsize,
418		first: Vec<u8>,
419		second: Vec<u8>,
420	}
421
422	#[async_trait]
423	impl CrlFetcher for SwapFetcher {
424		async fn fetch(&self, _src: &CrlSourceId) -> Result<Vec<u8>, String> {
425			let n = self.calls.fetch_add(1, Ordering::SeqCst);
426			Ok(if n == 0 { self.first.clone() } else { self.second.clone() })
427		}
428	}
429
430	#[tokio::test(flavor = "multi_thread")]
431	async fn client_verifier_reuses_cached_inner_until_crl_arc_changes() {
432		install_crypto_once();
433		let (cas, issuer) = ca_only_root_store();
434		let bytes_v1 = fixture_crl(&issuer, &[1]);
435		let bytes_v2 = fixture_crl(&issuer, &[1, 2]);
436		let fetcher =
437			Arc::new(SwapFetcher { calls: AtomicUsize::new(0), first: bytes_v1, second: bytes_v2 });
438		let cache = CrlCache::new(fetcher);
439		let src = CrlSourceId::Url("https://crl.example/cached".into());
440		cache.ensure_loaded(&[(src.clone(), CrlFetchFailure::Tolerate)]).expect("load v1");
441		let v = RefreshableClientCertVerifier::new(cache.clone(), vec![src.clone()], cas, false);
442		let a = v.build_inner().expect("build a");
443		let b = v.build_inner().expect("build b");
444		assert!(Arc::ptr_eq(&a, &b), "cache hit reuses the same inner verifier Arc");
445
446		// Refresh: cache now serves the v2 bytes, so a new build is
447		// required.
448		cache.ensure_loaded(&[(src, CrlFetchFailure::Tolerate)]).expect("refresh to v2");
449		let c = v.build_inner().expect("build c");
450		assert!(!Arc::ptr_eq(&b, &c), "post-refresh forces a rebuild");
451	}
452
453	#[tokio::test(flavor = "multi_thread")]
454	async fn allow_unauthenticated_flips_mandatory_flag() {
455		install_crypto_once();
456		let (cas, _issuer) = ca_only_root_store();
457		let fetcher = Arc::new(StaticFetcher { bytes: vec![], count: AtomicUsize::new(0) });
458		let cache = CrlCache::new(fetcher);
459		let v_mandatory =
460			RefreshableClientCertVerifier::new(Arc::clone(&cache), Vec::new(), Arc::clone(&cas), false);
461		let v_request = RefreshableClientCertVerifier::new(cache, Vec::new(), cas, true);
462		assert!(v_mandatory.client_auth_mandatory());
463		assert!(!v_request.client_auth_mandatory());
464	}
465}