Skip to main content

zentinel_proxy/
tls.rs

1//! TLS Configuration and SNI Support
2//!
3//! This module provides TLS configuration with Server Name Indication (SNI) support
4//! for serving multiple certificates based on the requested hostname.
5//!
6//! # Features
7//!
8//! - SNI-based certificate selection
9//! - Wildcard certificate matching (e.g., `*.example.com`)
10//! - Automatic CN/SAN extraction when hostnames are omitted
11//! - Default certificate fallback
12//! - Certificate validation at startup
13//! - mTLS client certificate verification
14//! - Certificate hot-reload on SIGHUP
15//! - OCSP stapling support
16//!
17//! # Example KDL Configuration
18//!
19//! ```kdl
20//! listener "https" {
21//!     address "0.0.0.0:443"
22//!     protocol "https"
23//!     tls {
24//!         cert-file "/etc/certs/default.crt"
25//!         key-file "/etc/certs/default.key"
26//!
27//!         // SNI certificates with explicit hostnames
28//!         sni {
29//!             hostnames "example.com" "www.example.com"
30//!             cert-file "/etc/certs/example.crt"
31//!             key-file "/etc/certs/example.key"
32//!         }
33//!         sni {
34//!             hostnames "*.api.example.com"
35//!             cert-file "/etc/certs/api-wildcard.crt"
36//!             key-file "/etc/certs/api-wildcard.key"
37//!         }
38//!
39//!         // SNI certificate with auto-extracted hostnames from CN/SAN
40//!         sni {
41//!             cert-file "/etc/certs/premium.crt"
42//!             key-file "/etc/certs/premium.key"
43//!         }
44//!
45//!         // SNI certificate with priority tie-breaking (auto-extracts all SANs,
46//!         // but this cert wins for "shared.example.com" if contested)
47//!         sni {
48//!             priority-hostnames "shared.example.com"
49//!             cert-file "/etc/certs/shared.crt"
50//!             key-file "/etc/certs/shared.key"
51//!         }
52//!
53//!         // mTLS configuration
54//!         ca-file "/etc/certs/ca.crt"
55//!         client-auth true
56//!
57//!         // OCSP stapling
58//!         ocsp-stapling true
59//!     }
60//! }
61//! ```
62
63use std::collections::{BTreeMap, HashMap, HashSet};
64use std::fs::File;
65use std::io::BufReader;
66use std::path::{Path, PathBuf};
67use std::sync::Arc;
68use std::time::{Duration, Instant};
69
70use parking_lot::RwLock;
71use rustls::client::ClientConfig;
72use rustls::pki_types::CertificateDer;
73use rustls::server::{ClientHello, ResolvesServerCert};
74use rustls::sign::CertifiedKey;
75use rustls::{RootCertStore, ServerConfig};
76use tracing::{debug, error, info, trace, warn};
77
78use zentinel_config::{SniCertFolder, SniCertificate, TlsConfig, UpstreamTlsConfig};
79
80/// Error type for TLS operations
81#[derive(Debug)]
82pub enum TlsError {
83    /// Failed to load certificate file
84    CertificateLoad(String),
85    /// Failed to load private key file
86    KeyLoad(String),
87    /// Failed to build TLS configuration
88    ConfigBuild(String),
89    /// Certificate/key mismatch
90    CertKeyMismatch(String),
91    /// Invalid certificate
92    InvalidCertificate(String),
93    /// OCSP fetch error
94    OcspFetch(String),
95}
96
97impl std::fmt::Display for TlsError {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        match self {
100            TlsError::CertificateLoad(e) => write!(f, "Failed to load certificate: {}", e),
101            TlsError::KeyLoad(e) => write!(f, "Failed to load private key: {}", e),
102            TlsError::ConfigBuild(e) => write!(f, "Failed to build TLS config: {}", e),
103            TlsError::CertKeyMismatch(e) => write!(f, "Certificate/key mismatch: {}", e),
104            TlsError::InvalidCertificate(e) => write!(f, "Invalid certificate: {}", e),
105            TlsError::OcspFetch(e) => write!(f, "Failed to fetch OCSP response: {}", e),
106        }
107    }
108}
109
110impl std::error::Error for TlsError {}
111
112/// SNI-aware certificate resolver
113///
114/// Resolves certificates based on the Server Name Indication (SNI) extension
115/// in the TLS handshake. Supports:
116/// - Exact hostname matches
117/// - Wildcard certificates (e.g., `*.example.com`)
118/// - Default certificate fallback
119#[derive(Debug)]
120pub struct SniResolver {
121    /// Default certificate (used when no SNI match)
122    default_cert: Arc<CertifiedKey>,
123    /// SNI hostname to certificate mapping
124    /// Key is lowercase hostname, value is the certified key
125    sni_certs: HashMap<String, Arc<CertifiedKey>>,
126    /// Wildcard certificates (e.g., "*.example.com" -> cert)
127    wildcard_certs: HashMap<String, Arc<CertifiedKey>>,
128}
129
130/// The name used for the default certificate in reload diffs.
131///
132/// It has no SNI hostname of its own but is what an unmatched name is served,
133/// so a silent rotation of it would be exactly as invisible as any other.
134const DEFAULT_CERT_LABEL: &str = "<default>";
135
136/// SHA-256 of a leaf certificate's DER encoding, as lowercase hex.
137///
138/// This is the same digest `openssl x509 -fingerprint -sha256 -noout` prints, so
139/// a fingerprint in the log can be matched against a file on disk without
140/// guesswork. Truncated to 16 hex characters: enough to distinguish the
141/// certificates on one listener, short enough to stay readable in a log line.
142fn cert_fingerprint(cert: &CertifiedKey) -> String {
143    use sha2::{Digest, Sha256};
144
145    let Some(leaf) = cert.cert.first() else {
146        return "unknown".to_string();
147    };
148    let digest = Sha256::digest(leaf.as_ref());
149    digest.iter().take(8).map(|b| format!("{b:02x}")).collect()
150}
151
152/// What a reload actually changed, in terms an operator can act on.
153///
154/// Counts alone cannot answer "did anything change?": the common case is a
155/// renewal, where a certificate is replaced by one covering the same names and
156/// every count stays identical. `replaced` is what makes that visible.
157#[derive(Debug, Default, PartialEq, Eq)]
158pub(crate) struct CertDiff {
159    /// Hostnames served now that were not served before.
160    pub added: Vec<String>,
161    /// Hostnames no longer served.
162    pub removed: Vec<String>,
163    /// Hostnames still served, but by a different certificate — i.e. a renewal.
164    pub replaced: Vec<String>,
165}
166
167impl CertDiff {
168    /// Whether the reload was a no-op as far as served certificates go.
169    pub(crate) fn is_empty(&self) -> bool {
170        self.added.is_empty() && self.removed.is_empty() && self.replaced.is_empty()
171    }
172
173    /// Compute the diff between two snapshots of `hostname -> fingerprint`.
174    fn between(before: &BTreeMap<String, String>, after: &BTreeMap<String, String>) -> Self {
175        let mut diff = Self::default();
176
177        for (hostname, new_fp) in after {
178            match before.get(hostname) {
179                None => diff.added.push(hostname.clone()),
180                Some(old_fp) if old_fp != new_fp => {
181                    diff.replaced
182                        .push(format!("{hostname} ({old_fp}->{new_fp})"));
183                }
184                Some(_) => {}
185            }
186        }
187        for hostname in before.keys() {
188            if !after.contains_key(hostname) {
189                diff.removed.push(hostname.clone());
190            }
191        }
192
193        diff
194    }
195
196    /// Render one field of the diff for logging, or `-` when it is empty.
197    fn render(names: &[String]) -> String {
198        if names.is_empty() {
199            "-".to_string()
200        } else {
201            names.join(", ")
202        }
203    }
204}
205
206impl SniResolver {
207    /// Every hostname this resolver serves, mapped to its certificate
208    /// fingerprint.
209    ///
210    /// Built on demand at reload time only, so the hot path pays nothing for it.
211    pub fn served_certs(&self) -> BTreeMap<String, String> {
212        let mut served = BTreeMap::new();
213        served.insert(
214            DEFAULT_CERT_LABEL.to_string(),
215            cert_fingerprint(&self.default_cert),
216        );
217        for (hostname, cert) in self.sni_certs.iter().chain(self.wildcard_certs.iter()) {
218            served.insert(hostname.clone(), cert_fingerprint(cert));
219        }
220        served
221    }
222
223    /// Create a new SNI resolver from TLS configuration
224    pub fn from_config(config: &TlsConfig, listener_id: Option<&str>) -> Result<Self, TlsError> {
225        let listener_id_str = listener_id.unwrap_or("unknown");
226
227        // Get cert_file and key_file - manual certs or ACME-managed paths
228        let (cert_path_buf, key_path_buf);
229        let (cert_file, key_file) = match (&config.cert_file, &config.key_file) {
230            (Some(cert), Some(key)) => (cert.as_path(), key.as_path()),
231            _ if config.acme.is_some() => {
232                let acme = config.acme.as_ref().unwrap();
233                let primary = acme.domains.first().ok_or_else(|| {
234                    TlsError::ConfigBuild(
235                        "ACME configuration has no domains for cert path resolution".to_string(),
236                    )
237                })?;
238                cert_path_buf = acme.storage.join("domains").join(primary).join("cert.pem");
239                key_path_buf = acme.storage.join("domains").join(primary).join("key.pem");
240                (cert_path_buf.as_path(), key_path_buf.as_path())
241            }
242            _ => {
243                return Err(TlsError::ConfigBuild(
244                    "TLS configuration requires cert_file and key_file (or ACME block)".to_string(),
245                ));
246            }
247        };
248
249        // Load default certificate
250        let default_cert = load_certified_key(cert_file, key_file)?;
251
252        info!(
253            listener_id = %listener_id_str,
254            cert_file = %cert_file.display(),
255            "Loaded default TLS certificate"
256        );
257
258        let mut sni_certs = HashMap::new();
259        let mut wildcard_certs = HashMap::new();
260
261        // Track which hostnames were registered with priority, so we can resolve
262        // conflicts during build. These sets are not stored in the final resolver
263        // because priority is a build-time concept only.
264        let mut priority_exact: HashSet<String> = HashSet::new();
265        let mut priority_wildcard: HashSet<String> = HashSet::new();
266
267        // Certificates found by scanning configured folders are registered
268        // exactly like explicit `sni` blocks with no `hostnames`: their names
269        // come from the certificate's CN and SANs. Feeding them through the
270        // same loop keeps one implementation of hostname extraction, priority
271        // handling and overlap detection.
272        let scanned = scan_cert_folders(&config.cert_folders, listener_id_str);
273        let all_sni_certs: Vec<&SniCertificate> = config
274            .additional_certs
275            .iter()
276            .chain(scanned.iter())
277            .collect();
278
279        // Load SNI certificates
280        for (i, sni_config) in all_sni_certs.into_iter().enumerate() {
281            // Resolve paths for this SNI cert
282            let (sni_cert_path_buf, sni_key_path_buf);
283            let (sni_cert_path, sni_key_path) = match (&sni_config.cert_file, &sni_config.key_file)
284            {
285                (Some(cert), Some(key)) => (cert.as_path(), key.as_path()),
286                _ if sni_config.acme.is_some() => {
287                    let acme = sni_config.acme.as_ref().unwrap();
288                    let primary = acme.domains.first().ok_or_else(|| {
289                        TlsError::ConfigBuild("SNI ACME configuration has no domains".to_string())
290                    })?;
291                    sni_cert_path_buf = acme.storage.join("domains").join(primary).join("cert.pem");
292                    sni_key_path_buf = acme.storage.join("domains").join(primary).join("key.pem");
293                    (sni_cert_path_buf.as_path(), sni_key_path_buf.as_path())
294                }
295                _ => unreachable!("Config validation ensures certs or acme"),
296            };
297
298            let cert = match load_certified_key(sni_cert_path, sni_key_path) {
299                Ok(cert) => Arc::new(cert),
300                Err(e) => {
301                    // If ACME is configured, the certificate might not exist yet.
302                    // We log a warning and skip this certificate for now.
303                    // It will be loaded later via hot-reload once issued.
304                    if let Some(acme) = &sni_config.acme {
305                        let primary = acme
306                            .domains
307                            .first()
308                            .map(|s| s.as_str())
309                            .unwrap_or("unknown");
310                        warn!(
311                            listener_id = %listener_id_str,
312                            sni_index = i,
313                            primary_domain = %primary,
314                            error = %e,
315                            "ACME SNI certificate not yet available, skipping initial load"
316                        );
317
318                        // Record metric for observability
319                        if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
320                            metrics.record_sni_cert_skip(listener_id_str, primary);
321                        }
322
323                        continue;
324                    } else {
325                        return Err(e);
326                    }
327                }
328            };
329
330            // Build priority set for this cert (lowercased for consistent matching)
331            let priority_set: HashSet<String> = sni_config
332                .priority_hostnames
333                .iter()
334                .map(|h| h.to_lowercase())
335                .collect();
336            let has_priority = !priority_set.is_empty();
337
338            // Determine hostnames: use explicit config, acme domains, or auto-extract from certificate.
339            let hostnames = if !sni_config.hostnames.is_empty() {
340                sni_config.hostnames.clone()
341            } else if !priority_set.is_empty() {
342                // When priority_hostnames is set, we always auto-extract.
343                extract_hostnames_from_cert(cert.cert.first().unwrap())?
344            } else if let Some(ref acme) = sni_config.acme {
345                // If ACME is present and no explicit hostnames, use ACME domains.
346                acme.domains.clone()
347            } else {
348                // Fallback to auto-extraction
349                extract_hostnames_from_cert(cert.cert.first().unwrap())?
350            };
351
352            if has_priority {
353                info!(
354                    cert_file = %sni_cert_path.display(),
355                    hostnames = ?hostnames,
356                    priority_hostnames = ?sni_config.priority_hostnames,
357                    "Loaded SNI certificate with priority tie-breaking"
358                );
359            } else if sni_config.hostnames.is_empty() && sni_config.acme.is_none() {
360                info!(
361                    cert_file = %sni_cert_path.display(),
362                    hostnames = ?hostnames,
363                    "Loaded SNI certificate (auto-extracted hostnames)"
364                );
365            } else {
366                info!(
367                    cert_file = %sni_cert_path.display(),
368                    hostnames = ?hostnames,
369                    "Loaded SNI certificate"
370                );
371            }
372
373            for hostname in &hostnames {
374                let hostname_lower = hostname.to_lowercase();
375                let is_priority = priority_set.contains(&hostname_lower);
376
377                if hostname_lower.starts_with("*.") {
378                    // Wildcard certificate
379                    let domain = hostname_lower.strip_prefix("*.").unwrap().to_string();
380
381                    if let Some(existing) = wildcard_certs.get(&domain) {
382                        if !Arc::ptr_eq(existing, &cert) {
383                            let existing_has_priority = priority_wildcard.contains(&domain);
384
385                            if is_priority && existing_has_priority {
386                                // Both certs claim priority for the same wildcard
387                                return Err(TlsError::ConfigBuild(format!(
388                                    "Conflicting priority-hostnames: wildcard '*.{}' is claimed as priority by multiple certificates (including {:?}).",
389                                    domain,
390                                    sni_cert_path
391                                )));
392                            } else if is_priority {
393                                // New cert has priority, overwrite the existing one
394                                debug!(
395                                    pattern = %hostname,
396                                    domain = %domain,
397                                    cert_file = %sni_cert_path.display(),
398                                    "Priority wildcard SNI certificate overwrites previous registration"
399                                );
400                            } else if existing_has_priority {
401                                // Existing cert has priority, skip the new one
402                                debug!(
403                                    pattern = %hostname,
404                                    domain = %domain,
405                                    cert_file = %sni_cert_path.display(),
406                                    "Skipping wildcard SNI registration, existing cert has priority"
407                                );
408                                continue;
409                            } else if config.allow_sni_overlaps {
410                                // Overlaps accepted: the first registration
411                                // wins. Certificates are registered in sorted
412                                // path order, so the winner is the same on
413                                // every machine and across reloads rather
414                                // than whatever the filesystem listed first.
415                                warn!(
416                                    listener_id = %listener_id_str,
417                                    pattern = %hostname,
418                                    cert_file = %sni_cert_path.display(),
419                                    "Overlapping wildcard SNI certificate ignored; \
420                                     an earlier certificate already claims this name"
421                                );
422                                continue;
423                            } else {
424                                // Neither has priority, ambiguity error
425                                return Err(TlsError::ConfigBuild(format!(
426                                    "Ambiguous SNI configuration: wildcard '*.{}' matches multiple certificates (including {:?}). \
427                                     Use explicit 'hostnames' or 'priority-hostnames' to resolve the conflict, \
428                                     or set 'allow-sni-overlaps true' to accept the first match in path order.",
429                                    domain,
430                                    sni_cert_path
431                                )));
432                            }
433                        }
434                    }
435
436                    wildcard_certs.insert(domain.clone(), cert.clone());
437                    if is_priority {
438                        priority_wildcard.insert(domain.clone());
439                    }
440                    debug!(
441                        pattern = %hostname,
442                        domain = %domain,
443                        priority = is_priority,
444                        cert_file = %sni_cert_path.display(),
445                        "Registered wildcard SNI certificate"
446                    );
447                } else {
448                    // Exact hostname match
449                    if let Some(existing) = sni_certs.get(&hostname_lower) {
450                        if !Arc::ptr_eq(existing, &cert) {
451                            let existing_has_priority = priority_exact.contains(&hostname_lower);
452
453                            if is_priority && existing_has_priority {
454                                // Both certs claim priority for the same hostname
455                                return Err(TlsError::ConfigBuild(format!(
456                                    "Conflicting priority-hostnames: hostname '{}' is claimed as priority by multiple certificates (including {:?}).",
457                                    hostname_lower,
458                                    sni_cert_path
459                                )));
460                            } else if is_priority {
461                                // New cert has priority, overwrite
462                                debug!(
463                                    hostname = %hostname_lower,
464                                    cert_file = %sni_cert_path.display(),
465                                    "Priority SNI certificate overwrites previous registration"
466                                );
467                            } else if existing_has_priority {
468                                // Existing cert has priority, skip
469                                debug!(
470                                    hostname = %hostname_lower,
471                                    cert_file = %sni_cert_path.display(),
472                                    "Skipping SNI registration, existing cert has priority"
473                                );
474                                continue;
475                            } else if config.allow_sni_overlaps {
476                                warn!(
477                                    listener_id = %listener_id_str,
478                                    hostname = %hostname_lower,
479                                    cert_file = %sni_cert_path.display(),
480                                    "Overlapping SNI certificate ignored; \
481                                     an earlier certificate already claims this name"
482                                );
483                                continue;
484                            } else {
485                                // Neither has priority, ambiguity error
486                                return Err(TlsError::ConfigBuild(format!(
487                                    "Ambiguous SNI configuration: hostname '{}' matches multiple certificates (including {:?}). \
488                                     Use explicit 'hostnames' or 'priority-hostnames' to resolve the conflict, \
489                                     or set 'allow-sni-overlaps true' to accept the first match in path order.",
490                                    hostname_lower,
491                                    sni_cert_path
492                                )));
493                            }
494                        }
495                    }
496
497                    sni_certs.insert(hostname_lower.clone(), cert.clone());
498                    if is_priority {
499                        priority_exact.insert(hostname_lower.clone());
500                    }
501                    debug!(
502                        hostname = %hostname_lower,
503                        priority = is_priority,
504                        cert_file = %sni_cert_path.display(),
505                        "Registered SNI certificate"
506                    );
507                }
508            }
509        }
510
511        info!(
512            listener_id = %listener_id_str,
513            exact_certs = sni_certs.len(),
514            wildcard_certs = wildcard_certs.len(),
515            "SNI resolver initialized"
516        );
517
518        if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
519            // The default certificate counts too: it is what an unmatched
520            // name is served.
521            metrics.set_certificates_loaded(
522                listener_id_str,
523                1 + sni_certs.len() + wildcard_certs.len(),
524            );
525        }
526
527        Ok(Self {
528            default_cert: Arc::new(default_cert),
529            sni_certs,
530            wildcard_certs,
531        })
532    }
533
534    /// Resolve certificate for a given server name
535    ///
536    /// This is the core resolution logic. For the rustls trait implementation,
537    /// see `ResolvesServerCert`.
538    pub fn resolve(&self, server_name: Option<&str>) -> Arc<CertifiedKey> {
539        let Some(name) = server_name else {
540            debug!("No SNI provided, using default certificate");
541            return self.default_cert.clone();
542        };
543
544        let name_lower = name.to_lowercase();
545
546        // Try exact match first
547        if let Some(cert) = self.sni_certs.get(&name_lower) {
548            debug!(hostname = %name_lower, "SNI exact match found");
549            return cert.clone();
550        }
551
552        // Try wildcard match
553        // For "foo.bar.example.com", try "bar.example.com", then "example.com"
554        let parts: Vec<&str> = name_lower.split('.').collect();
555        for i in 1..parts.len() {
556            let domain = parts[i..].join(".");
557            if let Some(cert) = self.wildcard_certs.get(&domain) {
558                debug!(
559                    hostname = %name_lower,
560                    wildcard_domain = %domain,
561                    "SNI wildcard match found"
562                );
563                return cert.clone();
564            }
565        }
566
567        debug!(
568            hostname = %name_lower,
569            "No SNI match found, using default certificate"
570        );
571        self.default_cert.clone()
572    }
573}
574
575impl ResolvesServerCert for SniResolver {
576    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
577        Some(self.resolve(client_hello.server_name()))
578    }
579}
580
581// ============================================================================
582// Hot-Reloadable Certificate Support
583// ============================================================================
584
585/// Hot-reloadable SNI certificate resolver
586///
587/// Wraps an SniResolver behind an RwLock to allow certificate hot-reload
588/// without restarting the server. On SIGHUP, the inner resolver is replaced
589/// with a newly loaded one.
590pub struct HotReloadableSniResolver {
591    /// Inner resolver (protected by RwLock for hot-reload)
592    inner: RwLock<Arc<SniResolver>>,
593    /// Original config for reloading
594    config: RwLock<TlsConfig>,
595    /// Listener ID for observability
596    listener_id: String,
597    /// Last reload time
598    last_reload: RwLock<Instant>,
599}
600
601impl std::fmt::Debug for HotReloadableSniResolver {
602    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603        f.debug_struct("HotReloadableSniResolver")
604            .field("last_reload", &*self.last_reload.read())
605            .field("listener_id", &self.listener_id)
606            .finish()
607    }
608}
609
610impl HotReloadableSniResolver {
611    /// Create a new hot-reloadable resolver from TLS configuration
612    pub fn from_config(
613        config: TlsConfig,
614        listener_id: impl Into<String>,
615    ) -> Result<Self, TlsError> {
616        let listener_id = listener_id.into();
617        let resolver = SniResolver::from_config(&config, Some(&listener_id))?;
618
619        Ok(Self {
620            inner: RwLock::new(Arc::new(resolver)),
621            config: RwLock::new(config),
622            listener_id,
623            last_reload: RwLock::new(Instant::now()),
624        })
625    }
626
627    /// Reload certificates from disk
628    ///
629    /// This is called on SIGHUP to pick up new certificates without restart.
630    /// If the reload fails, the old certificates continue to be used.
631    pub fn reload(&self) -> Result<(), TlsError> {
632        let config = self.config.read();
633
634        let cert_file_display = config
635            .cert_file
636            .as_ref()
637            .map(|p| p.display().to_string())
638            .unwrap_or_else(|| "(acme-managed)".to_string());
639
640        info!(
641            listener_id = %self.listener_id,
642            cert_file = %cert_file_display,
643            sni_count = config.additional_certs.len(),
644            "Reloading TLS certificates"
645        );
646
647        // Try to load new certificates. A failure leaves the previous ones in
648        // place, which is invisible in traffic -- hence the counter.
649        let new_resolver = match SniResolver::from_config(&config, Some(&self.listener_id)) {
650            Ok(resolver) => resolver,
651            Err(e) => {
652                if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
653                    metrics.record_reload(&self.listener_id, false);
654                }
655                return Err(e);
656            }
657        };
658
659        // Snapshot both sides before the swap. With `reload-mode "watch"` the
660        // reload is unattended by design, so this log line is the only record an
661        // operator has of what a rescan actually did.
662        let before = self.inner.read().served_certs();
663        let after = new_resolver.served_certs();
664        let diff = CertDiff::between(&before, &after);
665
666        // Swap in the new resolver atomically
667        *self.inner.write() = Arc::new(new_resolver);
668        *self.last_reload.write() = Instant::now();
669
670        if diff.is_empty() {
671            info!(
672                listener_id = %self.listener_id,
673                hostnames = after.len(),
674                "TLS certificates reloaded successfully; no change to served certificates"
675            );
676        } else {
677            info!(
678                listener_id = %self.listener_id,
679                hostnames = after.len(),
680                added = %CertDiff::render(&diff.added),
681                removed = %CertDiff::render(&diff.removed),
682                replaced = %CertDiff::render(&diff.replaced),
683                "TLS certificates reloaded successfully"
684            );
685        }
686        if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
687            metrics.record_reload(&self.listener_id, true);
688        }
689        Ok(())
690    }
691
692    /// Update configuration and reload
693    pub fn update_config(&self, new_config: TlsConfig) -> Result<(), TlsError> {
694        // Load with new config first
695        let new_resolver = SniResolver::from_config(&new_config, Some(&self.listener_id))?;
696
697        // Update both config and resolver
698        *self.config.write() = new_config;
699        *self.inner.write() = Arc::new(new_resolver);
700        *self.last_reload.write() = Instant::now();
701
702        info!(
703            listener_id = %self.listener_id,
704            "TLS configuration updated and certificates reloaded"
705        );
706        Ok(())
707    }
708
709    /// Get time since last reload
710    pub fn last_reload_age(&self) -> Duration {
711        self.last_reload.read().elapsed()
712    }
713
714    /// Resolve certificate for a given server name
715    ///
716    /// This is the core resolution logic exposed for testing.
717    pub fn resolve(&self, server_name: Option<&str>) -> Arc<CertifiedKey> {
718        self.inner.read().resolve(server_name)
719    }
720}
721
722impl ResolvesServerCert for HotReloadableSniResolver {
723    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
724        Some(self.inner.read().resolve(client_hello.server_name()))
725    }
726}
727
728/// Extensions treated as certificates, paired with a key of the same stem.
729const CERT_EXTENSIONS: &[&str] = &["crt", "pem", "cert"];
730
731/// Extensions treated as private keys.
732const KEY_EXTENSIONS: &[&str] = &["key"];
733
734/// Scan configured folders for certificate/key pairs.
735///
736/// A pair is a certificate file and a key file sharing a stem — `a.crt` with
737/// `a.key`. Entries are returned sorted by path so that registration order,
738/// and therefore any tie-break between overlapping certificates, does not
739/// depend on the order the filesystem happens to return.
740///
741/// Problems with individual files are skipped and warned about rather than
742/// failing the scan. A folder is a moving target — certificates are written
743/// there by other processes, sometimes non-atomically — so one half-written
744/// file must not take down every other certificate on the listener. The
745/// warning is what keeps that from being silent.
746fn scan_cert_folders(folders: &[SniCertFolder], listener_id: &str) -> Vec<SniCertificate> {
747    let mut found = Vec::new();
748
749    for folder in folders {
750        let dir = &folder.cert_folder;
751        let entries = match std::fs::read_dir(dir) {
752            Ok(entries) => entries,
753            Err(e) => {
754                warn!(
755                    listener_id = %listener_id,
756                    cert_folder = %dir.display(),
757                    error = %e,
758                    "Certificate folder could not be read; no certificates loaded from it"
759                );
760                continue;
761            }
762        };
763
764        // Collect and sort first: read_dir order is unspecified, and a
765        // tie-break that depends on it would resolve differently between
766        // machines or after a reload.
767        let mut paths: Vec<PathBuf> = entries
768            .filter_map(|e| e.ok().map(|e| e.path()))
769            .filter(|p| p.is_file())
770            .collect();
771        paths.sort();
772
773        let mut pairs = 0usize;
774        for cert_path in &paths {
775            let Some(extension) = cert_path.extension().and_then(|e| e.to_str()) else {
776                continue;
777            };
778            if !CERT_EXTENSIONS.contains(&extension.to_ascii_lowercase().as_str()) {
779                continue;
780            }
781
782            let Some(key_path) = matching_key_path(cert_path) else {
783                warn!(
784                    listener_id = %listener_id,
785                    cert_file = %cert_path.display(),
786                    "Certificate in scanned folder has no matching key file; skipping"
787                );
788                if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
789                    metrics.record_folder_entry_skipped(listener_id, "no_key");
790                }
791                continue;
792            };
793
794            // Load it here purely to reject unusable pairs with a precise
795            // message. The pair is loaded again by the caller, which is cheap
796            // next to serving traffic with a certificate that turns out to be
797            // unreadable.
798            if let Err(e) = load_certified_key(cert_path, &key_path) {
799                warn!(
800                    listener_id = %listener_id,
801                    cert_file = %cert_path.display(),
802                    key_file = %key_path.display(),
803                    error = %e,
804                    "Certificate pair in scanned folder could not be loaded; skipping"
805                );
806                if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
807                    metrics.record_folder_entry_skipped(listener_id, "unreadable");
808                }
809                continue;
810            }
811
812            found.push(SniCertificate {
813                hostnames: Vec::new(),
814                priority_hostnames: Vec::new(),
815                cert_file: Some(cert_path.clone()),
816                key_file: Some(key_path),
817                acme: None,
818            });
819            pairs += 1;
820        }
821
822        info!(
823            listener_id = %listener_id,
824            cert_folder = %dir.display(),
825            certificates = pairs,
826            reload_mode = %folder.reload_mode,
827            "Scanned certificate folder"
828        );
829    }
830
831    found
832}
833
834/// Find the key file belonging to a certificate, by stem.
835///
836/// `server.crt` pairs with `server.key`. A `.pem` certificate also accepts a
837/// `.pem` key only when they are separate files, since a combined PEM holding
838/// both is loaded from the one path.
839fn matching_key_path(cert_path: &Path) -> Option<PathBuf> {
840    for extension in KEY_EXTENSIONS {
841        let candidate = cert_path.with_extension(extension);
842        if candidate != cert_path && candidate.is_file() {
843            return Some(candidate);
844        }
845    }
846    // A PEM bundle may carry the key alongside the certificate.
847    if cert_path
848        .extension()
849        .and_then(|e| e.to_str())?
850        .eq_ignore_ascii_case("pem")
851        && load_certified_key(cert_path, cert_path).is_ok()
852    {
853        return Some(cert_path.to_path_buf());
854    }
855    None
856}
857
858/// Certificate reload manager
859///
860/// Tracks all TLS listeners and provides a unified reload interface.
861pub struct CertificateReloader {
862    /// Map of listener ID to hot-reloadable resolver
863    resolvers: RwLock<HashMap<String, Arc<HotReloadableSniResolver>>>,
864}
865
866impl CertificateReloader {
867    /// Create a new certificate reloader
868    pub fn new() -> Self {
869        Self {
870            resolvers: RwLock::new(HashMap::new()),
871        }
872    }
873
874    /// Register a resolver for a listener
875    pub fn register(&self, listener_id: &str, resolver: Arc<HotReloadableSniResolver>) {
876        debug!(listener_id = %listener_id, "Registering TLS resolver for hot-reload");
877        self.resolvers
878            .write()
879            .insert(listener_id.to_string(), resolver);
880    }
881
882    /// Reload all registered certificates
883    ///
884    /// Returns the number of successfully reloaded listeners and any errors.
885    pub fn reload_all(&self) -> (usize, Vec<(String, TlsError)>) {
886        let resolvers = self.resolvers.read();
887        let mut success_count = 0;
888        let mut errors = Vec::new();
889
890        info!(
891            listener_count = resolvers.len(),
892            "Reloading certificates for all TLS listeners"
893        );
894
895        for (listener_id, resolver) in resolvers.iter() {
896            match resolver.reload() {
897                Ok(()) => {
898                    success_count += 1;
899                    debug!(listener_id = %listener_id, "Certificate reload successful");
900                }
901                Err(e) => {
902                    error!(listener_id = %listener_id, error = %e, "Certificate reload failed");
903                    errors.push((listener_id.clone(), e));
904                }
905            }
906        }
907
908        if errors.is_empty() {
909            info!(
910                success_count = success_count,
911                "All certificates reloaded successfully"
912            );
913        } else {
914            warn!(
915                success_count = success_count,
916                error_count = errors.len(),
917                "Certificate reload completed with errors"
918            );
919        }
920
921        (success_count, errors)
922    }
923
924    /// Get reload status for all listeners
925    pub fn status(&self) -> HashMap<String, Duration> {
926        self.resolvers
927            .read()
928            .iter()
929            .map(|(id, resolver)| (id.clone(), resolver.last_reload_age()))
930            .collect()
931    }
932}
933
934impl Default for CertificateReloader {
935    fn default() -> Self {
936        Self::new()
937    }
938}
939
940// ============================================================================
941// OCSP Stapling Support
942// ============================================================================
943
944/// OCSP response cache entry
945#[derive(Debug, Clone)]
946pub struct OcspCacheEntry {
947    /// DER-encoded OCSP response
948    pub response: Vec<u8>,
949    /// When this response was fetched
950    pub fetched_at: Instant,
951    /// When this response expires (from nextUpdate field)
952    pub expires_at: Option<Instant>,
953}
954
955/// OCSP stapling manager
956///
957/// Fetches and caches OCSP responses for certificates.
958pub struct OcspStapler {
959    /// Cache of OCSP responses by certificate fingerprint
960    cache: RwLock<HashMap<String, OcspCacheEntry>>,
961    /// Refresh interval for OCSP responses (default 1 hour)
962    refresh_interval: Duration,
963}
964
965impl OcspStapler {
966    /// Create a new OCSP stapler
967    pub fn new() -> Self {
968        Self {
969            cache: RwLock::new(HashMap::new()),
970            refresh_interval: Duration::from_secs(3600), // 1 hour default
971        }
972    }
973
974    /// Create with custom refresh interval
975    pub fn with_refresh_interval(interval: Duration) -> Self {
976        Self {
977            cache: RwLock::new(HashMap::new()),
978            refresh_interval: interval,
979        }
980    }
981
982    /// Get cached OCSP response for a certificate
983    pub fn get_response(&self, cert_fingerprint: &str) -> Option<Vec<u8>> {
984        let cache = self.cache.read();
985        if let Some(entry) = cache.get(cert_fingerprint) {
986            // Check if response is still valid
987            if entry.fetched_at.elapsed() < self.refresh_interval {
988                trace!(fingerprint = %cert_fingerprint, "OCSP cache hit");
989                return Some(entry.response.clone());
990            }
991            trace!(fingerprint = %cert_fingerprint, "OCSP cache expired");
992        }
993        None
994    }
995
996    /// Fetch OCSP response for a certificate
997    ///
998    /// This performs an HTTP request to the OCSP responder specified in the
999    /// certificate's Authority Information Access extension.
1000    pub fn fetch_ocsp_response(
1001        &self,
1002        cert_der: &[u8],
1003        issuer_der: &[u8],
1004    ) -> Result<Vec<u8>, TlsError> {
1005        use x509_parser::prelude::*;
1006
1007        // Parse the end-entity certificate
1008        let (_, cert) = X509Certificate::from_der(cert_der)
1009            .map_err(|e| TlsError::OcspFetch(format!("Failed to parse certificate: {}", e)))?;
1010
1011        // Parse the issuer certificate
1012        let (_, issuer) = X509Certificate::from_der(issuer_der).map_err(|e| {
1013            TlsError::OcspFetch(format!("Failed to parse issuer certificate: {}", e))
1014        })?;
1015
1016        // Extract OCSP responder URL from AIA extension
1017        let ocsp_url = extract_ocsp_responder_url(&cert)?;
1018        debug!(url = %ocsp_url, "Found OCSP responder URL");
1019
1020        // Build OCSP request
1021        let ocsp_request = build_ocsp_request(&cert, &issuer)?;
1022
1023        // Send request synchronously (blocking context)
1024        // Note: In production, this should be async with proper timeout handling
1025        let response = send_ocsp_request_sync(&ocsp_url, &ocsp_request)?;
1026
1027        // Calculate fingerprint for caching
1028        let fingerprint = calculate_cert_fingerprint(cert_der);
1029
1030        // Cache the response
1031        let entry = OcspCacheEntry {
1032            response: response.clone(),
1033            fetched_at: Instant::now(),
1034            expires_at: None, // Could parse nextUpdate from response
1035        };
1036        self.cache.write().insert(fingerprint, entry);
1037
1038        info!("Successfully fetched and cached OCSP response");
1039        Ok(response)
1040    }
1041
1042    /// Async version of fetch_ocsp_response
1043    pub async fn fetch_ocsp_response_async(
1044        &self,
1045        cert_der: &[u8],
1046        issuer_der: &[u8],
1047    ) -> Result<Vec<u8>, TlsError> {
1048        use x509_parser::prelude::*;
1049
1050        // Parse the end-entity certificate
1051        let (_, cert) = X509Certificate::from_der(cert_der)
1052            .map_err(|e| TlsError::OcspFetch(format!("Failed to parse certificate: {}", e)))?;
1053
1054        // Parse the issuer certificate
1055        let (_, issuer) = X509Certificate::from_der(issuer_der).map_err(|e| {
1056            TlsError::OcspFetch(format!("Failed to parse issuer certificate: {}", e))
1057        })?;
1058
1059        // Extract OCSP responder URL from AIA extension
1060        let ocsp_url = extract_ocsp_responder_url(&cert)?;
1061        debug!(url = %ocsp_url, "Found OCSP responder URL");
1062
1063        // Build OCSP request
1064        let ocsp_request = build_ocsp_request(&cert, &issuer)?;
1065
1066        // Send request asynchronously
1067        let response = send_ocsp_request_async(&ocsp_url, &ocsp_request).await?;
1068
1069        // Calculate fingerprint for caching
1070        let fingerprint = calculate_cert_fingerprint(cert_der);
1071
1072        // Cache the response
1073        let entry = OcspCacheEntry {
1074            response: response.clone(),
1075            fetched_at: Instant::now(),
1076            expires_at: None,
1077        };
1078        self.cache.write().insert(fingerprint, entry);
1079
1080        info!("Successfully fetched and cached OCSP response (async)");
1081        Ok(response)
1082    }
1083
1084    /// Prefetch OCSP responses for all certificates in a config
1085    pub fn prefetch_for_config(&self, config: &TlsConfig) -> Vec<String> {
1086        let mut warnings = Vec::new();
1087
1088        if !config.ocsp_stapling {
1089            trace!("OCSP stapling disabled in config");
1090            return warnings;
1091        }
1092
1093        info!("Prefetching OCSP responses for certificates");
1094
1095        // For now, just log that we would prefetch
1096        // Full implementation would iterate certificates and fetch OCSP responses
1097        warnings.push("OCSP stapling prefetch not yet fully implemented".to_string());
1098
1099        warnings
1100    }
1101
1102    /// Clear the OCSP cache
1103    pub fn clear_cache(&self) {
1104        self.cache.write().clear();
1105        info!("OCSP cache cleared");
1106    }
1107}
1108
1109impl Default for OcspStapler {
1110    fn default() -> Self {
1111        Self::new()
1112    }
1113}
1114
1115// ============================================================================
1116// OCSP Helper Functions
1117// ============================================================================
1118
1119/// Extract OCSP responder URL from certificate's Authority Information Access extension
1120fn extract_ocsp_responder_url(
1121    cert: &x509_parser::certificate::X509Certificate,
1122) -> Result<String, TlsError> {
1123    use x509_parser::prelude::*;
1124
1125    // Find the AIA extension
1126    let aia = cert
1127        .extensions()
1128        .iter()
1129        .find(|ext| ext.oid == oid_registry::OID_PKIX_AUTHORITY_INFO_ACCESS)
1130        .ok_or_else(|| {
1131            TlsError::OcspFetch(
1132                "Certificate does not have Authority Information Access extension".to_string(),
1133            )
1134        })?;
1135
1136    // Parse AIA extension
1137    let aia_value = match aia.parsed_extension() {
1138        ParsedExtension::AuthorityInfoAccess(aia) => aia,
1139        _ => {
1140            return Err(TlsError::OcspFetch(
1141                "Failed to parse Authority Information Access extension".to_string(),
1142            ))
1143        }
1144    };
1145
1146    // Find OCSP access method
1147    for access in &aia_value.accessdescs {
1148        if access.access_method == oid_registry::OID_PKIX_ACCESS_DESCRIPTOR_OCSP {
1149            match &access.access_location {
1150                GeneralName::URI(url) => {
1151                    return Ok(url.to_string());
1152                }
1153                _ => continue,
1154            }
1155        }
1156    }
1157
1158    Err(TlsError::OcspFetch(
1159        "Certificate AIA does not contain OCSP responder URL".to_string(),
1160    ))
1161}
1162
1163/// Build an OCSP request for the given certificate
1164///
1165/// This builds a minimal OCSP request with SHA-256 hashes
1166fn build_ocsp_request(
1167    cert: &x509_parser::certificate::X509Certificate,
1168    issuer: &x509_parser::certificate::X509Certificate,
1169) -> Result<Vec<u8>, TlsError> {
1170    use sha2::{Digest, Sha256};
1171
1172    // Per RFC 6960, an OCSP request contains:
1173    // - Hash of issuer name
1174    // - Hash of issuer public key
1175    // - Certificate serial number
1176
1177    // Hash issuer name (Distinguished Name)
1178    let issuer_name_hash = {
1179        let mut hasher = Sha256::new();
1180        hasher.update(issuer.subject().as_raw());
1181        hasher.finalize()
1182    };
1183
1184    // Hash issuer public key (the BIT STRING content, not including tag/length)
1185    let issuer_key_hash = {
1186        let mut hasher = Sha256::new();
1187        hasher.update(issuer.public_key().subject_public_key.data.as_ref());
1188        hasher.finalize()
1189    };
1190
1191    // Get certificate serial number
1192    let serial = cert.serial.to_bytes_be();
1193
1194    // Build ASN.1 DER encoded OCSP request
1195    // This is a minimal implementation of the OCSP request structure
1196    let request = build_ocsp_request_der(&issuer_name_hash, &issuer_key_hash, &serial);
1197
1198    Ok(request)
1199}
1200
1201/// Build DER-encoded OCSP request
1202fn build_ocsp_request_der(
1203    issuer_name_hash: &[u8],
1204    issuer_key_hash: &[u8],
1205    serial_number: &[u8],
1206) -> Vec<u8> {
1207    // OID for SHA-256
1208    let sha256_oid: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
1209
1210    // Build CertID structure
1211    let hash_algorithm = der_sequence(&[&der_oid(sha256_oid), &der_null()]);
1212
1213    let cert_id = der_sequence(&[
1214        &hash_algorithm,
1215        &der_octet_string(issuer_name_hash),
1216        &der_octet_string(issuer_key_hash),
1217        &der_integer(serial_number),
1218    ]);
1219
1220    // Build Request structure
1221    let request = der_sequence(&[&cert_id]);
1222
1223    // Build requestList (SEQUENCE OF Request)
1224    let request_list = der_sequence(&[&request]);
1225
1226    // Build TBSRequest
1227    let tbs_request = der_sequence(&[&request_list]);
1228
1229    // Build OCSPRequest
1230    der_sequence(&[&tbs_request])
1231}
1232
1233// DER encoding helpers
1234fn der_sequence(items: &[&[u8]]) -> Vec<u8> {
1235    let mut content = Vec::new();
1236    for item in items {
1237        content.extend_from_slice(item);
1238    }
1239    let mut result = vec![0x30]; // SEQUENCE tag
1240    result.extend(der_length(content.len()));
1241    result.extend(content);
1242    result
1243}
1244
1245fn der_oid(oid: &[u8]) -> Vec<u8> {
1246    let mut result = vec![0x06]; // OID tag
1247    result.extend(der_length(oid.len()));
1248    result.extend_from_slice(oid);
1249    result
1250}
1251
1252fn der_null() -> Vec<u8> {
1253    vec![0x05, 0x00] // NULL
1254}
1255
1256fn der_octet_string(data: &[u8]) -> Vec<u8> {
1257    let mut result = vec![0x04]; // OCTET STRING tag
1258    result.extend(der_length(data.len()));
1259    result.extend_from_slice(data);
1260    result
1261}
1262
1263fn der_integer(data: &[u8]) -> Vec<u8> {
1264    let mut result = vec![0x02]; // INTEGER tag
1265                                 // Remove leading zeros but ensure at least one byte
1266    let data = match data.iter().position(|&b| b != 0) {
1267        Some(pos) => &data[pos..],
1268        None => &[0],
1269    };
1270    // Add leading zero if high bit is set (to ensure positive)
1271    if !data.is_empty() && data[0] & 0x80 != 0 {
1272        result.extend(der_length(data.len() + 1));
1273        result.push(0x00);
1274    } else {
1275        result.extend(der_length(data.len()));
1276    }
1277    result.extend_from_slice(data);
1278    result
1279}
1280
1281fn der_length(len: usize) -> Vec<u8> {
1282    if len < 128 {
1283        vec![len as u8]
1284    } else if len < 256 {
1285        vec![0x81, len as u8]
1286    } else {
1287        vec![0x82, (len >> 8) as u8, len as u8]
1288    }
1289}
1290
1291/// Send OCSP request synchronously (blocking)
1292fn send_ocsp_request_sync(url: &str, request: &[u8]) -> Result<Vec<u8>, TlsError> {
1293    use std::io::{Read, Write};
1294    use std::net::TcpStream;
1295    use std::time::Duration;
1296
1297    // Parse URL to get host, port, and path
1298    let url = url::Url::parse(url)
1299        .map_err(|e| TlsError::OcspFetch(format!("Invalid OCSP URL: {}", e)))?;
1300
1301    let host = url
1302        .host_str()
1303        .ok_or_else(|| TlsError::OcspFetch("OCSP URL has no host".to_string()))?;
1304    let port = url.port().unwrap_or(80);
1305    let path = if url.path().is_empty() {
1306        "/"
1307    } else {
1308        url.path()
1309    };
1310
1311    // Connect to server
1312    let addr = format!("{}:{}", host, port);
1313    let mut stream = TcpStream::connect(&addr)
1314        .map_err(|e| TlsError::OcspFetch(format!("Failed to connect to OCSP responder: {}", e)))?;
1315
1316    stream
1317        .set_read_timeout(Some(Duration::from_secs(10)))
1318        .map_err(|e| TlsError::OcspFetch(format!("Failed to set timeout: {}", e)))?;
1319    stream
1320        .set_write_timeout(Some(Duration::from_secs(10)))
1321        .map_err(|e| TlsError::OcspFetch(format!("Failed to set timeout: {}", e)))?;
1322
1323    // Build HTTP POST request
1324    let http_request = format!(
1325        "POST {} HTTP/1.1\r\n\
1326         Host: {}\r\n\
1327         Content-Type: application/ocsp-request\r\n\
1328         Content-Length: {}\r\n\
1329         Connection: close\r\n\
1330         \r\n",
1331        path,
1332        host,
1333        request.len()
1334    );
1335
1336    // Send request
1337    stream
1338        .write_all(http_request.as_bytes())
1339        .map_err(|e| TlsError::OcspFetch(format!("Failed to send OCSP request: {}", e)))?;
1340    stream
1341        .write_all(request)
1342        .map_err(|e| TlsError::OcspFetch(format!("Failed to send OCSP request body: {}", e)))?;
1343
1344    // Read response
1345    let mut response = Vec::new();
1346    stream
1347        .read_to_end(&mut response)
1348        .map_err(|e| TlsError::OcspFetch(format!("Failed to read OCSP response: {}", e)))?;
1349
1350    // Parse HTTP response - find body after headers
1351    let headers_end = response
1352        .windows(4)
1353        .position(|w| w == b"\r\n\r\n")
1354        .ok_or_else(|| TlsError::OcspFetch("Invalid HTTP response: no headers end".to_string()))?;
1355
1356    let body = &response[headers_end + 4..];
1357    if body.is_empty() {
1358        return Err(TlsError::OcspFetch("Empty OCSP response body".to_string()));
1359    }
1360
1361    Ok(body.to_vec())
1362}
1363
1364/// Send OCSP request asynchronously
1365async fn send_ocsp_request_async(url: &str, request: &[u8]) -> Result<Vec<u8>, TlsError> {
1366    let client = reqwest::Client::builder()
1367        .timeout(Duration::from_secs(10))
1368        .build()
1369        .map_err(|e| TlsError::OcspFetch(format!("Failed to create HTTP client: {}", e)))?;
1370
1371    let response = client
1372        .post(url)
1373        .header("Content-Type", "application/ocsp-request")
1374        .body(request.to_vec())
1375        .send()
1376        .await
1377        .map_err(|e| TlsError::OcspFetch(format!("OCSP request failed: {}", e)))?;
1378
1379    if !response.status().is_success() {
1380        return Err(TlsError::OcspFetch(format!(
1381            "OCSP responder returned status: {}",
1382            response.status()
1383        )));
1384    }
1385
1386    let body = response
1387        .bytes()
1388        .await
1389        .map_err(|e| TlsError::OcspFetch(format!("Failed to read OCSP response: {}", e)))?;
1390
1391    Ok(body.to_vec())
1392}
1393
1394/// Calculate certificate fingerprint for cache key
1395fn calculate_cert_fingerprint(cert_der: &[u8]) -> String {
1396    use sha2::{Digest, Sha256};
1397    let mut hasher = Sha256::new();
1398    hasher.update(cert_der);
1399    let result = hasher.finalize();
1400    hex::encode(result)
1401}
1402
1403// ============================================================================
1404// Upstream mTLS Support (Client Certificates)
1405// ============================================================================
1406
1407/// Load client certificate and key for mTLS to upstreams
1408///
1409/// This function loads PEM-encoded certificates and private key and converts
1410/// them to Pingora's CertKey format for use with `HttpPeer.client_cert_key`.
1411///
1412/// # Arguments
1413///
1414/// * `cert_path` - Path to PEM-encoded certificate (may include chain)
1415/// * `key_path` - Path to PEM-encoded private key
1416///
1417/// # Returns
1418///
1419/// An `Arc<CertKey>` that can be set on `peer.client_cert_key` for mTLS
1420pub fn load_client_cert_key(
1421    cert_path: &Path,
1422    key_path: &Path,
1423) -> Result<Arc<pingora_core::utils::tls::CertKey>, TlsError> {
1424    // Read certificate chain (PEM format, may contain intermediates)
1425    let cert_file = File::open(cert_path)
1426        .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1427    let mut cert_reader = BufReader::new(cert_file);
1428
1429    // Parse certificates from PEM to DER
1430    let cert_ders: Vec<Vec<u8>> = rustls_pemfile::certs(&mut cert_reader)
1431        .collect::<Result<Vec<_>, _>>()
1432        .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?
1433        .into_iter()
1434        .map(|c| c.to_vec())
1435        .collect();
1436
1437    if cert_ders.is_empty() {
1438        return Err(TlsError::CertificateLoad(format!(
1439            "{}: No certificates found in PEM file",
1440            cert_path.display()
1441        )));
1442    }
1443
1444    // Read private key (PEM format)
1445    let key_file = File::open(key_path)
1446        .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?;
1447    let mut key_reader = BufReader::new(key_file);
1448
1449    // Parse private key from PEM to DER
1450    let key_der = rustls_pemfile::private_key(&mut key_reader)
1451        .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?
1452        .ok_or_else(|| {
1453            TlsError::KeyLoad(format!(
1454                "{}: No private key found in PEM file",
1455                key_path.display()
1456            ))
1457        })?
1458        .secret_der()
1459        .to_vec();
1460
1461    // Create Pingora's CertKey (certificates: Vec<Vec<u8>>, key: Vec<u8>)
1462    let cert_key = pingora_core::utils::tls::CertKey::new(cert_ders, key_der);
1463
1464    debug!(
1465        cert_path = %cert_path.display(),
1466        key_path = %key_path.display(),
1467        "Loaded mTLS client certificate for upstream connections"
1468    );
1469
1470    Ok(Arc::new(cert_key))
1471}
1472
1473/// Build a TLS client configuration for upstream connections with mTLS
1474///
1475/// This creates a rustls ClientConfig that can be used when Zentinel
1476/// connects to backends that require client certificate authentication.
1477pub fn build_upstream_tls_config(config: &UpstreamTlsConfig) -> Result<ClientConfig, TlsError> {
1478    let mut root_store = RootCertStore::empty();
1479
1480    // Load CA certificates for server verification
1481    if let Some(ca_path) = &config.ca_cert {
1482        let ca_file = File::open(ca_path)
1483            .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1484        let mut ca_reader = BufReader::new(ca_file);
1485
1486        let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
1487            .collect::<Result<Vec<_>, _>>()
1488            .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1489
1490        for cert in certs {
1491            root_store.add(cert).map_err(|e| {
1492                TlsError::InvalidCertificate(format!("Failed to add CA certificate: {}", e))
1493            })?;
1494        }
1495
1496        debug!(
1497            ca_file = %ca_path.display(),
1498            cert_count = root_store.len(),
1499            "Loaded upstream CA certificates"
1500        );
1501    } else if !config.insecure_skip_verify {
1502        // Use webpki roots for standard TLS
1503        root_store = RootCertStore {
1504            roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
1505        };
1506        trace!("Using webpki-roots for upstream TLS verification");
1507    }
1508
1509    // Build the client config
1510    let builder = ClientConfig::builder().with_root_certificates(root_store);
1511
1512    let client_config = if let (Some(cert_path), Some(key_path)) =
1513        (&config.client_cert, &config.client_key)
1514    {
1515        // Load client certificate for mTLS
1516        let cert_file = File::open(cert_path)
1517            .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1518        let mut cert_reader = BufReader::new(cert_file);
1519
1520        let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
1521            .collect::<Result<Vec<_>, _>>()
1522            .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1523
1524        if certs.is_empty() {
1525            return Err(TlsError::CertificateLoad(format!(
1526                "{}: No certificates found",
1527                cert_path.display()
1528            )));
1529        }
1530
1531        // Load client private key
1532        let key_file = File::open(key_path)
1533            .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?;
1534        let mut key_reader = BufReader::new(key_file);
1535
1536        let key = rustls_pemfile::private_key(&mut key_reader)
1537            .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?
1538            .ok_or_else(|| {
1539                TlsError::KeyLoad(format!("{}: No private key found", key_path.display()))
1540            })?;
1541
1542        info!(
1543            cert_file = %cert_path.display(),
1544            "Configured mTLS client certificate for upstream connections"
1545        );
1546
1547        builder
1548            .with_client_auth_cert(certs, key)
1549            .map_err(|e| TlsError::CertKeyMismatch(format!("Failed to set client auth: {}", e)))?
1550    } else {
1551        // No client certificate
1552        builder.with_no_client_auth()
1553    };
1554
1555    debug!("Upstream TLS configuration built successfully");
1556    Ok(client_config)
1557}
1558
1559/// Validate upstream TLS configuration
1560pub fn validate_upstream_tls_config(config: &UpstreamTlsConfig) -> Result<(), TlsError> {
1561    // Validate CA certificate if specified
1562    if let Some(ca_path) = &config.ca_cert {
1563        if !ca_path.exists() {
1564            return Err(TlsError::CertificateLoad(format!(
1565                "Upstream CA certificate not found: {}",
1566                ca_path.display()
1567            )));
1568        }
1569    }
1570
1571    // Validate client certificate pair if mTLS is configured
1572    if let Some(cert_path) = &config.client_cert {
1573        if !cert_path.exists() {
1574            return Err(TlsError::CertificateLoad(format!(
1575                "Upstream client certificate not found: {}",
1576                cert_path.display()
1577            )));
1578        }
1579
1580        // If cert is specified, key must also be specified
1581        match &config.client_key {
1582            Some(key_path) if !key_path.exists() => {
1583                return Err(TlsError::KeyLoad(format!(
1584                    "Upstream client key not found: {}",
1585                    key_path.display()
1586                )));
1587            }
1588            None => {
1589                return Err(TlsError::ConfigBuild(
1590                    "client_cert specified without client_key".to_string(),
1591                ));
1592            }
1593            _ => {}
1594        }
1595    }
1596
1597    if config.client_key.is_some() && config.client_cert.is_none() {
1598        return Err(TlsError::ConfigBuild(
1599            "client_key specified without client_cert".to_string(),
1600        ));
1601    }
1602
1603    Ok(())
1604}
1605
1606// ============================================================================
1607// Certificate Loading Functions
1608// ============================================================================
1609
1610/// Load a certificate chain and private key from files
1611fn load_certified_key(cert_path: &Path, key_path: &Path) -> Result<CertifiedKey, TlsError> {
1612    // Load certificate chain
1613    let cert_file = File::open(cert_path)
1614        .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1615    let mut cert_reader = BufReader::new(cert_file);
1616
1617    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
1618        .collect::<Result<Vec<_>, _>>()
1619        .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1620
1621    if certs.is_empty() {
1622        return Err(TlsError::CertificateLoad(format!(
1623            "{}: No certificates found in file",
1624            cert_path.display()
1625        )));
1626    }
1627
1628    // Load private key
1629    let key_file = File::open(key_path)
1630        .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?;
1631    let mut key_reader = BufReader::new(key_file);
1632
1633    let key = rustls_pemfile::private_key(&mut key_reader)
1634        .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?
1635        .ok_or_else(|| {
1636            TlsError::KeyLoad(format!(
1637                "{}: No private key found in file",
1638                key_path.display()
1639            ))
1640        })?;
1641
1642    // Create signing key using the default crypto provider
1643    let provider = rustls::crypto::CryptoProvider::get_default()
1644        .cloned()
1645        .unwrap_or_else(|| Arc::new(rustls::crypto::aws_lc_rs::default_provider()));
1646
1647    let signing_key = provider
1648        .key_provider
1649        .load_private_key(key)
1650        .map_err(|e| TlsError::CertKeyMismatch(format!("Failed to load private key: {:?}", e)))?;
1651
1652    Ok(CertifiedKey::new(certs, signing_key))
1653}
1654
1655/// Extract DNS hostnames from a certificate's CN and Subject Alternative Names.
1656///
1657/// Returns a list of DNS names (e.g., "example.com", "*.example.com") found in:
1658/// 1. Subject Alternative Name (SAN) DNS entries (preferred)
1659/// 2. Common Name (CN) as fallback if no SAN DNS entries exist
1660///
1661/// IP addresses in SANs are ignored since SNI operates on hostnames only.
1662fn extract_hostnames_from_cert(cert_der: &CertificateDer<'_>) -> Result<Vec<String>, TlsError> {
1663    use x509_parser::prelude::*;
1664
1665    let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
1666        TlsError::InvalidCertificate(format!("Failed to parse X.509 certificate: {}", e))
1667    })?;
1668
1669    let mut hostnames = Vec::new();
1670
1671    // Try SAN extension first (RFC 6125: SAN takes precedence over CN)
1672    if let Ok(Some(san_ext)) = cert.subject_alternative_name() {
1673        for name in &san_ext.value.general_names {
1674            if let GeneralName::DNSName(dns) = name {
1675                hostnames.push(dns.to_lowercase());
1676            }
1677        }
1678    }
1679
1680    // Fall back to CN only if no SAN DNS names were found
1681    if hostnames.is_empty() {
1682        for attr in cert.subject().iter_common_name() {
1683            if let Ok(cn) = attr.as_str() {
1684                hostnames.push(cn.to_lowercase());
1685            }
1686        }
1687    }
1688
1689    if hostnames.is_empty() {
1690        return Err(TlsError::InvalidCertificate(
1691            "Certificate has no DNS names in SAN or CN".to_string(),
1692        ));
1693    }
1694
1695    Ok(hostnames)
1696}
1697
1698/// Load CA certificates for client verification (mTLS)
1699pub fn load_client_ca(ca_path: &Path) -> Result<RootCertStore, TlsError> {
1700    let ca_file = File::open(ca_path)
1701        .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1702    let mut ca_reader = BufReader::new(ca_file);
1703
1704    let mut root_store = RootCertStore::empty();
1705
1706    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
1707        .collect::<Result<Vec<_>, _>>()
1708        .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1709
1710    for cert in certs {
1711        root_store.add(cert).map_err(|e| {
1712            TlsError::InvalidCertificate(format!("Failed to add CA certificate: {}", e))
1713        })?;
1714    }
1715
1716    if root_store.is_empty() {
1717        return Err(TlsError::CertificateLoad(format!(
1718            "{}: No CA certificates found",
1719            ca_path.display()
1720        )));
1721    }
1722
1723    info!(
1724        ca_file = %ca_path.display(),
1725        cert_count = root_store.len(),
1726        "Loaded client CA certificates"
1727    );
1728
1729    Ok(root_store)
1730}
1731
1732/// Resolve TLS protocol versions from config into rustls version references.
1733fn resolve_protocol_versions(config: &TlsConfig) -> Vec<&'static rustls::SupportedProtocolVersion> {
1734    use zentinel_common::types::TlsVersion;
1735
1736    let min = &config.min_version;
1737    let max = config.max_version.as_ref().unwrap_or(&TlsVersion::Tls13);
1738
1739    let mut versions = Vec::new();
1740
1741    // Include TLS 1.2 if within the min..=max range
1742    if matches!(min, TlsVersion::Tls12) {
1743        versions.push(&rustls::version::TLS12);
1744    }
1745
1746    // Include TLS 1.3 if within the min..=max range
1747    if matches!(max, TlsVersion::Tls13) {
1748        versions.push(&rustls::version::TLS13);
1749    }
1750
1751    if versions.is_empty() {
1752        // Shouldn't happen with valid config, but be safe
1753        warn!("No valid TLS versions resolved from config, falling back to TLS 1.2 + 1.3");
1754        versions.push(&rustls::version::TLS12);
1755        versions.push(&rustls::version::TLS13);
1756    }
1757
1758    versions
1759}
1760
1761/// Resolve cipher suite names from config to rustls `SupportedCipherSuite` values.
1762///
1763/// Uses the aws-lc-rs crypto provider's available cipher suites.
1764fn resolve_cipher_suites(names: &[String]) -> Result<Vec<rustls::SupportedCipherSuite>, TlsError> {
1765    use rustls::crypto::aws_lc_rs::cipher_suite;
1766
1767    // Map of canonical IANA names to rustls cipher suite values
1768    let known: &[(&str, rustls::SupportedCipherSuite)] = &[
1769        // TLS 1.3
1770        (
1771            "TLS_AES_256_GCM_SHA384",
1772            cipher_suite::TLS13_AES_256_GCM_SHA384,
1773        ),
1774        (
1775            "TLS_AES_128_GCM_SHA256",
1776            cipher_suite::TLS13_AES_128_GCM_SHA256,
1777        ),
1778        (
1779            "TLS_CHACHA20_POLY1305_SHA256",
1780            cipher_suite::TLS13_CHACHA20_POLY1305_SHA256,
1781        ),
1782        // TLS 1.2
1783        (
1784            "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
1785            cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
1786        ),
1787        (
1788            "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
1789            cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
1790        ),
1791        (
1792            "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
1793            cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
1794        ),
1795        (
1796            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
1797            cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
1798        ),
1799        (
1800            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
1801            cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
1802        ),
1803        (
1804            "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
1805            cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
1806        ),
1807    ];
1808
1809    let mut suites = Vec::with_capacity(names.len());
1810    for name in names {
1811        let normalized = name.to_uppercase().replace('-', "_");
1812        match known.iter().find(|(n, _)| *n == normalized) {
1813            Some((_, suite)) => suites.push(*suite),
1814            None => {
1815                let available: Vec<&str> = known.iter().map(|(n, _)| *n).collect();
1816                return Err(TlsError::ConfigBuild(format!(
1817                    "Unknown cipher suite '{}'. Available: {}",
1818                    name,
1819                    available.join(", ")
1820                )));
1821            }
1822        }
1823    }
1824
1825    Ok(suites)
1826}
1827
1828/// Build a TLS ServerConfig from our configuration.
1829///
1830/// Applies protocol versions, cipher suites, session resumption, mTLS,
1831/// and SNI certificate resolution from the Zentinel TLS config.
1832///
1833/// The certificate resolver is built fresh from `config` and never changes
1834/// afterwards. Use [`build_server_config_with_resolver`] to supply a
1835/// [`HotReloadableSniResolver`] instead, so that certificates reloaded from
1836/// disk are picked up by connections already being served.
1837pub fn build_server_config(
1838    config: &TlsConfig,
1839    listener_id: &str,
1840) -> Result<ServerConfig, TlsError> {
1841    let resolver = SniResolver::from_config(config, Some(listener_id))?;
1842    build_server_config_with_resolver(config, Arc::new(resolver))
1843}
1844
1845/// Build a TLS ServerConfig around a caller-supplied certificate resolver.
1846///
1847/// Everything except certificate selection still comes from `config`:
1848/// protocol versions, cipher suites, client authentication and session
1849/// resumption. Separating the resolver lets the caller keep a handle on it,
1850/// which is what makes certificate hot-reload possible — the resolver
1851/// installed in the [`ServerConfig`] and the one being reloaded have to be
1852/// the same object, or reloads update something no connection consults.
1853pub fn build_server_config_with_resolver(
1854    config: &TlsConfig,
1855    resolver: Arc<dyn ResolvesServerCert>,
1856) -> Result<ServerConfig, TlsError> {
1857    // Resolve protocol versions from config
1858    let versions = resolve_protocol_versions(config);
1859    info!(
1860        versions = ?versions.iter().map(|v| format!("{:?}", v.version)).collect::<Vec<_>>(),
1861        "TLS protocol versions configured"
1862    );
1863
1864    // Build the ServerConfig builder, with custom cipher suites if specified
1865    let builder = if !config.cipher_suites.is_empty() {
1866        let suites = resolve_cipher_suites(&config.cipher_suites)?;
1867        info!(
1868            cipher_suites = ?config.cipher_suites,
1869            count = suites.len(),
1870            "Custom TLS cipher suites configured"
1871        );
1872        let provider = rustls::crypto::CryptoProvider {
1873            cipher_suites: suites,
1874            ..rustls::crypto::aws_lc_rs::default_provider()
1875        };
1876        ServerConfig::builder_with_provider(Arc::new(provider))
1877            .with_protocol_versions(&versions)
1878            .map_err(|e| {
1879                TlsError::ConfigBuild(format!("Invalid TLS protocol/cipher configuration: {}", e))
1880            })?
1881    } else {
1882        ServerConfig::builder_with_protocol_versions(&versions)
1883    };
1884
1885    // Configure client authentication (mTLS)
1886    let server_config = if config.client_auth {
1887        if let Some(ca_path) = &config.ca_file {
1888            let root_store = load_client_ca(ca_path)?;
1889            let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
1890                .build()
1891                .map_err(|e| {
1892                    TlsError::ConfigBuild(format!("Failed to build client verifier: {}", e))
1893                })?;
1894
1895            info!("mTLS enabled: client certificates required");
1896
1897            builder
1898                .with_client_cert_verifier(verifier)
1899                .with_cert_resolver(resolver.clone())
1900        } else {
1901            // Config validation rejects this combination, so reaching here
1902            // means a Config was built in code rather than parsed. Failing is
1903            // still the right answer: quietly serving without client
1904            // authentication is the outcome an operator asking for mTLS would
1905            // least expect, and a warning in the startup log is not a
1906            // proportionate signal for it.
1907            return Err(TlsError::ConfigBuild(
1908                "client_auth is enabled but no ca_file is configured. Client certificates \
1909                 cannot be verified without a CA, and serving without client authentication \
1910                 would contradict the configuration."
1911                    .to_string(),
1912            ));
1913        }
1914    } else {
1915        builder
1916            .with_no_client_auth()
1917            .with_cert_resolver(resolver.clone())
1918    };
1919
1920    // Configure ALPN for HTTP/2 support
1921    let mut server_config = server_config;
1922    server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1923
1924    // Disable session resumption if configured
1925    if !config.session_resumption {
1926        server_config.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
1927        info!("TLS session resumption disabled");
1928    }
1929
1930    debug!("TLS configuration built successfully");
1931
1932    Ok(server_config)
1933}
1934
1935/// Validate TLS configuration files exist and are readable
1936pub fn validate_tls_config(config: &TlsConfig) -> Result<(), TlsError> {
1937    // If ACME is configured, skip manual cert file validation
1938    if config.acme.is_some() {
1939        // ACME-managed certificates don't need cert_file/key_file to exist
1940        trace!("Skipping manual cert validation for ACME-managed TLS");
1941    } else {
1942        // Check default certificate (required for non-ACME configs)
1943        match (&config.cert_file, &config.key_file) {
1944            (Some(cert_file), Some(key_file)) => {
1945                if !cert_file.exists() {
1946                    return Err(TlsError::CertificateLoad(format!(
1947                        "Certificate file not found: {}",
1948                        cert_file.display()
1949                    )));
1950                }
1951                if !key_file.exists() {
1952                    return Err(TlsError::KeyLoad(format!(
1953                        "Key file not found: {}",
1954                        key_file.display()
1955                    )));
1956                }
1957            }
1958            _ => {
1959                return Err(TlsError::ConfigBuild(
1960                    "TLS configuration requires cert_file and key_file (or ACME block)".to_string(),
1961                ));
1962            }
1963        }
1964    }
1965
1966    // Check SNI certificates
1967    for sni in &config.additional_certs {
1968        // If ACME is configured for this SNI cert, skip existence check
1969        if sni.acme.is_some() {
1970            trace!("Skipping manual cert validation for ACME-managed SNI certificate");
1971            continue;
1972        }
1973
1974        // Standard certificate validation
1975        match (&sni.cert_file, &sni.key_file) {
1976            (Some(cert_file), Some(key_file)) => {
1977                if !cert_file.exists() {
1978                    return Err(TlsError::CertificateLoad(format!(
1979                        "SNI certificate file not found: {}",
1980                        cert_file.display()
1981                    )));
1982                }
1983                if !key_file.exists() {
1984                    return Err(TlsError::KeyLoad(format!(
1985                        "SNI key file not found: {}",
1986                        key_file.display()
1987                    )));
1988                }
1989            }
1990            _ => {
1991                return Err(TlsError::ConfigBuild(
1992                    "SNI certificate requires cert_file and key_file (or ACME block)".to_string(),
1993                ));
1994            }
1995        }
1996    }
1997
1998    // Check CA file if mTLS enabled
1999    if config.client_auth {
2000        if let Some(ca_path) = &config.ca_file {
2001            if !ca_path.exists() {
2002                return Err(TlsError::CertificateLoad(format!(
2003                    "CA certificate file not found: {}",
2004                    ca_path.display()
2005                )));
2006            }
2007        }
2008    }
2009
2010    Ok(())
2011}
2012
2013#[cfg(test)]
2014mod tests {
2015
2016    #[test]
2017    fn test_wildcard_matching() {
2018        // Create a mock resolver without actual certs
2019        // Just test the matching logic
2020        let name = "foo.bar.example.com";
2021        let parts: Vec<&str> = name.split('.').collect();
2022
2023        assert_eq!(parts.len(), 4);
2024
2025        // Check domain extraction for wildcard matching
2026        let domain1 = parts[1..].join(".");
2027        assert_eq!(domain1, "bar.example.com");
2028
2029        let domain2 = parts[2..].join(".");
2030        assert_eq!(domain2, "example.com");
2031    }
2032
2033    #[test]
2034    fn test_hostname_normalization() {
2035        let hostname = "Example.COM";
2036        let normalized = hostname.to_lowercase();
2037        assert_eq!(normalized, "example.com");
2038    }
2039}
2040
2041#[cfg(test)]
2042mod cert_diff_tests {
2043    use super::*;
2044
2045    fn snapshot(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
2046        pairs
2047            .iter()
2048            .map(|(h, f)| (h.to_string(), f.to_string()))
2049            .collect()
2050    }
2051
2052    #[test]
2053    fn an_unchanged_folder_reports_no_change() {
2054        let before = snapshot(&[("<default>", "aa"), ("a.test", "bb")]);
2055        let diff = CertDiff::between(&before, &before);
2056        assert!(diff.is_empty());
2057    }
2058
2059    #[test]
2060    fn a_new_certificate_is_reported_as_added() {
2061        let before = snapshot(&[("a.test", "aa")]);
2062        let after = snapshot(&[("a.test", "aa"), ("b.test", "bb")]);
2063        let diff = CertDiff::between(&before, &after);
2064        assert_eq!(diff.added, vec!["b.test".to_string()]);
2065        assert!(diff.removed.is_empty());
2066        assert!(diff.replaced.is_empty());
2067    }
2068
2069    #[test]
2070    fn a_deleted_certificate_is_reported_as_removed() {
2071        let before = snapshot(&[("a.test", "aa"), ("b.test", "bb")]);
2072        let after = snapshot(&[("a.test", "aa")]);
2073        let diff = CertDiff::between(&before, &after);
2074        assert_eq!(diff.removed, vec!["b.test".to_string()]);
2075        assert!(diff.added.is_empty());
2076        assert!(diff.replaced.is_empty());
2077    }
2078
2079    /// The case the counts cannot see, and the reason this diff exists: a
2080    /// renewal swaps the certificate while every hostname and every count stays
2081    /// exactly the same.
2082    #[test]
2083    fn a_renewal_for_the_same_hostname_is_reported_as_replaced() {
2084        let before = snapshot(&[("a.test", "old00000")]);
2085        let after = snapshot(&[("a.test", "new11111")]);
2086        let diff = CertDiff::between(&before, &after);
2087
2088        assert!(diff.added.is_empty(), "hostname set is unchanged");
2089        assert!(diff.removed.is_empty(), "hostname set is unchanged");
2090        assert_eq!(
2091            diff.replaced,
2092            vec!["a.test (old00000->new11111)".to_string()]
2093        );
2094        assert!(!diff.is_empty(), "a renewal is a change and must be logged");
2095    }
2096
2097    /// A 1-for-1 rotation: one name retired, its replacement added, counts equal
2098    /// on both sides.
2099    #[test]
2100    fn a_one_for_one_rotation_shows_both_sides() {
2101        let before = snapshot(&[("<default>", "dd"), ("old.test", "aa")]);
2102        let after = snapshot(&[("<default>", "dd"), ("new.test", "bb")]);
2103        let diff = CertDiff::between(&before, &after);
2104
2105        assert_eq!(before.len(), after.len(), "counts alone show nothing");
2106        assert_eq!(diff.added, vec!["new.test".to_string()]);
2107        assert_eq!(diff.removed, vec!["old.test".to_string()]);
2108    }
2109
2110    #[test]
2111    fn rotating_the_default_certificate_is_visible() {
2112        let before = snapshot(&[(DEFAULT_CERT_LABEL, "old00000")]);
2113        let after = snapshot(&[(DEFAULT_CERT_LABEL, "new11111")]);
2114        assert!(!CertDiff::between(&before, &after).is_empty());
2115    }
2116
2117    #[test]
2118    fn empty_diff_fields_render_as_a_dash() {
2119        assert_eq!(CertDiff::render(&[]), "-");
2120        assert_eq!(
2121            CertDiff::render(&["a.test".to_string(), "b.test".to_string()]),
2122            "a.test, b.test"
2123        );
2124    }
2125}