1use 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#[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
130impl SniResolver {
131 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 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 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 let mut priority_exact: HashSet<String> = HashSet::new();
173 let mut priority_wildcard: HashSet<String> = HashSet::new();
174
175 for (i, sni_config) in config.additional_certs.iter().enumerate() {
177 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 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 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 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 let hostnames = if !sni_config.hostnames.is_empty() {
236 sni_config.hostnames.clone()
237 } else if !priority_set.is_empty() {
238 extract_hostnames_from_cert(cert.cert.first().unwrap())?
240 } else if let Some(ref acme) = sni_config.acme {
241 acme.domains.clone()
243 } else {
244 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 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 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 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 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 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 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 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 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 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 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 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 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 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
443pub struct HotReloadableSniResolver {
453 inner: RwLock<Arc<SniResolver>>,
455 config: RwLock<TlsConfig>,
457 listener_id: String,
459 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 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 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 let new_resolver = SniResolver::from_config(&config, Some(&self.listener_id))?;
511
512 *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 pub fn update_config(&self, new_config: TlsConfig) -> Result<(), TlsError> {
525 let new_resolver = SniResolver::from_config(&new_config, Some(&self.listener_id))?;
527
528 *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 pub fn last_reload_age(&self) -> Duration {
542 self.last_reload.read().elapsed()
543 }
544
545 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
559pub struct CertificateReloader {
563 resolvers: RwLock<HashMap<String, Arc<HotReloadableSniResolver>>>,
565}
566
567impl CertificateReloader {
568 pub fn new() -> Self {
570 Self {
571 resolvers: RwLock::new(HashMap::new()),
572 }
573 }
574
575 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 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 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#[derive(Debug, Clone)]
647pub struct OcspCacheEntry {
648 pub response: Vec<u8>,
650 pub fetched_at: Instant,
652 pub expires_at: Option<Instant>,
654}
655
656pub struct OcspStapler {
660 cache: RwLock<HashMap<String, OcspCacheEntry>>,
662 refresh_interval: Duration,
664}
665
666impl OcspStapler {
667 pub fn new() -> Self {
669 Self {
670 cache: RwLock::new(HashMap::new()),
671 refresh_interval: Duration::from_secs(3600), }
673 }
674
675 pub fn with_refresh_interval(interval: Duration) -> Self {
677 Self {
678 cache: RwLock::new(HashMap::new()),
679 refresh_interval: interval,
680 }
681 }
682
683 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 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 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 let (_, cert) = X509Certificate::from_der(cert_der)
710 .map_err(|e| TlsError::OcspFetch(format!("Failed to parse certificate: {}", e)))?;
711
712 let (_, issuer) = X509Certificate::from_der(issuer_der).map_err(|e| {
714 TlsError::OcspFetch(format!("Failed to parse issuer certificate: {}", e))
715 })?;
716
717 let ocsp_url = extract_ocsp_responder_url(&cert)?;
719 debug!(url = %ocsp_url, "Found OCSP responder URL");
720
721 let ocsp_request = build_ocsp_request(&cert, &issuer)?;
723
724 let response = send_ocsp_request_sync(&ocsp_url, &ocsp_request)?;
727
728 let fingerprint = calculate_cert_fingerprint(cert_der);
730
731 let entry = OcspCacheEntry {
733 response: response.clone(),
734 fetched_at: Instant::now(),
735 expires_at: None, };
737 self.cache.write().insert(fingerprint, entry);
738
739 info!("Successfully fetched and cached OCSP response");
740 Ok(response)
741 }
742
743 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 let (_, cert) = X509Certificate::from_der(cert_der)
753 .map_err(|e| TlsError::OcspFetch(format!("Failed to parse certificate: {}", e)))?;
754
755 let (_, issuer) = X509Certificate::from_der(issuer_der).map_err(|e| {
757 TlsError::OcspFetch(format!("Failed to parse issuer certificate: {}", e))
758 })?;
759
760 let ocsp_url = extract_ocsp_responder_url(&cert)?;
762 debug!(url = %ocsp_url, "Found OCSP responder URL");
763
764 let ocsp_request = build_ocsp_request(&cert, &issuer)?;
766
767 let response = send_ocsp_request_async(&ocsp_url, &ocsp_request).await?;
769
770 let fingerprint = calculate_cert_fingerprint(cert_der);
772
773 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 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 warnings.push("OCSP stapling prefetch not yet fully implemented".to_string());
799
800 warnings
801 }
802
803 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
816fn extract_ocsp_responder_url(
822 cert: &x509_parser::certificate::X509Certificate,
823) -> Result<String, TlsError> {
824 use x509_parser::prelude::*;
825
826 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 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 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
864fn 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 let issuer_name_hash = {
880 let mut hasher = Sha256::new();
881 hasher.update(issuer.subject().as_raw());
882 hasher.finalize()
883 };
884
885 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 let serial = cert.serial.to_bytes_be();
894
895 let request = build_ocsp_request_der(&issuer_name_hash, &issuer_key_hash, &serial);
898
899 Ok(request)
900}
901
902fn build_ocsp_request_der(
904 issuer_name_hash: &[u8],
905 issuer_key_hash: &[u8],
906 serial_number: &[u8],
907) -> Vec<u8> {
908 let sha256_oid: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
910
911 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 let request = der_sequence(&[&cert_id]);
923
924 let request_list = der_sequence(&[&request]);
926
927 let tbs_request = der_sequence(&[&request_list]);
929
930 der_sequence(&[&tbs_request])
932}
933
934fn 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]; 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]; 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] }
956
957fn der_octet_string(data: &[u8]) -> Vec<u8> {
958 let mut result = vec![0x04]; 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]; let data = match data.iter().position(|&b| b != 0) {
968 Some(pos) => &data[pos..],
969 None => &[0],
970 };
971 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
992fn 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 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 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 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 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 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 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
1065async 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
1095fn 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
1104pub fn load_client_cert_key(
1122 cert_path: &Path,
1123 key_path: &Path,
1124) -> Result<Arc<pingora_core::utils::tls::CertKey>, TlsError> {
1125 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 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 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 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 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
1174pub fn build_upstream_tls_config(config: &UpstreamTlsConfig) -> Result<ClientConfig, TlsError> {
1179 let mut root_store = RootCertStore::empty();
1180
1181 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 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 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 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 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 builder.with_no_client_auth()
1254 };
1255
1256 debug!("Upstream TLS configuration built successfully");
1257 Ok(client_config)
1258}
1259
1260pub fn validate_upstream_tls_config(config: &UpstreamTlsConfig) -> Result<(), TlsError> {
1262 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 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 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
1307fn load_certified_key(cert_path: &Path, key_path: &Path) -> Result<CertifiedKey, TlsError> {
1313 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 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 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
1356fn 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 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 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
1399pub 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
1433fn 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 if matches!(min, TlsVersion::Tls12) {
1444 versions.push(&rustls::version::TLS12);
1445 }
1446
1447 if matches!(max, TlsVersion::Tls13) {
1449 versions.push(&rustls::version::TLS13);
1450 }
1451
1452 if versions.is_empty() {
1453 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
1462fn resolve_cipher_suites(names: &[String]) -> Result<Vec<rustls::SupportedCipherSuite>, TlsError> {
1466 use rustls::crypto::aws_lc_rs::cipher_suite;
1467
1468 let known: &[(&str, rustls::SupportedCipherSuite)] = &[
1470 (
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 (
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
1529pub 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 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 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 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 let mut server_config = server_config;
1605 server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1606
1607 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
1618pub fn validate_tls_config(config: &TlsConfig) -> Result<(), TlsError> {
1620 if config.acme.is_some() {
1622 trace!("Skipping manual cert validation for ACME-managed TLS");
1624 } else {
1625 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 for sni in &config.additional_certs {
1651 if sni.acme.is_some() {
1653 trace!("Skipping manual cert validation for ACME-managed SNI certificate");
1654 continue;
1655 }
1656
1657 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 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 let name = "foo.bar.example.com";
1704 let parts: Vec<&str> = name.split('.').collect();
1705
1706 assert_eq!(parts.len(), 4);
1707
1708 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}