1use std::collections::{BTreeMap, HashMap, HashSet};
64use std::fs::File;
65use std::io::BufReader;
66use std::path::{Path, PathBuf};
67use std::sync::Arc;
68use std::time::{Duration, Instant};
69
70use parking_lot::RwLock;
71use rustls::client::ClientConfig;
72use rustls::pki_types::CertificateDer;
73use rustls::server::{ClientHello, ResolvesServerCert};
74use rustls::sign::CertifiedKey;
75use rustls::{RootCertStore, ServerConfig};
76use tracing::{debug, error, info, trace, warn};
77
78use zentinel_config::{SniCertFolder, SniCertificate, TlsConfig, UpstreamTlsConfig};
79
80#[derive(Debug)]
82pub enum TlsError {
83 CertificateLoad(String),
85 KeyLoad(String),
87 ConfigBuild(String),
89 CertKeyMismatch(String),
91 InvalidCertificate(String),
93 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#[derive(Debug)]
120pub struct SniResolver {
121 default_cert: Arc<CertifiedKey>,
123 sni_certs: HashMap<String, Arc<CertifiedKey>>,
126 wildcard_certs: HashMap<String, Arc<CertifiedKey>>,
128}
129
130const DEFAULT_CERT_LABEL: &str = "<default>";
135
136fn cert_fingerprint(cert: &CertifiedKey) -> String {
143 use sha2::{Digest, Sha256};
144
145 let Some(leaf) = cert.cert.first() else {
146 return "unknown".to_string();
147 };
148 let digest = Sha256::digest(leaf.as_ref());
149 digest.iter().take(8).map(|b| format!("{b:02x}")).collect()
150}
151
152#[derive(Debug, Default, PartialEq, Eq)]
158pub(crate) struct CertDiff {
159 pub added: Vec<String>,
161 pub removed: Vec<String>,
163 pub replaced: Vec<String>,
165}
166
167impl CertDiff {
168 pub(crate) fn is_empty(&self) -> bool {
170 self.added.is_empty() && self.removed.is_empty() && self.replaced.is_empty()
171 }
172
173 fn between(before: &BTreeMap<String, String>, after: &BTreeMap<String, String>) -> Self {
175 let mut diff = Self::default();
176
177 for (hostname, new_fp) in after {
178 match before.get(hostname) {
179 None => diff.added.push(hostname.clone()),
180 Some(old_fp) if old_fp != new_fp => {
181 diff.replaced
182 .push(format!("{hostname} ({old_fp}->{new_fp})"));
183 }
184 Some(_) => {}
185 }
186 }
187 for hostname in before.keys() {
188 if !after.contains_key(hostname) {
189 diff.removed.push(hostname.clone());
190 }
191 }
192
193 diff
194 }
195
196 fn render(names: &[String]) -> String {
198 if names.is_empty() {
199 "-".to_string()
200 } else {
201 names.join(", ")
202 }
203 }
204}
205
206impl SniResolver {
207 pub fn served_certs(&self) -> BTreeMap<String, String> {
212 let mut served = BTreeMap::new();
213 served.insert(
214 DEFAULT_CERT_LABEL.to_string(),
215 cert_fingerprint(&self.default_cert),
216 );
217 for (hostname, cert) in self.sni_certs.iter().chain(self.wildcard_certs.iter()) {
218 served.insert(hostname.clone(), cert_fingerprint(cert));
219 }
220 served
221 }
222
223 pub fn from_config(config: &TlsConfig, listener_id: Option<&str>) -> Result<Self, TlsError> {
225 let listener_id_str = listener_id.unwrap_or("unknown");
226
227 let (cert_path_buf, key_path_buf);
229 let (cert_file, key_file) = match (&config.cert_file, &config.key_file) {
230 (Some(cert), Some(key)) => (cert.as_path(), key.as_path()),
231 _ if config.acme.is_some() => {
232 let acme = config.acme.as_ref().unwrap();
233 let primary = acme.domains.first().ok_or_else(|| {
234 TlsError::ConfigBuild(
235 "ACME configuration has no domains for cert path resolution".to_string(),
236 )
237 })?;
238 cert_path_buf = acme.storage.join("domains").join(primary).join("cert.pem");
239 key_path_buf = acme.storage.join("domains").join(primary).join("key.pem");
240 (cert_path_buf.as_path(), key_path_buf.as_path())
241 }
242 _ => {
243 return Err(TlsError::ConfigBuild(
244 "TLS configuration requires cert_file and key_file (or ACME block)".to_string(),
245 ));
246 }
247 };
248
249 let default_cert = load_certified_key(cert_file, key_file)?;
251
252 info!(
253 listener_id = %listener_id_str,
254 cert_file = %cert_file.display(),
255 "Loaded default TLS certificate"
256 );
257
258 let mut sni_certs = HashMap::new();
259 let mut wildcard_certs = HashMap::new();
260
261 let mut priority_exact: HashSet<String> = HashSet::new();
265 let mut priority_wildcard: HashSet<String> = HashSet::new();
266
267 let scanned = scan_cert_folders(&config.cert_folders, listener_id_str);
273 let all_sni_certs: Vec<&SniCertificate> = config
274 .additional_certs
275 .iter()
276 .chain(scanned.iter())
277 .collect();
278
279 for (i, sni_config) in all_sni_certs.into_iter().enumerate() {
281 let (sni_cert_path_buf, sni_key_path_buf);
283 let (sni_cert_path, sni_key_path) = match (&sni_config.cert_file, &sni_config.key_file)
284 {
285 (Some(cert), Some(key)) => (cert.as_path(), key.as_path()),
286 _ if sni_config.acme.is_some() => {
287 let acme = sni_config.acme.as_ref().unwrap();
288 let primary = acme.domains.first().ok_or_else(|| {
289 TlsError::ConfigBuild("SNI ACME configuration has no domains".to_string())
290 })?;
291 sni_cert_path_buf = acme.storage.join("domains").join(primary).join("cert.pem");
292 sni_key_path_buf = acme.storage.join("domains").join(primary).join("key.pem");
293 (sni_cert_path_buf.as_path(), sni_key_path_buf.as_path())
294 }
295 _ => unreachable!("Config validation ensures certs or acme"),
296 };
297
298 let cert = match load_certified_key(sni_cert_path, sni_key_path) {
299 Ok(cert) => Arc::new(cert),
300 Err(e) => {
301 if let Some(acme) = &sni_config.acme {
305 let primary = acme
306 .domains
307 .first()
308 .map(|s| s.as_str())
309 .unwrap_or("unknown");
310 warn!(
311 listener_id = %listener_id_str,
312 sni_index = i,
313 primary_domain = %primary,
314 error = %e,
315 "ACME SNI certificate not yet available, skipping initial load"
316 );
317
318 if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
320 metrics.record_sni_cert_skip(listener_id_str, primary);
321 }
322
323 continue;
324 } else {
325 return Err(e);
326 }
327 }
328 };
329
330 let priority_set: HashSet<String> = sni_config
332 .priority_hostnames
333 .iter()
334 .map(|h| h.to_lowercase())
335 .collect();
336 let has_priority = !priority_set.is_empty();
337
338 let hostnames = if !sni_config.hostnames.is_empty() {
340 sni_config.hostnames.clone()
341 } else if !priority_set.is_empty() {
342 extract_hostnames_from_cert(cert.cert.first().unwrap())?
344 } else if let Some(ref acme) = sni_config.acme {
345 acme.domains.clone()
347 } else {
348 extract_hostnames_from_cert(cert.cert.first().unwrap())?
350 };
351
352 if has_priority {
353 info!(
354 cert_file = %sni_cert_path.display(),
355 hostnames = ?hostnames,
356 priority_hostnames = ?sni_config.priority_hostnames,
357 "Loaded SNI certificate with priority tie-breaking"
358 );
359 } else if sni_config.hostnames.is_empty() && sni_config.acme.is_none() {
360 info!(
361 cert_file = %sni_cert_path.display(),
362 hostnames = ?hostnames,
363 "Loaded SNI certificate (auto-extracted hostnames)"
364 );
365 } else {
366 info!(
367 cert_file = %sni_cert_path.display(),
368 hostnames = ?hostnames,
369 "Loaded SNI certificate"
370 );
371 }
372
373 for hostname in &hostnames {
374 let hostname_lower = hostname.to_lowercase();
375 let is_priority = priority_set.contains(&hostname_lower);
376
377 if hostname_lower.starts_with("*.") {
378 let domain = hostname_lower.strip_prefix("*.").unwrap().to_string();
380
381 if let Some(existing) = wildcard_certs.get(&domain) {
382 if !Arc::ptr_eq(existing, &cert) {
383 let existing_has_priority = priority_wildcard.contains(&domain);
384
385 if is_priority && existing_has_priority {
386 return Err(TlsError::ConfigBuild(format!(
388 "Conflicting priority-hostnames: wildcard '*.{}' is claimed as priority by multiple certificates (including {:?}).",
389 domain,
390 sni_cert_path
391 )));
392 } else if is_priority {
393 debug!(
395 pattern = %hostname,
396 domain = %domain,
397 cert_file = %sni_cert_path.display(),
398 "Priority wildcard SNI certificate overwrites previous registration"
399 );
400 } else if existing_has_priority {
401 debug!(
403 pattern = %hostname,
404 domain = %domain,
405 cert_file = %sni_cert_path.display(),
406 "Skipping wildcard SNI registration, existing cert has priority"
407 );
408 continue;
409 } else if config.allow_sni_overlaps {
410 warn!(
416 listener_id = %listener_id_str,
417 pattern = %hostname,
418 cert_file = %sni_cert_path.display(),
419 "Overlapping wildcard SNI certificate ignored; \
420 an earlier certificate already claims this name"
421 );
422 continue;
423 } else {
424 return Err(TlsError::ConfigBuild(format!(
426 "Ambiguous SNI configuration: wildcard '*.{}' matches multiple certificates (including {:?}). \
427 Use explicit 'hostnames' or 'priority-hostnames' to resolve the conflict, \
428 or set 'allow-sni-overlaps true' to accept the first match in path order.",
429 domain,
430 sni_cert_path
431 )));
432 }
433 }
434 }
435
436 wildcard_certs.insert(domain.clone(), cert.clone());
437 if is_priority {
438 priority_wildcard.insert(domain.clone());
439 }
440 debug!(
441 pattern = %hostname,
442 domain = %domain,
443 priority = is_priority,
444 cert_file = %sni_cert_path.display(),
445 "Registered wildcard SNI certificate"
446 );
447 } else {
448 if let Some(existing) = sni_certs.get(&hostname_lower) {
450 if !Arc::ptr_eq(existing, &cert) {
451 let existing_has_priority = priority_exact.contains(&hostname_lower);
452
453 if is_priority && existing_has_priority {
454 return Err(TlsError::ConfigBuild(format!(
456 "Conflicting priority-hostnames: hostname '{}' is claimed as priority by multiple certificates (including {:?}).",
457 hostname_lower,
458 sni_cert_path
459 )));
460 } else if is_priority {
461 debug!(
463 hostname = %hostname_lower,
464 cert_file = %sni_cert_path.display(),
465 "Priority SNI certificate overwrites previous registration"
466 );
467 } else if existing_has_priority {
468 debug!(
470 hostname = %hostname_lower,
471 cert_file = %sni_cert_path.display(),
472 "Skipping SNI registration, existing cert has priority"
473 );
474 continue;
475 } else if config.allow_sni_overlaps {
476 warn!(
477 listener_id = %listener_id_str,
478 hostname = %hostname_lower,
479 cert_file = %sni_cert_path.display(),
480 "Overlapping SNI certificate ignored; \
481 an earlier certificate already claims this name"
482 );
483 continue;
484 } else {
485 return Err(TlsError::ConfigBuild(format!(
487 "Ambiguous SNI configuration: hostname '{}' matches multiple certificates (including {:?}). \
488 Use explicit 'hostnames' or 'priority-hostnames' to resolve the conflict, \
489 or set 'allow-sni-overlaps true' to accept the first match in path order.",
490 hostname_lower,
491 sni_cert_path
492 )));
493 }
494 }
495 }
496
497 sni_certs.insert(hostname_lower.clone(), cert.clone());
498 if is_priority {
499 priority_exact.insert(hostname_lower.clone());
500 }
501 debug!(
502 hostname = %hostname_lower,
503 priority = is_priority,
504 cert_file = %sni_cert_path.display(),
505 "Registered SNI certificate"
506 );
507 }
508 }
509 }
510
511 info!(
512 listener_id = %listener_id_str,
513 exact_certs = sni_certs.len(),
514 wildcard_certs = wildcard_certs.len(),
515 "SNI resolver initialized"
516 );
517
518 if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
519 metrics.set_certificates_loaded(
522 listener_id_str,
523 1 + sni_certs.len() + wildcard_certs.len(),
524 );
525 }
526
527 Ok(Self {
528 default_cert: Arc::new(default_cert),
529 sni_certs,
530 wildcard_certs,
531 })
532 }
533
534 pub fn resolve(&self, server_name: Option<&str>) -> Arc<CertifiedKey> {
539 let Some(name) = server_name else {
540 debug!("No SNI provided, using default certificate");
541 return self.default_cert.clone();
542 };
543
544 let name_lower = name.to_lowercase();
545
546 if let Some(cert) = self.sni_certs.get(&name_lower) {
548 debug!(hostname = %name_lower, "SNI exact match found");
549 return cert.clone();
550 }
551
552 let parts: Vec<&str> = name_lower.split('.').collect();
555 for i in 1..parts.len() {
556 let domain = parts[i..].join(".");
557 if let Some(cert) = self.wildcard_certs.get(&domain) {
558 debug!(
559 hostname = %name_lower,
560 wildcard_domain = %domain,
561 "SNI wildcard match found"
562 );
563 return cert.clone();
564 }
565 }
566
567 debug!(
568 hostname = %name_lower,
569 "No SNI match found, using default certificate"
570 );
571 self.default_cert.clone()
572 }
573}
574
575impl ResolvesServerCert for SniResolver {
576 fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
577 Some(self.resolve(client_hello.server_name()))
578 }
579}
580
581pub struct HotReloadableSniResolver {
591 inner: RwLock<Arc<SniResolver>>,
593 config: RwLock<TlsConfig>,
595 listener_id: String,
597 last_reload: RwLock<Instant>,
599}
600
601impl std::fmt::Debug for HotReloadableSniResolver {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 f.debug_struct("HotReloadableSniResolver")
604 .field("last_reload", &*self.last_reload.read())
605 .field("listener_id", &self.listener_id)
606 .finish()
607 }
608}
609
610impl HotReloadableSniResolver {
611 pub fn from_config(
613 config: TlsConfig,
614 listener_id: impl Into<String>,
615 ) -> Result<Self, TlsError> {
616 let listener_id = listener_id.into();
617 let resolver = SniResolver::from_config(&config, Some(&listener_id))?;
618
619 Ok(Self {
620 inner: RwLock::new(Arc::new(resolver)),
621 config: RwLock::new(config),
622 listener_id,
623 last_reload: RwLock::new(Instant::now()),
624 })
625 }
626
627 pub fn reload(&self) -> Result<(), TlsError> {
632 let config = self.config.read();
633
634 let cert_file_display = config
635 .cert_file
636 .as_ref()
637 .map(|p| p.display().to_string())
638 .unwrap_or_else(|| "(acme-managed)".to_string());
639
640 info!(
641 listener_id = %self.listener_id,
642 cert_file = %cert_file_display,
643 sni_count = config.additional_certs.len(),
644 "Reloading TLS certificates"
645 );
646
647 let new_resolver = match SniResolver::from_config(&config, Some(&self.listener_id)) {
650 Ok(resolver) => resolver,
651 Err(e) => {
652 if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
653 metrics.record_reload(&self.listener_id, false);
654 }
655 return Err(e);
656 }
657 };
658
659 let before = self.inner.read().served_certs();
663 let after = new_resolver.served_certs();
664 let diff = CertDiff::between(&before, &after);
665
666 *self.inner.write() = Arc::new(new_resolver);
668 *self.last_reload.write() = Instant::now();
669
670 if diff.is_empty() {
671 info!(
672 listener_id = %self.listener_id,
673 hostnames = after.len(),
674 "TLS certificates reloaded successfully; no change to served certificates"
675 );
676 } else {
677 info!(
678 listener_id = %self.listener_id,
679 hostnames = after.len(),
680 added = %CertDiff::render(&diff.added),
681 removed = %CertDiff::render(&diff.removed),
682 replaced = %CertDiff::render(&diff.replaced),
683 "TLS certificates reloaded successfully"
684 );
685 }
686 if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
687 metrics.record_reload(&self.listener_id, true);
688 }
689 Ok(())
690 }
691
692 pub fn update_config(&self, new_config: TlsConfig) -> Result<(), TlsError> {
694 let new_resolver = SniResolver::from_config(&new_config, Some(&self.listener_id))?;
696
697 *self.config.write() = new_config;
699 *self.inner.write() = Arc::new(new_resolver);
700 *self.last_reload.write() = Instant::now();
701
702 info!(
703 listener_id = %self.listener_id,
704 "TLS configuration updated and certificates reloaded"
705 );
706 Ok(())
707 }
708
709 pub fn last_reload_age(&self) -> Duration {
711 self.last_reload.read().elapsed()
712 }
713
714 pub fn resolve(&self, server_name: Option<&str>) -> Arc<CertifiedKey> {
718 self.inner.read().resolve(server_name)
719 }
720}
721
722impl ResolvesServerCert for HotReloadableSniResolver {
723 fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
724 Some(self.inner.read().resolve(client_hello.server_name()))
725 }
726}
727
728const CERT_EXTENSIONS: &[&str] = &["crt", "pem", "cert"];
730
731const KEY_EXTENSIONS: &[&str] = &["key"];
733
734fn scan_cert_folders(folders: &[SniCertFolder], listener_id: &str) -> Vec<SniCertificate> {
747 let mut found = Vec::new();
748
749 for folder in folders {
750 let dir = &folder.cert_folder;
751 let entries = match std::fs::read_dir(dir) {
752 Ok(entries) => entries,
753 Err(e) => {
754 warn!(
755 listener_id = %listener_id,
756 cert_folder = %dir.display(),
757 error = %e,
758 "Certificate folder could not be read; no certificates loaded from it"
759 );
760 continue;
761 }
762 };
763
764 let mut paths: Vec<PathBuf> = entries
768 .filter_map(|e| e.ok().map(|e| e.path()))
769 .filter(|p| p.is_file())
770 .collect();
771 paths.sort();
772
773 let mut pairs = 0usize;
774 for cert_path in &paths {
775 let Some(extension) = cert_path.extension().and_then(|e| e.to_str()) else {
776 continue;
777 };
778 if !CERT_EXTENSIONS.contains(&extension.to_ascii_lowercase().as_str()) {
779 continue;
780 }
781
782 let Some(key_path) = matching_key_path(cert_path) else {
783 warn!(
784 listener_id = %listener_id,
785 cert_file = %cert_path.display(),
786 "Certificate in scanned folder has no matching key file; skipping"
787 );
788 if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
789 metrics.record_folder_entry_skipped(listener_id, "no_key");
790 }
791 continue;
792 };
793
794 if let Err(e) = load_certified_key(cert_path, &key_path) {
799 warn!(
800 listener_id = %listener_id,
801 cert_file = %cert_path.display(),
802 key_file = %key_path.display(),
803 error = %e,
804 "Certificate pair in scanned folder could not be loaded; skipping"
805 );
806 if let Some(metrics) = crate::tls_metrics::get_tls_metrics() {
807 metrics.record_folder_entry_skipped(listener_id, "unreadable");
808 }
809 continue;
810 }
811
812 found.push(SniCertificate {
813 hostnames: Vec::new(),
814 priority_hostnames: Vec::new(),
815 cert_file: Some(cert_path.clone()),
816 key_file: Some(key_path),
817 acme: None,
818 });
819 pairs += 1;
820 }
821
822 info!(
823 listener_id = %listener_id,
824 cert_folder = %dir.display(),
825 certificates = pairs,
826 reload_mode = %folder.reload_mode,
827 "Scanned certificate folder"
828 );
829 }
830
831 found
832}
833
834fn matching_key_path(cert_path: &Path) -> Option<PathBuf> {
840 for extension in KEY_EXTENSIONS {
841 let candidate = cert_path.with_extension(extension);
842 if candidate != cert_path && candidate.is_file() {
843 return Some(candidate);
844 }
845 }
846 if cert_path
848 .extension()
849 .and_then(|e| e.to_str())?
850 .eq_ignore_ascii_case("pem")
851 && load_certified_key(cert_path, cert_path).is_ok()
852 {
853 return Some(cert_path.to_path_buf());
854 }
855 None
856}
857
858pub struct CertificateReloader {
862 resolvers: RwLock<HashMap<String, Arc<HotReloadableSniResolver>>>,
864}
865
866impl CertificateReloader {
867 pub fn new() -> Self {
869 Self {
870 resolvers: RwLock::new(HashMap::new()),
871 }
872 }
873
874 pub fn register(&self, listener_id: &str, resolver: Arc<HotReloadableSniResolver>) {
876 debug!(listener_id = %listener_id, "Registering TLS resolver for hot-reload");
877 self.resolvers
878 .write()
879 .insert(listener_id.to_string(), resolver);
880 }
881
882 pub fn reload_all(&self) -> (usize, Vec<(String, TlsError)>) {
886 let resolvers = self.resolvers.read();
887 let mut success_count = 0;
888 let mut errors = Vec::new();
889
890 info!(
891 listener_count = resolvers.len(),
892 "Reloading certificates for all TLS listeners"
893 );
894
895 for (listener_id, resolver) in resolvers.iter() {
896 match resolver.reload() {
897 Ok(()) => {
898 success_count += 1;
899 debug!(listener_id = %listener_id, "Certificate reload successful");
900 }
901 Err(e) => {
902 error!(listener_id = %listener_id, error = %e, "Certificate reload failed");
903 errors.push((listener_id.clone(), e));
904 }
905 }
906 }
907
908 if errors.is_empty() {
909 info!(
910 success_count = success_count,
911 "All certificates reloaded successfully"
912 );
913 } else {
914 warn!(
915 success_count = success_count,
916 error_count = errors.len(),
917 "Certificate reload completed with errors"
918 );
919 }
920
921 (success_count, errors)
922 }
923
924 pub fn status(&self) -> HashMap<String, Duration> {
926 self.resolvers
927 .read()
928 .iter()
929 .map(|(id, resolver)| (id.clone(), resolver.last_reload_age()))
930 .collect()
931 }
932}
933
934impl Default for CertificateReloader {
935 fn default() -> Self {
936 Self::new()
937 }
938}
939
940#[derive(Debug, Clone)]
946pub struct OcspCacheEntry {
947 pub response: Vec<u8>,
949 pub fetched_at: Instant,
951 pub expires_at: Option<Instant>,
953}
954
955pub struct OcspStapler {
959 cache: RwLock<HashMap<String, OcspCacheEntry>>,
961 refresh_interval: Duration,
963}
964
965impl OcspStapler {
966 pub fn new() -> Self {
968 Self {
969 cache: RwLock::new(HashMap::new()),
970 refresh_interval: Duration::from_secs(3600), }
972 }
973
974 pub fn with_refresh_interval(interval: Duration) -> Self {
976 Self {
977 cache: RwLock::new(HashMap::new()),
978 refresh_interval: interval,
979 }
980 }
981
982 pub fn get_response(&self, cert_fingerprint: &str) -> Option<Vec<u8>> {
984 let cache = self.cache.read();
985 if let Some(entry) = cache.get(cert_fingerprint) {
986 if entry.fetched_at.elapsed() < self.refresh_interval {
988 trace!(fingerprint = %cert_fingerprint, "OCSP cache hit");
989 return Some(entry.response.clone());
990 }
991 trace!(fingerprint = %cert_fingerprint, "OCSP cache expired");
992 }
993 None
994 }
995
996 pub fn fetch_ocsp_response(
1001 &self,
1002 cert_der: &[u8],
1003 issuer_der: &[u8],
1004 ) -> Result<Vec<u8>, TlsError> {
1005 use x509_parser::prelude::*;
1006
1007 let (_, cert) = X509Certificate::from_der(cert_der)
1009 .map_err(|e| TlsError::OcspFetch(format!("Failed to parse certificate: {}", e)))?;
1010
1011 let (_, issuer) = X509Certificate::from_der(issuer_der).map_err(|e| {
1013 TlsError::OcspFetch(format!("Failed to parse issuer certificate: {}", e))
1014 })?;
1015
1016 let ocsp_url = extract_ocsp_responder_url(&cert)?;
1018 debug!(url = %ocsp_url, "Found OCSP responder URL");
1019
1020 let ocsp_request = build_ocsp_request(&cert, &issuer)?;
1022
1023 let response = send_ocsp_request_sync(&ocsp_url, &ocsp_request)?;
1026
1027 let fingerprint = calculate_cert_fingerprint(cert_der);
1029
1030 let entry = OcspCacheEntry {
1032 response: response.clone(),
1033 fetched_at: Instant::now(),
1034 expires_at: None, };
1036 self.cache.write().insert(fingerprint, entry);
1037
1038 info!("Successfully fetched and cached OCSP response");
1039 Ok(response)
1040 }
1041
1042 pub async fn fetch_ocsp_response_async(
1044 &self,
1045 cert_der: &[u8],
1046 issuer_der: &[u8],
1047 ) -> Result<Vec<u8>, TlsError> {
1048 use x509_parser::prelude::*;
1049
1050 let (_, cert) = X509Certificate::from_der(cert_der)
1052 .map_err(|e| TlsError::OcspFetch(format!("Failed to parse certificate: {}", e)))?;
1053
1054 let (_, issuer) = X509Certificate::from_der(issuer_der).map_err(|e| {
1056 TlsError::OcspFetch(format!("Failed to parse issuer certificate: {}", e))
1057 })?;
1058
1059 let ocsp_url = extract_ocsp_responder_url(&cert)?;
1061 debug!(url = %ocsp_url, "Found OCSP responder URL");
1062
1063 let ocsp_request = build_ocsp_request(&cert, &issuer)?;
1065
1066 let response = send_ocsp_request_async(&ocsp_url, &ocsp_request).await?;
1068
1069 let fingerprint = calculate_cert_fingerprint(cert_der);
1071
1072 let entry = OcspCacheEntry {
1074 response: response.clone(),
1075 fetched_at: Instant::now(),
1076 expires_at: None,
1077 };
1078 self.cache.write().insert(fingerprint, entry);
1079
1080 info!("Successfully fetched and cached OCSP response (async)");
1081 Ok(response)
1082 }
1083
1084 pub fn prefetch_for_config(&self, config: &TlsConfig) -> Vec<String> {
1086 let mut warnings = Vec::new();
1087
1088 if !config.ocsp_stapling {
1089 trace!("OCSP stapling disabled in config");
1090 return warnings;
1091 }
1092
1093 info!("Prefetching OCSP responses for certificates");
1094
1095 warnings.push("OCSP stapling prefetch not yet fully implemented".to_string());
1098
1099 warnings
1100 }
1101
1102 pub fn clear_cache(&self) {
1104 self.cache.write().clear();
1105 info!("OCSP cache cleared");
1106 }
1107}
1108
1109impl Default for OcspStapler {
1110 fn default() -> Self {
1111 Self::new()
1112 }
1113}
1114
1115fn extract_ocsp_responder_url(
1121 cert: &x509_parser::certificate::X509Certificate,
1122) -> Result<String, TlsError> {
1123 use x509_parser::prelude::*;
1124
1125 let aia = cert
1127 .extensions()
1128 .iter()
1129 .find(|ext| ext.oid == oid_registry::OID_PKIX_AUTHORITY_INFO_ACCESS)
1130 .ok_or_else(|| {
1131 TlsError::OcspFetch(
1132 "Certificate does not have Authority Information Access extension".to_string(),
1133 )
1134 })?;
1135
1136 let aia_value = match aia.parsed_extension() {
1138 ParsedExtension::AuthorityInfoAccess(aia) => aia,
1139 _ => {
1140 return Err(TlsError::OcspFetch(
1141 "Failed to parse Authority Information Access extension".to_string(),
1142 ))
1143 }
1144 };
1145
1146 for access in &aia_value.accessdescs {
1148 if access.access_method == oid_registry::OID_PKIX_ACCESS_DESCRIPTOR_OCSP {
1149 match &access.access_location {
1150 GeneralName::URI(url) => {
1151 return Ok(url.to_string());
1152 }
1153 _ => continue,
1154 }
1155 }
1156 }
1157
1158 Err(TlsError::OcspFetch(
1159 "Certificate AIA does not contain OCSP responder URL".to_string(),
1160 ))
1161}
1162
1163fn build_ocsp_request(
1167 cert: &x509_parser::certificate::X509Certificate,
1168 issuer: &x509_parser::certificate::X509Certificate,
1169) -> Result<Vec<u8>, TlsError> {
1170 use sha2::{Digest, Sha256};
1171
1172 let issuer_name_hash = {
1179 let mut hasher = Sha256::new();
1180 hasher.update(issuer.subject().as_raw());
1181 hasher.finalize()
1182 };
1183
1184 let issuer_key_hash = {
1186 let mut hasher = Sha256::new();
1187 hasher.update(issuer.public_key().subject_public_key.data.as_ref());
1188 hasher.finalize()
1189 };
1190
1191 let serial = cert.serial.to_bytes_be();
1193
1194 let request = build_ocsp_request_der(&issuer_name_hash, &issuer_key_hash, &serial);
1197
1198 Ok(request)
1199}
1200
1201fn build_ocsp_request_der(
1203 issuer_name_hash: &[u8],
1204 issuer_key_hash: &[u8],
1205 serial_number: &[u8],
1206) -> Vec<u8> {
1207 let sha256_oid: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
1209
1210 let hash_algorithm = der_sequence(&[&der_oid(sha256_oid), &der_null()]);
1212
1213 let cert_id = der_sequence(&[
1214 &hash_algorithm,
1215 &der_octet_string(issuer_name_hash),
1216 &der_octet_string(issuer_key_hash),
1217 &der_integer(serial_number),
1218 ]);
1219
1220 let request = der_sequence(&[&cert_id]);
1222
1223 let request_list = der_sequence(&[&request]);
1225
1226 let tbs_request = der_sequence(&[&request_list]);
1228
1229 der_sequence(&[&tbs_request])
1231}
1232
1233fn der_sequence(items: &[&[u8]]) -> Vec<u8> {
1235 let mut content = Vec::new();
1236 for item in items {
1237 content.extend_from_slice(item);
1238 }
1239 let mut result = vec![0x30]; result.extend(der_length(content.len()));
1241 result.extend(content);
1242 result
1243}
1244
1245fn der_oid(oid: &[u8]) -> Vec<u8> {
1246 let mut result = vec![0x06]; result.extend(der_length(oid.len()));
1248 result.extend_from_slice(oid);
1249 result
1250}
1251
1252fn der_null() -> Vec<u8> {
1253 vec![0x05, 0x00] }
1255
1256fn der_octet_string(data: &[u8]) -> Vec<u8> {
1257 let mut result = vec![0x04]; result.extend(der_length(data.len()));
1259 result.extend_from_slice(data);
1260 result
1261}
1262
1263fn der_integer(data: &[u8]) -> Vec<u8> {
1264 let mut result = vec![0x02]; let data = match data.iter().position(|&b| b != 0) {
1267 Some(pos) => &data[pos..],
1268 None => &[0],
1269 };
1270 if !data.is_empty() && data[0] & 0x80 != 0 {
1272 result.extend(der_length(data.len() + 1));
1273 result.push(0x00);
1274 } else {
1275 result.extend(der_length(data.len()));
1276 }
1277 result.extend_from_slice(data);
1278 result
1279}
1280
1281fn der_length(len: usize) -> Vec<u8> {
1282 if len < 128 {
1283 vec![len as u8]
1284 } else if len < 256 {
1285 vec![0x81, len as u8]
1286 } else {
1287 vec![0x82, (len >> 8) as u8, len as u8]
1288 }
1289}
1290
1291fn send_ocsp_request_sync(url: &str, request: &[u8]) -> Result<Vec<u8>, TlsError> {
1293 use std::io::{Read, Write};
1294 use std::net::TcpStream;
1295 use std::time::Duration;
1296
1297 let url = url::Url::parse(url)
1299 .map_err(|e| TlsError::OcspFetch(format!("Invalid OCSP URL: {}", e)))?;
1300
1301 let host = url
1302 .host_str()
1303 .ok_or_else(|| TlsError::OcspFetch("OCSP URL has no host".to_string()))?;
1304 let port = url.port().unwrap_or(80);
1305 let path = if url.path().is_empty() {
1306 "/"
1307 } else {
1308 url.path()
1309 };
1310
1311 let addr = format!("{}:{}", host, port);
1313 let mut stream = TcpStream::connect(&addr)
1314 .map_err(|e| TlsError::OcspFetch(format!("Failed to connect to OCSP responder: {}", e)))?;
1315
1316 stream
1317 .set_read_timeout(Some(Duration::from_secs(10)))
1318 .map_err(|e| TlsError::OcspFetch(format!("Failed to set timeout: {}", e)))?;
1319 stream
1320 .set_write_timeout(Some(Duration::from_secs(10)))
1321 .map_err(|e| TlsError::OcspFetch(format!("Failed to set timeout: {}", e)))?;
1322
1323 let http_request = format!(
1325 "POST {} HTTP/1.1\r\n\
1326 Host: {}\r\n\
1327 Content-Type: application/ocsp-request\r\n\
1328 Content-Length: {}\r\n\
1329 Connection: close\r\n\
1330 \r\n",
1331 path,
1332 host,
1333 request.len()
1334 );
1335
1336 stream
1338 .write_all(http_request.as_bytes())
1339 .map_err(|e| TlsError::OcspFetch(format!("Failed to send OCSP request: {}", e)))?;
1340 stream
1341 .write_all(request)
1342 .map_err(|e| TlsError::OcspFetch(format!("Failed to send OCSP request body: {}", e)))?;
1343
1344 let mut response = Vec::new();
1346 stream
1347 .read_to_end(&mut response)
1348 .map_err(|e| TlsError::OcspFetch(format!("Failed to read OCSP response: {}", e)))?;
1349
1350 let headers_end = response
1352 .windows(4)
1353 .position(|w| w == b"\r\n\r\n")
1354 .ok_or_else(|| TlsError::OcspFetch("Invalid HTTP response: no headers end".to_string()))?;
1355
1356 let body = &response[headers_end + 4..];
1357 if body.is_empty() {
1358 return Err(TlsError::OcspFetch("Empty OCSP response body".to_string()));
1359 }
1360
1361 Ok(body.to_vec())
1362}
1363
1364async fn send_ocsp_request_async(url: &str, request: &[u8]) -> Result<Vec<u8>, TlsError> {
1366 let client = reqwest::Client::builder()
1367 .timeout(Duration::from_secs(10))
1368 .build()
1369 .map_err(|e| TlsError::OcspFetch(format!("Failed to create HTTP client: {}", e)))?;
1370
1371 let response = client
1372 .post(url)
1373 .header("Content-Type", "application/ocsp-request")
1374 .body(request.to_vec())
1375 .send()
1376 .await
1377 .map_err(|e| TlsError::OcspFetch(format!("OCSP request failed: {}", e)))?;
1378
1379 if !response.status().is_success() {
1380 return Err(TlsError::OcspFetch(format!(
1381 "OCSP responder returned status: {}",
1382 response.status()
1383 )));
1384 }
1385
1386 let body = response
1387 .bytes()
1388 .await
1389 .map_err(|e| TlsError::OcspFetch(format!("Failed to read OCSP response: {}", e)))?;
1390
1391 Ok(body.to_vec())
1392}
1393
1394fn calculate_cert_fingerprint(cert_der: &[u8]) -> String {
1396 use sha2::{Digest, Sha256};
1397 let mut hasher = Sha256::new();
1398 hasher.update(cert_der);
1399 let result = hasher.finalize();
1400 hex::encode(result)
1401}
1402
1403pub fn load_client_cert_key(
1421 cert_path: &Path,
1422 key_path: &Path,
1423) -> Result<Arc<pingora_core::utils::tls::CertKey>, TlsError> {
1424 let cert_file = File::open(cert_path)
1426 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1427 let mut cert_reader = BufReader::new(cert_file);
1428
1429 let cert_ders: Vec<Vec<u8>> = rustls_pemfile::certs(&mut cert_reader)
1431 .collect::<Result<Vec<_>, _>>()
1432 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?
1433 .into_iter()
1434 .map(|c| c.to_vec())
1435 .collect();
1436
1437 if cert_ders.is_empty() {
1438 return Err(TlsError::CertificateLoad(format!(
1439 "{}: No certificates found in PEM file",
1440 cert_path.display()
1441 )));
1442 }
1443
1444 let key_file = File::open(key_path)
1446 .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?;
1447 let mut key_reader = BufReader::new(key_file);
1448
1449 let key_der = rustls_pemfile::private_key(&mut key_reader)
1451 .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?
1452 .ok_or_else(|| {
1453 TlsError::KeyLoad(format!(
1454 "{}: No private key found in PEM file",
1455 key_path.display()
1456 ))
1457 })?
1458 .secret_der()
1459 .to_vec();
1460
1461 let cert_key = pingora_core::utils::tls::CertKey::new(cert_ders, key_der);
1463
1464 debug!(
1465 cert_path = %cert_path.display(),
1466 key_path = %key_path.display(),
1467 "Loaded mTLS client certificate for upstream connections"
1468 );
1469
1470 Ok(Arc::new(cert_key))
1471}
1472
1473pub fn build_upstream_tls_config(config: &UpstreamTlsConfig) -> Result<ClientConfig, TlsError> {
1478 let mut root_store = RootCertStore::empty();
1479
1480 if let Some(ca_path) = &config.ca_cert {
1482 let ca_file = File::open(ca_path)
1483 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1484 let mut ca_reader = BufReader::new(ca_file);
1485
1486 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
1487 .collect::<Result<Vec<_>, _>>()
1488 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1489
1490 for cert in certs {
1491 root_store.add(cert).map_err(|e| {
1492 TlsError::InvalidCertificate(format!("Failed to add CA certificate: {}", e))
1493 })?;
1494 }
1495
1496 debug!(
1497 ca_file = %ca_path.display(),
1498 cert_count = root_store.len(),
1499 "Loaded upstream CA certificates"
1500 );
1501 } else if !config.insecure_skip_verify {
1502 root_store = RootCertStore {
1504 roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
1505 };
1506 trace!("Using webpki-roots for upstream TLS verification");
1507 }
1508
1509 let builder = ClientConfig::builder().with_root_certificates(root_store);
1511
1512 let client_config = if let (Some(cert_path), Some(key_path)) =
1513 (&config.client_cert, &config.client_key)
1514 {
1515 let cert_file = File::open(cert_path)
1517 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1518 let mut cert_reader = BufReader::new(cert_file);
1519
1520 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
1521 .collect::<Result<Vec<_>, _>>()
1522 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1523
1524 if certs.is_empty() {
1525 return Err(TlsError::CertificateLoad(format!(
1526 "{}: No certificates found",
1527 cert_path.display()
1528 )));
1529 }
1530
1531 let key_file = File::open(key_path)
1533 .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?;
1534 let mut key_reader = BufReader::new(key_file);
1535
1536 let key = rustls_pemfile::private_key(&mut key_reader)
1537 .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?
1538 .ok_or_else(|| {
1539 TlsError::KeyLoad(format!("{}: No private key found", key_path.display()))
1540 })?;
1541
1542 info!(
1543 cert_file = %cert_path.display(),
1544 "Configured mTLS client certificate for upstream connections"
1545 );
1546
1547 builder
1548 .with_client_auth_cert(certs, key)
1549 .map_err(|e| TlsError::CertKeyMismatch(format!("Failed to set client auth: {}", e)))?
1550 } else {
1551 builder.with_no_client_auth()
1553 };
1554
1555 debug!("Upstream TLS configuration built successfully");
1556 Ok(client_config)
1557}
1558
1559pub fn validate_upstream_tls_config(config: &UpstreamTlsConfig) -> Result<(), TlsError> {
1561 if let Some(ca_path) = &config.ca_cert {
1563 if !ca_path.exists() {
1564 return Err(TlsError::CertificateLoad(format!(
1565 "Upstream CA certificate not found: {}",
1566 ca_path.display()
1567 )));
1568 }
1569 }
1570
1571 if let Some(cert_path) = &config.client_cert {
1573 if !cert_path.exists() {
1574 return Err(TlsError::CertificateLoad(format!(
1575 "Upstream client certificate not found: {}",
1576 cert_path.display()
1577 )));
1578 }
1579
1580 match &config.client_key {
1582 Some(key_path) if !key_path.exists() => {
1583 return Err(TlsError::KeyLoad(format!(
1584 "Upstream client key not found: {}",
1585 key_path.display()
1586 )));
1587 }
1588 None => {
1589 return Err(TlsError::ConfigBuild(
1590 "client_cert specified without client_key".to_string(),
1591 ));
1592 }
1593 _ => {}
1594 }
1595 }
1596
1597 if config.client_key.is_some() && config.client_cert.is_none() {
1598 return Err(TlsError::ConfigBuild(
1599 "client_key specified without client_cert".to_string(),
1600 ));
1601 }
1602
1603 Ok(())
1604}
1605
1606fn load_certified_key(cert_path: &Path, key_path: &Path) -> Result<CertifiedKey, TlsError> {
1612 let cert_file = File::open(cert_path)
1614 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1615 let mut cert_reader = BufReader::new(cert_file);
1616
1617 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
1618 .collect::<Result<Vec<_>, _>>()
1619 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", cert_path.display(), e)))?;
1620
1621 if certs.is_empty() {
1622 return Err(TlsError::CertificateLoad(format!(
1623 "{}: No certificates found in file",
1624 cert_path.display()
1625 )));
1626 }
1627
1628 let key_file = File::open(key_path)
1630 .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?;
1631 let mut key_reader = BufReader::new(key_file);
1632
1633 let key = rustls_pemfile::private_key(&mut key_reader)
1634 .map_err(|e| TlsError::KeyLoad(format!("{}: {}", key_path.display(), e)))?
1635 .ok_or_else(|| {
1636 TlsError::KeyLoad(format!(
1637 "{}: No private key found in file",
1638 key_path.display()
1639 ))
1640 })?;
1641
1642 let provider = rustls::crypto::CryptoProvider::get_default()
1644 .cloned()
1645 .unwrap_or_else(|| Arc::new(rustls::crypto::aws_lc_rs::default_provider()));
1646
1647 let signing_key = provider
1648 .key_provider
1649 .load_private_key(key)
1650 .map_err(|e| TlsError::CertKeyMismatch(format!("Failed to load private key: {:?}", e)))?;
1651
1652 Ok(CertifiedKey::new(certs, signing_key))
1653}
1654
1655fn extract_hostnames_from_cert(cert_der: &CertificateDer<'_>) -> Result<Vec<String>, TlsError> {
1663 use x509_parser::prelude::*;
1664
1665 let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
1666 TlsError::InvalidCertificate(format!("Failed to parse X.509 certificate: {}", e))
1667 })?;
1668
1669 let mut hostnames = Vec::new();
1670
1671 if let Ok(Some(san_ext)) = cert.subject_alternative_name() {
1673 for name in &san_ext.value.general_names {
1674 if let GeneralName::DNSName(dns) = name {
1675 hostnames.push(dns.to_lowercase());
1676 }
1677 }
1678 }
1679
1680 if hostnames.is_empty() {
1682 for attr in cert.subject().iter_common_name() {
1683 if let Ok(cn) = attr.as_str() {
1684 hostnames.push(cn.to_lowercase());
1685 }
1686 }
1687 }
1688
1689 if hostnames.is_empty() {
1690 return Err(TlsError::InvalidCertificate(
1691 "Certificate has no DNS names in SAN or CN".to_string(),
1692 ));
1693 }
1694
1695 Ok(hostnames)
1696}
1697
1698pub fn load_client_ca(ca_path: &Path) -> Result<RootCertStore, TlsError> {
1700 let ca_file = File::open(ca_path)
1701 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1702 let mut ca_reader = BufReader::new(ca_file);
1703
1704 let mut root_store = RootCertStore::empty();
1705
1706 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
1707 .collect::<Result<Vec<_>, _>>()
1708 .map_err(|e| TlsError::CertificateLoad(format!("{}: {}", ca_path.display(), e)))?;
1709
1710 for cert in certs {
1711 root_store.add(cert).map_err(|e| {
1712 TlsError::InvalidCertificate(format!("Failed to add CA certificate: {}", e))
1713 })?;
1714 }
1715
1716 if root_store.is_empty() {
1717 return Err(TlsError::CertificateLoad(format!(
1718 "{}: No CA certificates found",
1719 ca_path.display()
1720 )));
1721 }
1722
1723 info!(
1724 ca_file = %ca_path.display(),
1725 cert_count = root_store.len(),
1726 "Loaded client CA certificates"
1727 );
1728
1729 Ok(root_store)
1730}
1731
1732fn resolve_protocol_versions(config: &TlsConfig) -> Vec<&'static rustls::SupportedProtocolVersion> {
1734 use zentinel_common::types::TlsVersion;
1735
1736 let min = &config.min_version;
1737 let max = config.max_version.as_ref().unwrap_or(&TlsVersion::Tls13);
1738
1739 let mut versions = Vec::new();
1740
1741 if matches!(min, TlsVersion::Tls12) {
1743 versions.push(&rustls::version::TLS12);
1744 }
1745
1746 if matches!(max, TlsVersion::Tls13) {
1748 versions.push(&rustls::version::TLS13);
1749 }
1750
1751 if versions.is_empty() {
1752 warn!("No valid TLS versions resolved from config, falling back to TLS 1.2 + 1.3");
1754 versions.push(&rustls::version::TLS12);
1755 versions.push(&rustls::version::TLS13);
1756 }
1757
1758 versions
1759}
1760
1761fn resolve_cipher_suites(names: &[String]) -> Result<Vec<rustls::SupportedCipherSuite>, TlsError> {
1765 use rustls::crypto::aws_lc_rs::cipher_suite;
1766
1767 let known: &[(&str, rustls::SupportedCipherSuite)] = &[
1769 (
1771 "TLS_AES_256_GCM_SHA384",
1772 cipher_suite::TLS13_AES_256_GCM_SHA384,
1773 ),
1774 (
1775 "TLS_AES_128_GCM_SHA256",
1776 cipher_suite::TLS13_AES_128_GCM_SHA256,
1777 ),
1778 (
1779 "TLS_CHACHA20_POLY1305_SHA256",
1780 cipher_suite::TLS13_CHACHA20_POLY1305_SHA256,
1781 ),
1782 (
1784 "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
1785 cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
1786 ),
1787 (
1788 "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
1789 cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
1790 ),
1791 (
1792 "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
1793 cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
1794 ),
1795 (
1796 "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
1797 cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
1798 ),
1799 (
1800 "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
1801 cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
1802 ),
1803 (
1804 "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
1805 cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
1806 ),
1807 ];
1808
1809 let mut suites = Vec::with_capacity(names.len());
1810 for name in names {
1811 let normalized = name.to_uppercase().replace('-', "_");
1812 match known.iter().find(|(n, _)| *n == normalized) {
1813 Some((_, suite)) => suites.push(*suite),
1814 None => {
1815 let available: Vec<&str> = known.iter().map(|(n, _)| *n).collect();
1816 return Err(TlsError::ConfigBuild(format!(
1817 "Unknown cipher suite '{}'. Available: {}",
1818 name,
1819 available.join(", ")
1820 )));
1821 }
1822 }
1823 }
1824
1825 Ok(suites)
1826}
1827
1828pub fn build_server_config(
1838 config: &TlsConfig,
1839 listener_id: &str,
1840) -> Result<ServerConfig, TlsError> {
1841 let resolver = SniResolver::from_config(config, Some(listener_id))?;
1842 build_server_config_with_resolver(config, Arc::new(resolver))
1843}
1844
1845pub fn build_server_config_with_resolver(
1854 config: &TlsConfig,
1855 resolver: Arc<dyn ResolvesServerCert>,
1856) -> Result<ServerConfig, TlsError> {
1857 let versions = resolve_protocol_versions(config);
1859 info!(
1860 versions = ?versions.iter().map(|v| format!("{:?}", v.version)).collect::<Vec<_>>(),
1861 "TLS protocol versions configured"
1862 );
1863
1864 let builder = if !config.cipher_suites.is_empty() {
1866 let suites = resolve_cipher_suites(&config.cipher_suites)?;
1867 info!(
1868 cipher_suites = ?config.cipher_suites,
1869 count = suites.len(),
1870 "Custom TLS cipher suites configured"
1871 );
1872 let provider = rustls::crypto::CryptoProvider {
1873 cipher_suites: suites,
1874 ..rustls::crypto::aws_lc_rs::default_provider()
1875 };
1876 ServerConfig::builder_with_provider(Arc::new(provider))
1877 .with_protocol_versions(&versions)
1878 .map_err(|e| {
1879 TlsError::ConfigBuild(format!("Invalid TLS protocol/cipher configuration: {}", e))
1880 })?
1881 } else {
1882 ServerConfig::builder_with_protocol_versions(&versions)
1883 };
1884
1885 let server_config = if config.client_auth {
1887 if let Some(ca_path) = &config.ca_file {
1888 let root_store = load_client_ca(ca_path)?;
1889 let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
1890 .build()
1891 .map_err(|e| {
1892 TlsError::ConfigBuild(format!("Failed to build client verifier: {}", e))
1893 })?;
1894
1895 info!("mTLS enabled: client certificates required");
1896
1897 builder
1898 .with_client_cert_verifier(verifier)
1899 .with_cert_resolver(resolver.clone())
1900 } else {
1901 return Err(TlsError::ConfigBuild(
1908 "client_auth is enabled but no ca_file is configured. Client certificates \
1909 cannot be verified without a CA, and serving without client authentication \
1910 would contradict the configuration."
1911 .to_string(),
1912 ));
1913 }
1914 } else {
1915 builder
1916 .with_no_client_auth()
1917 .with_cert_resolver(resolver.clone())
1918 };
1919
1920 let mut server_config = server_config;
1922 server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1923
1924 if !config.session_resumption {
1926 server_config.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
1927 info!("TLS session resumption disabled");
1928 }
1929
1930 debug!("TLS configuration built successfully");
1931
1932 Ok(server_config)
1933}
1934
1935pub fn validate_tls_config(config: &TlsConfig) -> Result<(), TlsError> {
1937 if config.acme.is_some() {
1939 trace!("Skipping manual cert validation for ACME-managed TLS");
1941 } else {
1942 match (&config.cert_file, &config.key_file) {
1944 (Some(cert_file), Some(key_file)) => {
1945 if !cert_file.exists() {
1946 return Err(TlsError::CertificateLoad(format!(
1947 "Certificate file not found: {}",
1948 cert_file.display()
1949 )));
1950 }
1951 if !key_file.exists() {
1952 return Err(TlsError::KeyLoad(format!(
1953 "Key file not found: {}",
1954 key_file.display()
1955 )));
1956 }
1957 }
1958 _ => {
1959 return Err(TlsError::ConfigBuild(
1960 "TLS configuration requires cert_file and key_file (or ACME block)".to_string(),
1961 ));
1962 }
1963 }
1964 }
1965
1966 for sni in &config.additional_certs {
1968 if sni.acme.is_some() {
1970 trace!("Skipping manual cert validation for ACME-managed SNI certificate");
1971 continue;
1972 }
1973
1974 match (&sni.cert_file, &sni.key_file) {
1976 (Some(cert_file), Some(key_file)) => {
1977 if !cert_file.exists() {
1978 return Err(TlsError::CertificateLoad(format!(
1979 "SNI certificate file not found: {}",
1980 cert_file.display()
1981 )));
1982 }
1983 if !key_file.exists() {
1984 return Err(TlsError::KeyLoad(format!(
1985 "SNI key file not found: {}",
1986 key_file.display()
1987 )));
1988 }
1989 }
1990 _ => {
1991 return Err(TlsError::ConfigBuild(
1992 "SNI certificate requires cert_file and key_file (or ACME block)".to_string(),
1993 ));
1994 }
1995 }
1996 }
1997
1998 if config.client_auth {
2000 if let Some(ca_path) = &config.ca_file {
2001 if !ca_path.exists() {
2002 return Err(TlsError::CertificateLoad(format!(
2003 "CA certificate file not found: {}",
2004 ca_path.display()
2005 )));
2006 }
2007 }
2008 }
2009
2010 Ok(())
2011}
2012
2013#[cfg(test)]
2014mod tests {
2015
2016 #[test]
2017 fn test_wildcard_matching() {
2018 let name = "foo.bar.example.com";
2021 let parts: Vec<&str> = name.split('.').collect();
2022
2023 assert_eq!(parts.len(), 4);
2024
2025 let domain1 = parts[1..].join(".");
2027 assert_eq!(domain1, "bar.example.com");
2028
2029 let domain2 = parts[2..].join(".");
2030 assert_eq!(domain2, "example.com");
2031 }
2032
2033 #[test]
2034 fn test_hostname_normalization() {
2035 let hostname = "Example.COM";
2036 let normalized = hostname.to_lowercase();
2037 assert_eq!(normalized, "example.com");
2038 }
2039}
2040
2041#[cfg(test)]
2042mod cert_diff_tests {
2043 use super::*;
2044
2045 fn snapshot(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
2046 pairs
2047 .iter()
2048 .map(|(h, f)| (h.to_string(), f.to_string()))
2049 .collect()
2050 }
2051
2052 #[test]
2053 fn an_unchanged_folder_reports_no_change() {
2054 let before = snapshot(&[("<default>", "aa"), ("a.test", "bb")]);
2055 let diff = CertDiff::between(&before, &before);
2056 assert!(diff.is_empty());
2057 }
2058
2059 #[test]
2060 fn a_new_certificate_is_reported_as_added() {
2061 let before = snapshot(&[("a.test", "aa")]);
2062 let after = snapshot(&[("a.test", "aa"), ("b.test", "bb")]);
2063 let diff = CertDiff::between(&before, &after);
2064 assert_eq!(diff.added, vec!["b.test".to_string()]);
2065 assert!(diff.removed.is_empty());
2066 assert!(diff.replaced.is_empty());
2067 }
2068
2069 #[test]
2070 fn a_deleted_certificate_is_reported_as_removed() {
2071 let before = snapshot(&[("a.test", "aa"), ("b.test", "bb")]);
2072 let after = snapshot(&[("a.test", "aa")]);
2073 let diff = CertDiff::between(&before, &after);
2074 assert_eq!(diff.removed, vec!["b.test".to_string()]);
2075 assert!(diff.added.is_empty());
2076 assert!(diff.replaced.is_empty());
2077 }
2078
2079 #[test]
2083 fn a_renewal_for_the_same_hostname_is_reported_as_replaced() {
2084 let before = snapshot(&[("a.test", "old00000")]);
2085 let after = snapshot(&[("a.test", "new11111")]);
2086 let diff = CertDiff::between(&before, &after);
2087
2088 assert!(diff.added.is_empty(), "hostname set is unchanged");
2089 assert!(diff.removed.is_empty(), "hostname set is unchanged");
2090 assert_eq!(
2091 diff.replaced,
2092 vec!["a.test (old00000->new11111)".to_string()]
2093 );
2094 assert!(!diff.is_empty(), "a renewal is a change and must be logged");
2095 }
2096
2097 #[test]
2100 fn a_one_for_one_rotation_shows_both_sides() {
2101 let before = snapshot(&[("<default>", "dd"), ("old.test", "aa")]);
2102 let after = snapshot(&[("<default>", "dd"), ("new.test", "bb")]);
2103 let diff = CertDiff::between(&before, &after);
2104
2105 assert_eq!(before.len(), after.len(), "counts alone show nothing");
2106 assert_eq!(diff.added, vec!["new.test".to_string()]);
2107 assert_eq!(diff.removed, vec!["old.test".to_string()]);
2108 }
2109
2110 #[test]
2111 fn rotating_the_default_certificate_is_visible() {
2112 let before = snapshot(&[(DEFAULT_CERT_LABEL, "old00000")]);
2113 let after = snapshot(&[(DEFAULT_CERT_LABEL, "new11111")]);
2114 assert!(!CertDiff::between(&before, &after).is_empty());
2115 }
2116
2117 #[test]
2118 fn empty_diff_fields_render_as_a_dash() {
2119 assert_eq!(CertDiff::render(&[]), "-");
2120 assert_eq!(
2121 CertDiff::render(&["a.test".to_string(), "b.test".to_string()]),
2122 "a.test, b.test"
2123 );
2124 }
2125}