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::pattern_trie::{Key, KeyValue, TrieNode};
34
35macro_rules! log_module_context {
44 () => {{
45 let (open, reset, _, _, _) = ansi_palette();
46 format!(
47 "{open}TLS-RESOLVER{reset}\t >>>",
48 open = open,
49 reset = reset
50 )
51 }};
52}
53
54static DEFAULT_CERTIFICATE: LazyLock<Option<Arc<CertifiedKey>>> = LazyLock::new(|| {
58 let add = AddCertificate {
59 certificate: CertificateAndKey {
60 certificate: include_str!("../assets/certificate.pem").to_string(),
61 certificate_chain: vec![include_str!("../assets/certificate_chain.pem").to_string()],
62 key: include_str!("../assets/key.pem").to_string(),
63 versions: vec![],
64 names: vec![],
65 },
66 address: SocketAddress::new_v4(0, 0, 0, 0, 8080), expired_at: None,
68 };
69 CertifiedKeyWrapper::try_from(&add).ok().map(|c| c.inner)
70});
71
72#[derive(thiserror::Error, Debug)]
73pub enum CertificateResolverError {
74 #[error("failed to get common name and subject alternate names from pem, {0}")]
75 InvalidCommonNameAndSubjectAlternateNames(CertificateError),
76 #[error("invalid private key: {0}")]
77 InvalidPrivateKey(String),
78 #[error("empty key")]
79 EmptyKeys,
80 #[error("error parsing x509 cert from bytes: {0}")]
81 ParseX509(CertificateError),
82 #[error("error parsing pem formated certificate from bytes: {0}")]
83 ParsePem(CertificateError),
84 #[error("error parsing overriding names in new certificate: {0}")]
85 ParseOverridingNames(CertificateError),
86}
87
88#[derive(Clone, Debug)]
92pub struct CertifiedKeyWrapper {
93 inner: Arc<CertifiedKey>,
94 names: Vec<String>,
96 expiration: i64,
97 fingerprint: Fingerprint,
98}
99
100impl TryFrom<&AddCertificate> for CertifiedKeyWrapper {
103 type Error = CertificateResolverError;
104
105 fn try_from(add: &AddCertificate) -> Result<Self, Self::Error> {
106 let cert = add.certificate.clone();
107
108 let pem =
109 parse_pem(cert.certificate.as_bytes()).map_err(CertificateResolverError::ParsePem)?;
110
111 let x509 = parse_x509(&pem.contents).map_err(CertificateResolverError::ParseX509)?;
112
113 let overriding_names = if add.certificate.names.is_empty() {
114 get_cn_and_san_attributes(&x509)
115 } else {
116 add.certificate.names.clone()
117 };
118
119 let expiration = add
120 .expired_at
121 .unwrap_or(x509.validity().not_after.timestamp());
122
123 let fingerprint = Fingerprint(Sha256::digest(&pem.contents).iter().cloned().collect());
124
125 let leaf_der = pem.contents;
135 let mut chain = vec![CertificateDer::from(leaf_der.to_owned())];
136 let mut dropped_duplicates = 0usize;
137 for cert in &cert.certificate_chain {
138 for split_pem in split_certificate_chain(cert.to_owned()) {
139 let chain_link = parse_pem(split_pem.as_bytes())
140 .map_err(CertificateResolverError::ParsePem)?
141 .contents;
142
143 if chain_link == leaf_der {
144 dropped_duplicates += 1;
145 continue;
146 }
147 chain.push(CertificateDer::from(chain_link));
148 }
149 }
150 if dropped_duplicates > 0 {
151 debug!(
152 "{} dropped {} duplicate leaf certificate(s) from the supplied chain",
153 log_module_context!(),
154 dropped_duplicates
155 );
156 }
157
158 let private_key = PrivateKeyDer::from_pem_slice(cert.key.as_bytes())
167 .map_err(|_| CertificateResolverError::EmptyKeys)?;
168
169 debug_assert!(
174 !chain.is_empty(),
175 "assembled certificate chain must contain at least the leaf"
176 );
177 debug_assert_eq!(
178 chain[0].as_ref(),
179 leaf_der.as_slice(),
180 "the leaf must remain at index 0 of the chain"
181 );
182 debug_assert_eq!(
185 fingerprint.0.len(),
186 32,
187 "a SHA-256 fingerprint must be 32 bytes"
188 );
189
190 match any_supported_type(&private_key) {
191 Ok(signing_key) => {
192 let stored_certificate = CertifiedKeyWrapper {
193 inner: Arc::new(CertifiedKey::new(chain, signing_key)),
194 names: overriding_names,
195 expiration,
196 fingerprint,
197 };
198 Ok(stored_certificate)
199 }
200 Err(sign_error) => Err(CertificateResolverError::InvalidPrivateKey(
201 sign_error.to_string(),
202 )),
203 }
204 }
205}
206
207#[derive(Default, Debug)]
214pub struct CertificateResolver {
215 pub domains: TrieNode<Fingerprint>,
217 certificates: HashMap<Fingerprint, CertifiedKeyWrapper>,
219 name_fingerprint_idx: HashMap<String, Vec<(Fingerprint, i64)>>,
223}
224
225impl CertificateResolver {
226 pub fn get_certificate(&self, fingerprint: &Fingerprint) -> Option<CertifiedKeyWrapper> {
228 self.certificates.get(fingerprint).map(ToOwned::to_owned)
229 }
230
231 fn publish_min_expiration_gauge(&self) {
245 let Some(min_expiration) = self.certificates.values().map(|c| c.expiration).min() else {
246 return;
254 };
255 let clamped = min_expiration.max(0) as usize;
256 gauge!(names::tls::CERT_MIN_EXPIRES_AT_SECONDS, clamped);
257 }
258
259 pub fn add_certificate(
262 &mut self,
263 add: &AddCertificate,
264 ) -> Result<Fingerprint, CertificateResolverError> {
265 let cert_to_add = CertifiedKeyWrapper::try_from(add)?;
266
267 trace!(
268 "{} adding certificate {:?}",
269 log_module_context!(),
270 cert_to_add
271 );
272
273 if self.certificates.contains_key(&cert_to_add.fingerprint) {
274 return Ok(cert_to_add.fingerprint);
275 }
276
277 let certificates_before = self.certificates.len();
282 let new_fingerprint = cert_to_add.fingerprint.clone();
283 debug_assert!(
284 !self.certificates.contains_key(&new_fingerprint),
285 "add_certificate past the dedup guard must be inserting a new fingerprint"
286 );
287
288 for new_name in &cert_to_add.names {
289 let fingerprints_for_this_name = self
290 .name_fingerprint_idx
291 .entry(new_name.to_owned())
292 .or_default();
293
294 fingerprints_for_this_name
295 .push((cert_to_add.fingerprint.clone(), cert_to_add.expiration));
296
297 fingerprints_for_this_name.sort_by_key(|t| t.1);
299
300 let longest_lived_cert = match fingerprints_for_this_name.last() {
301 Some(cert) => cert,
302 None => {
303 error!(
304 "{} no fingerprint for this name, this should not happen",
305 log_module_context!()
306 );
307 continue;
308 }
309 };
310
311 self.domains.remove(&new_name.to_owned().into_bytes());
313 self.domains.insert(
314 new_name.to_owned().into_bytes(),
315 longest_lived_cert.0.to_owned(),
316 );
317 }
318
319 self.certificates
320 .insert(cert_to_add.fingerprint.to_owned(), cert_to_add.clone());
321 self.publish_min_expiration_gauge();
322
323 debug_assert!(
327 self.certificates.contains_key(&new_fingerprint),
328 "add_certificate must store the new certificate"
329 );
330 debug_assert_eq!(
331 self.certificates.len(),
332 certificates_before + 1,
333 "add_certificate must grow the store by exactly one"
334 );
335 debug_assert!(
336 cert_to_add.names.iter().all(|name| {
337 self.name_fingerprint_idx
338 .get(name)
339 .is_some_and(|fps| fps.iter().any(|(fp, _)| *fp == new_fingerprint))
340 }),
341 "every name of the added cert must be indexed to its fingerprint"
342 );
343
344 trace!("{} {:#?}", log_module_context!(), self);
345
346 Ok(cert_to_add.fingerprint)
347 }
348
349 pub fn remove_certificate(
352 &mut self,
353 fingerprint: &Fingerprint,
354 ) -> Result<(), CertificateResolverError> {
355 let certificates_before = self.certificates.len();
360 let was_present = self.certificates.contains_key(fingerprint);
361
362 if let Some(certificate_to_remove) = self.get_certificate(fingerprint) {
363 #[cfg(debug_assertions)]
367 let removed_names = certificate_to_remove.names.clone();
368 for name in certificate_to_remove.names {
369 self.domains.domain_remove(&name.as_bytes().to_vec());
370
371 if let std::collections::hash_map::Entry::Occupied(mut entry) =
372 self.name_fingerprint_idx.entry(name.to_owned())
373 {
374 entry.get_mut().retain(|t| &t.0 != fingerprint);
376
377 if let Some(longest_lived_cert) = entry.get().last() {
379 self.domains
380 .insert(name.as_bytes().to_vec(), longest_lived_cert.0.to_owned());
381 }
382
383 if entry.get().is_empty() {
385 entry.remove();
386 }
387 }
388 }
389
390 self.certificates.remove(fingerprint);
391 self.publish_min_expiration_gauge();
392
393 debug_assert!(
398 !self.certificates.contains_key(fingerprint),
399 "remove_certificate must evict the fingerprint"
400 );
401 debug_assert_eq!(
402 self.certificates.len(),
403 certificates_before - 1,
404 "removing a present cert must shrink the store by exactly one"
405 );
406 #[cfg(debug_assertions)]
407 debug_assert!(
408 removed_names.iter().all(|name| {
409 self.name_fingerprint_idx
410 .get(name)
411 .is_none_or(|fps| fps.iter().all(|(fp, _)| fp != fingerprint))
412 }),
413 "no name may still index the removed fingerprint"
414 );
415 } else {
416 debug_assert!(
418 !was_present,
419 "the absent-cert branch must only run when the fingerprint was not stored"
420 );
421 debug_assert_eq!(
422 self.certificates.len(),
423 certificates_before,
424 "removing an absent cert must not change the store"
425 );
426 }
427 trace!("{} {:#?}", log_module_context!(), self);
428
429 Ok(())
430 }
431
432 pub fn replace_certificate(
436 &mut self,
437 replace: &ReplaceCertificate,
438 ) -> Result<Fingerprint, CertificateResolverError> {
439 let add = AddCertificate {
440 address: replace.address.to_owned(),
441 certificate: replace.new_certificate.to_owned(),
442 expired_at: replace.new_expired_at.to_owned(),
443 };
444
445 let new_cert = CertifiedKeyWrapper::try_from(&add)?;
460 let new_fingerprint = new_cert.fingerprint.to_owned();
461
462 if let Ok(old_fingerprint) = Fingerprint::from_str(&replace.old_fingerprint) {
463 if old_fingerprint == new_fingerprint {
464 let stored_before = self.certificates.contains_key(&new_fingerprint);
469 self.publish_min_expiration_gauge();
472 debug_assert_eq!(
473 self.certificates.contains_key(&new_fingerprint),
474 stored_before,
475 "idempotent replace must not change whether the cert is stored"
476 );
477 return Ok(new_fingerprint);
478 }
479 }
480
481 let new_fingerprint = self.add_certificate(&add)?;
482
483 debug_assert!(
488 self.certificates.contains_key(&new_fingerprint),
489 "the replacement certificate must be stored before the old one is removed"
490 );
491
492 match Fingerprint::from_str(&replace.old_fingerprint) {
493 Ok(old_fingerprint) => self.remove_certificate(&old_fingerprint)?,
494 Err(err) => {
495 warn!(
500 "{} new certificate added but could not remove old one: \
501 failed to parse old fingerprint, {}",
502 log_module_context!(),
503 err
504 );
505 }
506 }
507
508 Ok(new_fingerprint)
509 }
510
511 #[cfg(test)]
514 fn find_certificates_by_names(
515 &self,
516 names: &HashSet<String>,
517 ) -> Result<HashSet<Fingerprint>, CertificateResolverError> {
518 let mut fingerprints = HashSet::new();
519 for name in names {
520 if let Some(fprints) = self.name_fingerprint_idx.get(name) {
521 fprints.iter().for_each(|fingerprint| {
522 fingerprints.insert(fingerprint.to_owned().0);
523 });
524 }
525 }
526
527 Ok(fingerprints)
528 }
529
530 #[cfg(test)]
533 fn certificate_names(
534 &self,
535 fingerprint: &Fingerprint,
536 ) -> Result<HashSet<String>, CertificateResolverError> {
537 if let Some(cert) = self.certificates.get(fingerprint) {
538 return Ok(cert.names.iter().cloned().collect());
539 }
540 Ok(HashSet::new())
541 }
542
543 pub fn domain_lookup(
544 &self,
545 domain: &[u8],
546 accept_wildcard: bool,
547 ) -> Option<&KeyValue<Key, Fingerprint>> {
548 self.domains.domain_lookup(domain, accept_wildcard)
549 }
550
551 pub fn names_for_sni(&self, domain: &[u8]) -> Option<Vec<String>> {
563 let (_, fingerprint) = self.domain_lookup(domain, true)?;
564 self.certificates
565 .get(fingerprint)
566 .map(|cert| cert.names.clone())
567 }
568}
569
570#[derive(Default)]
574pub struct MutexCertificateResolver(pub Mutex<CertificateResolver>);
575
576impl ResolvesServerCert for MutexCertificateResolver {
577 fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
578 let server_name = client_hello.server_name();
579 let sigschemes = client_hello.signature_schemes();
580
581 let Some(name) = server_name else {
582 error!(
583 "{} cannot look up certificate: no SNI from session",
584 log_module_context!()
585 );
586 return None;
587 };
588 trace!(
589 "{} trying to resolve name: {:?} for signature scheme: {:?}",
590 log_module_context!(),
591 name,
592 sigschemes
593 );
594 let resolver = match self.0.lock() {
604 Ok(guard) => guard,
605 Err(poisoned) => {
606 error!(
607 "{} cert resolver mutex poisoned, returning default cert: {:?}",
608 log_module_context!(),
609 poisoned
610 );
611 return DEFAULT_CERTIFICATE.clone();
612 }
613 };
614 if let Some((_, fingerprint)) = resolver.domains.domain_lookup(name.as_bytes(), true) {
615 trace!(
616 "{} looking for certificate for {:?} with fingerprint {:?}",
617 log_module_context!(),
618 name,
619 fingerprint
620 );
621
622 let cert = resolver
631 .certificates
632 .get(fingerprint)
633 .map(|cert| cert.inner.clone());
634 debug_assert!(
635 cert.is_none()
636 || resolver
637 .certificates
638 .get(fingerprint)
639 .is_some_and(|stored| Arc::ptr_eq(&stored.inner, cert.as_ref().unwrap())),
640 "resolved certificate must be the one stored under the looked-up fingerprint"
641 );
642
643 trace!(
644 "{} found for fingerprint {}: {}",
645 log_module_context!(),
646 fingerprint,
647 cert.is_some()
648 );
649 return cert;
650 }
651 drop(resolver);
652
653 debug!(
657 "{} default certificate is used for {}",
658 log_module_context!(),
659 name
660 );
661 incr!(names::tls::DEFAULT_CERT_USED);
662 DEFAULT_CERTIFICATE.clone()
663 }
664}
665
666impl MutexCertificateResolver {
667 pub fn names_for_sni(&self, domain: &[u8]) -> Option<Vec<String>> {
673 match self.0.lock() {
674 Ok(guard) => guard.names_for_sni(domain),
675 Err(poisoned) => {
676 error!(
677 "{} cert resolver mutex poisoned, treating as no SAN match: {:?}",
678 log_module_context!(),
679 poisoned
680 );
681 None
682 }
683 }
684 }
685}
686
687impl fmt::Debug for MutexCertificateResolver {
688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689 f.write_str("MutexWrappedCertificateResolver")
690 }
691}
692
693#[cfg(test)]
697mod tests {
698 use std::{
699 collections::HashSet,
700 error::Error,
701 time::{Duration, SystemTime},
702 };
703
704 use sozu_command::proto::command::{
706 AddCertificate, CertificateAndKey, ReplaceCertificate, SocketAddress,
707 };
708
709 use super::CertificateResolver;
710
711 #[test]
712 fn lifecycle() -> Result<(), Box<dyn Error + Send + Sync>> {
713 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
714 let mut resolver = CertificateResolver::default();
715 let certificate_and_key = CertificateAndKey {
716 certificate: String::from(include_str!("../assets/certificate.pem")),
717 key: String::from(include_str!("../assets/key.pem")),
718 ..Default::default()
719 };
720
721 let fingerprint = resolver
722 .add_certificate(&AddCertificate {
723 address,
724 certificate: certificate_and_key,
725 expired_at: None,
726 })
727 .expect("could not add certificate");
728
729 if resolver.get_certificate(&fingerprint).is_none() {
730 return Err("failed to retrieve certificate".into());
731 }
732
733 let names = resolver.certificate_names(&fingerprint)?;
735
736 if let Err(err) = resolver.remove_certificate(&fingerprint) {
737 return Err(format!("the certificate was not removed, {err}").into());
738 }
739
740 if resolver.get_certificate(&fingerprint).is_some() {
741 return Err("We have retrieved the certificate that should be deleted".into());
742 }
743
744 if !resolver.find_certificates_by_names(&names)?.is_empty() {
745 return Err(
746 "The certificate should be deleted but one of its names is in the index".into(),
747 );
748 }
749
750 Ok(())
751 }
752
753 #[test]
754 fn name_override() -> Result<(), Box<dyn Error + Send + Sync>> {
755 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
756 let mut resolver = CertificateResolver::default();
757 let certificate_and_key = CertificateAndKey {
758 certificate: String::from(include_str!("../assets/certificate.pem")),
759 key: String::from(include_str!("../assets/key.pem")),
760 names: vec!["localhost".into(), "lolcatho.st".into()],
761 ..Default::default()
762 };
763
764 let fingerprint = resolver.add_certificate(&AddCertificate {
765 address,
766 certificate: certificate_and_key,
767 expired_at: None,
768 })?;
769
770 if resolver.get_certificate(&fingerprint).is_none() {
771 return Err("failed to retrieve certificate".into());
772 }
773
774 let mut lolcat = HashSet::new();
775 lolcat.insert(String::from("lolcatho.st"));
776 if resolver.find_certificates_by_names(&lolcat)?.is_empty()
777 || resolver.get_certificate(&fingerprint).is_none()
778 {
779 return Err("failed to retrieve certificate with custom names".into());
780 }
781
782 if let Err(err) = resolver.remove_certificate(&fingerprint) {
783 return Err(format!("the certificate could not be removed, {err}").into());
784 }
785
786 let names = resolver.certificate_names(&fingerprint)?;
787 if !resolver.find_certificates_by_names(&names)?.is_empty()
788 && resolver.get_certificate(&fingerprint).is_some()
789 {
790 return Err("We have retrieved the certificate that should be deleted".into());
791 }
792
793 Ok(())
794 }
795
796 #[test]
797 fn keep_resolving_with_wildcard() -> Result<(), Box<dyn Error + Send + Sync>> {
798 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
799 let mut resolver = CertificateResolver::default();
800
801 let wildcard_example_org = CertificateAndKey {
804 certificate: String::from(include_str!("../assets/tests/certificate-3.pem")),
805 key: String::from(include_str!("../assets/tests/key.pem")),
806 ..Default::default()
807 };
808
809 let wildcard_example_org_fingerprint = resolver.add_certificate(&AddCertificate {
810 address,
811 certificate: wildcard_example_org,
812 expired_at: Some(
813 (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
814 + Duration::from_secs(365 * 24 * 3600))
815 .as_secs() as i64,
816 ),
817 })?;
818
819 if resolver
820 .get_certificate(&wildcard_example_org_fingerprint)
821 .is_none()
822 {
823 return Err("could not load the 2-year-valid certificate".into());
824 }
825
826 let www_example_org = CertificateAndKey {
830 certificate: String::from(include_str!("../assets/tests/certificate-2.pem")),
831 key: String::from(include_str!("../assets/tests/key.pem")),
832 ..Default::default()
833 };
834
835 let www_example_org_fingerprint = resolver.add_certificate(&AddCertificate {
836 address,
837 certificate: www_example_org,
838 expired_at: Some(
839 (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
840 + Duration::from_secs(2 * 365 * 24 * 3600))
841 .as_secs() as i64,
842 ),
843 })?;
844
845 let www_example_org = resolver
846 .domain_lookup("www.example.org".as_bytes(), true)
847 .expect("there should be a www.example.org cert");
848 assert_eq!(www_example_org.1, www_example_org_fingerprint);
849
850 let test_example_org = resolver
851 .domain_lookup("test.example.org".as_bytes(), true)
852 .expect("there should be a test.example.org cert");
853 assert_eq!(test_example_org.1, wildcard_example_org_fingerprint);
854
855 let example_org = resolver
856 .domain_lookup("example.org".as_bytes(), true)
857 .expect("there should be a example.org cert");
858 assert_eq!(example_org.1, www_example_org_fingerprint);
859
860 resolver
863 .remove_certificate(&www_example_org_fingerprint)
864 .expect("should be able to remove the 2-year certificate");
865
866 let should_be_wildcard_fingerprint = resolver
867 .domain_lookup("www.example.org".as_bytes(), true)
868 .expect("there should be a www.example.org cert");
869 assert_eq!(
870 should_be_wildcard_fingerprint.1,
871 wildcard_example_org_fingerprint
872 );
873
874 assert!(
875 resolver
876 .domain_lookup("example.org".as_bytes(), true)
877 .is_none()
878 );
879
880 Ok(())
881 }
882
883 #[test]
884 fn resolve_the_longer_lived_cert() -> Result<(), Box<dyn Error + Send + Sync>> {
885 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
886 let mut resolver = CertificateResolver::default();
887
888 let certificate_and_key_2y = CertificateAndKey {
891 certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
892 key: String::from(include_str!("../assets/tests/key-2y.pem")),
893 ..Default::default()
894 };
895
896 let fingerprint_2y = resolver.add_certificate(&AddCertificate {
897 address,
898 certificate: certificate_and_key_2y,
899 expired_at: None,
900 })?;
901
902 if resolver.get_certificate(&fingerprint_2y).is_none() {
903 return Err("could not load the 2-year-valid certificate".into());
904 }
905
906 let certificate_and_key_1y = CertificateAndKey {
909 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
910 key: String::from(include_str!("../assets/tests/key-1y.pem")),
911 ..Default::default()
912 };
913
914 let fingerprint_1y = resolver.add_certificate(&AddCertificate {
915 address,
916 certificate: certificate_and_key_1y,
917 ..Default::default()
918 })?;
919
920 let localhost_cert = resolver
921 .domain_lookup("localhost".as_bytes(), true)
922 .expect("there should be a localhost cert");
923
924 assert_eq!(localhost_cert.1, fingerprint_2y);
925
926 resolver
930 .remove_certificate(&fingerprint_2y)
931 .expect("should be able to remove the 2-year certificate");
932
933 let localhost_cert = resolver
934 .domain_lookup("localhost".as_bytes(), true)
935 .expect("there should be a localhost cert");
936
937 assert_eq!(localhost_cert.1, fingerprint_1y);
938
939 Ok(())
940 }
941
942 #[test]
943 fn expiration_override() -> Result<(), Box<dyn Error + Send + Sync>> {
944 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
945 let mut resolver = CertificateResolver::default();
946
947 let certificate_and_key_1y = CertificateAndKey {
950 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
951 key: String::from(include_str!("../assets/tests/key-1y.pem")),
952 ..Default::default()
953 };
954
955 let fingerprint_1y_overriden = resolver.add_certificate(&AddCertificate {
956 address,
957 certificate: certificate_and_key_1y,
958 expired_at: Some(
959 (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
960 + Duration::from_secs(3 * 365 * 24 * 3600))
961 .as_secs() as i64,
962 ),
963 })?;
964
965 if resolver
966 .get_certificate(&fingerprint_1y_overriden)
967 .is_none()
968 {
969 return Err("failed to retrieve certificate".into());
970 }
971
972 let certificate_and_key_2y = CertificateAndKey {
975 certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
976 key: String::from(include_str!("../assets/tests/key-2y.pem")),
977 ..Default::default()
978 };
979
980 let fingerprint_2y = resolver.add_certificate(&AddCertificate {
981 address,
982 certificate: certificate_and_key_2y,
983 expired_at: None,
984 })?;
985
986 let localhost_cert = resolver
987 .domain_lookup("localhost".as_bytes(), true)
988 .expect("there should be a localhost cert");
989
990 assert_eq!(localhost_cert.1, fingerprint_1y_overriden);
991
992 resolver
996 .remove_certificate(&fingerprint_1y_overriden)
997 .expect("should be able to remove the 1-year (3-year-overriden) certificate");
998
999 let localhost_cert = resolver
1000 .domain_lookup("localhost".as_bytes(), true)
1001 .expect("there should be a localhost cert");
1002
1003 assert_eq!(localhost_cert.1, fingerprint_2y);
1004
1005 Ok(())
1006 }
1007
1008 #[test]
1011 fn replace_certificate_add_before_remove() -> Result<(), Box<dyn Error + Send + Sync>> {
1012 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1013 let mut resolver = CertificateResolver::default();
1014
1015 let cert_1y = CertificateAndKey {
1017 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1018 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1019 ..Default::default()
1020 };
1021
1022 let fingerprint_1y = resolver.add_certificate(&AddCertificate {
1023 address,
1024 certificate: cert_1y,
1025 expired_at: None,
1026 })?;
1027
1028 assert!(
1030 resolver
1031 .domain_lookup("localhost".as_bytes(), true)
1032 .is_some(),
1033 "initial certificate should be resolvable"
1034 );
1035
1036 let cert_2y = CertificateAndKey {
1038 certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1039 key: String::from(include_str!("../assets/tests/key-2y.pem")),
1040 ..Default::default()
1041 };
1042
1043 let new_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1044 address,
1045 new_certificate: cert_2y,
1046 old_fingerprint: fingerprint_1y.to_string(),
1047 new_expired_at: None,
1048 })?;
1049
1050 assert!(
1052 resolver.get_certificate(&fingerprint_1y).is_none(),
1053 "old certificate should have been removed"
1054 );
1055
1056 assert!(
1058 resolver.get_certificate(&new_fingerprint).is_some(),
1059 "new certificate should be present"
1060 );
1061 let resolved = resolver
1062 .domain_lookup("localhost".as_bytes(), true)
1063 .expect("a certificate should resolve for localhost");
1064 assert_eq!(
1065 resolved.1, new_fingerprint,
1066 "resolved certificate should be the replacement"
1067 );
1068
1069 Ok(())
1070 }
1071
1072 #[test]
1081 fn replace_certificate_with_same_fingerprint_is_noop()
1082 -> Result<(), Box<dyn Error + Send + Sync>> {
1083 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1084 let mut resolver = CertificateResolver::default();
1085
1086 let cert = CertificateAndKey {
1087 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1088 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1089 ..Default::default()
1090 };
1091
1092 let initial_fingerprint = resolver.add_certificate(&AddCertificate {
1093 address,
1094 certificate: cert.clone(),
1095 expired_at: None,
1096 })?;
1097
1098 let returned_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1100 address,
1101 new_certificate: cert,
1102 old_fingerprint: initial_fingerprint.to_string(),
1103 new_expired_at: None,
1104 })?;
1105
1106 assert_eq!(
1107 returned_fingerprint, initial_fingerprint,
1108 "idempotent replace should return the existing fingerprint"
1109 );
1110
1111 assert!(
1112 resolver.get_certificate(&initial_fingerprint).is_some(),
1113 "idempotent replace must NOT delete the existing certificate"
1114 );
1115
1116 let resolved = resolver
1117 .domain_lookup("localhost".as_bytes(), true)
1118 .expect("certificate should still resolve after idempotent replace");
1119 assert_eq!(
1120 resolved.1, initial_fingerprint,
1121 "resolver should still hand back the original fingerprint"
1122 );
1123
1124 Ok(())
1125 }
1126
1127 #[test]
1130 fn removal_cleans_up_empty_index_entries() -> Result<(), Box<dyn Error + Send + Sync>> {
1131 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1132 let mut resolver = CertificateResolver::default();
1133
1134 let cert = CertificateAndKey {
1135 certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1136 key: String::from(include_str!("../assets/tests/key-1y.pem")),
1137 ..Default::default()
1138 };
1139
1140 let fingerprint = resolver.add_certificate(&AddCertificate {
1141 address,
1142 certificate: cert,
1143 expired_at: None,
1144 })?;
1145
1146 let names = resolver.certificate_names(&fingerprint)?;
1148 assert!(
1149 !names.is_empty(),
1150 "certificate should have at least one name"
1151 );
1152
1153 for name in &names {
1155 assert!(
1156 resolver.name_fingerprint_idx.contains_key(name),
1157 "name_fingerprint_idx should contain '{name}' before removal"
1158 );
1159 }
1160
1161 resolver.remove_certificate(&fingerprint)?;
1162
1163 for name in &names {
1165 assert!(
1166 !resolver.name_fingerprint_idx.contains_key(name),
1167 "name_fingerprint_idx should not contain empty entry for '{name}' after removal"
1168 );
1169 }
1170
1171 Ok(())
1172 }
1173
1174 #[test]
1188 fn certificate_chain_dedup_drops_duplicate_leaf() -> Result<(), Box<dyn Error + Send + Sync>> {
1189 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1190 let mut resolver = CertificateResolver::default();
1191
1192 let leaf_pem = String::from(include_str!("../assets/certificate.pem"));
1193
1194 let cert_with_duplicated_leaf = CertificateAndKey {
1195 certificate: leaf_pem.clone(),
1196 certificate_chain: vec![leaf_pem],
1197 key: String::from(include_str!("../assets/key.pem")),
1198 ..Default::default()
1199 };
1200
1201 let fingerprint = resolver.add_certificate(&AddCertificate {
1202 address,
1203 certificate: cert_with_duplicated_leaf,
1204 expired_at: None,
1205 })?;
1206
1207 let stored = resolver
1208 .get_certificate(&fingerprint)
1209 .ok_or("resolver lost the certificate after add")?;
1210
1211 assert_eq!(
1212 stored.inner.cert.len(),
1213 1,
1214 "expected dedup to drop the duplicate leaf, got chain of {} cert(s)",
1215 stored.inner.cert.len()
1216 );
1217
1218 Ok(())
1219 }
1220
1221 #[test]
1235 fn certificate_chain_handles_multi_pem_single_entry() -> Result<(), Box<dyn Error + Send + Sync>>
1236 {
1237 let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1238 let mut resolver = CertificateResolver::default();
1239
1240 let leaf_pem = String::from(include_str!("../assets/certificate.pem"));
1241 let multi_pem_chain_entry = format!("{leaf_pem}\n{leaf_pem}");
1244
1245 let cert = CertificateAndKey {
1246 certificate: leaf_pem,
1247 certificate_chain: vec![multi_pem_chain_entry],
1248 key: String::from(include_str!("../assets/key.pem")),
1249 ..Default::default()
1250 };
1251
1252 let fingerprint = resolver.add_certificate(&AddCertificate {
1253 address,
1254 certificate: cert,
1255 expired_at: None,
1256 })?;
1257
1258 let stored = resolver
1259 .get_certificate(&fingerprint)
1260 .ok_or("resolver lost the certificate after add")?;
1261
1262 assert_eq!(
1267 stored.inner.cert.len(),
1268 1,
1269 "expected split + dedup to leave only the leaf, got chain of {} cert(s)",
1270 stored.inner.cert.len()
1271 );
1272
1273 Ok(())
1274 }
1275}