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