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, /// Derived from explicitly confirmed Vhosts; never accepted from TLS JSON. #[serde(skip)] pub forced_mismatches: HashSet<(String, String)>, } impl Default for Settings { fn default() -> Self { Self { default_certificate: None, certificates: Vec::new(), policy: ProtocolPolicy::default(), forced_mismatches: HashSet::new(), } } } 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. 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)) }