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