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    InconsistentKeys,
17    pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
18    server::{ClientHello, ResolvesServerCert},
19    sign::CertifiedKey,
20};
21
22use crate::crypto::any_supported_type;
23use sha2::{Digest, Sha256};
24use sozu_command::{
25    certificate::{
26        CertificateError, Fingerprint, get_cn_and_san_attributes, parse_pem, parse_x509,
27        split_certificate_chain,
28    },
29    logging::ansi_palette,
30    proto::command::{AddCertificate, CertificateAndKey, ReplaceCertificate, SocketAddress},
31};
32
33use crate::metrics::names;
34use crate::router::MAX_HOSTNAME_LENGTH;
35use crate::router::pattern_trie::{InsertResult, Key, KeyValue, TrieNode};
36
37/// Module-level prefix used on every log line emitted from this module.
38/// Produces a bold bright-white `TLS-RESOLVER` label (uniform across every
39/// protocol) when the logger is in colored mode. The certificate resolver
40/// runs at listener scope -- it has no per-session state -- so this is the
41/// only macro the module needs. `RUSTLS` covers the protocol-side logs in
42/// `lib/src/protocol/rustls.rs`; `TLS-RESOLVER` is intentionally distinct so
43/// operators can tell handshake failures (RUSTLS) apart from cert-store
44/// management noise (TLS-RESOLVER).
45macro_rules! log_module_context {
46    () => {{
47        let (open, reset, _, _, _) = ansi_palette();
48        format!(
49            "{open}TLS-RESOLVER{reset}\t >>>",
50            open = open,
51            reset = reset
52        )
53    }};
54}
55
56// -----------------------------------------------------------------------------
57// Default ParsedCertificateAndKey
58
59static DEFAULT_CERTIFICATE: LazyLock<Option<Arc<CertifiedKey>>> = LazyLock::new(|| {
60    let add = AddCertificate {
61        certificate: CertificateAndKey {
62            certificate: include_str!("../assets/certificate.pem").to_string(),
63            certificate_chain: vec![include_str!("../assets/certificate_chain.pem").to_string()],
64            key: include_str!("../assets/key.pem").to_string(),
65            versions: vec![],
66            names: vec![],
67        },
68        address: SocketAddress::new_v4(0, 0, 0, 0, 8080), // not used anyway
69        expired_at: None,
70    };
71    build_validated_certificate(&add).ok().map(|c| c.inner)
72});
73
74#[derive(thiserror::Error, Debug)]
75pub enum CertificateResolverError {
76    #[error("failed to get common name and subject alternate names from pem, {0}")]
77    InvalidCommonNameAndSubjectAlternateNames(CertificateError),
78    #[error("invalid private key: {0}")]
79    InvalidPrivateKey(String),
80    #[error("empty key")]
81    EmptyKeys,
82    #[error("error parsing x509 cert from bytes: {0}")]
83    ParseX509(CertificateError),
84    #[error("error parsing pem formated certificate from bytes: {0}")]
85    ParsePem(CertificateError),
86    #[error("error parsing overriding names in new certificate: {0}")]
87    ParseOverridingNames(CertificateError),
88    #[error("the SNI route table cannot host a certificate name, name_bytes={}", .0.len())]
89    InvalidName(String),
90}
91
92/// A wrapper around the Rustls
93/// [`CertifiedKey` type](https://docs.rs/rustls/latest/rustls/sign/struct.CertifiedKey.html),
94/// stored and returned by the certificate resolver.
95#[derive(Clone)]
96pub struct CertifiedKeyWrapper {
97    inner: Arc<CertifiedKey>,
98    /// domain names, override what can be found in the cert
99    names: Vec<String>,
100    expiration: i64,
101    fingerprint: Fingerprint,
102    /// Canonical source material identity used to distinguish a true retry
103    /// from a same-leaf replacement that changes chain, key, protocol
104    /// versions, or names. It is deliberately omitted from `Debug`.
105    material_digest: [u8; 32],
106}
107
108impl fmt::Debug for CertifiedKeyWrapper {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let names_len = self
111            .names
112            .iter()
113            .map(String::len)
114            .fold(0usize, usize::saturating_add);
115
116        f.debug_struct("CertifiedKeyWrapper")
117            .field("certificate", &"[redacted]")
118            .field("certificate_chain_count", &self.inner.cert.len())
119            .field("private_key", &"[redacted]")
120            .field("names_count", &self.names.len())
121            .field("names_len", &names_len)
122            .field("expiration", &self.expiration)
123            .field("fingerprint_bytes", &self.fingerprint.0.len())
124            .finish()
125    }
126}
127
128fn update_digest_bytes(hasher: &mut Sha256, bytes: &[u8]) {
129    hasher.update((bytes.len() as u64).to_be_bytes());
130    hasher.update(bytes);
131}
132
133fn certificate_material_digest(
134    certificate: &CertificateAndKey,
135    canonical_names: &[String],
136) -> [u8; 32] {
137    let mut hasher = Sha256::new();
138    hasher.update(b"sozu-certificate-material-v1");
139    update_digest_bytes(&mut hasher, certificate.certificate.as_bytes());
140    hasher.update((certificate.certificate_chain.len() as u64).to_be_bytes());
141    for chain_entry in &certificate.certificate_chain {
142        update_digest_bytes(&mut hasher, chain_entry.as_bytes());
143    }
144    update_digest_bytes(&mut hasher, certificate.key.as_bytes());
145    hasher.update((certificate.versions.len() as u64).to_be_bytes());
146    for version in &certificate.versions {
147        hasher.update(version.to_be_bytes());
148    }
149    hasher.update((canonical_names.len() as u64).to_be_bytes());
150    for name in canonical_names {
151        update_digest_bytes(&mut hasher, name.as_bytes());
152    }
153    hasher.finalize().into()
154}
155
156/// Convert an AddCertificate request into the Rustls format.
157/// Support RSA and ECDSA certificates.
158impl TryFrom<&AddCertificate> for CertifiedKeyWrapper {
159    type Error = CertificateResolverError;
160
161    fn try_from(add: &AddCertificate) -> Result<Self, Self::Error> {
162        let cert = add.certificate.clone();
163
164        let pem =
165            parse_pem(cert.certificate.as_bytes()).map_err(CertificateResolverError::ParsePem)?;
166
167        let x509 = parse_x509(&pem.contents).map_err(CertificateResolverError::ParseX509)?;
168
169        // ASCII-lowercase every name at the single point where the set
170        // is built, so the SNI trie key, the name index, the removal
171        // walk and the SAN snapshot all agree by construction.
172        //
173        // The lookup side is already lowercase and there is nothing we
174        // can do about it: rustls lowercases the SNI inside
175        // `process_client_hello` (`DnsName::to_lowercase_owned`) before
176        // `ResolvesServerCert::resolve` ever sees it, so a name stored
177        // with uppercase — an X.509 SAN carrying it (RFC 5280 does not
178        // forbid it) or an operator `names` override — was reachable by
179        // no SNI at all and every handshake for it fell back to
180        // `DEFAULT_CERTIFICATE`. DNS names compare case-insensitively
181        // (RFC 4343), so normalising loses nothing.
182        let canonical_names = if add.certificate.names.is_empty() {
183            get_cn_and_san_attributes(&x509)
184        } else {
185            add.certificate.names.clone()
186        };
187        let material_digest = certificate_material_digest(&cert, &canonical_names);
188        let overriding_names: Vec<String> = canonical_names
189            .into_iter()
190            .map(|mut name| {
191                name.make_ascii_lowercase();
192                name
193            })
194            .collect();
195
196        let expiration = add
197            .expired_at
198            .unwrap_or(x509.validity().not_after.timestamp());
199
200        let fingerprint = Fingerprint(Sha256::digest(&pem.contents).iter().cloned().collect());
201
202        // The leaf is at index 0; chain entries follow. ACME clients
203        // emitting `fullchain.pem` (Certbot default, lego, acme.sh)
204        // place the leaf at the start, which would store
205        // `[leaf, leaf, intermediate, root]` and fail strict
206        // validators (Node.js `UNABLE_TO_VERIFY_LEAF_SIGNATURE`).
207        // Each entry is split through `split_certificate_chain` so a
208        // single multi-PEM string fans out (`parse_pem` would
209        // otherwise stop at the first block); each split entry is
210        // dedup'd against the leaf's DER bytes.
211        let leaf_der = pem.contents;
212        let mut chain = vec![CertificateDer::from(leaf_der.to_owned())];
213        let mut dropped_duplicates = 0usize;
214        for cert in &cert.certificate_chain {
215            for split_pem in split_certificate_chain(cert.to_owned()) {
216                let chain_link = parse_pem(split_pem.as_bytes())
217                    .map_err(CertificateResolverError::ParsePem)?
218                    .contents;
219
220                if chain_link == leaf_der {
221                    dropped_duplicates += 1;
222                    continue;
223                }
224                chain.push(CertificateDer::from(chain_link));
225            }
226        }
227        if dropped_duplicates > 0 {
228            debug!(
229                "{} dropped {} duplicate leaf certificate(s) from the supplied chain",
230                log_module_context!(),
231                dropped_duplicates
232            );
233        }
234
235        // Parse the PEM-encoded private key into a `PrivateKeyDer` via
236        // `rustls-pki-types`'s `PemObject` trait. `from_pem_slice` accepts
237        // PKCS1 / PKCS8 / SEC1 key formats the same way the old
238        // `rustls-pemfile::read_one` + per-variant `From::from` chain did,
239        // and folds the empty-input / no-PEM-object / unsupported-format
240        // cases into a single `Err` we surface as `EmptyKeys` (the
241        // existing variant covers any failure to extract a key from the
242        // supplied PEM blob).
243        let private_key = PrivateKeyDer::from_pem_slice(cert.key.as_bytes())
244            .map_err(|_| CertificateResolverError::EmptyKeys)?;
245
246        // Postconditions of chain assembly: the leaf was pushed first, so the
247        // chain is never empty and its head is exactly the parsed leaf DER.
248        // Dedup only ever *drops* entries it recognises as the leaf, so the
249        // assembled length can never exceed leaf + supplied links.
250        debug_assert!(
251            !chain.is_empty(),
252            "assembled certificate chain must contain at least the leaf"
253        );
254        debug_assert_eq!(
255            chain[0].as_ref(),
256            leaf_der.as_slice(),
257            "the leaf must remain at index 0 of the chain"
258        );
259        // SHA-256 fingerprint is exactly 32 bytes — anything else means the
260        // digest pipeline changed underneath us.
261        debug_assert_eq!(
262            fingerprint.0.len(),
263            32,
264            "a SHA-256 fingerprint must be 32 bytes"
265        );
266
267        match any_supported_type(&private_key) {
268            Ok(signing_key) => {
269                let stored_certificate = CertifiedKeyWrapper {
270                    inner: Arc::new(CertifiedKey::new(chain, signing_key)),
271                    names: overriding_names,
272                    expiration,
273                    fingerprint,
274                    material_digest,
275                };
276                Ok(stored_certificate)
277            }
278            Err(sign_error) => Err(CertificateResolverError::InvalidPrivateKey(
279                sign_error.to_string(),
280            )),
281        }
282    }
283}
284
285/// Build the exact certificate representation a worker resolver will store,
286/// without mutating a resolver or publishing metrics.
287///
288/// The main process calls this before committing certificate material to
289/// `ConfigState`, and [`CertificateResolver::add_certificate`] calls it before
290/// touching its indexes. Keeping key consistency and SNI-name admission here
291/// prevents the control-plane preflight from drifting from the worker runtime.
292pub fn build_validated_certificate(
293    add: &AddCertificate,
294) -> Result<CertifiedKeyWrapper, CertificateResolverError> {
295    let certificate = CertifiedKeyWrapper::try_from(add)?;
296
297    match certificate.inner.keys_match() {
298        Ok(()) | Err(rustls::Error::InconsistentKeys(InconsistentKeys::Unknown)) => {}
299        Err(error) => {
300            return Err(CertificateResolverError::InvalidPrivateKey(
301                error.to_string(),
302            ));
303        }
304    }
305
306    let mut scratch: TrieNode<()> = TrieNode::root();
307    for name in &certificate.names {
308        if name.len() > MAX_HOSTNAME_LENGTH
309            || name.contains('/')
310            || scratch.domain_insert(name.to_owned().into_bytes(), ()) == InsertResult::Failed
311        {
312            return Err(CertificateResolverError::InvalidName(name.to_owned()));
313        }
314    }
315
316    Ok(certificate)
317}
318
319/// Parses and stores TLS certificates, makes them available to Rustls for TLS handshakes
320///
321/// the `domains` TrieNode is an addressing system to resolve a certificate
322/// for a given domain name.
323/// Certificates are stored in a hashmap that may contain unreachable certificates if
324/// no domain name points to it.
325/// `Clone` lets an HTTPS listener added next to a sibling on the same address
326/// take over its certificates (`HttpsProxy::add_listener`, `lib/src/https.rs`).
327#[derive(Default, Clone)]
328pub struct CertificateResolver {
329    /// routing one domain name to one certificate for fast resolving
330    pub domains: TrieNode<Fingerprint>,
331    /// a storage map: fingerprint -> stored_certificate
332    certificates: HashMap<Fingerprint, CertifiedKeyWrapper>,
333    /// maps each domain name to several compatible certificates, sorted by expiration date
334    /// map of domain_name -> all fingerprints (and expiration) linked to this domain name
335    //  the vector of (fingerprint, expiration) is sorted by expiration
336    name_fingerprint_idx: HashMap<String, Vec<(Fingerprint, i64)>>,
337}
338
339impl fmt::Debug for CertificateResolver {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        let indexed_certificate_links = self
342            .name_fingerprint_idx
343            .values()
344            .map(Vec::len)
345            .fold(0usize, usize::saturating_add);
346
347        f.debug_struct("CertificateResolver")
348            .field("domains", &"[redacted]")
349            .field("certificates_count", &self.certificates.len())
350            .field("indexed_names_count", &self.name_fingerprint_idx.len())
351            .field("indexed_certificate_links", &indexed_certificate_links)
352            .finish()
353    }
354}
355
356impl CertificateResolver {
357    /// return the certificate in the Rustls-usable form
358    pub fn get_certificate(&self, fingerprint: &Fingerprint) -> Option<CertifiedKeyWrapper> {
359        self.certificates.get(fingerprint).map(ToOwned::to_owned)
360    }
361
362    /// Recompute the aggregate `tls.cert.min_expires_at_seconds` gauge across
363    /// every certificate currently loaded. Per-SNI granularity would explode
364    /// statsd key cardinality (the resolver can easily hold tens of thousands
365    /// of names on a public endpoint) and the existing `gauge!` macro has no
366    /// label support, so we expose a single absolute unix-seconds reading of
367    /// the soonest-expiring cert. Dashboards alert on this as the "next cert
368    /// to rotate" deadline; operators query per-cert detail through the
369    /// command API. `x509` timestamps are signed but `set_gauge` takes a
370    /// `usize`, so we clamp already-expired certs to 0 (which is still a
371    /// monotonic "panic now" signal to any alerting rule).
372    ///
373    /// Called from `add_certificate` / `remove_certificate` — i.e. only when
374    /// the cert set actually changes, never on the hot TLS handshake path.
375    fn publish_min_expiration_gauge(&self) {
376        let Some(min_expiration) = self.certificates.values().map(|c| c.expiration).min() else {
377            // SECURITY: an empty resolver is not "every cert just
378            // expired"; it is "no cert
379            // has been loaded yet" — typical at process boot before the
380            // first AddCertificate request lands. Writing 0 here pages
381            // SOC tooling on every restart with the same alert as a real
382            // expired-cert event. Skip the emit so the gauge reflects the
383            // last known good state instead of being clobbered to 0.
384            return;
385        };
386        let clamped = min_expiration.max(0) as usize;
387        gauge!(names::tls::CERT_MIN_EXPIRES_AT_SECONDS, clamped);
388    }
389
390    /// Insert one already validated wrapper without publishing metrics.
391    /// Callers perform every fallible step before mutating the resolver.
392    fn insert_validated_certificate(&mut self, certificate: CertifiedKeyWrapper) {
393        for name in &certificate.names {
394            let fingerprints_for_this_name = self
395                .name_fingerprint_idx
396                .entry(name.to_owned())
397                .or_default();
398            fingerprints_for_this_name
399                .push((certificate.fingerprint.to_owned(), certificate.expiration));
400            fingerprints_for_this_name.sort_by_key(|(_, expiration)| *expiration);
401
402            let longest_lived_cert = fingerprints_for_this_name
403                .last()
404                .expect("inserting a certificate must leave its name index non-empty");
405            self.domains.remove(&name.to_owned().into_bytes());
406            let insert_result = self.domains.insert(
407                name.to_owned().into_bytes(),
408                longest_lived_cert.0.to_owned(),
409            );
410            debug_assert_ne!(
411                insert_result,
412                InsertResult::Failed,
413                "validated certificate names must remain insertable"
414            );
415            if insert_result == InsertResult::Failed {
416                error!(
417                    "{} the SNI trie rejected a certificate name despite passing \
418                     the shared preflight validation, name_bytes={}",
419                    log_module_context!(),
420                    name.len(),
421                );
422            }
423        }
424
425        self.certificates
426            .insert(certificate.fingerprint.to_owned(), certificate);
427    }
428
429    /// Remove one wrapper and all of its indexes without publishing metrics.
430    /// Returns whether the fingerprint was present.
431    fn remove_certificate_without_publishing(&mut self, fingerprint: &Fingerprint) -> bool {
432        let Some(certificate_to_remove) = self.certificates.get(fingerprint).cloned() else {
433            return false;
434        };
435
436        for name in certificate_to_remove.names {
437            self.domains.domain_remove(&name.as_bytes().to_vec());
438
439            if let std::collections::hash_map::Entry::Occupied(mut entry) =
440                self.name_fingerprint_idx.entry(name.to_owned())
441            {
442                entry.get_mut().retain(|(stored, _)| stored != fingerprint);
443                if let Some((longest_lived, _)) = entry.get().last() {
444                    let insert_result = self
445                        .domains
446                        .insert(name.as_bytes().to_vec(), longest_lived.to_owned());
447                    debug_assert_ne!(
448                        insert_result,
449                        InsertResult::Failed,
450                        "validated certificate names must remain insertable"
451                    );
452                    if insert_result == InsertResult::Failed {
453                        error!(
454                            "{} the SNI trie rejected a re-inserted certificate name \
455                             that was validated on add, name_bytes={}",
456                            log_module_context!(),
457                            name.len(),
458                        );
459                    }
460                }
461                if entry.get().is_empty() {
462                    entry.remove();
463                }
464            }
465        }
466
467        self.certificates.remove(fingerprint);
468        true
469    }
470
471    /// persist a certificate, after ensuring validity, and checking if it can replace another certificate.
472    /// return the certificate fingerprint regardless of having inserted it or not
473    pub fn add_certificate(
474        &mut self,
475        add: &AddCertificate,
476    ) -> Result<Fingerprint, CertificateResolverError> {
477        let cert_to_add = build_validated_certificate(add)?;
478
479        trace!(
480            "{} adding certificate {:?}",
481            log_module_context!(),
482            cert_to_add
483        );
484
485        if self.certificates.contains_key(&cert_to_add.fingerprint) {
486            return Ok(cert_to_add.fingerprint);
487        }
488
489        // Past the duplicate guard the fingerprint is genuinely new, so the
490        // store will grow by exactly one. Snapshot the count to assert that
491        // delta below (ungated `let` — read only inside the debug_assert, so
492        // the optimizer drops it in release while it still compiles).
493        let certificates_before = self.certificates.len();
494        let new_fingerprint = cert_to_add.fingerprint.clone();
495        debug_assert!(
496            !self.certificates.contains_key(&new_fingerprint),
497            "add_certificate past the dedup guard must be inserting a new fingerprint"
498        );
499
500        self.insert_validated_certificate(cert_to_add.to_owned());
501        self.publish_min_expiration_gauge();
502
503        // Postconditions: the new fingerprint is now stored, the store grew by
504        // exactly one (the dedup guard above ruled out an overwrite), and
505        // every name the cert advertises now resolves to it through the index.
506        debug_assert!(
507            self.certificates.contains_key(&new_fingerprint),
508            "add_certificate must store the new certificate"
509        );
510        debug_assert_eq!(
511            self.certificates.len(),
512            certificates_before + 1,
513            "add_certificate must grow the store by exactly one"
514        );
515        debug_assert!(
516            cert_to_add.names.iter().all(|name| {
517                self.name_fingerprint_idx
518                    .get(name)
519                    .is_some_and(|fps| fps.iter().any(|(fp, _)| *fp == new_fingerprint))
520            }),
521            "every name of the added cert must be indexed to its fingerprint"
522        );
523
524        trace!("{} {:#?}", log_module_context!(), self);
525
526        Ok(cert_to_add.fingerprint)
527    }
528
529    /// Delete a certificate from the resolver. May fail if there is no alternative for
530    // a domain name
531    pub fn remove_certificate(
532        &mut self,
533        fingerprint: &Fingerprint,
534    ) -> Result<(), CertificateResolverError> {
535        // Snapshot the store size so the postcondition can assert that a
536        // present cert drops the count by exactly one and an absent one is a
537        // no-op. Ungated `let`: read only inside the debug_asserts below, so
538        // it compiles in release and the optimizer drops it.
539        let certificates_before = self.certificates.len();
540        let was_present = self.certificates.contains_key(fingerprint);
541        #[cfg(debug_assertions)]
542        let removed_names = self
543            .certificates
544            .get(fingerprint)
545            .map(|certificate| certificate.names.to_owned());
546        let removed = self.remove_certificate_without_publishing(fingerprint);
547        if removed {
548            self.publish_min_expiration_gauge();
549
550            // Postconditions on the present-cert path: the fingerprint is
551            // truly gone, the store shrank by exactly one, and no name still
552            // points at the removed fingerprint in the index (a leftover would
553            // let `resolve` hand back a fingerprint with no backing cert).
554            debug_assert!(
555                !self.certificates.contains_key(fingerprint),
556                "remove_certificate must evict the fingerprint"
557            );
558            debug_assert_eq!(
559                self.certificates.len(),
560                certificates_before - 1,
561                "removing a present cert must shrink the store by exactly one"
562            );
563            #[cfg(debug_assertions)]
564            debug_assert!(
565                removed_names
566                    .as_ref()
567                    .is_some_and(|names| names.iter().all(|name| {
568                        self.name_fingerprint_idx
569                            .get(name)
570                            .is_none_or(|fingerprints| {
571                                fingerprints.iter().all(|(stored, _)| stored != fingerprint)
572                            })
573                    })),
574                "no name may still index the removed fingerprint"
575            );
576        } else {
577            // Absent-cert path is a pure no-op: the store size is unchanged.
578            debug_assert!(
579                !was_present,
580                "the absent-cert branch must only run when the fingerprint was not stored"
581            );
582            debug_assert_eq!(
583                self.certificates.len(),
584                certificates_before,
585                "removing an absent cert must not change the store"
586            );
587        }
588        trace!("{} {:#?}", log_module_context!(), self);
589
590        Ok(())
591    }
592
593    /// Validate the complete replacement before changing any resolver state,
594    /// then update the indexes and publish the aggregate gauge once.
595    /// A byte-for-byte effective retry keeps the existing wrapper in place so
596    /// it also preserves name-index ordering.
597    pub fn replace_certificate(
598        &mut self,
599        replace: &ReplaceCertificate,
600    ) -> Result<Fingerprint, CertificateResolverError> {
601        let add = AddCertificate {
602            address: replace.address.to_owned(),
603            certificate: replace.new_certificate.to_owned(),
604            expired_at: replace.new_expired_at.to_owned(),
605        };
606        let new_cert = build_validated_certificate(&add)?;
607        let new_fingerprint = new_cert.fingerprint.to_owned();
608        // `ConfigState` rejects an invalid hexadecimal old fingerprint before
609        // dispatching to a worker. Direct users of this public resolver method
610        // historically received `Ok(new_fingerprint)` after the replacement
611        // was stored, with a warning that the old entry could not be removed.
612        // Parse before mutation, but preserve that public 2.3.x contract and
613        // the existing `CertificateResolverError` variant set.
614        let old_fingerprint = Fingerprint::from_str(&replace.old_fingerprint);
615        let new_material_already_stored =
616            self.certificates
617                .get(&new_fingerprint)
618                .is_some_and(|stored| {
619                    stored.material_digest == new_cert.material_digest
620                        && stored.expiration == new_cert.expiration
621                });
622
623        if old_fingerprint
624            .as_ref()
625            .is_ok_and(|old| old == &new_fingerprint)
626            && new_material_already_stored
627        {
628            return Ok(new_fingerprint);
629        }
630
631        let mut resolver_changed = false;
632        if !new_material_already_stored {
633            self.remove_certificate_without_publishing(&new_fingerprint);
634            self.insert_validated_certificate(new_cert);
635            resolver_changed = true;
636        }
637        if let Ok(old_fingerprint) = old_fingerprint.as_ref()
638            && old_fingerprint != &new_fingerprint
639        {
640            resolver_changed |= self.remove_certificate_without_publishing(old_fingerprint);
641        }
642        if resolver_changed {
643            self.publish_min_expiration_gauge();
644        }
645
646        debug_assert!(
647            self.certificates.contains_key(&new_fingerprint),
648            "replace_certificate must leave the replacement stored"
649        );
650        debug_assert!(
651            old_fingerprint.as_ref().is_err()
652                || old_fingerprint.as_ref().is_ok_and(|old| {
653                    old == &new_fingerprint || !self.certificates.contains_key(old)
654                }),
655            "replace_certificate must remove a distinct old fingerprint"
656        );
657
658        if let Err(error) = old_fingerprint {
659            warn!(
660                "{} new certificate stored but could not remove old one: \
661                 failed to parse old fingerprint, {}",
662                log_module_context!(),
663                error
664            );
665        }
666
667        Ok(new_fingerprint)
668    }
669
670    /// return all fingerprints that are available for these domain names,
671    /// provided at least one name is given
672    #[cfg(test)]
673    fn find_certificates_by_names(
674        &self,
675        names: &HashSet<String>,
676    ) -> Result<HashSet<Fingerprint>, CertificateResolverError> {
677        let mut fingerprints = HashSet::new();
678        for name in names {
679            if let Some(fprints) = self.name_fingerprint_idx.get(name) {
680                fprints.iter().for_each(|fingerprint| {
681                    fingerprints.insert(fingerprint.to_owned().0);
682                });
683            }
684        }
685
686        Ok(fingerprints)
687    }
688
689    /// return the hashset of subjects that the certificate is able to handle.
690    /// the certificate must be already persisted for this check
691    #[cfg(test)]
692    fn certificate_names(
693        &self,
694        fingerprint: &Fingerprint,
695    ) -> Result<HashSet<String>, CertificateResolverError> {
696        if let Some(cert) = self.certificates.get(fingerprint) {
697            return Ok(cert.names.iter().cloned().collect());
698        }
699        Ok(HashSet::new())
700    }
701
702    pub fn domain_lookup(
703        &self,
704        domain: &[u8],
705        accept_wildcard: bool,
706    ) -> Option<&KeyValue<Key, Fingerprint>> {
707        self.domains.domain_lookup(domain, accept_wildcard)
708    }
709
710    /// Resolve the SAN set Sōzu would serve for `domain` (the same trie
711    /// lookup rustls uses, wildcard-aware via `domain_lookup`). Returns the
712    /// certificate's `names` exactly as stored — wildcards retain their
713    /// leading `*.` so the caller can apply RFC 6125 §6.4.3 matching. `None`
714    /// when no cert covers `domain` (rustls would fall back to
715    /// `DEFAULT_CERTIFICATE`).
716    ///
717    /// Mirrors `MutexCertificateResolver::resolve` minus the rustls glue, so
718    /// the SAN snapshot taken at handshake matches the certificate the peer
719    /// actually validated (RFC 7540 §9.1.1 / RFC 9113 §9.1.1 connection
720    /// reuse).
721    pub fn names_for_sni(&self, domain: &[u8]) -> Option<Vec<String>> {
722        let (_, fingerprint) = self.domain_lookup(domain, true)?;
723        self.certificates
724            .get(fingerprint)
725            .map(|cert| cert.names.clone())
726    }
727}
728
729// -----------------------------------------------------------------------------
730// MutexWrappedCertificateResolver struct
731
732#[derive(Default)]
733pub struct MutexCertificateResolver(pub Mutex<CertificateResolver>);
734
735impl ResolvesServerCert for MutexCertificateResolver {
736    fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
737        let server_name = client_hello.server_name();
738        let sigschemes = client_hello.signature_schemes();
739
740        let Some(name) = server_name else {
741            error!(
742                "{} cannot look up certificate: no SNI from session",
743                log_module_context!()
744            );
745            return None;
746        };
747        trace!(
748            "{} trying to resolve certificate with name_bytes={} signature_schemes_count={}",
749            log_module_context!(),
750            name.len(),
751            sigschemes.len()
752        );
753        // Every other site uses blocking `lock()`, and silently falling
754        // back to `DEFAULT_CERTIFICATE` on lock
755        // contention is an attacker-detectable mismatch (different
756        // chain → different fingerprint) and a footgun the moment
757        // multi-threading enters the worker. Block here. Lock-poisoning
758        // (panic-while-holding) is mapped to the same default-cert
759        // fallback the previous `try_lock` Err arm produced — preserves
760        // the existing observable behaviour for that one corner case
761        // without inventing a new failure mode.
762        let resolver = match self.0.lock() {
763            Ok(guard) => guard,
764            Err(_poisoned) => {
765                error!(
766                    "{} cert resolver mutex poisoned, returning default cert",
767                    log_module_context!()
768                );
769                return DEFAULT_CERTIFICATE.clone();
770            }
771        };
772        if let Some((_, fingerprint)) = resolver.domains.domain_lookup(name.as_bytes(), true) {
773            trace!(
774                "{} looking for certificate with name_bytes={} fingerprint_bytes={}",
775                log_module_context!(),
776                name.len(),
777                fingerprint.0.len()
778            );
779
780            // Strict-binding invariant: when the SNI trie resolves a name to a
781            // fingerprint, the served cert is exactly the one stored under
782            // that fingerprint — never a substitute. This guards cert
783            // SELECTION (our own store consistency), not the peer-supplied SNI
784            // itself, so a `debug_assert` is correct here (a violation is a
785            // resolver bug, not hostile traffic). The cert may legitimately be
786            // `None` if the trie still indexes a fingerprint whose cert was
787            // concurrently removed; we only assert the positive case.
788            let cert = resolver
789                .certificates
790                .get(fingerprint)
791                .map(|cert| cert.inner.clone());
792            debug_assert!(
793                cert.is_none()
794                    || resolver
795                        .certificates
796                        .get(fingerprint)
797                        .is_some_and(|stored| Arc::ptr_eq(&stored.inner, cert.as_ref().unwrap())),
798                "resolved certificate must be the one stored under the looked-up fingerprint"
799            );
800
801            trace!(
802                "{} certificate lookup fingerprint_bytes={} found={}",
803                log_module_context!(),
804                fingerprint.0.len(),
805                cert.is_some()
806            );
807            return cert;
808        }
809        drop(resolver);
810
811        // error!("could not look up a certificate for server name '{}'", name);
812        // This certificate is used for TLS tunneling with another TLS termination endpoint
813        // Note that this is unsafe and you should provide a valid certificate
814        debug!(
815            "{} default certificate is used for name_bytes={}",
816            log_module_context!(),
817            name.len()
818        );
819        incr!(names::tls::DEFAULT_CERT_USED);
820        DEFAULT_CERTIFICATE.clone()
821    }
822}
823
824impl MutexCertificateResolver {
825    /// Snapshot of the SAN set Sōzu would serve for `domain`. Acquires the
826    /// resolver lock once. Returns `None` when the underlying mutex is
827    /// poisoned — the caller is expected to treat poison the same as
828    /// "default cert served" (legacy fallback), mirroring `resolve`'s
829    /// own poison handling at the rustls hot path.
830    pub fn names_for_sni(&self, domain: &[u8]) -> Option<Vec<String>> {
831        match self.0.lock() {
832            Ok(guard) => guard.names_for_sni(domain),
833            Err(_poisoned) => {
834                error!(
835                    "{} cert resolver mutex poisoned, treating as no SAN match",
836                    log_module_context!()
837                );
838                None
839            }
840        }
841    }
842}
843
844impl fmt::Debug for MutexCertificateResolver {
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        f.write_str("MutexWrappedCertificateResolver")
847    }
848}
849
850// -----------------------------------------------------------------------------
851// Unit tests
852
853#[cfg(test)]
854mod tests {
855    use std::{
856        collections::HashSet,
857        error::Error,
858        io::Cursor,
859        sync::Arc,
860        time::{Duration, SystemTime},
861    };
862
863    // use rand::{seq::SliceRandom, thread_rng};
864    use sozu_command::proto::command::{
865        AddCertificate, CertificateAndKey, ReplaceCertificate, SocketAddress,
866    };
867
868    use super::{
869        CertificateResolver, CertificateResolverError, CertifiedKeyWrapper,
870        MutexCertificateResolver, build_validated_certificate,
871    };
872
873    fn drive_client_hello(resolver: Arc<MutexCertificateResolver>, server_name: String) {
874        let provider = Arc::new(crate::crypto::default_provider());
875        let server_config = rustls::ServerConfig::builder_with_provider(provider.clone())
876            .with_protocol_versions(&[&rustls::version::TLS13])
877            .expect("test server provider must support TLS 1.3")
878            .with_no_client_auth()
879            .with_cert_resolver(resolver);
880        let client_config = rustls::ClientConfig::builder_with_provider(provider)
881            .with_protocol_versions(&[&rustls::version::TLS13])
882            .expect("test client provider must support TLS 1.3")
883            .with_root_certificates(rustls::RootCertStore::empty())
884            .with_no_client_auth();
885        let server_name = rustls::pki_types::ServerName::try_from(server_name)
886            .expect("test SNI must be a valid DNS name");
887        let mut client = rustls::ClientConnection::new(Arc::new(client_config), server_name)
888            .expect("test client connection must initialize");
889        let mut server = rustls::ServerConnection::new(Arc::new(server_config))
890            .expect("test server connection must initialize");
891
892        let mut client_hello = Vec::new();
893        client
894            .write_tls(&mut client_hello)
895            .expect("test client hello must serialize");
896        server
897            .read_tls(&mut Cursor::new(client_hello))
898            .expect("test server must read the client hello");
899        server
900            .process_new_packets()
901            .expect("test server must process the client hello");
902    }
903
904    /// A certificate carrying a name the SNI route table cannot host must
905    /// be rejected as a whole, BEFORE any resolver state is touched. On
906    /// `main` such a name panicked inside `TrieNode::insert`; once the
907    /// insert reports `InsertResult::Failed` gracefully, silently
908    /// discarding the result would register the certificate while
909    /// `domains` never learns the name -- a dead SNI route failing every
910    /// handshake with no diagnostic on either side.
911    #[test]
912    fn add_certificate_rejects_a_name_the_route_table_cannot_host() {
913        for name in ["sni-secret.example/", ".sni-secret.example"] {
914            let mut resolver = CertificateResolver::default();
915            let result = resolver.add_certificate(&AddCertificate {
916                address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
917                certificate: CertificateAndKey {
918                    certificate: include_str!("../assets/certificate.pem").to_owned(),
919                    key: include_str!("../assets/key.pem").to_owned(),
920                    names: vec![(*name).to_owned()],
921                    ..Default::default()
922                },
923                expired_at: None,
924            });
925            assert!(result.is_err(), "{name:?} must be rejected, not stored");
926            assert!(
927                resolver.certificates.is_empty(),
928                "{name:?} was rejected but the certificate store was mutated",
929            );
930            assert!(
931                resolver.name_fingerprint_idx.is_empty(),
932                "{name:?} was rejected but the name index was mutated",
933            );
934            assert!(
935                resolver.domains.is_empty(),
936                "{name:?} was rejected but the SNI trie was mutated",
937            );
938        }
939    }
940
941    #[test]
942    fn shared_preflight_rejects_a_certificate_with_a_mismatched_private_key() {
943        let result = build_validated_certificate(&AddCertificate {
944            address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
945            certificate: CertificateAndKey {
946                certificate: include_str!("../assets/tests/certificate-1y.pem").to_owned(),
947                key: include_str!("../assets/tests/key-2y.pem").to_owned(),
948                names: vec!["mismatch.example.com".to_owned()],
949                ..Default::default()
950            },
951            expired_at: None,
952        });
953
954        assert!(
955            matches!(result, Err(CertificateResolverError::InvalidPrivateKey(_))),
956            "a mismatched certificate/private-key pair must be refused, got {result:?}"
957        );
958    }
959
960    // Asserts on `trace!` output: gate on the same condition that compiles it in.
961    #[cfg(any(debug_assertions, feature = "logs-trace"))]
962    #[test]
963    fn certificate_resolver_logs_redact_matching_sni_and_fingerprint() {
964        const SNI_SECRET: &str = "resolver-sni-secret.example";
965
966        let certificate = CertificateAndKey {
967            certificate: include_str!("../assets/certificate.pem").to_owned(),
968            key: include_str!("../assets/key.pem").to_owned(),
969            names: vec![SNI_SECRET.to_owned()],
970            ..Default::default()
971        };
972        let fingerprint = certificate
973            .fingerprint()
974            .expect("test certificate fingerprint must be computable")
975            .to_string();
976        let output = crate::capture_test_logs_at_level("trace", move || {
977            let resolver = Arc::new(MutexCertificateResolver::default());
978            resolver
979                .0
980                .lock()
981                .expect("test resolver lock must be available")
982                .add_certificate(&AddCertificate {
983                    address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
984                    certificate,
985                    expired_at: None,
986                })
987                .expect("test certificate must load into the resolver");
988            drive_client_hello(resolver, SNI_SECRET.to_owned());
989        });
990
991        assert!(
992            !output.contains(SNI_SECRET),
993            "TLS resolver logs leaked the matching SNI: {output}"
994        );
995        assert!(
996            !output.contains(&fingerprint),
997            "TLS resolver logs leaked the matching certificate fingerprint: {output}"
998        );
999        for metadata in [
1000            format!("name_bytes={}", SNI_SECRET.len()),
1001            "fingerprint_bytes=32".to_owned(),
1002        ] {
1003            assert!(
1004                output.contains(&metadata),
1005                "TLS resolver logs omitted bounded metadata {metadata}: {output}"
1006            );
1007        }
1008        assert!(
1009            output.len() <= 4096,
1010            "TLS resolver log capture is not bounded: {} bytes",
1011            output.len()
1012        );
1013    }
1014
1015    // Asserts on `trace!` output: gate on the same condition that compiles it in.
1016    #[cfg(any(debug_assertions, feature = "logs-trace"))]
1017    #[test]
1018    fn certificate_resolver_logs_redact_fallback_sni() {
1019        const SNI_SECRET: &str = "fallback-sni-secret.example";
1020
1021        let output = crate::capture_test_logs_at_level("trace", move || {
1022            drive_client_hello(
1023                Arc::new(MutexCertificateResolver::default()),
1024                SNI_SECRET.to_owned(),
1025            );
1026        });
1027
1028        assert!(
1029            !output.contains(SNI_SECRET),
1030            "TLS resolver logs leaked the fallback SNI: {output}"
1031        );
1032        assert!(
1033            output.contains(&format!("name_bytes={}", SNI_SECRET.len())),
1034            "TLS resolver fallback log omitted bounded SNI metadata: {output}"
1035        );
1036        assert!(
1037            output.len() <= 2048,
1038            "TLS resolver fallback log capture is not bounded: {} bytes",
1039            output.len()
1040        );
1041    }
1042
1043    #[test]
1044    fn certificate_resolver_poison_logs_are_bounded() {
1045        const SNI_SECRET: &str = "poison-sni-secret.example";
1046        const QUERY_SECRET: &str = "POISON_QUERY_SECRET_SENTINEL";
1047
1048        let query = format!("{QUERY_SECRET}{}", "x".repeat(4096));
1049        let output = crate::capture_test_logs_at_level("trace", move || {
1050            let resolver = Arc::new(MutexCertificateResolver::default());
1051            let poison_target = resolver.clone();
1052            assert!(
1053                std::thread::spawn(move || {
1054                    let _guard = poison_target
1055                        .0
1056                        .lock()
1057                        .expect("test resolver lock must initially be available");
1058                    panic!("intentionally poison the test resolver lock");
1059                })
1060                .join()
1061                .is_err(),
1062                "test poison thread must panic"
1063            );
1064
1065            drive_client_hello(resolver.clone(), SNI_SECRET.to_owned());
1066            assert!(resolver.names_for_sni(query.as_bytes()).is_none());
1067        });
1068
1069        assert!(
1070            !output.contains(SNI_SECRET),
1071            "TLS resolver poison log leaked the handshake SNI: {output}"
1072        );
1073        assert!(
1074            !output.contains(QUERY_SECRET),
1075            "TLS resolver poison log leaked the 4 KiB direct query: {output}"
1076        );
1077        assert!(
1078            output.len() <= 2048,
1079            "TLS resolver poison log capture is not bounded: {} bytes",
1080            output.len()
1081        );
1082    }
1083
1084    #[test]
1085    fn certified_key_wrapper_debug_redacts_runtime_certificate_material() {
1086        const NAME_SECRET: &str = "CERTIFIED_KEY_NAME_SECRET_SENTINEL";
1087
1088        let add = AddCertificate {
1089            address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1090            certificate: CertificateAndKey {
1091                certificate: include_str!("../assets/certificate.pem").to_owned(),
1092                key: include_str!("../assets/key.pem").to_owned(),
1093                names: vec![format!("{NAME_SECRET}{}", "x".repeat(4096))],
1094                ..Default::default()
1095            },
1096            expired_at: None,
1097        };
1098        let certified_key =
1099            CertifiedKeyWrapper::try_from(&add).expect("test certificate must parse");
1100        let fingerprint = certified_key.fingerprint.to_string();
1101        let output = format!("{certified_key:?}");
1102
1103        assert!(
1104            !output.contains(NAME_SECRET),
1105            "CertifiedKeyWrapper Debug leaked an overridden certificate name: {output}"
1106        );
1107        assert!(
1108            !output.contains(&fingerprint),
1109            "CertifiedKeyWrapper Debug leaked the certificate fingerprint: {output}"
1110        );
1111        assert!(
1112            output.contains("fingerprint_bytes: 32"),
1113            "CertifiedKeyWrapper Debug omitted bounded fingerprint metadata: {output}"
1114        );
1115        assert!(
1116            output.contains("[redacted]"),
1117            "CertifiedKeyWrapper Debug must make certificate redaction explicit: {output}"
1118        );
1119        assert!(
1120            output.len() <= 1024,
1121            "CertifiedKeyWrapper Debug output is not bounded: {} bytes",
1122            output.len()
1123        );
1124    }
1125
1126    #[test]
1127    fn certificate_resolver_debug_redacts_indexed_names() {
1128        const NAME_SECRET: &str = "CERTIFICATE_RESOLVER_NAME_SECRET_SENTINEL";
1129
1130        let mut resolver = CertificateResolver::default();
1131        resolver
1132            .add_certificate(&AddCertificate {
1133                address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1134                certificate: CertificateAndKey {
1135                    certificate: include_str!("../assets/certificate.pem").to_owned(),
1136                    key: include_str!("../assets/key.pem").to_owned(),
1137                    // Below `MAX_HOSTNAME_LENGTH` so the name passes the
1138                    // add-time validation and reaches the Debug formatting
1139                    // under test; the redaction property is
1140                    // length-independent.
1141                    names: vec![format!("{NAME_SECRET}{}", "x".repeat(2048))],
1142                    ..Default::default()
1143                },
1144                expired_at: None,
1145            })
1146            .expect("test certificate must load into the resolver");
1147        let output = format!("{resolver:?}");
1148
1149        assert!(
1150            !output.contains(NAME_SECRET),
1151            "CertificateResolver Debug leaked an indexed certificate name: {output}"
1152        );
1153        assert!(
1154            output.len() <= 1024,
1155            "CertificateResolver Debug output is not bounded: {} bytes",
1156            output.len()
1157        );
1158    }
1159
1160    #[test]
1161    fn lifecycle() -> Result<(), Box<dyn Error + Send + Sync>> {
1162        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1163        let mut resolver = CertificateResolver::default();
1164        let certificate_and_key = CertificateAndKey {
1165            certificate: String::from(include_str!("../assets/certificate.pem")),
1166            key: String::from(include_str!("../assets/key.pem")),
1167            ..Default::default()
1168        };
1169
1170        let fingerprint = resolver
1171            .add_certificate(&AddCertificate {
1172                address,
1173                certificate: certificate_and_key,
1174                expired_at: None,
1175            })
1176            .expect("could not add certificate");
1177
1178        if resolver.get_certificate(&fingerprint).is_none() {
1179            return Err("failed to retrieve certificate".into());
1180        }
1181
1182        // get the names to try and retrieve the certificate AFTER it is supposed to be removed
1183        let names = resolver.certificate_names(&fingerprint)?;
1184
1185        if let Err(err) = resolver.remove_certificate(&fingerprint) {
1186            return Err(format!("the certificate was not removed, {err}").into());
1187        }
1188
1189        if resolver.get_certificate(&fingerprint).is_some() {
1190            return Err("We have retrieved the certificate that should be deleted".into());
1191        }
1192
1193        if !resolver.find_certificates_by_names(&names)?.is_empty() {
1194            return Err(
1195                "The certificate should be deleted but one of its names is in the index".into(),
1196            );
1197        }
1198
1199        Ok(())
1200    }
1201
1202    #[test]
1203    fn name_override() -> Result<(), Box<dyn Error + Send + Sync>> {
1204        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1205        let mut resolver = CertificateResolver::default();
1206        let certificate_and_key = CertificateAndKey {
1207            certificate: String::from(include_str!("../assets/certificate.pem")),
1208            key: String::from(include_str!("../assets/key.pem")),
1209            names: vec!["localhost".into(), "lolcatho.st".into()],
1210            ..Default::default()
1211        };
1212
1213        let fingerprint = resolver.add_certificate(&AddCertificate {
1214            address,
1215            certificate: certificate_and_key,
1216            expired_at: None,
1217        })?;
1218
1219        if resolver.get_certificate(&fingerprint).is_none() {
1220            return Err("failed to retrieve certificate".into());
1221        }
1222
1223        let mut lolcat = HashSet::new();
1224        lolcat.insert(String::from("lolcatho.st"));
1225        if resolver.find_certificates_by_names(&lolcat)?.is_empty()
1226            || resolver.get_certificate(&fingerprint).is_none()
1227        {
1228            return Err("failed to retrieve certificate with custom names".into());
1229        }
1230
1231        if let Err(err) = resolver.remove_certificate(&fingerprint) {
1232            return Err(format!("the certificate could not be removed, {err}").into());
1233        }
1234
1235        let names = resolver.certificate_names(&fingerprint)?;
1236        if !resolver.find_certificates_by_names(&names)?.is_empty()
1237            && resolver.get_certificate(&fingerprint).is_some()
1238        {
1239            return Err("We have retrieved the certificate that should be deleted".into());
1240        }
1241
1242        Ok(())
1243    }
1244
1245    #[test]
1246    fn keep_resolving_with_wildcard() -> Result<(), Box<dyn Error + Send + Sync>> {
1247        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1248        let mut resolver = CertificateResolver::default();
1249
1250        // ---------------------------------------------------------------------
1251        // load the wildcard certificate,  expiring in 3 years
1252        let wildcard_example_org = CertificateAndKey {
1253            certificate: String::from(include_str!("../assets/tests/certificate-3.pem")),
1254            key: String::from(include_str!("../assets/tests/key.pem")),
1255            ..Default::default()
1256        };
1257
1258        let wildcard_example_org_fingerprint = resolver.add_certificate(&AddCertificate {
1259            address,
1260            certificate: wildcard_example_org,
1261            expired_at: Some(
1262                (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
1263                    + Duration::from_secs(365 * 24 * 3600))
1264                .as_secs() as i64,
1265            ),
1266        })?;
1267
1268        if resolver
1269            .get_certificate(&wildcard_example_org_fingerprint)
1270            .is_none()
1271        {
1272            return Err("could not load the 2-year-valid certificate".into());
1273        }
1274
1275        // ---------------------------------------------------------------------
1276        // try loading the ordinary certificate, expiring in 2 years
1277        // this one has two names: example.org and www.example.org
1278        let www_example_org = CertificateAndKey {
1279            certificate: String::from(include_str!("../assets/tests/certificate-2.pem")),
1280            key: String::from(include_str!("../assets/tests/key.pem")),
1281            ..Default::default()
1282        };
1283
1284        let www_example_org_fingerprint = resolver.add_certificate(&AddCertificate {
1285            address,
1286            certificate: www_example_org,
1287            expired_at: Some(
1288                (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
1289                    + Duration::from_secs(2 * 365 * 24 * 3600))
1290                .as_secs() as i64,
1291            ),
1292        })?;
1293
1294        let www_example_org = resolver
1295            .domain_lookup("www.example.org".as_bytes(), true)
1296            .expect("there should be a www.example.org cert");
1297        assert_eq!(www_example_org.1, www_example_org_fingerprint);
1298
1299        let test_example_org = resolver
1300            .domain_lookup("test.example.org".as_bytes(), true)
1301            .expect("there should be a test.example.org cert");
1302        assert_eq!(test_example_org.1, wildcard_example_org_fingerprint);
1303
1304        let example_org = resolver
1305            .domain_lookup("example.org".as_bytes(), true)
1306            .expect("there should be a example.org cert");
1307        assert_eq!(example_org.1, www_example_org_fingerprint);
1308
1309        // check that when removing the www.example.org certificate
1310        // the resolver falls back on the wildcard
1311        resolver
1312            .remove_certificate(&www_example_org_fingerprint)
1313            .expect("should be able to remove the 2-year certificate");
1314
1315        let should_be_wildcard_fingerprint = resolver
1316            .domain_lookup("www.example.org".as_bytes(), true)
1317            .expect("there should be a www.example.org cert");
1318        assert_eq!(
1319            should_be_wildcard_fingerprint.1,
1320            wildcard_example_org_fingerprint
1321        );
1322
1323        assert!(
1324            resolver
1325                .domain_lookup("example.org".as_bytes(), true)
1326                .is_none()
1327        );
1328
1329        Ok(())
1330    }
1331
1332    #[test]
1333    fn resolve_the_longer_lived_cert() -> Result<(), Box<dyn Error + Send + Sync>> {
1334        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1335        let mut resolver = CertificateResolver::default();
1336
1337        // ---------------------------------------------------------------------
1338        // load the 2-year valid certificate
1339        let certificate_and_key_2y = CertificateAndKey {
1340            certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1341            key: String::from(include_str!("../assets/tests/key-2y.pem")),
1342            ..Default::default()
1343        };
1344
1345        let fingerprint_2y = resolver.add_certificate(&AddCertificate {
1346            address,
1347            certificate: certificate_and_key_2y,
1348            expired_at: None,
1349        })?;
1350
1351        if resolver.get_certificate(&fingerprint_2y).is_none() {
1352            return Err("could not load the 2-year-valid certificate".into());
1353        }
1354
1355        // ---------------------------------------------------------------------
1356        // try loading the 1-year valid certificate
1357        let certificate_and_key_1y = CertificateAndKey {
1358            certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1359            key: String::from(include_str!("../assets/tests/key-1y.pem")),
1360            ..Default::default()
1361        };
1362
1363        let fingerprint_1y = resolver.add_certificate(&AddCertificate {
1364            address,
1365            certificate: certificate_and_key_1y,
1366            ..Default::default()
1367        })?;
1368
1369        let localhost_cert = resolver
1370            .domain_lookup("localhost".as_bytes(), true)
1371            .expect("there should be a localhost cert");
1372
1373        assert_eq!(localhost_cert.1, fingerprint_2y);
1374
1375        // check that when removing the longer-lived certificate,
1376        // the resolver falls back on the shorter-lived one
1377
1378        resolver
1379            .remove_certificate(&fingerprint_2y)
1380            .expect("should be able to remove the 2-year certificate");
1381
1382        let localhost_cert = resolver
1383            .domain_lookup("localhost".as_bytes(), true)
1384            .expect("there should be a localhost cert");
1385
1386        assert_eq!(localhost_cert.1, fingerprint_1y);
1387
1388        Ok(())
1389    }
1390
1391    #[test]
1392    fn expiration_override() -> Result<(), Box<dyn Error + Send + Sync>> {
1393        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1394        let mut resolver = CertificateResolver::default();
1395
1396        // ---------------------------------------------------------------------
1397        // load first certificate
1398        let certificate_and_key_1y = CertificateAndKey {
1399            certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1400            key: String::from(include_str!("../assets/tests/key-1y.pem")),
1401            ..Default::default()
1402        };
1403
1404        let fingerprint_1y_overriden = resolver.add_certificate(&AddCertificate {
1405            address,
1406            certificate: certificate_and_key_1y,
1407            expired_at: Some(
1408                (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?
1409                    + Duration::from_secs(3 * 365 * 24 * 3600))
1410                .as_secs() as i64,
1411            ),
1412        })?;
1413
1414        if resolver
1415            .get_certificate(&fingerprint_1y_overriden)
1416            .is_none()
1417        {
1418            return Err("failed to retrieve certificate".into());
1419        }
1420
1421        // ---------------------------------------------------------------------
1422        // load second certificate
1423        let certificate_and_key_2y = CertificateAndKey {
1424            certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1425            key: String::from(include_str!("../assets/tests/key-2y.pem")),
1426            ..Default::default()
1427        };
1428
1429        let fingerprint_2y = resolver.add_certificate(&AddCertificate {
1430            address,
1431            certificate: certificate_and_key_2y,
1432            expired_at: None,
1433        })?;
1434
1435        let localhost_cert = resolver
1436            .domain_lookup("localhost".as_bytes(), true)
1437            .expect("there should be a localhost cert");
1438
1439        assert_eq!(localhost_cert.1, fingerprint_1y_overriden);
1440
1441        // check that when removing the overriden certificate,
1442        // the resolver falls back on the other one
1443
1444        resolver
1445            .remove_certificate(&fingerprint_1y_overriden)
1446            .expect("should be able to remove the 1-year (3-year-overriden) certificate");
1447
1448        let localhost_cert = resolver
1449            .domain_lookup("localhost".as_bytes(), true)
1450            .expect("there should be a localhost cert");
1451
1452        assert_eq!(localhost_cert.1, fingerprint_2y);
1453
1454        Ok(())
1455    }
1456
1457    /// Verify that `replace_certificate` adds the new cert before removing
1458    /// the old one, so lookup always returns a valid certificate.
1459    #[test]
1460    fn replace_certificate_add_before_remove() -> Result<(), Box<dyn Error + Send + Sync>> {
1461        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1462        let mut resolver = CertificateResolver::default();
1463
1464        // add the initial (1-year) certificate
1465        let cert_1y = CertificateAndKey {
1466            certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1467            key: String::from(include_str!("../assets/tests/key-1y.pem")),
1468            ..Default::default()
1469        };
1470
1471        let fingerprint_1y = resolver.add_certificate(&AddCertificate {
1472            address,
1473            certificate: cert_1y,
1474            expired_at: None,
1475        })?;
1476
1477        // sanity: the 1y cert is resolvable
1478        assert!(
1479            resolver
1480                .domain_lookup("localhost".as_bytes(), true)
1481                .is_some(),
1482            "initial certificate should be resolvable"
1483        );
1484
1485        // replace with the 2-year certificate
1486        let cert_2y = CertificateAndKey {
1487            certificate: String::from(include_str!("../assets/tests/certificate-2y.pem")),
1488            key: String::from(include_str!("../assets/tests/key-2y.pem")),
1489            ..Default::default()
1490        };
1491
1492        let new_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1493            address,
1494            new_certificate: cert_2y,
1495            old_fingerprint: fingerprint_1y.to_string(),
1496            new_expired_at: None,
1497        })?;
1498
1499        // the old certificate should be gone
1500        assert!(
1501            resolver.get_certificate(&fingerprint_1y).is_none(),
1502            "old certificate should have been removed"
1503        );
1504
1505        // the new certificate should be present and resolvable
1506        assert!(
1507            resolver.get_certificate(&new_fingerprint).is_some(),
1508            "new certificate should be present"
1509        );
1510        let resolved = resolver
1511            .domain_lookup("localhost".as_bytes(), true)
1512            .expect("a certificate should resolve for localhost");
1513        assert_eq!(
1514            resolved.1, new_fingerprint,
1515            "resolved certificate should be the replacement"
1516        );
1517
1518        Ok(())
1519    }
1520
1521    /// Direct resolver callers historically receive success when the old
1522    /// fingerprint is malformed: the validated replacement is stored, the
1523    /// unaddressable old entry is retained, and the cleanup failure is logged.
1524    /// `ConfigState` rejects this input before worker dispatch, but the public
1525    /// resolver API must retain its 2.3.x behavior.
1526    #[test]
1527    fn replace_certificate_with_invalid_old_fingerprint_keeps_compatibility()
1528    -> Result<(), Box<dyn Error + Send + Sync>> {
1529        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1530        let mut resolver = CertificateResolver::default();
1531        let old_fingerprint = resolver.add_certificate(&AddCertificate {
1532            address,
1533            certificate: CertificateAndKey {
1534                certificate: include_str!("../assets/tests/certificate-1y.pem").to_owned(),
1535                key: include_str!("../assets/tests/key-1y.pem").to_owned(),
1536                ..Default::default()
1537            },
1538            expired_at: None,
1539        })?;
1540
1541        let new_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1542            address,
1543            new_certificate: CertificateAndKey {
1544                certificate: include_str!("../assets/tests/certificate-2y.pem").to_owned(),
1545                key: include_str!("../assets/tests/key-2y.pem").to_owned(),
1546                ..Default::default()
1547            },
1548            old_fingerprint: "not-a-hex-fingerprint".to_owned(),
1549            new_expired_at: None,
1550        })?;
1551
1552        assert!(resolver.certificates.contains_key(&old_fingerprint));
1553        assert!(resolver.certificates.contains_key(&new_fingerprint));
1554
1555        Ok(())
1556    }
1557
1558    /// When `ReplaceCertificate` carries the same certificate / fingerprint
1559    /// as the existing entry, the previous implementation called
1560    /// `add_certificate` (which early-returns on duplicate fingerprint
1561    /// without inserting) and then unconditionally removed the old
1562    /// fingerprint — i.e. the *current* entry — leaving the resolver
1563    /// without a certificate for that name. The fix short-circuits the
1564    /// idempotent case and keeps the existing entry in place. An
1565    /// idempotent ACME / operator retry must therefore still resolve.
1566    #[test]
1567    fn replace_certificate_with_same_fingerprint_is_noop()
1568    -> Result<(), Box<dyn Error + Send + Sync>> {
1569        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1570        let mut resolver = CertificateResolver::default();
1571
1572        let cert = CertificateAndKey {
1573            certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1574            key: String::from(include_str!("../assets/tests/key-1y.pem")),
1575            ..Default::default()
1576        };
1577
1578        let initial_fingerprint = resolver.add_certificate(&AddCertificate {
1579            address,
1580            certificate: cert.clone(),
1581            expired_at: None,
1582        })?;
1583
1584        // Replace with the SAME PEM/key — identical fingerprint expected.
1585        let returned_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1586            address,
1587            new_certificate: cert,
1588            old_fingerprint: initial_fingerprint.to_string(),
1589            new_expired_at: None,
1590        })?;
1591
1592        assert_eq!(
1593            returned_fingerprint, initial_fingerprint,
1594            "idempotent replace should return the existing fingerprint"
1595        );
1596
1597        assert!(
1598            resolver.get_certificate(&initial_fingerprint).is_some(),
1599            "idempotent replace must NOT delete the existing certificate"
1600        );
1601
1602        let resolved = resolver
1603            .domain_lookup("localhost".as_bytes(), true)
1604            .expect("certificate should still resolve after idempotent replace");
1605        assert_eq!(
1606            resolved.1, initial_fingerprint,
1607            "resolver should still hand back the original fingerprint"
1608        );
1609
1610        Ok(())
1611    }
1612
1613    /// A strict retry must keep the exact stored wrapper and the stable
1614    /// tie-break between certificates that cover the same name with the same
1615    /// expiration. Rebuilding the wrapper is observably different even when
1616    /// the winner happens to stay the same after the stable index sort.
1617    #[test]
1618    fn strict_same_fingerprint_retry_preserves_tie_break_and_wrapper()
1619    -> Result<(), Box<dyn Error + Send + Sync>> {
1620        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1621        let mut resolver = CertificateResolver::default();
1622        let shared_expiration = 2_000_000_000;
1623
1624        let other_certificate = CertificateAndKey {
1625            certificate: include_str!("../assets/tests/certificate-2y.pem").to_owned(),
1626            key: include_str!("../assets/tests/key-2y.pem").to_owned(),
1627            names: vec!["tie.example.com".to_owned()],
1628            ..Default::default()
1629        };
1630        resolver.add_certificate(&AddCertificate {
1631            address,
1632            certificate: other_certificate,
1633            expired_at: Some(shared_expiration),
1634        })?;
1635
1636        let target_certificate = CertificateAndKey {
1637            certificate: include_str!("../assets/tests/certificate-1y.pem").to_owned(),
1638            key: include_str!("../assets/tests/key-1y.pem").to_owned(),
1639            names: vec!["tie.example.com".to_owned()],
1640            ..Default::default()
1641        };
1642        let target_fingerprint = resolver.add_certificate(&AddCertificate {
1643            address,
1644            certificate: target_certificate.to_owned(),
1645            expired_at: Some(shared_expiration),
1646        })?;
1647
1648        let index_before = resolver.name_fingerprint_idx.to_owned();
1649        let domains_before = format!("{:?}", resolver.domains);
1650        let winner_before = resolver
1651            .domain_lookup(b"tie.example.com", true)
1652            .expect("the tied certificates must have a selected winner")
1653            .1
1654            .to_owned();
1655        let wrapper_before = resolver
1656            .certificates
1657            .get(&target_fingerprint)
1658            .expect("the target certificate must be stored")
1659            .inner
1660            .to_owned();
1661
1662        let returned_fingerprint = resolver.replace_certificate(&ReplaceCertificate {
1663            address,
1664            new_certificate: target_certificate,
1665            old_fingerprint: target_fingerprint.to_string(),
1666            new_expired_at: Some(shared_expiration),
1667        })?;
1668
1669        assert_eq!(returned_fingerprint, target_fingerprint);
1670        assert_eq!(winner_before, target_fingerprint);
1671        assert_eq!(resolver.name_fingerprint_idx, index_before);
1672        assert_eq!(format!("{:?}", resolver.domains), domains_before);
1673        assert_eq!(
1674            resolver
1675                .domain_lookup(b"tie.example.com", true)
1676                .expect("the strict retry must preserve the selected winner")
1677                .1,
1678            target_fingerprint,
1679        );
1680        assert!(Arc::ptr_eq(
1681            &wrapper_before,
1682            &resolver
1683                .certificates
1684                .get(&target_fingerprint)
1685                .expect("the strict retry must preserve the stored certificate")
1686                .inner,
1687        ));
1688
1689        Ok(())
1690    }
1691
1692    /// Verify that removing the last certificate for a domain cleans up
1693    /// the empty entry in `name_fingerprint_idx` (no memory leak).
1694    #[test]
1695    fn removal_cleans_up_empty_index_entries() -> Result<(), Box<dyn Error + Send + Sync>> {
1696        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1697        let mut resolver = CertificateResolver::default();
1698
1699        let cert = CertificateAndKey {
1700            certificate: String::from(include_str!("../assets/tests/certificate-1y.pem")),
1701            key: String::from(include_str!("../assets/tests/key-1y.pem")),
1702            ..Default::default()
1703        };
1704
1705        let fingerprint = resolver.add_certificate(&AddCertificate {
1706            address,
1707            certificate: cert,
1708            expired_at: None,
1709        })?;
1710
1711        // record the names associated with this cert
1712        let names = resolver.certificate_names(&fingerprint)?;
1713        assert!(
1714            !names.is_empty(),
1715            "certificate should have at least one name"
1716        );
1717
1718        // verify index is populated
1719        for name in &names {
1720            assert!(
1721                resolver.name_fingerprint_idx.contains_key(name),
1722                "name_fingerprint_idx should contain '{name}' before removal"
1723            );
1724        }
1725
1726        resolver.remove_certificate(&fingerprint)?;
1727
1728        // after removal, all index entries for these names should be gone
1729        for name in &names {
1730            assert!(
1731                !resolver.name_fingerprint_idx.contains_key(name),
1732                "name_fingerprint_idx should not contain empty entry for '{name}' after removal"
1733            );
1734        }
1735
1736        Ok(())
1737    }
1738
1739    /// Many ACME clients (Certbot's `fullchain.pem`, lego, acme.sh) emit
1740    /// the leaf certificate at the START of the chain file. Without
1741    /// dedup, the resolver previously stored `[leaf, leaf, ...]` and
1742    /// the on-wire TLS handshake replayed the leaf twice — accepted by
1743    /// browsers but rejected by stricter validators (Node.js,
1744    /// `UNABLE_TO_VERIFY_LEAF_SIGNATURE`). The fix in
1745    /// `TryFrom<&AddCertificate> for CertifiedKeyWrapper` drops any
1746    /// chain entry whose DER bytes match the leaf. Closes #1135 / #1148.
1747    ///
1748    /// This test passes the SAME leaf PEM as both `certificate` and the
1749    /// sole `certificate_chain` entry (the `fullchain.pem` shape). The
1750    /// stored chain length must be `1` (leaf only), not `2`
1751    /// (`[leaf, leaf]`).
1752    #[test]
1753    fn certificate_chain_dedup_drops_duplicate_leaf() -> Result<(), Box<dyn Error + Send + Sync>> {
1754        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1755        let mut resolver = CertificateResolver::default();
1756
1757        let leaf_pem = String::from(include_str!("../assets/certificate.pem"));
1758
1759        let cert_with_duplicated_leaf = CertificateAndKey {
1760            certificate: leaf_pem.clone(),
1761            certificate_chain: vec![leaf_pem],
1762            key: String::from(include_str!("../assets/key.pem")),
1763            ..Default::default()
1764        };
1765
1766        let fingerprint = resolver.add_certificate(&AddCertificate {
1767            address,
1768            certificate: cert_with_duplicated_leaf,
1769            expired_at: None,
1770        })?;
1771
1772        let stored = resolver
1773            .get_certificate(&fingerprint)
1774            .ok_or("resolver lost the certificate after add")?;
1775
1776        assert_eq!(
1777            stored.inner.cert.len(),
1778            1,
1779            "expected dedup to drop the duplicate leaf, got chain of {} cert(s)",
1780            stored.inner.cert.len()
1781        );
1782
1783        Ok(())
1784    }
1785
1786    /// When an operator passes the entire `fullchain.pem` content as a
1787    /// SINGLE chain entry (one string containing multiple
1788    /// `-----BEGIN CERTIFICATE-----` blocks back-to-back), the previous
1789    /// code called `parse_pem` once on the multi-PEM string, which only
1790    /// consumes the first PEM block and silently drops the rest. The
1791    /// fix splits each chain entry through
1792    /// `split_certificate_chain` so multi-PEM strings fan out into one
1793    /// entry per CA before parsing.
1794    ///
1795    /// This test concatenates two PEM blocks into a single chain entry
1796    /// (the leaf duplicated, so dedup also kicks in). The stored chain
1797    /// must end up with length 1 — the leaf — proving (a) the multi-PEM
1798    /// entry was split correctly, (b) the duplicate leaf was dropped.
1799    #[test]
1800    fn certificate_chain_handles_multi_pem_single_entry() -> Result<(), Box<dyn Error + Send + Sync>>
1801    {
1802        let address = SocketAddress::new_v4(127, 0, 0, 1, 8080);
1803        let mut resolver = CertificateResolver::default();
1804
1805        let leaf_pem = String::from(include_str!("../assets/certificate.pem"));
1806        // Two PEM blocks concatenated into one string; both are the
1807        // leaf so dedup brings the result back to length 1.
1808        let multi_pem_chain_entry = format!("{leaf_pem}\n{leaf_pem}");
1809
1810        let cert = CertificateAndKey {
1811            certificate: leaf_pem,
1812            certificate_chain: vec![multi_pem_chain_entry],
1813            key: String::from(include_str!("../assets/key.pem")),
1814            ..Default::default()
1815        };
1816
1817        let fingerprint = resolver.add_certificate(&AddCertificate {
1818            address,
1819            certificate: cert,
1820            expired_at: None,
1821        })?;
1822
1823        let stored = resolver
1824            .get_certificate(&fingerprint)
1825            .ok_or("resolver lost the certificate after add")?;
1826
1827        // Without the split, `parse_pem` would only consume the first
1828        // PEM block and drop the second; with the split + dedup, both
1829        // get fanned out, both get recognised as the leaf, both get
1830        // dropped — leaving the original leaf at index 0 only.
1831        assert_eq!(
1832            stored.inner.cert.len(),
1833            1,
1834            "expected split + dedup to leave only the leaf, got chain of {} cert(s)",
1835            stored.inner.cert.len()
1836        );
1837
1838        Ok(())
1839    }
1840
1841    /// The SNI half of the same add/lookup case asymmetry, with the two
1842    /// sides swapped.
1843    ///
1844    /// RFC 4343 makes DNS names compare case-insensitively, and rustls
1845    /// hands `ResolvesServerCert::resolve` an SNI it has ALREADY
1846    /// lowercased: `rustls::server::hs::process_client_hello` stores
1847    /// `dns_name.to_lowercase_owned()` (rustls 0.23,
1848    /// `rustls-pki-types`' `DnsName::to_lowercase_owned` is an
1849    /// `to_ascii_lowercase`), and `ClientHello::server_name()` surfaces
1850    /// that field. The ADD side had no such normalisation: a SAN read off
1851    /// the X.509 (`command/src/certificate.rs`'s
1852    /// `get_cn_and_san_attributes`) or an operator-supplied `names`
1853    /// override went into the SNI trie and the name index verbatim.
1854    ///
1855    /// So a certificate carrying `MiXeD.Example.COM` was reachable by no
1856    /// SNI at all — every handshake for that name fell through to the
1857    /// `DEFAULT_CERTIFICATE` arm of `MutexCertificateResolver::resolve`,
1858    /// which also increments `names::tls::DEFAULT_CERT_USED`, while the
1859    /// certificate stayed recorded and queryable.
1860    ///
1861    /// To SEE THIS RED: in `CertifiedKeyWrapper::try_from`, delete the
1862    /// `let overriding_names: Vec<String> = overriding_names …` rebind
1863    /// that maps `|mut name| { name.make_ascii_lowercase(); name }`, so
1864    /// the names reach the trie as written. The FIRST assertion below
1865    /// fails — `domain_lookup(b"mixed.example.com", true)` is `None`, so
1866    /// the test panics there and the later ones are never reached.
1867    #[test]
1868    fn a_mixed_case_certificate_name_resolves_for_the_lowercased_sni() {
1869        const MIXED: &str = "MiXeD.Example.COM";
1870        const LOWER: &str = "mixed.example.com";
1871
1872        let mut resolver = CertificateResolver::default();
1873        let fingerprint = resolver
1874            .add_certificate(&AddCertificate {
1875                address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1876                certificate: CertificateAndKey {
1877                    certificate: include_str!("../assets/certificate.pem").to_owned(),
1878                    key: include_str!("../assets/key.pem").to_owned(),
1879                    names: vec![MIXED.to_owned()],
1880                    ..Default::default()
1881                },
1882                expired_at: None,
1883            })
1884            .expect("the mixed-case certificate must load");
1885
1886        assert!(
1887            resolver.domain_lookup(LOWER.as_bytes(), true).is_some(),
1888            "rustls only ever asks for the lowercased SNI, so that is the \
1889             only key the trie can usefully hold",
1890        );
1891        assert_eq!(
1892            resolver.names_for_sni(LOWER.as_bytes()),
1893            Some(vec![LOWER.to_owned()]),
1894            "the SAN snapshot feeding the H2 `:authority` binding must \
1895             carry the normalised name",
1896        );
1897
1898        // Removal walks the certificate's stored names, so it stays
1899        // symmetric with the add path by construction: normalise once at
1900        // the single point where `names` is built and both verbs agree.
1901        resolver
1902            .remove_certificate(&fingerprint)
1903            .expect("the mixed-case certificate must be removable");
1904        assert!(
1905            resolver.domain_lookup(LOWER.as_bytes(), true).is_none(),
1906            "removing the certificate must retire its normalised trie key",
1907        );
1908    }
1909
1910    /// A certificate name enters the SNI `TrieNode` verbatim, and that
1911    /// trie is the same `lib/src/router/pattern_trie.rs` the HTTP router
1912    /// uses: a dot-separated label wrapped in `/.../` is compiled as a
1913    /// REGEX segment (`insert_recursive` -> `anchored_segment` ->
1914    /// `Regex::new`). So an operator `names` entry such as
1915    /// `/te.*/.example.com` bound ONE certificate to an open-ended class
1916    /// of SNI values -- measured on `c7ac070e`, that name was served for
1917    /// both `test.example.com` and `tenant.example.com`
1918    /// (sozu-proxy/sozu#1378). A name like `/.*/.example.com` is one
1919    /// character away from a plausible typo, and the certificate
1920    /// inventory an operator reads shows a SAN-shaped string while the
1921    /// trie holds a pattern.
1922    ///
1923    /// The validation that already existed could not catch it: it
1924    /// dry-runs every name into a scratch trie and asks only whether the
1925    /// name INSERTS, and a regex segment inserts perfectly well. The
1926    /// check therefore has to be on the name's BYTES, and it has to run
1927    /// in the same pre-mutation block as the `MAX_HOSTNAME_LENGTH` bound
1928    /// so nothing is half-registered. `/` cannot occur in a DNS name, so
1929    /// nothing legitimate is refused -- and the ordinary `*.` wildcard
1930    /// name, which the trie handles as a wildcard and not as a regex,
1931    /// still loads; the second half of this test pins that the guard was
1932    /// not widened into "refuse anything that is not a literal".
1933    ///
1934    /// This mirrors `validate_sni_pattern` (`command/src/config.rs`),
1935    /// which already rejects `/` on every path into the TCP SNI route
1936    /// table; the certificate trie was the one SNI table left open.
1937    ///
1938    /// To SEE THIS RED: in `build_validated_certificate`'s pre-mutation
1939    /// dry-run, delete the `|| name.contains('/')` disjunct from the rejection
1940    /// condition. The certificate then loads and `expect_err` panics on the
1941    /// returned `Ok(fingerprint)`, so the test fails at its first statement.
1942    #[test]
1943    fn a_certificate_name_carrying_a_regex_segment_is_refused() {
1944        const REGEX_NAME: &str = "/te.*/.example.com";
1945
1946        let mut resolver = CertificateResolver::default();
1947        let error = resolver
1948            .add_certificate(&AddCertificate {
1949                address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1950                certificate: CertificateAndKey {
1951                    certificate: include_str!("../assets/certificate.pem").to_owned(),
1952                    key: include_str!("../assets/key.pem").to_owned(),
1953                    names: vec![REGEX_NAME.to_owned()],
1954                    ..Default::default()
1955                },
1956                expired_at: None,
1957            })
1958            .expect_err("a certificate name carrying a regex segment must be refused");
1959
1960        assert!(
1961            matches!(&error, CertificateResolverError::InvalidName(name) if name == REGEX_NAME),
1962            "the refusal must be InvalidName, got {error:?}",
1963        );
1964
1965        // Refused BEFORE any mutation, exactly like the
1966        // `MAX_HOSTNAME_LENGTH` bound it sits beside: no certificate
1967        // stored, no name indexed, and -- the point of the report -- no
1968        // open-ended class of SNI values bound in the trie.
1969        assert!(
1970            resolver.certificates.is_empty(),
1971            "a refused certificate must not be stored",
1972        );
1973        assert!(
1974            resolver.name_fingerprint_idx.is_empty(),
1975            "a refused certificate must not index any name",
1976        );
1977        for sni in ["test.example.com", "tenant.example.com", REGEX_NAME] {
1978            assert!(
1979                resolver.domain_lookup(sni.as_bytes(), true).is_none(),
1980                "a refused certificate name must bind no SNI, yet {sni} resolved",
1981            );
1982        }
1983
1984        // The guard is on `/` alone. An ordinary wildcard SAN is not a
1985        // regex segment, still loads, and still serves its subtree.
1986        resolver
1987            .add_certificate(&AddCertificate {
1988                address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1989                certificate: CertificateAndKey {
1990                    certificate: include_str!("../assets/certificate.pem").to_owned(),
1991                    key: include_str!("../assets/key.pem").to_owned(),
1992                    names: vec!["*.example.com".to_owned()],
1993                    ..Default::default()
1994                },
1995                expired_at: None,
1996            })
1997            .expect("a wildcard certificate name must still load");
1998        assert!(
1999            resolver
2000                .domain_lookup(b"tenant.example.com", true)
2001                .is_some(),
2002            "the wildcard certificate must still serve a subdomain",
2003        );
2004    }
2005}