/// Extract the DNS SubjectAltName set from the leaf certificate. This parser is /// deliberately narrow: it walks only the DER structures required to locate the /// SAN extension and ignores non-DNS GeneralName variants. Certificate validity /// and key matching are still delegated to rustls. fn certificate_dns_names(certificate: &[u8]) -> Result> { let mut reader = certificate; let certs = rustls_pemfile::certs(&mut reader).collect::>>()?; let leaf = certs.first().context("certificate chain is empty")?; let der = leaf.as_ref(); let mut root_offset = 0; let (tag, certificate_sequence) = der_tlv(der, &mut root_offset)?; ensure!( tag == 0x30 && root_offset == der.len(), "invalid X.509 certificate DER" ); let mut certificate_offset = 0; let (tag, tbs) = der_tlv(certificate_sequence, &mut certificate_offset)?; ensure!(tag == 0x30, "invalid X.509 TBSCertificate"); let mut tbs_offset = 0; if tbs.first() == Some(&0xa0) { let _ = der_tlv(tbs, &mut tbs_offset)?; } // serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo for _ in 0..6 { let _ = der_tlv(tbs, &mut tbs_offset)?; } let mut names = Vec::new(); while tbs_offset < tbs.len() { let (tag, value) = der_tlv(tbs, &mut tbs_offset)?; if tag != 0xa3 { continue; } let mut wrapper_offset = 0; let (tag, extensions) = der_tlv(value, &mut wrapper_offset)?; ensure!(tag == 0x30, "invalid X.509 extensions"); let mut extensions_offset = 0; while extensions_offset < extensions.len() { let (tag, extension) = der_tlv(extensions, &mut extensions_offset)?; ensure!(tag == 0x30, "invalid X.509 extension"); let mut extension_offset = 0; let (oid_tag, oid) = der_tlv(extension, &mut extension_offset)?; ensure!(oid_tag == 0x06, "invalid X.509 extension OID"); if extension_offset < extension.len() && extension[extension_offset] == 0x01 { let _ = der_tlv(extension, &mut extension_offset)?; } let (value_tag, encoded_value) = der_tlv(extension, &mut extension_offset)?; ensure!(value_tag == 0x04, "invalid X.509 extension value"); if oid != [0x55, 0x1d, 0x11] { continue; } let mut san_offset = 0; let (san_tag, general_names) = der_tlv(encoded_value, &mut san_offset)?; ensure!(san_tag == 0x30, "invalid SubjectAltName extension"); let mut names_offset = 0; while names_offset < general_names.len() { let (name_tag, raw) = der_tlv(general_names, &mut names_offset)?; if name_tag != 0x82 { continue; } let Ok(name) = std::str::from_utf8(raw) else { continue; }; if let Ok(name) = normalize_host_pattern(name) { names.push(name); } } } break; } names.sort(); names.dedup(); Ok(names) } #[derive(Debug, Clone, Serialize)] pub struct CertificateMetadata { pub covered_hosts: Vec, pub not_before: Option, pub not_after: Option, pub not_before_unix: Option, pub not_after_unix: Option, pub days_remaining: Option, pub issuer: Option, pub expiry_status: &'static str, } fn der_text(tag: u8, raw: &[u8]) -> Option { let value = match tag { 0x0c | 0x13 | 0x14 | 0x16 => std::str::from_utf8(raw).ok()?.to_owned(), 0x1e if raw.len() % 2 == 0 => { let units: Vec = raw .chunks_exact(2) .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])) .collect(); String::from_utf16(&units).ok()? } _ => return None, }; let value: String = value .chars() .map(|ch| if ch.is_control() { ' ' } else { ch }) .take(256) .collect(); (!value.trim().is_empty()).then(|| value.trim().to_owned()) } fn certificate_issuer_name(encoded: &[u8]) -> Option { let mut offset = 0; let mut parts = Vec::new(); while offset < encoded.len() && parts.len() < 16 { let (set_tag, set_value) = der_tlv(encoded, &mut offset).ok()?; if set_tag != 0x31 { continue; } let mut set_offset = 0; while set_offset < set_value.len() && parts.len() < 16 { let (sequence_tag, sequence) = der_tlv(set_value, &mut set_offset).ok()?; if sequence_tag != 0x30 { continue; } let mut sequence_offset = 0; let (oid_tag, oid) = der_tlv(sequence, &mut sequence_offset).ok()?; if oid_tag != 0x06 || sequence_offset >= sequence.len() { continue; } let (value_tag, value) = der_tlv(sequence, &mut sequence_offset).ok()?; let Some(value) = der_text(value_tag, value) else { continue; }; let label = if oid == [0x55, 0x04, 0x03] { "CN" } else if oid == [0x55, 0x04, 0x06] { "C" } else if oid == [0x55, 0x04, 0x07] { "L" } else if oid == [0x55, 0x04, 0x08] { "ST" } else if oid == [0x55, 0x04, 0x0a] { "O" } else if oid == [0x55, 0x04, 0x0b] { "OU" } else { continue; }; parts.push(format!("{label}={value}")); } } (!parts.is_empty()).then(|| parts.join(", ")) } fn x509_days_in_month(year: i32, month: u32) -> Option { let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); Some(match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 if leap => 29, 2 => 28, _ => return None, }) } fn x509_days_from_civil(year: i32, month: u32, day: u32) -> i64 { let year = year - if month <= 2 { 1 } else { 0 }; let era = (if year >= 0 { year } else { year - 399 }) / 400; let yoe = year - era * 400; let month = i32::try_from(month).unwrap_or(0); let day = i32::try_from(day).unwrap_or(0); let doy = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; i64::from(era * 146_097 + doe - 719_468) } fn x509_decimal(raw: &[u8]) -> Option { let text = std::str::from_utf8(raw).ok()?; text.parse().ok() } fn x509_time(tag: u8, raw: &[u8]) -> Option<(String, i64)> { let year_digits = match tag { 0x17 => 2, 0x18 => 4, _ => return None, }; let base_digits = year_digits + 10; if raw.len() < base_digits + 1 || !raw[..base_digits].iter().all(|byte| byte.is_ascii_digit()) { return None; } let mut year = i32::try_from(x509_decimal(&raw[..year_digits])?).ok()?; if year_digits == 2 { year += if year >= 50 { 1900 } else { 2000 }; } let mut cursor = year_digits; let month = x509_decimal(&raw[cursor..cursor + 2])?; cursor += 2; let day = x509_decimal(&raw[cursor..cursor + 2])?; cursor += 2; let hour = x509_decimal(&raw[cursor..cursor + 2])?; cursor += 2; let minute = x509_decimal(&raw[cursor..cursor + 2])?; cursor += 2; let second = x509_decimal(&raw[cursor..cursor + 2])?; cursor += 2; if day == 0 || day > x509_days_in_month(year, month)? || hour > 23 || minute > 59 || second > 60 { return None; } while cursor < raw.len() && matches!(raw[cursor], b'.' | b',') { cursor += 1; while cursor < raw.len() && raw[cursor].is_ascii_digit() { cursor += 1; } } let (offset_seconds, suffix) = match raw.get(cursor).copied()? { b'Z' if cursor + 1 == raw.len() => (0i64, "Z".to_owned()), sign @ (b'+' | b'-') if cursor + 5 == raw.len() => { let zone_hour = x509_decimal(&raw[cursor + 1..cursor + 3])?; let zone_minute = x509_decimal(&raw[cursor + 3..cursor + 5])?; if zone_hour > 23 || zone_minute > 59 { return None; } let seconds = i64::from(zone_hour * 3600 + zone_minute * 60) * if sign == b'+' { 1 } else { -1 }; ( seconds, format!( "{}{:02}:{:02}", if sign == b'+' { '+' } else { '-' }, zone_hour, zone_minute ), ) } _ => return None, }; let unix = x509_days_from_civil(year, month, day) .checked_mul(86_400)? .checked_add(i64::from(hour * 3600 + minute * 60 + second.min(59)))? .checked_sub(offset_seconds)?; let display = format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}{suffix}"); Some((display, unix)) } fn certificate_public_metadata(certificate: &[u8]) -> Result { let covered_hosts = certificate_dns_names(certificate)?; let mut reader = certificate; let certs = rustls_pemfile::certs(&mut reader).collect::>>()?; let leaf = certs.first().context("certificate chain is empty")?; let der = leaf.as_ref(); let mut root_offset = 0; let (tag, certificate_sequence) = der_tlv(der, &mut root_offset)?; ensure!( tag == 0x30 && root_offset == der.len(), "invalid X.509 certificate DER" ); let mut certificate_offset = 0; let (tag, tbs) = der_tlv(certificate_sequence, &mut certificate_offset)?; ensure!(tag == 0x30, "invalid X.509 TBSCertificate"); let mut tbs_offset = 0; if tbs.first() == Some(&0xa0) { let _ = der_tlv(tbs, &mut tbs_offset)?; } let _serial = der_tlv(tbs, &mut tbs_offset)?; let _signature = der_tlv(tbs, &mut tbs_offset)?; let (issuer_tag, issuer) = der_tlv(tbs, &mut tbs_offset)?; ensure!(issuer_tag == 0x30, "invalid X.509 issuer"); let (validity_tag, validity) = der_tlv(tbs, &mut tbs_offset)?; ensure!(validity_tag == 0x30, "invalid X.509 validity"); let mut validity_offset = 0; let (not_before_tag, not_before_raw) = der_tlv(validity, &mut validity_offset)?; let (not_after_tag, not_after_raw) = der_tlv(validity, &mut validity_offset)?; let not_before = x509_time(not_before_tag, not_before_raw); let not_after = x509_time(not_after_tag, not_after_raw); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|value| i64::try_from(value.as_secs()).unwrap_or(i64::MAX)) .unwrap_or(0); let days_remaining = not_after.as_ref().map(|(_, unix)| { let delta = unix.saturating_sub(now); if delta >= 0 { delta.saturating_add(86_399) / 86_400 } else { -(delta.saturating_abs().saturating_add(86_399) / 86_400) } }); let expiry_status = match not_after.as_ref() { Some((_, unix)) if *unix < now => "expired", Some(_) if days_remaining.is_some_and(|days| days <= 30) => "warning", Some(_) => "ok", None => "unknown", }; Ok(CertificateMetadata { covered_hosts, not_before: not_before.as_ref().map(|(value, _)| value.clone()), not_after: not_after.as_ref().map(|(value, _)| value.clone()), not_before_unix: not_before.map(|(_, value)| value), not_after_unix: not_after.map(|(_, value)| value), days_remaining, issuer: certificate_issuer_name(issuer), expiry_status, }) } /// Read DNS SubjectAltName coverage directly from an existing certificate file. /// Used by the authenticated certificate inventory to enrich older/manual entries /// that predate persisted `covered_hosts` metadata. Private key material is not read. pub fn certificate_covered_hosts(directory: &Path, cert_file: &str) -> Result> { let certificate = read_configured_pem(directory, cert_file, false)?; certificate_dns_names(&certificate) } /// Read non-secret metadata directly from the configured public leaf certificate. /// Private keys are never opened by this path. pub fn certificate_metadata(directory: &Path, cert_file: &str) -> Result { let certificate = read_configured_pem(directory, cert_file, false)?; certificate_public_metadata(&certificate) } fn parse_pair(certificate: &[u8], key: &[u8]) -> Result { let mut certificate_reader = certificate; let certs = rustls_pemfile::certs(&mut certificate_reader).collect::>>()?; ensure!( !certs.is_empty() && certs.len() <= 16, "certificate chain must contain 1..16 PEM certificates" ); let mut key_reader = key; let key = rustls_pemfile::private_key(&mut key_reader)? .context("no supported PEM private key found")?; let certified = CertifiedKey::from_der(certs, key, &rustls::crypto::ring::default_provider())?; certified .keys_match() .context("certificate and private key do not match")?; Ok(certified) } fn load_pair(directory: &Path, cert: &str, key: &str) -> Result { certificate_files_are_external(cert, key)?; parse_pair( &read_configured_pem(directory, cert, false)?, &read_configured_pem(directory, key, true)?, ) } /// Runs on a bounded control-plane blocking worker, never from a handshake. pub fn compile(settings: &Settings, directory: Option<&Path>) -> Result>> { settings.policy.validate()?; let Some(directory) = directory else { ensure!( settings.default_certificate.is_none() && settings.certificates.is_empty(), "certificate configuration requires a certificate directory" ); return Ok(None); }; let mut names = HashMap::>::new(); let mut ids = HashSet::new(); let mut hosts = HashSet::new(); let mut default = None; for cert in &settings.certificates { safe_name(&cert.id)?; ensure!(ids.insert(cert.id.clone()), "duplicate certificate id"); let key = load_pair(directory, &cert.cert_file, &cert.key_file)?; for host in &cert.hosts { let host = normalize_host_pattern(host)?; ensure!(hosts.insert(host.clone()), "duplicate TLS hostname binding"); // Validate certificate/name compatibility with a concrete SNI name. // rustls' stock resolver does not model routing wildcard precedence, // so the immutable RustProxy resolver below owns exact/wildcard lookup. let validation_name = host .strip_prefix("*.") .map(|base| format!("rustproxy-wildcard-check.{base}")) .unwrap_or_else(|| host.clone()); if !settings.forced_mismatches.contains(&(cert.id.clone(), host.clone())) { let mut checker = ResolvesServerCertUsingSni::new(); checker .add(&validation_name, key.clone()) .with_context(|| format!("certificate does not cover {host}; explicit Vhost consent is required to override"))?; } else { tracing::warn!(certificate = %cert.id, hostname = %host, "Explicitly serving a TLS certificate without hostname verification; clients may reject it"); } names.insert(host, Arc::new(key.clone())); } if settings.default_certificate.as_deref() == Some(cert.id.as_str()) { default = Some(Arc::new(key)); } } if settings.default_certificate.is_some() { ensure!( default.is_some(), "default_certificate references an unknown id" ); } let fallback = match default { Some(key) => key, None => Arc::new(load_pair(directory, "default.pem", "default.key")?), }; Ok(Some(server_config( Arc::new(Resolver { names, fallback }), &settings.policy, true, )?)) } /// Reuse the already validated immutable certificate resolver while narrowing /// protocols/ciphers for one frontend. No certificate I/O occurs here. pub fn variant( base: &Arc, policy: &ProtocolPolicy, http2: bool, ) -> Result> { server_config(base.cert_resolver.clone(), policy, http2) }