use crate::config::{normalize_host, normalize_host_pattern}; use anyhow::{ensure, Context, Result}; use rustls::{ server::{ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni}, sign::CertifiedKey, ServerConfig, }; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs::{self, OpenOptions}, io::{self, Read, Write}, path::{Path, PathBuf}, sync::Arc, }; use zeroize::Zeroizing; const MAX_PEM: u64 = 131_072; const MAX_CERTIFICATE_PATH: usize = 4096; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct Settings { #[serde(default, skip_serializing_if = "Option::is_none")] pub default_certificate: Option, #[serde(default)] pub certificates: Vec, pub policy: ProtocolPolicy, } impl Default for Settings { fn default() -> Self { Self { default_certificate: None, certificates: Vec::new(), policy: ProtocolPolicy::default(), } } } impl Settings { pub fn is_empty(&self) -> bool { self.default_certificate.is_none() && self.certificates.is_empty() && self.policy == ProtocolPolicy::default() } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct ProtocolPolicy { pub tls12: bool, pub tls13: bool, /// Rustls cipher-suite names. Empty keeps the ring provider defaults. pub cipher_suites: Vec, } impl Default for ProtocolPolicy { fn default() -> Self { Self { tls12: true, tls13: true, cipher_suites: Vec::new(), } } } impl ProtocolPolicy { pub fn validate(&self) -> Result<()> { ensure!( self.tls12 || self.tls13, "at least one TLS protocol version must be enabled" ); ensure!( self.cipher_suites.len() <= 16, "at most 16 TLS cipher suites may be selected" ); let available = cipher_suite_names(); let mut seen = HashSet::new(); for suite in &self.cipher_suites { ensure!( suite.len() <= 96 && available.iter().any(|name| name == suite), "unknown TLS cipher suite: {suite}" ); ensure!(seen.insert(suite), "duplicate TLS cipher suite: {suite}"); } let effective = if self.cipher_suites.is_empty() { available } else { self.cipher_suites.clone() }; ensure!( effective.iter().any(|suite| { (self.tls13 && suite.starts_with("TLS13_")) || (self.tls12 && !suite.starts_with("TLS13_")) }), "selected TLS cipher suites do not match the enabled protocol versions" ); Ok(()) } pub fn effective_cipher_suites(&self) -> Vec { let selected = if self.cipher_suites.is_empty() { cipher_suite_names() } else { self.cipher_suites.clone() }; selected .into_iter() .filter(|suite| { (self.tls13 && suite.starts_with("TLS13_")) || (self.tls12 && !suite.starts_with("TLS13_")) }) .collect() } pub fn allows_http3(&self) -> bool { self.tls13 && (self.cipher_suites.is_empty() || self .cipher_suites .iter() .any(|suite| suite.starts_with("TLS13_"))) } /// Per-vhost policy may only remove protocols/suites from the global policy. /// This keeps the global setting a real security ceiling rather than a default /// that an individual vhost can silently widen. pub fn validate_narrower_than(&self, global: &Self) -> Result<()> { self.validate()?; global.validate()?; ensure!( !self.tls12 || global.tls12, "per-vhost TLS policy cannot enable TLS 1.2 when it is disabled globally" ); ensure!( !self.tls13 || global.tls13, "per-vhost TLS policy cannot enable TLS 1.3 when it is disabled globally" ); if !global.cipher_suites.is_empty() { ensure!( !self.cipher_suites.is_empty(), "per-vhost TLS policy must select from the globally allowed cipher suites" ); for suite in &self.cipher_suites { ensure!( global.cipher_suites.iter().any(|allowed| allowed == suite), "per-vhost TLS cipher suite is not allowed globally: {suite}" ); } } Ok(()) } } pub fn cipher_suite_names() -> Vec { rustls::crypto::ring::default_provider() .cipher_suites .iter() .map(|suite| format!("{:?}", suite.suite())) .collect() } fn provider_for(policy: &ProtocolPolicy) -> Result { policy.validate()?; let mut provider = rustls::crypto::ring::default_provider(); if !policy.cipher_suites.is_empty() { provider.cipher_suites.retain(|suite| { let name = format!("{:?}", suite.suite()); policy .cipher_suites .iter() .any(|selected| selected == &name) }); } ensure!( !provider.cipher_suites.is_empty(), "TLS cipher selection is empty" ); Ok(provider) } fn protocol_versions(policy: &ProtocolPolicy) -> Vec<&'static rustls::SupportedProtocolVersion> { let mut versions = Vec::new(); if policy.tls13 { versions.push(&rustls::version::TLS13); } if policy.tls12 { versions.push(&rustls::version::TLS12); } versions } fn server_config( resolver: Arc, policy: &ProtocolPolicy, http2: bool, ) -> Result> { let provider = provider_for(policy)?; let versions = protocol_versions(policy); let mut config = ServerConfig::builder_with_provider(Arc::new(provider)) .with_protocol_versions(&versions)? .with_no_client_auth() .with_cert_resolver(resolver); config.alpn_protocols = if http2 { vec![b"h2".to_vec(), b"http/1.1".to_vec()] } else { vec![b"http/1.1".to_vec()] }; config.max_early_data_size = 0; Ok(Arc::new(config)) } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Issuer { pub provider: String, pub environment: String, #[serde(default = "renew_by_default")] pub auto_renew: bool, } fn renew_by_default() -> bool { true } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Certificate { #[serde(default, skip_serializing_if = "Option::is_none")] pub issuer: Option, pub id: String, /// DNS SubjectAltName values extracted from the leaf certificate during /// upload or ACME import. This is certificate coverage, not active SNI binding. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub covered_hosts: Vec, /// Active/configured SNI bindings. `for_vhosts` may rewrite this list. pub hosts: Vec, pub cert_file: String, pub key_file: String, } #[derive(Debug)] struct Resolver { names: HashMap>, fallback: Arc, } impl Resolver { fn named(&self, server_name: &str) -> Option> { let host = normalize_host(server_name).ok()?; if let Some(key) = self.names.get(&host) { return Some(key.clone()); } let labels: Vec<&str> = host.split('.').collect(); for index in 1..labels.len().saturating_sub(1) { let wildcard = format!("*.{}", labels[index..].join(".")); if let Some(key) = self.names.get(&wildcard) { return Some(key.clone()); } } None } } impl ResolvesServerCert for Resolver { fn resolve(&self, hello: ClientHello<'_>) -> Option> { hello .server_name() .and_then(|name| self.named(name)) .or_else(|| Some(self.fallback.clone())) } } pub fn validate_certificate_removals( previous: &crate::config::Config, next: &crate::config::Config, ) -> Result<()> { let next_ids: HashSet<&str> = next .tls .certificates .iter() .map(|certificate| certificate.id.as_str()) .collect(); for certificate in &previous.tls.certificates { if next_ids.contains(certificate.id.as_str()) { continue; } let id = certificate.id.as_str(); let mut owners = Vec::new(); if previous.tls.default_certificate.as_deref() == Some(id) { owners.push("default TLS certificate".to_owned()); } if previous.administration.certificate_id.as_deref() == Some(id) { owners.push("administration panel".to_owned()); } owners.extend( previous .vhosts .iter() .filter(|host| host.certificate_id.as_deref() == Some(id)) .map(|host| format!("vhost {}", host.id)), ); owners.extend( previous .redirects .iter() .filter(|host| host.certificate_id.as_deref() == Some(id)) .map(|host| format!("redirect {}", host.id)), ); owners.extend( previous .not_found_hosts .iter() .filter(|host| host.certificate_id.as_deref() == Some(id)) .map(|host| format!("404 host {}", host.id)), ); owners.extend( previous .docker_agents .agents .iter() .filter(|agent| agent.certificate_id.as_deref() == Some(id)) .map(|agent| format!("Docker agent {}", agent.id)), ); ensure!(owners.is_empty(), "certificate {id} is still assigned to {}; unassign it and save before removing the certificate", owners.join(", ")); } Ok(()) } pub fn safe_name(name: &str) -> Result<()> { ensure!( !name.is_empty() && name.len() <= 96 && !name.contains("..") && name .bytes() .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_')), "certificate filenames must be simple relative filenames, not paths" ); Ok(()) } /// Validate how a configured certificate pair is addressed. Legacy/simple names /// remain private to `/certs`; an explicitly selected external pair uses /// two absolute paths. Mixing the two modes is rejected so the source is never /// ambiguous. pub fn certificate_files_are_external(cert_file: &str, key_file: &str) -> Result { let cert_external = Path::new(cert_file).is_absolute(); let key_external = Path::new(key_file).is_absolute(); ensure!( cert_external == key_external, "certificate and private key must both use filenames from the RustProxy certificate directory or both use absolute external paths" ); if cert_external { ensure!( !cert_file.is_empty() && !key_file.is_empty() && cert_file.len() <= MAX_CERTIFICATE_PATH && key_file.len() <= MAX_CERTIFICATE_PATH, "external certificate paths must be absolute paths up to 4096 bytes" ); Ok(true) } else { safe_name(cert_file)?; safe_name(key_file)?; Ok(false) } } fn configured_certificate_path(directory: &Path, value: &str) -> Result { if Path::new(value).is_absolute() { ensure!( !value.is_empty() && value.len() <= MAX_CERTIFICATE_PATH, "external certificate path must be an absolute path up to 4096 bytes" ); // Certbot's live/ entries are symlinks to archive/. Resolve them on every // reload, then open the final regular file with O_NOFOLLOW in read_pem. return fs::canonicalize(value) .with_context(|| format!("cannot resolve external certificate path {value}")); } safe_name(value)?; Ok(directory.join(value)) } fn read_configured_pem( directory: &Path, value: &str, private: bool, ) -> Result>> { let external = Path::new(value).is_absolute(); let path = configured_certificate_path(directory, value)?; read_pem_with_policy(&path, private, external) } fn read_pem(path: &Path, private: bool) -> Result>> { read_pem_with_policy(path, private, false) } fn read_pem_with_policy( path: &Path, private: bool, external_group_read: bool, ) -> Result>> { let mut options = OpenOptions::new(); options.read(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.custom_flags(libc::O_NOFOLLOW); } let file = options .open(path) .with_context(|| format!("cannot open certificate input {}", path.display()))?; let meta = file.metadata()?; ensure!( meta.is_file() && meta.len() <= MAX_PEM, "certificate inputs must be regular files up to 128 KiB" ); #[cfg(unix)] if private { use std::os::unix::fs::PermissionsExt; let mode = meta.permissions().mode(); if external_group_read { ensure!( mode & 0o137 == 0, "external private key may be owner/group-readable but must not be executable, group-writable, or world-accessible: {}", path.display() ); } else { ensure!( mode & 0o077 == 0, "private key permissions must not grant group/world access: {}", path.display() ); } } #[cfg(not(unix))] let _ = (private, external_group_read); let mut data = Zeroizing::new(Vec::new()); file.take(MAX_PEM + 1).read_to_end(&mut data)?; ensure!( data.len() <= MAX_PEM as usize, "PEM file grew beyond its limit" ); Ok(data) } fn der_tlv<'a>(input: &'a [u8], offset: &mut usize) -> Result<(u8, &'a [u8])> { ensure!(*offset < input.len(), "truncated DER object"); let tag = input[*offset]; *offset += 1; ensure!(*offset < input.len(), "truncated DER length"); let first = input[*offset]; *offset += 1; let length = if first & 0x80 == 0 { first as usize } else { let count = (first & 0x7f) as usize; ensure!(count > 0 && count <= 4, "unsupported DER length"); ensure!(*offset + count <= input.len(), "truncated DER length"); let mut length = 0usize; for byte in &input[*offset..*offset + count] { length = length .checked_mul(256) .and_then(|value| value.checked_add(*byte as usize)) .context("DER length overflow")?; } *offset += count; length }; let end = (*offset) .checked_add(length) .context("DER length overflow")?; ensure!(end <= input.len(), "truncated DER value"); let value = &input[*offset..end]; *offset = end; Ok((tag, value)) }