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