/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use std::{ io::Cursor, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, time::Duration, }; use ahash::{AHashMap, AHashSet}; use base64::{ Engine, engine::general_purpose::{self, STANDARD}, }; use dns_update::{DnsUpdater, TsigAlgorithm, providers::rfc2136::DnsAddress}; use rcgen::generate_simple_self_signed; use rustls::{ SupportedProtocolVersion, crypto::ring::sign::any_supported_type, sign::CertifiedKey, version::{TLS12, TLS13}, }; use rustls_pemfile::{Item, certs, read_one}; use rustls_pki_types::PrivateKeyDer; use utils::config::Config; use x509_parser::{ certificate::X509Certificate, der_parser::asn1_rs::FromDer, extensions::{GeneralName, ParsedExtension}, }; use crate::listener::{ acme::{ AcmeProvider, ChallengeSettings, EabSettings, directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, }, tls::AcmeProviders, }; pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; impl AcmeProviders { pub fn parse(config: &mut Config) -> Self { let mut providers = AHashMap::new(); // Parse ACME providers 'outer: for acme_id in config.sub_keys("acme", ".directory") { let acme_id = acme_id.as_str(); let directory = config .value(("acme", acme_id, "directory")) .unwrap_or(LETS_ENCRYPT_PRODUCTION_DIRECTORY) .trim() .to_string(); let contact = config .values(("acme", acme_id, "contact")) .filter_map(|(_, v)| { let v = v.trim().to_string(); if !v.is_empty() { Some(v) } else { None } }) .collect::>(); let renew_before: Duration = config .property_or_default(("acme", acme_id, "renew-before"), "30d") .unwrap_or_else(|| Duration::from_secs(30 * 24 * 60 * 60)); if directory.is_empty() { config.new_parse_error(format!("acme.{acme_id}.directory"), "Missing property"); continue; } if contact.is_empty() { config.new_parse_error(format!("acme.{acme_id}.contact"), "Missing property"); continue; } // Parse challenge type let challenge = match config .value(("acme", acme_id, "challenge")) .unwrap_or("tls-alpn-01") { "tls-alpn-01" => ChallengeSettings::TlsAlpn01, "http-01" => ChallengeSettings::Http01, "dns-01" => match build_dns_updater(config, acme_id) { Some(updater) => ChallengeSettings::Dns01 { updater, origin: config .value(("acme", acme_id, "origin")) .map(|s| s.to_string()), polling_interval: config .property_or_default(("acme", acme_id, "polling-interval"), "15s") .unwrap_or_else(|| Duration::from_secs(15)), propagation_timeout: config .property_or_default(("acme", acme_id, "propagation-timeout"), "1m") .unwrap_or_else(|| Duration::from_secs(60)), ttl: config .property_or_default(("acme", acme_id, "ttl"), "5m") .unwrap_or_else(|| Duration::from_secs(5 * 60)) .as_secs() as u32, }, None => { continue; } }, _ => { config .new_parse_error(("acme", acme_id, "challenge"), "Invalid challenge type"); continue; } }; // Domains covered by this ACME manager let domains = config .values(("acme", acme_id, "domains")) .map(|(_, s)| s.trim().to_string()) .collect::>(); if !matches!(challenge, ChallengeSettings::Dns01 { .. }) && domains.iter().any(|d| d.starts_with("*.")) { config.new_parse_error( ("acme", acme_id, "domains"), "Wildcard domains are only supported with DNS-01 challenge", ); continue 'outer; } // Obtain EAB settings let eab = if let (Some(eab_kid), Some(eab_hmac_key)) = ( config .value(("acme", acme_id, "eab.kid")) .filter(|s| !s.is_empty()), config .value(("acme", acme_id, "eab.hmac-key")) .filter(|s| !s.is_empty()), ) { if let Ok(hmac_key) = general_purpose::URL_SAFE_NO_PAD.decode(eab_hmac_key.trim().as_bytes()) { EabSettings { kid: eab_kid.to_string(), hmac_key, } .into() } else { config.new_build_error( format!("acme.{acme_id}.eab.hmac-key"), "Failed to base64 decode HMAC key", ); None } } else { None }; // This ACME manager is the default when SNI is not available let default = config .property::(("acme", acme_id, "default")) .unwrap_or_default(); if !domains.is_empty() { match AcmeProvider::new( acme_id.to_string(), directory, domains, contact, challenge, eab, renew_before, default, ) { Ok(acme_provider) => { providers.insert(acme_id.to_string(), acme_provider); } Err(err) => { config.new_build_error(format!("acme.{acme_id}"), err.to_string()); } } } } AcmeProviders { providers } } } #[allow(clippy::unnecessary_to_owned)] fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option { let timeout = config .property_or_default(("acme", acme_id, "timeout"), "30s") .unwrap_or_else(|| Duration::from_secs(30)); match config.value_require(("acme", acme_id, "provider"))? { "rfc2136-tsig" => { let algorithm: TsigAlgorithm = config .value_require(("acme", acme_id, "tsig-algorithm"))? .parse() .map_err(|_| { config.new_parse_error(("acme", acme_id, "tsig-algorithm"), "Invalid algorithm") }) .ok()?; let key = STANDARD .decode(config.value_require(("acme", acme_id, "secret"))?.trim()) .map_err(|_| { config.new_parse_error( ("acme", acme_id, "secret"), "Failed to base64 decode secret", ) }) .ok()?; let host = config.property_require::(("acme", acme_id, "host"))?; let port = config .property_or_default::(("acme", acme_id, "port"), "53") .unwrap_or(53); let addr = if config.value(("acme", acme_id, "protocol")) == Some("tcp") { DnsAddress::Tcp(SocketAddr::new(host, port)) } else { DnsAddress::Udp(SocketAddr::new(host, port)) }; DnsUpdater::new_rfc2136_tsig( addr, config .value_require(("acme", acme_id, "key"))? .trim() .to_string(), key, algorithm, ) .map_err(|err| { config.new_build_error( ("acme", acme_id, "provider"), format!("Failed to create RFC2136-TSIG DNS updater: {err}"), ) }) .ok() } "cloudflare" => DnsUpdater::new_cloudflare( config .value_require(("acme", acme_id, "secret"))? .trim() .to_string(), config.value(("acme", acme_id, "user")).map(|s| s.trim()), timeout.into(), ) .map_err(|err| { config.new_build_error( ("acme", acme_id, "provider"), format!("Failed to create Cloudflare DNS updater: {err}"), ) }) .ok(), "digitalocean" => DnsUpdater::new_digitalocean( config .value_require(("acme", acme_id, "secret"))? .trim() .to_string(), timeout.into(), ) .map_err(|err| { config.new_build_error( ("acme", acme_id, "provider"), format!("Failed to create DigitalOcean DNS updater: {err}"), ) }) .ok(), "desec" => DnsUpdater::new_desec( config .value_require(("acme", acme_id, "secret"))? .trim() .to_string(), timeout.into(), ) .map_err(|err| { config.new_build_error( ("acme", acme_id, "provider"), format!("Failed to create Desec DNS updater: {err}"), ) }) .ok(), "ovh" => DnsUpdater::new_ovh( config .value_require(("acme", acme_id, "key")) .map(|s| s.trim())? .to_string(), config .value_require(("acme", acme_id, "secret"))? .trim() .to_string(), config .value_require(("acme", acme_id, "consumer-key"))? .trim() .to_string(), config .value_require(("acme", acme_id, "ovh-endpoint"))? .parse() .map_err(|_| { config .new_parse_error(("acme", acme_id, "ovh-endpoint"), "Invalid OVH endpoint") }) .ok()?, timeout.into(), ) .map_err(|err| { config.new_build_error( ("acme", acme_id, "provider"), format!("Failed to create Desec DNS updater: {err}"), ) }) .ok(), _ => { config.new_parse_error(("acme", acme_id, "provider"), "Unsupported provider"); None } } } pub(crate) fn parse_certificates( config: &mut Config, certificates: &mut AHashMap>, subject_names: &mut AHashSet, ) { // Parse certificates for cert_id in config.sub_keys("certificate", ".cert") { let cert_id = cert_id.as_str(); let key_cert = ("certificate", cert_id, "cert"); let key_pk = ("certificate", cert_id, "private-key"); let cert = config .value_require(key_cert) .map(|s| s.as_bytes().to_vec()); let pk = config.value_require(key_pk).map(|s| s.as_bytes().to_vec()); if let (Some(cert), Some(pk)) = (cert, pk) { match build_certified_key(cert, pk) { Ok(cert) => { match cert .end_entity_cert() .map_err(|err| format!("Failed to obtain end entity cert: {err}")) .and_then(|cert| { X509Certificate::from_der(cert.as_ref()) .map_err(|err| format!("Failed to parse end entity cert: {err}")) }) { Ok((_, parsed)) => { // Add CNs and SANs to the list of names let mut names = AHashSet::new(); for name in parsed.subject().iter_common_name() { if let Ok(name) = name.as_str() { names.insert(name.to_string()); } } for ext in parsed.extensions() { if let ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() { for name in &san.general_names { let name = match name { GeneralName::DNSName(name) => name.to_string(), GeneralName::IPAddress(ip) => match ip.len() { 4 => Ipv4Addr::from( <[u8; 4]>::try_from(*ip).unwrap(), ) .to_string(), 16 => Ipv6Addr::from( <[u8; 16]>::try_from(*ip).unwrap(), ) .to_string(), _ => continue, }, _ => { continue; } }; names.insert(name); } } } // Add custom SNIs names.extend( config .values(("certificate", cert_id, "subjects")) .map(|(_, v)| v.trim().to_string()), ); // Add domain names subject_names.extend(names.iter().cloned()); // Add certificates let cert = Arc::new(cert); for name in names { certificates.insert( name.strip_prefix("*.") .map(|name| name.to_string()) .unwrap_or(name), cert.clone(), ); } // Add default certificate if config .property::(("certificate", cert_id, "default")) .unwrap_or_default() { certificates.insert("*".to_string(), cert.clone()); } } Err(err) => config.new_build_error(format!("certificate.{cert_id}"), err), } } Err(err) => config.new_build_error(format!("certificate.{cert_id}"), err), } } } } pub(crate) fn build_certified_key(cert: Vec, pk: Vec) -> Result { let cert = certs(&mut Cursor::new(cert)) .collect::, _>>() .map_err(|err| format!("Failed to read certificates: {err}"))?; if cert.is_empty() { return Err("No certificates found.".to_string()); } let pk = match read_one(&mut Cursor::new(pk)) .map_err(|err| format!("Failed to read private keys.: {err}",))? .into_iter() .next() { Some(Item::Pkcs8Key(key)) => PrivateKeyDer::Pkcs8(key), Some(Item::Pkcs1Key(key)) => PrivateKeyDer::Pkcs1(key), Some(Item::Sec1Key(key)) => PrivateKeyDer::Sec1(key), Some(_) => return Err("Unsupported private keys found.".to_string()), None => return Err("No private keys found.".to_string()), }; Ok(CertifiedKey { cert, key: any_supported_type(&pk) .map_err(|err| format!("Failed to sign certificate: {err}",))?, ocsp: None, }) } pub(crate) fn build_self_signed_cert( domains: impl Into>, ) -> Result { let cert = generate_simple_self_signed(domains) .map_err(|err| format!("Failed to generate self-signed certificate: {err}",))?; build_certified_key( cert.serialize_pem().unwrap().into_bytes(), cert.serialize_private_key_pem().into_bytes(), ) }