feat(mailer-smtp): add SCRAM-SHA-256 auth, Ed25519 DKIM, opportunistic TLS, SNI cert selection, pipelining and delivery/bridge improvements
This commit is contained in:
@@ -12,6 +12,7 @@ use crate::rate_limiter::{RateLimitConfig, RateLimiter};
|
||||
use hickory_resolver::TokioResolver;
|
||||
use mailer_security::MessageAuthenticator;
|
||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -263,6 +264,69 @@ async fn accept_loop(
|
||||
}
|
||||
}
|
||||
|
||||
/// SNI-based certificate resolver that selects the appropriate TLS certificate
|
||||
/// based on the client's requested hostname.
|
||||
struct SniCertResolver {
|
||||
/// Domain -> certified key mapping.
|
||||
certs: HashMap<String, Arc<rustls::sign::CertifiedKey>>,
|
||||
/// Default certificate for non-matching SNI or missing SNI.
|
||||
default: Arc<rustls::sign::CertifiedKey>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SniCertResolver {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SniCertResolver")
|
||||
.field("domains", &self.certs.keys().collect::<Vec<_>>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::server::ResolvesServerCert for SniCertResolver {
|
||||
fn resolve(
|
||||
&self,
|
||||
client_hello: rustls::server::ClientHello<'_>,
|
||||
) -> Option<Arc<rustls::sign::CertifiedKey>> {
|
||||
if let Some(sni) = client_hello.server_name() {
|
||||
let sni_lower = sni.to_lowercase();
|
||||
if let Some(key) = self.certs.get(&sni_lower) {
|
||||
return Some(key.clone());
|
||||
}
|
||||
}
|
||||
Some(self.default.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a PEM cert+key pair into a `CertifiedKey`.
|
||||
fn parse_certified_key(
|
||||
cert_pem: &str,
|
||||
key_pem: &str,
|
||||
) -> Result<rustls::sign::CertifiedKey, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let certs: Vec<CertificateDer<'static>> = {
|
||||
let mut reader = BufReader::new(cert_pem.as_bytes());
|
||||
rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
if certs.is_empty() {
|
||||
return Err("No certificates found in PEM".into());
|
||||
}
|
||||
|
||||
let key: PrivateKeyDer<'static> = {
|
||||
let mut reader = BufReader::new(key_pem.as_bytes());
|
||||
let mut keys = Vec::new();
|
||||
for item in rustls_pemfile::read_all(&mut reader) {
|
||||
match item? {
|
||||
rustls_pemfile::Item::Pkcs8Key(key) => keys.push(PrivateKeyDer::Pkcs8(key)),
|
||||
rustls_pemfile::Item::Pkcs1Key(key) => keys.push(PrivateKeyDer::Pkcs1(key)),
|
||||
rustls_pemfile::Item::Sec1Key(key) => keys.push(PrivateKeyDer::Sec1(key)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
keys.into_iter().next().ok_or("No private key found in PEM")?
|
||||
};
|
||||
|
||||
let signing_key = rustls::crypto::ring::sign::any_supported_type(&key)?;
|
||||
Ok(rustls::sign::CertifiedKey::new(certs, signing_key))
|
||||
}
|
||||
|
||||
/// Build a TLS acceptor from PEM cert/key strings.
|
||||
fn build_tls_acceptor(
|
||||
config: &SmtpServerConfig,
|
||||
@@ -311,9 +375,42 @@ fn build_tls_acceptor(
|
||||
.ok_or("No private key found in PEM")?
|
||||
};
|
||||
|
||||
let tls_config = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)?;
|
||||
// If additional TLS certs are configured, use SNI-based resolution
|
||||
let tls_config = if config.additional_tls_certs.is_empty() {
|
||||
rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)?
|
||||
} else {
|
||||
// Build default certified key
|
||||
let signing_key = rustls::crypto::ring::sign::any_supported_type(&key)?;
|
||||
let default_ck = Arc::new(rustls::sign::CertifiedKey::new(certs, signing_key));
|
||||
|
||||
// Build per-domain certs
|
||||
let mut domain_certs = HashMap::new();
|
||||
for domain_cert in &config.additional_tls_certs {
|
||||
match parse_certified_key(&domain_cert.cert_pem, &domain_cert.key_pem) {
|
||||
Ok(ck) => {
|
||||
let ck = Arc::new(ck);
|
||||
for domain in &domain_cert.domains {
|
||||
domain_certs.insert(domain.to_lowercase(), ck.clone());
|
||||
}
|
||||
info!("SNI cert loaded for domains: {:?}", domain_cert.domains);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to load SNI cert for domains {:?}: {}", domain_cert.domains, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resolver = SniCertResolver {
|
||||
certs: domain_certs,
|
||||
default: default_ck,
|
||||
};
|
||||
|
||||
rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver))
|
||||
};
|
||||
|
||||
Ok(tokio_rustls::TlsAcceptor::from(Arc::new(tls_config)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user