Skip to main content

sozu_lib/
tls.rs

1//! A unified certificate resolver for rustls.
2//!
3//! Persists certificates in the Rustls
4//! [`CertifiedKey` format](https://docs.rs/rustls/latest/rustls/sign/struct.CertifiedKey.html),
5//! exposes them to the HTTPS listener for TLS handshakes.
6#[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
36/// Module-level prefix used on every log line emitted from this module.
37/// Produces a bold bright-white `TLS-RESOLVER` label (uniform across every
38/// protocol) when the logger is in colored mode. The certificate resolver
39/// runs at listener scope -- it has no per-session state -- so this is the
40/// only macro the module needs. `RUSTLS` covers the protocol-side logs in
41/// `lib/src/protocol/rustls.rs`; `TLS-RESOLVER` is intentionally distinct so
42/// operators can tell handshake failures (RUSTLS) apart from cert-store
43/// management noise (TLS-RESOLVER).
44macro_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
55// -----------------------------------------------------------------------------
56// Default ParsedCertificateAndKey
57
58static 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), // not used anyway
68        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/// A wrapper around the Rustls
92/// [`CertifiedKey` type](https://docs.rs/rustls/latest/rustls/sign/struct.CertifiedKey.html),
93/// stored and returned by the certificate resolver.
94#[derive(Clone)]
95pub struct CertifiedKeyWrapper {
96    inner: Arc<CertifiedKey>,
97    /// domain names, override what can be found in the cert
98    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
123/// Convert an AddCertificate request into the Rustls format.
124/// Support RSA and ECDSA certificates.
125impl 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        // The leaf is at index 0; chain entries follow. ACME clients
149        // emitting `fullchain.pem` (Certbot default, lego, acme.sh)
150        // place the leaf at the start, which would store
151        // `[leaf, leaf, intermediate, root]` and fail strict
152        // validators (Node.js `UNABLE_TO_VERIFY_LEAF_SIGNATURE`).
153        // Each entry is split through `split_certificate_chain` so a
154        // single multi-PEM string fans out (`parse_pem` would
155        // otherwise stop at the first block); each split entry is
156        // dedup'd against the leaf's DER bytes.
157        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        // Parse the PEM-encoded private key into a `PrivateKeyDer` via
182        // `rustls-pki-types`'s `PemObject` trait. `from_pem_slice` accepts
183        // PKCS1 / PKCS8 / SEC1 key formats the same way the old
184        // `rustls-pemfile::read_one` + per-variant `From::from` chain did,
185        // and folds the empty-input / no-PEM-object / unsupported-format
186        // cases into a single `Err` we surface as `EmptyKeys` (the
187        // existing variant covers any failure to extract a key from the
188        // supplied PEM blob).
189        let private_key = PrivateKeyDer::from_pem_slice(cert.key.as_bytes())
190            .map_err(|_| CertificateResolverError::EmptyKeys)?;
191
192        // Postconditions of chain assembly: the leaf was pushed first, so the
193        // chain is never empty and its head is exactly the parsed leaf DER.
194        // Dedup only ever *drops* entries it recognises as the leaf, so the
195        // assembled length can never exceed leaf + supplied links.
196        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        // SHA-256 fingerprint is exactly 32 bytes — anything else means the
206        // digest pipeline changed underneath us.
207        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/// Parses and stores TLS certificates, makes them available to Rustls for TLS handshakes
231///
232/// the `domains` TrieNode is an addressing system to resolve a certificate
233/// for a given domain name.
234/// Certificates are stored in a hashmap that may contain unreachable certificates if
235/// no domain name points to it.
236#[derive(Default)]
237pub struct CertificateResolver {
238    /// routing one domain name to one certificate for fast resolving
239    pub domains: TrieNode<Fingerprint>,
240    /// a storage map: fingerprint -> stored_certificate
241    certificates: HashMap<Fingerprint, CertifiedKeyWrapper>,
242    /// maps each domain name to several compatible certificates, sorted by expiration date
243    /// map of domain_name -> all fingerprints (and expiration) linked to this domain name
244    //  the vector of (fingerprint, expiration) is sorted by expiration
245    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    /// return the certificate in the Rustls-usable form
267    pub fn get_certificate(&self, fingerprint: &Fingerprint) -> Option<CertifiedKeyWrapper> {
268        self.certificates.get(fingerprint).map(ToOwned::to_owned)
269    }
270
271    /// Recompute the aggregate `tls.cert.min_expires_at_seconds` gauge across
272    /// every certificate currently loaded. Per-SNI granularity would explode
273    /// statsd key cardinality (the resolver can easily hold tens of thousands
274    /// of names on a public endpoint) and the existing `gauge!` macro has no
275    /// label support, so we expose a single absolute unix-seconds reading of
276    /// the soonest-expiring cert. Dashboards alert on this as the "next cert
277    /// to rotate" deadline; operators query per-cert detail through the
278    /// command API. `x509` timestamps are signed but `set_gauge` takes a
279    /// `usize`, so we clamp already-expired certs to 0 (which is still a
280    /// monotonic "panic now" signal to any alerting rule).
281    ///
282    /// Called from `add_certificate` / `remove_certificate` — i.e. only when
283    /// the cert set actually changes, never on the hot TLS handshake path.
284    fn publish_min_expiration_gauge(&self) {
285        let Some(min_expiration) = self.certificates.values().map(|c| c.expiration).min() else {
286            // SECURITY: an empty resolver is not "every cert just
287            // expired"; it is "no cert
288            // has been loaded yet" — typical at process boot before the
289            // first AddCertificate request lands. Writing 0 here pages
290            // SOC tooling on every restart with the same alert as a real
291            // expired-cert event. Skip the emit so the gauge reflects the
292            // last known good state instead of being clobbered to 0.
293            return;
294        };
295        let clamped = min_expiration.max(0) as usize;
296        gauge!(names::tls::CERT_MIN_EXPIRES_AT_SECONDS, clamped);
297    }
298
299    /// persist a certificate, after ensuring validity, and checking if it can replace another certificate.
300    /// return the certificate fingerprint regardless of having inserted it or not
301    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        // Reject a certificate whose name the SNI route table cannot host
318        // BEFORE mutating any state, by dry-running every name against a
319        // scratch trie (the grammar reasons are the same as in
320        // `router::Router::add_tree_rule`). `TrieNode::insert` reports
321        // such a name as a graceful `InsertResult::Failed`; discarding
322        // that result below would register the certificate while
323        // `self.domains` never learns the name -- a dead SNI route
324        // failing every handshake with no diagnostic. The length bound
325        // mirrors the router's: the trie recurses once per label.
326        {
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        // Past the duplicate guard the fingerprint is genuinely new, so the
339        // store will grow by exactly one. Snapshot the count to assert that
340        // delta below (ungated `let` — read only inside the debug_assert, so
341        // the optimizer drops it in release while it still compiles).
342        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            // sort expiration ascending (longest-lived to the right)
359            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            // update the longest lived certificate in the TriNode
373            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            // Canary in the same shape as `tcp.rs::insert_sni_route`: the
379            // dry-run above already validated every name, so a `Failed`
380            // here is an internal bug, not a control-plane input error.
381            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        // Postconditions: the new fingerprint is now stored, the store grew by
401        // exactly one (the dedup guard above ruled out an overwrite), and
402        // every name the cert advertises now resolves to it through the index.
403        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    /// Delete a certificate from the resolver. May fail if there is no alternative for
427    // a domain name
428    pub fn remove_certificate(
429        &mut self,
430        fingerprint: &Fingerprint,
431    ) -> Result<(), CertificateResolverError> {
432        // Snapshot the store size so the postcondition can assert that a
433        // present cert drops the count by exactly one and an absent one is a
434        // no-op. Ungated `let`: read only inside the debug_asserts below, so
435        // it compiles in release and the optimizer drops it.
436        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            // Names snapshot used only by the index-cleanup postcondition.
441            // Gate BOTH the `let` and its assert with `#[cfg(debug_assertions)]`
442            // so the clone never runs (and never warns) in release.
443            #[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                    // remove fingerprints from the index for this name
452                    entry.get_mut().retain(|t| &t.0 != fingerprint);
453
454                    // reinsert the longest lived certificate in the TrieNode
455                    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                        // Same canary as in `add_certificate`: this name
460                        // was dry-run validated when its certificate was
461                        // added, so `Failed` cannot be an input error.
462                        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                    // clean up empty index entries to avoid memory leaks
478                    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            // Postconditions on the present-cert path: the fingerprint is
488            // truly gone, the store shrank by exactly one, and no name still
489            // points at the removed fingerprint in the index (a leftover would
490            // let `resolve` hand back a fingerprint with no backing cert).
491            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            // Absent-cert path is a pure no-op: the store size is unchanged.
511            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    /// Add the new certificate first, then remove the old one.
527    /// This ordering ensures that the old certificate remains available
528    /// if adding the new one fails.
529    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        // ── Idempotent-replace short-circuit ──
540        //
541        // Compute the new fingerprint *before* mutating the resolver so we
542        // can compare it with the old one. When `add_certificate` is
543        // called with a fingerprint that already exists it early-returns
544        // (lib/src/tls.rs add_certificate path) without inserting; if we
545        // then unconditionally called `remove_certificate(old)` and old
546        // equalled new, we would delete the entry the caller intended to
547        // *retain*. An idempotent renewal — typical for retry loops, dead
548        // ACME polls, or operator-driven `ReplaceCertificate` requests
549        // that resubmit the same PEM — must therefore short-circuit here.
550        // Any failure to materialise the wrapper (parse, sign-key check)
551        // surfaces as `CertificateResolverError`, identical to the path
552        // through `add_certificate`.
553        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            // Idempotent replace: the new cert is byte-identical to the
560            // one already serving this name. Removing `old == new` would
561            // delete the entry the caller meant to keep, so we must NOT
562            // touch the store — assert it is untouched on this path.
563            let stored_before = self.certificates.contains_key(&new_fingerprint);
564            // Re-publish the expiration gauge so dashboards observe the
565            // replace request even when the certificate set is unchanged.
566            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        // After a non-idempotent add the new certificate is in the store,
578        // ready to serve handshakes before the old one is torn down (the
579        // add-before-remove ordering that keeps the name continuously
580        // resolvable).
581        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                // The new certificate was already added above. If we can't parse the old
590                // fingerprint, the old certificate remains in the resolver (leaked).
591                // We return Ok to indicate the new certificate is available, but warn
592                // that cleanup of the old one failed.
593                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    /// return all fingerprints that are available for these domain names,
606    /// provided at least one name is given
607    #[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    /// return the hashset of subjects that the certificate is able to handle.
625    /// the certificate must be already persisted for this check
626    #[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    /// Resolve the SAN set Sōzu would serve for `domain` (the same trie
646    /// lookup rustls uses, wildcard-aware via `domain_lookup`). Returns the
647    /// certificate's `names` exactly as stored — wildcards retain their
648    /// leading `*.` so the caller can apply RFC 6125 §6.4.3 matching. `None`
649    /// when no cert covers `domain` (rustls would fall back to
650    /// `DEFAULT_CERTIFICATE`).
651    ///
652    /// Mirrors `MutexCertificateResolver::resolve` minus the rustls glue, so
653    /// the SAN snapshot taken at handshake matches the certificate the peer
654    /// actually validated (RFC 7540 §9.1.1 / RFC 9113 §9.1.1 connection
655    /// reuse).
656    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// -----------------------------------------------------------------------------
665// MutexWrappedCertificateResolver struct
666
667#[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        // Every other site uses blocking `lock()`, and silently falling
689        // back to `DEFAULT_CERTIFICATE` on lock
690        // contention is an attacker-detectable mismatch (different
691        // chain → different fingerprint) and a footgun the moment
692        // multi-threading enters the worker. Block here. Lock-poisoning
693        // (panic-while-holding) is mapped to the same default-cert
694        // fallback the previous `try_lock` Err arm produced — preserves
695        // the existing observable behaviour for that one corner case
696        // without inventing a new failure mode.
697        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            // Strict-binding invariant: when the SNI trie resolves a name to a
716            // fingerprint, the served cert is exactly the one stored under
717            // that fingerprint — never a substitute. This guards cert
718            // SELECTION (our own store consistency), not the peer-supplied SNI
719            // itself, so a `debug_assert` is correct here (a violation is a
720            // resolver bug, not hostile traffic). The cert may legitimately be
721            // `None` if the trie still indexes a fingerprint whose cert was
722            // concurrently removed; we only assert the positive case.
723            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        // error!("could not look up a certificate for server name '{}'", name);
747        // This certificate is used for TLS tunneling with another TLS termination endpoint
748        // Note that this is unsafe and you should provide a valid certificate
749        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    /// Snapshot of the SAN set Sōzu would serve for `domain`. Acquires the
761    /// resolver lock once. Returns `None` when the underlying mutex is
762    /// poisoned — the caller is expected to treat poison the same as
763    /// "default cert served" (legacy fallback), mirroring `resolve`'s
764    /// own poison handling at the rustls hot path.
765    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// -----------------------------------------------------------------------------
786// Unit tests
787
788#[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 rand::{seq::SliceRandom, thread_rng};
799    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    /// A certificate carrying a name the SNI route table cannot host must
837    /// be rejected as a whole, BEFORE any resolver state is touched. On
838    /// `main` such a name panicked inside `TrieNode::insert`; once the
839    /// insert reports `InsertResult::Failed` gracefully, silently
840    /// discarding the result would register the certificate while
841    /// `domains` never learns the name -- a dead SNI route failing every
842    /// handshake with no diagnostic on either side.
843    #[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                    // Below `MAX_HOSTNAME_LENGTH` so the name passes the
1047                    // add-time validation and reaches the Debug formatting
1048                    // under test; the redaction property is
1049                    // length-independent.
1050                    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        // get the names to try and retrieve the certificate AFTER it is supposed to be removed
1092        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        // ---------------------------------------------------------------------
1160        // load the wildcard certificate,  expiring in 3 years
1161        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        // ---------------------------------------------------------------------
1185        // try loading the ordinary certificate, expiring in 2 years
1186        // this one has two names: example.org and www.example.org
1187        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        // check that when removing the www.example.org certificate
1219        // the resolver falls back on the wildcard
1220        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        // ---------------------------------------------------------------------
1247        // load the 2-year valid certificate
1248        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        // ---------------------------------------------------------------------
1265        // try loading the 1-year valid certificate
1266        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        // check that when removing the longer-lived certificate,
1285        // the resolver falls back on the shorter-lived one
1286
1287        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        // ---------------------------------------------------------------------
1306        // load first certificate
1307        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        // ---------------------------------------------------------------------
1331        // load second certificate
1332        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        // check that when removing the overriden certificate,
1351        // the resolver falls back on the other one
1352
1353        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    /// Verify that `replace_certificate` adds the new cert before removing
1367    /// the old one, so lookup always returns a valid certificate.
1368    #[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        // add the initial (1-year) certificate
1374        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        // sanity: the 1y cert is resolvable
1387        assert!(
1388            resolver
1389                .domain_lookup("localhost".as_bytes(), true)
1390                .is_some(),
1391            "initial certificate should be resolvable"
1392        );
1393
1394        // replace with the 2-year certificate
1395        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        // the old certificate should be gone
1409        assert!(
1410            resolver.get_certificate(&fingerprint_1y).is_none(),
1411            "old certificate should have been removed"
1412        );
1413
1414        // the new certificate should be present and resolvable
1415        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    /// When `ReplaceCertificate` carries the same certificate / fingerprint
1431    /// as the existing entry, the previous implementation called
1432    /// `add_certificate` (which early-returns on duplicate fingerprint
1433    /// without inserting) and then unconditionally removed the old
1434    /// fingerprint — i.e. the *current* entry — leaving the resolver
1435    /// without a certificate for that name. The fix short-circuits the
1436    /// idempotent case and keeps the existing entry in place. An
1437    /// idempotent ACME / operator retry must therefore still resolve.
1438    #[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        // Replace with the SAME PEM/key — identical fingerprint expected.
1457        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    /// Verify that removing the last certificate for a domain cleans up
1486    /// the empty entry in `name_fingerprint_idx` (no memory leak).
1487    #[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        // record the names associated with this cert
1505        let names = resolver.certificate_names(&fingerprint)?;
1506        assert!(
1507            !names.is_empty(),
1508            "certificate should have at least one name"
1509        );
1510
1511        // verify index is populated
1512        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        // after removal, all index entries for these names should be gone
1522        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    /// Many ACME clients (Certbot's `fullchain.pem`, lego, acme.sh) emit
1533    /// the leaf certificate at the START of the chain file. Without
1534    /// dedup, the resolver previously stored `[leaf, leaf, ...]` and
1535    /// the on-wire TLS handshake replayed the leaf twice — accepted by
1536    /// browsers but rejected by stricter validators (Node.js,
1537    /// `UNABLE_TO_VERIFY_LEAF_SIGNATURE`). The fix in
1538    /// `TryFrom<&AddCertificate> for CertifiedKeyWrapper` drops any
1539    /// chain entry whose DER bytes match the leaf. Closes #1135 / #1148.
1540    ///
1541    /// This test passes the SAME leaf PEM as both `certificate` and the
1542    /// sole `certificate_chain` entry (the `fullchain.pem` shape). The
1543    /// stored chain length must be `1` (leaf only), not `2`
1544    /// (`[leaf, leaf]`).
1545    #[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    /// When an operator passes the entire `fullchain.pem` content as a
1580    /// SINGLE chain entry (one string containing multiple
1581    /// `-----BEGIN CERTIFICATE-----` blocks back-to-back), the previous
1582    /// code called `parse_pem` once on the multi-PEM string, which only
1583    /// consumes the first PEM block and silently drops the rest. The
1584    /// fix splits each chain entry through
1585    /// `split_certificate_chain` so multi-PEM strings fan out into one
1586    /// entry per CA before parsing.
1587    ///
1588    /// This test concatenates two PEM blocks into a single chain entry
1589    /// (the leaf duplicated, so dedup also kicks in). The stored chain
1590    /// must end up with length 1 — the leaf — proving (a) the multi-PEM
1591    /// entry was split correctly, (b) the duplicate leaf was dropped.
1592    #[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        // Two PEM blocks concatenated into one string; both are the
1600        // leaf so dedup brings the result back to length 1.
1601        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        // Without the split, `parse_pem` would only consume the first
1621        // PEM block and drop the second; with the split + dedup, both
1622        // get fanned out, both get recognised as the leaf, both get
1623        // dropped — leaving the original leaf at index 0 only.
1624        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}