1#[cfg(test)]
7use std::collections::HashSet;
8use std::{
9 collections::HashMap,
10 fmt,
11 str::FromStr,
12 sync::{Arc, LazyLock, Mutex},
13};
14
15use rustls::{
16 pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
17 server::{ClientHello, ResolvesServerCert},
18 sign::CertifiedKey,
19};
20
21use crate::crypto::any_supported_type;
22use sha2::{Digest, Sha256};
23use sozu_command::{
24 certificate::{
25 CertificateError, Fingerprint, get_cn_and_san_attributes, parse_pem, parse_x509,
26 split_certificate_chain,
27 },
28 logging::ansi_palette,
29 proto::command::{AddCertificate, CertificateAndKey, ReplaceCertificate, SocketAddress},
30};
31
32use crate::metrics::names;
33use crate::router::MAX_HOSTNAME_LENGTH;
34use crate::router::pattern_trie::{InsertResult, Key, KeyValue, TrieNode};
35
36macro_rules! log_module_context {
45 () => {{
46 let (open, reset, _, _, _) = ansi_palette();
47 format!(
48 "{open}TLS-RESOLVER{reset}\t >>>",
49 open = open,
50 reset = reset
51 )
52 }};
53}
54
55static DEFAULT_CERTIFICATE: LazyLock<Option<Arc<CertifiedKey>>> = LazyLock::new(|| {
59 let add = AddCertificate {
60 certificate: CertificateAndKey {
61 certificate: include_str!("../assets/certificate.pem").to_string(),
62 certificate_chain: vec![include_str!("../assets/certificate_chain.pem").to_string()],
63 key: include_str!("../assets/key.pem").to_string(),
64 versions: vec![],
65 names: vec![],
66 },
67 address: SocketAddress::new_v4(0, 0, 0, 0, 8080), expired_at: None,
69 };
70 CertifiedKeyWrapper::try_from(&add).ok().map(|c| c.inner)
71});
72
73#[derive(thiserror::Error, Debug)]
74pub enum CertificateResolverError {
75 #[error("failed to get common name and subject alternate names from pem, {0}")]
76 InvalidCommonNameAndSubjectAlternateNames(CertificateError),
77 #[error("invalid private key: {0}")]
78 InvalidPrivateKey(String),
79 #[error("empty key")]
80 EmptyKeys,
81 #[error("error parsing x509 cert from bytes: {0}")]
82 ParseX509(CertificateError),
83 #[error("error parsing pem formated certificate from bytes: {0}")]
84 ParsePem(CertificateError),
85 #[error("error parsing overriding names in new certificate: {0}")]
86 ParseOverridingNames(CertificateError),
87 #[error("the SNI route table cannot host a certificate name, name_bytes={}", .0.len())]
88 InvalidName(String),
89}
90
91#[derive(Clone)]
95pub struct CertifiedKeyWrapper {
96 inner: Arc<CertifiedKey>,
97 names: Vec<String>,
99 expiration: i64,
100 fingerprint: Fingerprint,
101}
102
103impl fmt::Debug for CertifiedKeyWrapper {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 let names_len = self
106 .names
107 .iter()
108 .map(String::len)
109 .fold(0usize, usize::saturating_add);
110
111 f.debug_struct("CertifiedKeyWrapper")
112 .field("certificate", &"[redacted]")
113 .field("certificate_chain_count", &self.inner.cert.len())
114 .field("private_key", &"[redacted]")
115 .field("names_count", &self.names.len())
116 .field("names_len", &names_len)
117 .field("expiration", &self.expiration)
118 .field("fingerprint_bytes", &self.fingerprint.0.len())
119 .finish()
120 }
121}
122
123impl TryFrom<&AddCertificate> for CertifiedKeyWrapper {
126 type Error = CertificateResolverError;
127
128 fn try_from(add: &AddCertificate) -> Result<Self, Self::Error> {
129 let cert = add.certificate.clone();
130
131 let pem =
132 parse_pem(cert.certificate.as_bytes()).map_err(CertificateResolverError::ParsePem)?;
133
134 let x509 = parse_x509(&pem.contents).map_err(CertificateResolverError::ParseX509)?;
135
136 let overriding_names = if add.certificate.names.is_empty() {
137 get_cn_and_san_attributes(&x509)
138 } else {
139 add.certificate.names.clone()
140 };
141
142 let expiration = add
143 .expired_at
144 .unwrap_or(x509.validity().not_after.timestamp());
145
146 let fingerprint = Fingerprint(Sha256::digest(&pem.contents).iter().cloned().collect());
147
148 let leaf_der = pem.contents;
158 let mut chain = vec![CertificateDer::from(leaf_der.to_owned())];
159 let mut dropped_duplicates = 0usize;
160 for cert in &cert.certificate_chain {
161 for split_pem in split_certificate_chain(cert.to_owned()) {
162 let chain_link = parse_pem(split_pem.as_bytes())
163 .map_err(CertificateResolverError::ParsePem)?
164 .contents;
165
166 if chain_link == leaf_der {
167 dropped_duplicates += 1;
168 continue;
169 }
170 chain.push(CertificateDer::from(chain_link));
171 }
172 }
173 if dropped_duplicates > 0 {
174 debug!(
175 "{} dropped {} duplicate leaf certificate(s) from the supplied chain",
176 log_module_context!(),
177 dropped_duplicates
178 );
179 }
180
181 let private_key = PrivateKeyDer::from_pem_slice(cert.key.as_bytes())
190 .map_err(|_| CertificateResolverError::EmptyKeys)?;
191
192 debug_assert!(
197 !chain.is_empty(),
198 "assembled certificate chain must contain at least the leaf"
199 );
200 debug_assert_eq!(
201 chain[0].as_ref(),
202 leaf_der.as_slice(),
203 "the leaf must remain at index 0 of the chain"
204 );
205 debug_assert_eq!(
208 fingerprint.0.len(),
209 32,
210 "a SHA-256 fingerprint must be 32 bytes"
211 );
212
213 match any_supported_type(&private_key) {
214 Ok(signing_key) => {
215 let stored_certificate = CertifiedKeyWrapper {
216 inner: Arc::new(CertifiedKey::new(chain, signing_key)),
217 names: overriding_names,
218 expiration,
219 fingerprint,
220 };
221 Ok(stored_certificate)
222 }
223 Err(sign_error) => Err(CertificateResolverError::InvalidPrivateKey(
224 sign_error.to_string(),
225 )),
226 }
227 }
228}
229
230#[derive(Default)]
237pub struct CertificateResolver {
238 pub domains: TrieNode<Fingerprint>,
240 certificates: HashMap<Fingerprint, CertifiedKeyWrapper>,
242 name_fingerprint_idx: HashMap<String, Vec<(Fingerprint, i64)>>,
246}
247
248impl fmt::Debug for CertificateResolver {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 let indexed_certificate_links = self
251 .name_fingerprint_idx
252 .values()
253 .map(Vec::len)
254 .fold(0usize, usize::saturating_add);
255
256 f.debug_struct("CertificateResolver")
257 .field("domains", &"[redacted]")
258 .field("certificates_count", &self.certificates.len())
259 .field("indexed_names_count", &self.name_fingerprint_idx.len())
260 .field("indexed_certificate_links", &indexed_certificate_links)
261 .finish()
262 }
263}
264
265impl CertificateResolver {
266 pub fn get_certificate(&self, fingerprint: &Fingerprint) -> Option<CertifiedKeyWrapper> {
268 self.certificates.get(fingerprint).map(ToOwned::to_owned)
269 }
270
271 fn publish_min_expiration_gauge(&self) {
285 let Some(min_expiration) = self.certificates.values().map(|c| c.expiration).min() else {
286 return;
294 };
295 let clamped = min_expiration.max(0) as usize;
296 gauge!(names::tls::CERT_MIN_EXPIRES_AT_SECONDS, clamped);
297 }
298
299 pub fn add_certificate(
302 &mut self,
303 add: &AddCertificate,
304 ) -> Result<Fingerprint, CertificateResolverError> {
305 let cert_to_add = CertifiedKeyWrapper::try_from(add)?;
306
307 trace!(
308 "{} adding certificate {:?}",
309 log_module_context!(),
310 cert_to_add
311 );
312
313 if self.certificates.contains_key(&cert_to_add.fingerprint) {
314 return Ok(cert_to_add.fingerprint);
315 }
316
317 {
327 let mut scratch: TrieNode<()> = TrieNode::root();
328 for name in &cert_to_add.names {
329 if name.len() > MAX_HOSTNAME_LENGTH
330 || scratch.domain_insert(name.to_owned().into_bytes(), ())
331 == InsertResult::Failed
332 {
333 return Err(CertificateResolverError::InvalidName(name.to_owned()));
334 }
335 }
336 }
337
338 let certificates_before = self.certificates.len();
343 let new_fingerprint = cert_to_add.fingerprint.clone();
344 debug_assert!(
345 !self.certificates.contains_key(&new_fingerprint),
346 "add_certificate past the dedup guard must be inserting a new fingerprint"
347 );
348
349 for new_name in &cert_to_add.names {
350 let fingerprints_for_this_name = self
351 .name_fingerprint_idx
352 .entry(new_name.to_owned())
353 .or_default();
354
355 fingerprints_for_this_name
356 .push((cert_to_add.fingerprint.clone(), cert_to_add.expiration));
357
358 fingerprints_for_this_name.sort_by_key(|t| t.1);
360
361 let longest_lived_cert = match fingerprints_for_this_name.last() {
362 Some(cert) => cert,
363 None => {
364 error!(
365 "{} no fingerprint for this name, this should not happen",
366 log_module_context!()
367 );
368 continue;
369 }
370 };
371
372 self.domains.remove(&new_name.to_owned().into_bytes());
374 let insert_result = self.domains.insert(
375 new_name.to_owned().into_bytes(),
376 longest_lived_cert.0.to_owned(),
377 );
378 debug_assert_ne!(
382 insert_result,
383 InsertResult::Failed,
384 "add_certificate's names must already be validated by its dry-run"
385 );
386 if insert_result == InsertResult::Failed {
387 error!(
388 "{} the SNI trie rejected a certificate name despite passing \
389 the dry-run validation, name_bytes={}",
390 log_module_context!(),
391 new_name.len(),
392 );
393 }
394 }
395
396 self.certificates
397 .insert(cert_to_add.fingerprint.to_owned(), cert_to_add.clone());
398 self.publish_min_expiration_gauge();
399
400 debug_assert!(
404 self.certificates.contains_key(&new_fingerprint),
405 "add_certificate must store the new certificate"
406 );
407 debug_assert_eq!(
408 self.certificates.len(),
409 certificates_before + 1,
410 "add_certificate must grow the store by exactly one"
411 );
412 debug_assert!(
413 cert_to_add.names.iter().all(|name| {
414 self.name_fingerprint_idx
415 .get(name)
416 .is_some_and(|fps| fps.iter().any(|(fp, _)| *fp == new_fingerprint))
417 }),
418 "every name of the added cert must be indexed to its fingerprint"
419 );
420
421 trace!("{} {:#?}", log_module_context!(), self);
422
423 Ok(cert_to_add.fingerprint)
424 }
425
426 pub fn remove_certificate(
429 &mut self,
430 fingerprint: &Fingerprint,
431 ) -> Result<(), CertificateResolverError> {
432 let certificates_before = self.certificates.len();
437 let was_present = self.certificates.contains_key(fingerprint);
438
439 if let Some(certificate_to_remove) = self.get_certificate(fingerprint) {
440 #[cfg(debug_assertions)]
444 let removed_names = certificate_to_remove.names.clone();
445 for name in certificate_to_remove.names {
446 self.domains.domain_remove(&name.as_bytes().to_vec());
447
448 if let std::collections::hash_map::Entry::Occupied(mut entry) =
449 self.name_fingerprint_idx.entry(name.to_owned())
450 {
451 entry.get_mut().retain(|t| &t.0 != fingerprint);
453
454 if let Some(longest_lived_cert) = entry.get().last() {
456 let insert_result = self
457 .domains
458 .insert(name.as_bytes().to_vec(), longest_lived_cert.0.to_owned());
459 debug_assert_ne!(
463 insert_result,
464 InsertResult::Failed,
465 "remove_certificate re-inserts names that were validated on add"
466 );
467 if insert_result == InsertResult::Failed {
468 error!(
469 "{} the SNI trie rejected a re-inserted certificate name \
470 that was validated on add, name_bytes={}",
471 log_module_context!(),
472 name.len(),
473 );
474 }
475 }
476
477 if entry.get().is_empty() {
479 entry.remove();
480 }
481 }
482 }
483
484 self.certificates.remove(fingerprint);
485 self.publish_min_expiration_gauge();
486
487 debug_assert!(
492 !self.certificates.contains_key(fingerprint),
493 "remove_certificate must evict the fingerprint"
494 );
495 debug_assert_eq!(
496 self.certificates.len(),
497 certificates_before - 1,
498 "removing a present cert must shrink the store by exactly one"
499 );
500 #[cfg(debug_assertions)]
501 debug_assert!(
502 removed_names.iter().all(|name| {
503 self.name_fingerprint_idx
504 .get(name)
505 .is_none_or(|fps| fps.iter().all(|(fp, _)| fp != fingerprint))
506 }),
507 "no name may still index the removed fingerprint"
508 );
509 } else {
510 debug_assert!(
512 !was_present,
513 "the absent-cert branch must only run when the fingerprint was not stored"
514 );
515 debug_assert_eq!(
516 self.certificates.len(),
517 certificates_before,
518 "removing an absent cert must not change the store"
519 );
520 }
521 trace!("{} {:#?}", log_module_context!(), self);
522
523 Ok(())
524 }
525
526 pub fn replace_certificate(
530 &mut self,
531 replace: &ReplaceCertificate,
532 ) -> Result<Fingerprint, CertificateResolverError> {
533 let add = AddCertificate {
534 address: replace.address.to_owned(),
535 certificate: replace.new_certificate.to_owned(),
536 expired_at: replace.new_expired_at.to_owned(),
537 };
538
539 let new_cert = CertifiedKeyWrapper::try_from(&add)?;
554 let new_fingerprint = new_cert.fingerprint.to_owned();
555
556 if let Ok(old_fingerprint) = Fingerprint::from_str(&replace.old_fingerprint)
557 && old_fingerprint == new_fingerprint
558 {
559 let stored_before = self.certificates.contains_key(&new_fingerprint);
564 self.publish_min_expiration_gauge();
567 debug_assert_eq!(
568 self.certificates.contains_key(&new_fingerprint),
569 stored_before,
570 "idempotent replace must not change whether the cert is stored"
571 );
572 return Ok(new_fingerprint);
573 }
574
575 let new_fingerprint = self.add_certificate(&add)?;
576
577 debug_assert!(
582 self.certificates.contains_key(&new_fingerprint),
583 "the replacement certificate must be stored before the old one is removed"
584 );
585
586 match Fingerprint::from_str(&replace.old_fingerprint) {
587 Ok(old_fingerprint) => self.remove_certificate(&old_fingerprint)?,
588 Err(err) => {
589 warn!(
594 "{} new certificate added but could not remove old one: \
595 failed to parse old fingerprint, {}",
596 log_module_context!(),
597 err
598 );
599 }
600 }
601
602 Ok(new_fingerprint)
603 }
604
605 #[cfg(test)]
608 fn find_certificates_by_names(
609 &self,
610 names: &HashSet<String>,
611 ) -> Result<HashSet<Fingerprint>, CertificateResolverError> {
612 let mut fingerprints = HashSet::new();
613 for name in names {
614 if let Some(fprints) = self.name_fingerprint_idx.get(name) {
615 fprints.iter().for_each(|fingerprint| {
616 fingerprints.insert(fingerprint.to_owned().0);
617 });
618 }
619 }
620
621 Ok(fingerprints)
622 }
623
624 #[cfg(test)]
627 fn certificate_names(
628 &self,
629 fingerprint: &Fingerprint,
630 ) -> Result<HashSet<String>, CertificateResolverError> {
631 if let Some(cert) = self.certificates.get(fingerprint) {
632 return Ok(cert.names.iter().cloned().collect());
633 }
634 Ok(HashSet::new())
635 }
636
637 pub fn domain_lookup(
638 &self,
639 domain: &[u8],
640 accept_wildcard: bool,
641 ) -> Option<&KeyValue<Key, Fingerprint>> {
642 self.domains.domain_lookup(domain, accept_wildcard)
643 }
644
645 pub fn names_for_sni(&self, domain: &[u8]) -> Option<Vec<String>> {
657 let (_, fingerprint) = self.domain_lookup(domain, true)?;
658 self.certificates
659 .get(fingerprint)
660 .map(|cert| cert.names.clone())
661 }
662}
663
664#[derive(Default)]
668pub struct MutexCertificateResolver(pub Mutex<CertificateResolver>);
669
670impl ResolvesServerCert for MutexCertificateResolver {
671 fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
672 let server_name = client_hello.server_name();
673 let sigschemes = client_hello.signature_schemes();
674
675 let Some(name) = server_name else {
676 error!(
677 "{} cannot look up certificate: no SNI from session",
678 log_module_context!()
679 );
680 return None;
681 };
682 trace!(
683 "{} trying to resolve certificate with name_bytes={} signature_schemes_count={}",
684 log_module_context!(),
685 name.len(),
686 sigschemes.len()
687 );
688 let resolver = match self.0.lock() {
698 Ok(guard) => guard,
699 Err(_poisoned) => {
700 error!(
701 "{} cert resolver mutex poisoned, returning default cert",
702 log_module_context!()
703 );
704 return DEFAULT_CERTIFICATE.clone();
705 }
706 };
707 if let Some((_, fingerprint)) = resolver.domains.domain_lookup(name.as_bytes(), true) {
708 trace!(
709 "{} looking for certificate with name_bytes={} fingerprint_bytes={}",
710 log_module_context!(),
711 name.len(),
712 fingerprint.0.len()
713 );
714
715 let cert = resolver
724 .certificates
725 .get(fingerprint)
726 .map(|cert| cert.inner.clone());
727 debug_assert!(
728 cert.is_none()
729 || resolver
730 .certificates
731 .get(fingerprint)
732 .is_some_and(|stored| Arc::ptr_eq(&stored.inner, cert.as_ref().unwrap())),
733 "resolved certificate must be the one stored under the looked-up fingerprint"
734 );
735
736 trace!(
737 "{} certificate lookup fingerprint_bytes={} found={}",
738 log_module_context!(),
739 fingerprint.0.len(),
740 cert.is_some()
741 );
742 return cert;
743 }
744 drop(resolver);
745
746 debug!(
750 "{} default certificate is used for name_bytes={}",
751 log_module_context!(),
752 name.len()
753 );
754 incr!(names::tls::DEFAULT_CERT_USED);
755 DEFAULT_CERTIFICATE.clone()
756 }
757}
758
759impl MutexCertificateResolver {
760 pub fn names_for_sni(&self, domain: &[u8]) -> Option<Vec<String>> {
766 match self.0.lock() {
767 Ok(guard) => guard.names_for_sni(domain),
768 Err(_poisoned) => {
769 error!(
770 "{} cert resolver mutex poisoned, treating as no SAN match",
771 log_module_context!()
772 );
773 None
774 }
775 }
776 }
777}
778
779impl fmt::Debug for MutexCertificateResolver {
780 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781 f.write_str("MutexWrappedCertificateResolver")
782 }
783}
784
785#[cfg(test)]
789mod tests {
790 use std::{
791 collections::HashSet,
792 error::Error,
793 io::Cursor,
794 sync::Arc,
795 time::{Duration, SystemTime},
796 };
797
798 use sozu_command::proto::command::{
800 AddCertificate, CertificateAndKey, ReplaceCertificate, SocketAddress,
801 };
802
803 use super::{CertificateResolver, CertifiedKeyWrapper, MutexCertificateResolver};
804
805 fn drive_client_hello(resolver: Arc<MutexCertificateResolver>, server_name: String) {
806 let provider = Arc::new(crate::crypto::default_provider());
807 let server_config = rustls::ServerConfig::builder_with_provider(provider.clone())
808 .with_protocol_versions(&[&rustls::version::TLS13])
809 .expect("test server provider must support TLS 1.3")
810 .with_no_client_auth()
811 .with_cert_resolver(resolver);
812 let client_config = rustls::ClientConfig::builder_with_provider(provider)
813 .with_protocol_versions(&[&rustls::version::TLS13])
814 .expect("test client provider must support TLS 1.3")
815 .with_root_certificates(rustls::RootCertStore::empty())
816 .with_no_client_auth();
817 let server_name = rustls::pki_types::ServerName::try_from(server_name)
818 .expect("test SNI must be a valid DNS name");
819 let mut client = rustls::ClientConnection::new(Arc::new(client_config), server_name)
820 .expect("test client connection must initialize");
821 let mut server = rustls::ServerConnection::new(Arc::new(server_config))
822 .expect("test server connection must initialize");
823
824 let mut client_hello = Vec::new();
825 client
826 .write_tls(&mut client_hello)
827 .expect("test client hello must serialize");
828 server
829 .read_tls(&mut Cursor::new(client_hello))
830 .expect("test server must read the client hello");
831 server
832 .process_new_packets()
833 .expect("test server must process the client hello");
834 }
835
836 #[test]
844 fn add_certificate_rejects_a_name_the_route_table_cannot_host() {
845 for name in ["sni-secret.example/", ".sni-secret.example"] {
846 let mut resolver = CertificateResolver::default();
847 let result = resolver.add_certificate(&AddCertificate {
848 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
849 certificate: CertificateAndKey {
850 certificate: include_str!("../assets/certificate.pem").to_owned(),
851 key: include_str!("../assets/key.pem").to_owned(),
852 names: vec![(*name).to_owned()],
853 ..Default::default()
854 },
855 expired_at: None,
856 });
857 assert!(result.is_err(), "{name:?} must be rejected, not stored");
858 assert!(
859 resolver.certificates.is_empty(),
860 "{name:?} was rejected but the certificate store was mutated",
861 );
862 assert!(
863 resolver.name_fingerprint_idx.is_empty(),
864 "{name:?} was rejected but the name index was mutated",
865 );
866 assert!(
867 resolver.domains.is_empty(),
868 "{name:?} was rejected but the SNI trie was mutated",
869 );
870 }
871 }
872
873 #[test]
874 fn certificate_resolver_logs_redact_matching_sni_and_fingerprint() {
875 const SNI_SECRET: &str = "resolver-sni-secret.example";
876
877 let certificate = CertificateAndKey {
878 certificate: include_str!("../assets/certificate.pem").to_owned(),
879 key: include_str!("../assets/key.pem").to_owned(),
880 names: vec![SNI_SECRET.to_owned()],
881 ..Default::default()
882 };
883 let fingerprint = certificate
884 .fingerprint()
885 .expect("test certificate fingerprint must be computable")
886 .to_string();
887 let output = crate::capture_test_logs_at_level("trace", move || {
888 let resolver = Arc::new(MutexCertificateResolver::default());
889 resolver
890 .0
891 .lock()
892 .expect("test resolver lock must be available")
893 .add_certificate(&AddCertificate {
894 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
895 certificate,
896 expired_at: None,
897 })
898 .expect("test certificate must load into the resolver");
899 drive_client_hello(resolver, SNI_SECRET.to_owned());
900 });
901
902 assert!(
903 !output.contains(SNI_SECRET),
904 "TLS resolver logs leaked the matching SNI: {output}"
905 );
906 assert!(
907 !output.contains(&fingerprint),
908 "TLS resolver logs leaked the matching certificate fingerprint: {output}"
909 );
910 for metadata in [
911 format!("name_bytes={}", SNI_SECRET.len()),
912 "fingerprint_bytes=32".to_owned(),
913 ] {
914 assert!(
915 output.contains(&metadata),
916 "TLS resolver logs omitted bounded metadata {metadata}: {output}"
917 );
918 }
919 assert!(
920 output.len() <= 4096,
921 "TLS resolver log capture is not bounded: {} bytes",
922 output.len()
923 );
924 }
925
926 #[test]
927 fn certificate_resolver_logs_redact_fallback_sni() {
928 const SNI_SECRET: &str = "fallback-sni-secret.example";
929
930 let output = crate::capture_test_logs_at_level("trace", move || {
931 drive_client_hello(
932 Arc::new(MutexCertificateResolver::default()),
933 SNI_SECRET.to_owned(),
934 );
935 });
936
937 assert!(
938 !output.contains(SNI_SECRET),
939 "TLS resolver logs leaked the fallback SNI: {output}"
940 );
941 assert!(
942 output.contains(&format!("name_bytes={}", SNI_SECRET.len())),
943 "TLS resolver fallback log omitted bounded SNI metadata: {output}"
944 );
945 assert!(
946 output.len() <= 2048,
947 "TLS resolver fallback log capture is not bounded: {} bytes",
948 output.len()
949 );
950 }
951
952 #[test]
953 fn certificate_resolver_poison_logs_are_bounded() {
954 const SNI_SECRET: &str = "poison-sni-secret.example";
955 const QUERY_SECRET: &str = "POISON_QUERY_SECRET_SENTINEL";
956
957 let query = format!("{QUERY_SECRET}{}", "x".repeat(4096));
958 let output = crate::capture_test_logs_at_level("trace", move || {
959 let resolver = Arc::new(MutexCertificateResolver::default());
960 let poison_target = resolver.clone();
961 assert!(
962 std::thread::spawn(move || {
963 let _guard = poison_target
964 .0
965 .lock()
966 .expect("test resolver lock must initially be available");
967 panic!("intentionally poison the test resolver lock");
968 })
969 .join()
970 .is_err(),
971 "test poison thread must panic"
972 );
973
974 drive_client_hello(resolver.clone(), SNI_SECRET.to_owned());
975 assert!(resolver.names_for_sni(query.as_bytes()).is_none());
976 });
977
978 assert!(
979 !output.contains(SNI_SECRET),
980 "TLS resolver poison log leaked the handshake SNI: {output}"
981 );
982 assert!(
983 !output.contains(QUERY_SECRET),
984 "TLS resolver poison log leaked the 4 KiB direct query: {output}"
985 );
986 assert!(
987 output.len() <= 2048,
988 "TLS resolver poison log capture is not bounded: {} bytes",
989 output.len()
990 );
991 }
992
993 #[test]
994 fn certified_key_wrapper_debug_redacts_runtime_certificate_material() {
995 const NAME_SECRET: &str = "CERTIFIED_KEY_NAME_SECRET_SENTINEL";
996
997 let add = AddCertificate {
998 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
999 certificate: CertificateAndKey {
1000 certificate: include_str!("../assets/certificate.pem").to_owned(),
1001 key: include_str!("../assets/key.pem").to_owned(),
1002 names: vec![format!("{NAME_SECRET}{}", "x".repeat(4096))],
1003 ..Default::default()
1004 },
1005 expired_at: None,
1006 };
1007 let certified_key =
1008 CertifiedKeyWrapper::try_from(&add).expect("test certificate must parse");
1009 let fingerprint = certified_key.fingerprint.to_string();
1010 let output = format!("{certified_key:?}");
1011
1012 assert!(
1013 !output.contains(NAME_SECRET),
1014 "CertifiedKeyWrapper Debug leaked an overridden certificate name: {output}"
1015 );
1016 assert!(
1017 !output.contains(&fingerprint),
1018 "CertifiedKeyWrapper Debug leaked the certificate fingerprint: {output}"
1019 );
1020 assert!(
1021 output.contains("fingerprint_bytes: 32"),
1022 "CertifiedKeyWrapper Debug omitted bounded fingerprint metadata: {output}"
1023 );
1024 assert!(
1025 output.contains("[redacted]"),
1026 "CertifiedKeyWrapper Debug must make certificate redaction explicit: {output}"
1027 );
1028 assert!(
1029 output.len() <= 1024,
1030 "CertifiedKeyWrapper Debug output is not bounded: {} bytes",
1031 output.len()
1032 );
1033 }
1034
1035 #[test]
1036 fn certificate_resolver_debug_redacts_indexed_names() {
1037 const NAME_SECRET: &str = "CERTIFICATE_RESOLVER_NAME_SECRET_SENTINEL";
1038
1039 let mut resolver = CertificateResolver::default();
1040 resolver
1041 .add_certificate(&AddCertificate {
1042 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1043 certificate: CertificateAndKey {
1044 certificate: include_str!("../assets/certificate.pem").to_owned(),
1045 key: include_str!("../assets/key.pem").to_owned(),
1046 names: vec![format!("{NAME_SECRET}{}", "x".repeat(2048))],
1051 ..Default::default()
1052 },
1053 expired_at: None,
1054 })
1055 .expect("test certificate must load into the resolver");
1056 let output = format!("{resolver:?}");
1057
1058 assert!(
1059 !output.contains(NAME_SECRET),
1060 "CertificateResolver Debug leaked an indexed certificate name: {output}"
1061 );
1062 assert!(
1063 output.len() <= 1024,
1064 "CertificateResolver Debug output is not bounded: {} bytes",
1065 output.len()
1066 );
1067 }
1068
1069 #[test]
1070 fn lifecycle() -> Result<(), Box<dyn Error + Send + Sync>> {
1071 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1072 let mut resolver = CertificateResolver::default();
1073 let certificate_and_key = CertificateAndKey {
1074 certificate: String::from(include_str!("../assets/certificate.pem")),
1075 key: String::from(include_str!("../assets/key.pem")),
1076 ..Default::default()
1077 };
1078
1079 let fingerprint = resolver
1080 .add_certificate(&AddCertificate {
1081 address,
1082 certificate: certificate_and_key,
1083 expired_at: None,
1084 })
1085 .expect("could not add certificate");
1086
1087 if resolver.get_certificate(&fingerprint).is_none() {
1088 return Err("failed to retrieve certificate".into());
1089 }
1090
1091 let names = resolver.certificate_names(&fingerprint)?;
1093
1094 if let Err(err) = resolver.remove_certificate(&fingerprint) {
1095 return Err(format!("the certificate was not removed, {err}").into());
1096 }
1097
1098 if resolver.get_certificate(&fingerprint).is_some() {
1099 return Err("We have retrieved the certificate that should be deleted".into());
1100 }
1101
1102 if !resolver.find_certificates_by_names(&names)?.is_empty() {
1103 return Err(
1104 "The certificate should be deleted but one of its names is in the index".into(),
1105 );
1106 }
1107
1108 Ok(())
1109 }
1110
1111 #[test]
1112 fn name_override() -> Result<(), Box<dyn Error + Send + Sync>> {
1113 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1114 let mut resolver = CertificateResolver::default();
1115 let certificate_and_key = CertificateAndKey {
1116 certificate: String::from(include_str!("../assets/certificate.pem")),
1117 key: String::from(include_str!("../assets/key.pem")),
1118 names: vec!["localhost".into(), "lolcatho.st".into()],
1119 ..Default::default()
1120 };
1121
1122 let fingerprint = resolver.add_certificate(&AddCertificate {
1123 address,
1124 certificate: certificate_and_key,
1125 expired_at: None,
1126 })?;
1127
1128 if resolver.get_certificate(&fingerprint).is_none() {
1129 return Err("failed to retrieve certificate".into());
1130 }
1131
1132 let mut lolcat = HashSet::new();
1133 lolcat.insert(String::from("lolcatho.st"));
1134 if resolver.find_certificates_by_names(&lolcat)?.is_empty()
1135 || resolver.get_certificate(&fingerprint).is_none()
1136 {
1137 return Err("failed to retrieve certificate with custom names".into());
1138 }
1139
1140 if let Err(err) = resolver.remove_certificate(&fingerprint) {
1141 return Err(format!("the certificate could not be removed, {err}").into());
1142 }
1143
1144 let names = resolver.certificate_names(&fingerprint)?;
1145 if !resolver.find_certificates_by_names(&names)?.is_empty()
1146 && resolver.get_certificate(&fingerprint).is_some()
1147 {
1148 return Err("We have retrieved the certificate that should be deleted".into());
1149 }
1150
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn keep_resolving_with_wildcard() -> Result<(), Box<dyn Error + Send + Sync>> {
1156 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1157 let mut resolver = CertificateResolver::default();
1158
1159 let wildcard_example_org = CertificateAndKey {
1162 certificate: String::from(include_str!("../assets/tests/certificate-3.pem")),
1163 key: String::from(include_str!("../assets/tests/key.pem")),
1164 ..Default::default()
1165 };
1166
1167 let wildcard_example_org_fingerprint = resolver.add_certificate(&AddCertificate {
1168 address,
1169 certificate: wildcard_example_org,
1170 expired_at: Some(
1171 (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
1172 + Duration::from_secs(365 * 24 * 3600))
1173 .as_secs() as i64,
1174 ),
1175 })?;
1176
1177 if resolver
1178 .get_certificate(&wildcard_example_org_fingerprint)
1179 .is_none()
1180 {
1181 return Err("could not load the 2-year-valid certificate".into());
1182 }
1183
1184 let www_example_org = CertificateAndKey {
1188 certificate: String::from(include_str!("../assets/tests/certificate-2.pem")),
1189 key: String::from(include_str!("../assets/tests/key.pem")),
1190 ..Default::default()
1191 };
1192
1193 let www_example_org_fingerprint = resolver.add_certificate(&AddCertificate {
1194 address,
1195 certificate: www_example_org,
1196 expired_at: Some(
1197 (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
1198 + Duration::from_secs(2 * 365 * 24 * 3600))
1199 .as_secs() as i64,
1200 ),
1201 })?;
1202
1203 let www_example_org = resolver
1204 .domain_lookup("www.example.org".as_bytes(), true)
1205 .expect("there should be a www.example.org cert");
1206 assert_eq!(www_example_org.1, www_example_org_fingerprint);
1207
1208 let test_example_org = resolver
1209 .domain_lookup("test.example.org".as_bytes(), true)
1210 .expect("there should be a test.example.org cert");
1211 assert_eq!(test_example_org.1, wildcard_example_org_fingerprint);
1212
1213 let example_org = resolver
1214 .domain_lookup("example.org".as_bytes(), true)
1215 .expect("there should be a example.org cert");
1216 assert_eq!(example_org.1, www_example_org_fingerprint);
1217
1218 resolver
1221 .remove_certificate(&www_example_org_fingerprint)
1222 .expect("should be able to remove the 2-year certificate");
1223
1224 let should_be_wildcard_fingerprint = resolver
1225 .domain_lookup("www.example.org".as_bytes(), true)
1226 .expect("there should be a www.example.org cert");
1227 assert_eq!(
1228 should_be_wildcard_fingerprint.1,
1229 wildcard_example_org_fingerprint
1230 );
1231
1232 assert!(
1233 resolver
1234 .domain_lookup("example.org".as_bytes(), true)
1235 .is_none()
1236 );
1237
1238 Ok(())
1239 }
1240
1241 #[test]
1242 fn resolve_the_longer_lived_cert() -> Result<(), Box<dyn Error + Send + Sync>> {
1243 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1244 let mut resolver = CertificateResolver::default();
1245
1246 let certificate_and_key_2y = CertificateAndKey {
1249 certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1250 key: String::from(include_str!("../assets/tests/key-2y.pem")),
1251 ..Default::default()
1252 };
1253
1254 let fingerprint_2y = resolver.add_certificate(&AddCertificate {
1255 address,
1256 certificate: certificate_and_key_2y,
1257 expired_at: None,
1258 })?;
1259
1260 if resolver.get_certificate(&fingerprint_2y).is_none() {
1261 return Err("could not load the 2-year-valid certificate".into());
1262 }
1263
1264 let certificate_and_key_1y = CertificateAndKey {
1267 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1268 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1269 ..Default::default()
1270 };
1271
1272 let fingerprint_1y = resolver.add_certificate(&AddCertificate {
1273 address,
1274 certificate: certificate_and_key_1y,
1275 ..Default::default()
1276 })?;
1277
1278 let localhost_cert = resolver
1279 .domain_lookup("localhost".as_bytes(), true)
1280 .expect("there should be a localhost cert");
1281
1282 assert_eq!(localhost_cert.1, fingerprint_2y);
1283
1284 resolver
1288 .remove_certificate(&fingerprint_2y)
1289 .expect("should be able to remove the 2-year certificate");
1290
1291 let localhost_cert = resolver
1292 .domain_lookup("localhost".as_bytes(), true)
1293 .expect("there should be a localhost cert");
1294
1295 assert_eq!(localhost_cert.1, fingerprint_1y);
1296
1297 Ok(())
1298 }
1299
1300 #[test]
1301 fn expiration_override() -> Result<(), Box<dyn Error + Send + Sync>> {
1302 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1303 let mut resolver = CertificateResolver::default();
1304
1305 let certificate_and_key_1y = CertificateAndKey {
1308 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1309 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1310 ..Default::default()
1311 };
1312
1313 let fingerprint_1y_overriden = resolver.add_certificate(&AddCertificate {
1314 address,
1315 certificate: certificate_and_key_1y,
1316 expired_at: Some(
1317 (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
1318 + Duration::from_secs(3 * 365 * 24 * 3600))
1319 .as_secs() as i64,
1320 ),
1321 })?;
1322
1323 if resolver
1324 .get_certificate(&fingerprint_1y_overriden)
1325 .is_none()
1326 {
1327 return Err("failed to retrieve certificate".into());
1328 }
1329
1330 let certificate_and_key_2y = CertificateAndKey {
1333 certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1334 key: String::from(include_str!("../assets/tests/key-2y.pem")),
1335 ..Default::default()
1336 };
1337
1338 let fingerprint_2y = resolver.add_certificate(&AddCertificate {
1339 address,
1340 certificate: certificate_and_key_2y,
1341 expired_at: None,
1342 })?;
1343
1344 let localhost_cert = resolver
1345 .domain_lookup("localhost".as_bytes(), true)
1346 .expect("there should be a localhost cert");
1347
1348 assert_eq!(localhost_cert.1, fingerprint_1y_overriden);
1349
1350 resolver
1354 .remove_certificate(&fingerprint_1y_overriden)
1355 .expect("should be able to remove the 1-year (3-year-overriden) certificate");
1356
1357 let localhost_cert = resolver
1358 .domain_lookup("localhost".as_bytes(), true)
1359 .expect("there should be a localhost cert");
1360
1361 assert_eq!(localhost_cert.1, fingerprint_2y);
1362
1363 Ok(())
1364 }
1365
1366 #[test]
1369 fn replace_certificate_add_before_remove() -> Result<(), Box<dyn Error + Send + Sync>> {
1370 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1371 let mut resolver = CertificateResolver::default();
1372
1373 let cert_1y = CertificateAndKey {
1375 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1376 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1377 ..Default::default()
1378 };
1379
1380 let fingerprint_1y = resolver.add_certificate(&AddCertificate {
1381 address,
1382 certificate: cert_1y,
1383 expired_at: None,
1384 })?;
1385
1386 assert!(
1388 resolver
1389 .domain_lookup("localhost".as_bytes(), true)
1390 .is_some(),
1391 "initial certificate should be resolvable"
1392 );
1393
1394 let cert_2y = CertificateAndKey {
1396 certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1397 key: String::from(include_str!("../assets/tests/key-2y.pem")),
1398 ..Default::default()
1399 };
1400
1401 let new_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1402 address,
1403 new_certificate: cert_2y,
1404 old_fingerprint: fingerprint_1y.to_string(),
1405 new_expired_at: None,
1406 })?;
1407
1408 assert!(
1410 resolver.get_certificate(&fingerprint_1y).is_none(),
1411 "old certificate should have been removed"
1412 );
1413
1414 assert!(
1416 resolver.get_certificate(&new_fingerprint).is_some(),
1417 "new certificate should be present"
1418 );
1419 let resolved = resolver
1420 .domain_lookup("localhost".as_bytes(), true)
1421 .expect("a certificate should resolve for localhost");
1422 assert_eq!(
1423 resolved.1, new_fingerprint,
1424 "resolved certificate should be the replacement"
1425 );
1426
1427 Ok(())
1428 }
1429
1430 #[test]
1439 fn replace_certificate_with_same_fingerprint_is_noop()
1440 -> Result<(), Box<dyn Error + Send + Sync>> {
1441 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1442 let mut resolver = CertificateResolver::default();
1443
1444 let cert = CertificateAndKey {
1445 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1446 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1447 ..Default::default()
1448 };
1449
1450 let initial_fingerprint = resolver.add_certificate(&AddCertificate {
1451 address,
1452 certificate: cert.clone(),
1453 expired_at: None,
1454 })?;
1455
1456 let returned_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1458 address,
1459 new_certificate: cert,
1460 old_fingerprint: initial_fingerprint.to_string(),
1461 new_expired_at: None,
1462 })?;
1463
1464 assert_eq!(
1465 returned_fingerprint, initial_fingerprint,
1466 "idempotent replace should return the existing fingerprint"
1467 );
1468
1469 assert!(
1470 resolver.get_certificate(&initial_fingerprint).is_some(),
1471 "idempotent replace must NOT delete the existing certificate"
1472 );
1473
1474 let resolved = resolver
1475 .domain_lookup("localhost".as_bytes(), true)
1476 .expect("certificate should still resolve after idempotent replace");
1477 assert_eq!(
1478 resolved.1, initial_fingerprint,
1479 "resolver should still hand back the original fingerprint"
1480 );
1481
1482 Ok(())
1483 }
1484
1485 #[test]
1488 fn removal_cleans_up_empty_index_entries() -> Result<(), Box<dyn Error + Send + Sync>> {
1489 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1490 let mut resolver = CertificateResolver::default();
1491
1492 let cert = CertificateAndKey {
1493 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1494 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1495 ..Default::default()
1496 };
1497
1498 let fingerprint = resolver.add_certificate(&AddCertificate {
1499 address,
1500 certificate: cert,
1501 expired_at: None,
1502 })?;
1503
1504 let names = resolver.certificate_names(&fingerprint)?;
1506 assert!(
1507 !names.is_empty(),
1508 "certificate should have at least one name"
1509 );
1510
1511 for name in &names {
1513 assert!(
1514 resolver.name_fingerprint_idx.contains_key(name),
1515 "name_fingerprint_idx should contain '{name}' before removal"
1516 );
1517 }
1518
1519 resolver.remove_certificate(&fingerprint)?;
1520
1521 for name in &names {
1523 assert!(
1524 !resolver.name_fingerprint_idx.contains_key(name),
1525 "name_fingerprint_idx should not contain empty entry for '{name}' after removal"
1526 );
1527 }
1528
1529 Ok(())
1530 }
1531
1532 #[test]
1546 fn certificate_chain_dedup_drops_duplicate_leaf() -> Result<(), Box<dyn Error + Send + Sync>> {
1547 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1548 let mut resolver = CertificateResolver::default();
1549
1550 let leaf_pem = String::from(include_str!("../assets/certificate.pem"));
1551
1552 let cert_with_duplicated_leaf = CertificateAndKey {
1553 certificate: leaf_pem.clone(),
1554 certificate_chain: vec![leaf_pem],
1555 key: String::from(include_str!("../assets/key.pem")),
1556 ..Default::default()
1557 };
1558
1559 let fingerprint = resolver.add_certificate(&AddCertificate {
1560 address,
1561 certificate: cert_with_duplicated_leaf,
1562 expired_at: None,
1563 })?;
1564
1565 let stored = resolver
1566 .get_certificate(&fingerprint)
1567 .ok_or("resolver lost the certificate after add")?;
1568
1569 assert_eq!(
1570 stored.inner.cert.len(),
1571 1,
1572 "expected dedup to drop the duplicate leaf, got chain of {} cert(s)",
1573 stored.inner.cert.len()
1574 );
1575
1576 Ok(())
1577 }
1578
1579 #[test]
1593 fn certificate_chain_handles_multi_pem_single_entry() -> Result<(), Box<dyn Error + Send + Sync>>
1594 {
1595 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1596 let mut resolver = CertificateResolver::default();
1597
1598 let leaf_pem = String::from(include_str!("../assets/certificate.pem"));
1599 let multi_pem_chain_entry = format!("{leaf_pem}\n{leaf_pem}");
1602
1603 let cert = CertificateAndKey {
1604 certificate: leaf_pem,
1605 certificate_chain: vec![multi_pem_chain_entry],
1606 key: String::from(include_str!("../assets/key.pem")),
1607 ..Default::default()
1608 };
1609
1610 let fingerprint = resolver.add_certificate(&AddCertificate {
1611 address,
1612 certificate: cert,
1613 expired_at: None,
1614 })?;
1615
1616 let stored = resolver
1617 .get_certificate(&fingerprint)
1618 .ok_or("resolver lost the certificate after add")?;
1619
1620 assert_eq!(
1625 stored.inner.cert.len(),
1626 1,
1627 "expected split + dedup to leave only the leaf, got chain of {} cert(s)",
1628 stored.inner.cert.len()
1629 );
1630
1631 Ok(())
1632 }
1633}