DKIM2 verification

This commit is contained in:
Maurus Decimus
2026-07-01 16:20:02 +02:00
parent e3c6f95bfd
commit 186d9feecb
33 changed files with 640 additions and 206 deletions

View File

@@ -15,7 +15,7 @@ use crate::{
DomainCache, EmailAddress, EmailAddressRef, EmailCache, MailingListCache, PermissionsGroup,
RECOVERY_ADMIN_ID, RoleCache, TenantCache, permissions::BuildPermissions,
},
config::smtp::auth::DkimSigner,
config::smtp::auth::DkimSigners,
expr::if_block::BootstrapExprExt,
network::mta::AddressResolver,
storage::{
@@ -839,7 +839,7 @@ impl Server {
}
}
pub async fn dkim_signers(&self, domain: &str) -> trc::Result<Option<Arc<[DkimSigner]>>> {
pub async fn dkim_signers(&self, domain: &str) -> trc::Result<Option<Arc<DkimSigners>>> {
let Some(domain) = self.domain(domain).await? else {
return Ok(None);
};
@@ -868,26 +868,24 @@ impl Server {
.equal(Property::DomainId, domain.id),
)
.await?;
let mut signatures = Vec::with_capacity(ids.len());
let domain_name = &domain.names[0];
let mut signers = DkimSigners {
dkim1: Vec::with_capacity(ids.len()),
dkim2: None,
};
for id in ids {
if let Some(signature) = self.registry().object::<DkimSignature>(id).await?
&& matches!(signature.stage(), DkimRotationStage::Active)
&& let Err(err) = signers.insert(domain_name.to_string(), signature).await
{
match DkimSigner::new(domain.names[0].to_string(), signature).await {
Ok(signer) => signatures.push(signer),
Err(err) => {
trc::error!(
err.ctx(trc::Key::Id, id.id()).caused_by(trc::location!())
);
}
}
trc::error!(err.ctx(trc::Key::Id, id.id()).caused_by(trc::location!()));
}
}
if !signatures.is_empty() {
let signatures: Arc<[DkimSigner]> = signatures.into();
let _ = guard.insert(signatures.clone());
Ok(Some(signatures))
if !signers.dkim1.is_empty() || signers.dkim2.is_some() {
let signers = Arc::new(signers);
let _ = guard.insert(signers.clone());
Ok(Some(signers))
} else {
Ok(None)
}

View File

@@ -13,7 +13,7 @@ use crate::{
mailstore::spamfilter::SpamClassifier,
server::tls::parse_certificates,
smtp::{
auth::DkimSigner,
auth::DkimSigners,
resolver::{Policy, Tlsa},
},
},
@@ -164,7 +164,7 @@ impl Caches {
),
dkim_signers: Cache::new(
cache.dkim_signatures,
(std::mem::size_of::<DkimSigner>() + 255) as u64,
(std::mem::size_of::<DkimSigners>() + 255) as u64,
),
dns_txt: CacheWithTtl::new(cache.dns_txt, (std::mem::size_of::<Txt>() + 255) as u64),
dns_mx: CacheWithTtl::new(cache.dns_mx, ((std::mem::size_of::<MX>() + 255) * 2) as u64),

View File

@@ -11,15 +11,16 @@ use crate::expr::{
use mail_auth::{
common::crypto::{Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey},
dkim::{Canonicalization, Done},
dkim2::{Dkim2Signer, Done as Dkim2Done, Flag},
};
use mail_parser::decoders::base64::base64_decode;
use registry::{
schema::{
enums::{self, ExpressionConstant},
enums::{self, Dkim2Flag, ExpressionConstant},
prelude::ObjectType,
structs::{Dkim1Signature, DkimSignature, SenderAuth},
},
types::ObjectImpl,
types::{ObjectImpl, map::Map},
};
use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, pem::PemObject};
use store::registry::bootstrap::Bootstrap;
@@ -71,14 +72,15 @@ pub enum VerifyStrategy {
Disable,
}
pub enum DkimSigner {
pub enum Dkim1Signer {
RsaSha256(mail_auth::dkim::DkimSigner<RsaKey<Sha256>, Done>),
Ed25519Sha256(mail_auth::dkim::DkimSigner<Ed25519Key, Done>),
}
pub enum ArcSealer {
RsaSha256(mail_auth::arc::ArcSealer<RsaKey<Sha256>, Done>),
Ed25519Sha256(mail_auth::arc::ArcSealer<Ed25519Key, Done>),
#[derive(Default)]
pub struct DkimSigners {
pub dkim1: Vec<Dkim1Signer>,
pub dkim2: Option<Dkim2Signer<Dkim2Done>>,
}
impl MailAuthConfig {
@@ -123,8 +125,8 @@ impl MailAuthConfig {
}
}
impl DkimSigner {
pub async fn new(domain: String, signature: DkimSignature) -> trc::Result<Self> {
impl DkimSigners {
pub async fn insert(&mut self, domain: String, signature: DkimSignature) -> trc::Result<()> {
let mut errors = vec![];
if !signature.validate(&mut errors) {
return Err(trc::DkimEvent::BuildError
@@ -156,9 +158,10 @@ impl DkimSigner {
.details("Failed to build ED25519 key")
})?;
Ok(DkimSigner::Ed25519Sha256(build_dkim1_signer(
domain, signature, key,
)))
self.dkim1
.push(Dkim1Signer::Ed25519Sha256(build_dkim1_signer(
domain, signature, key,
)));
}
DkimSignature::Dkim1RsaSha256(signature) => {
let private_key = signature
@@ -168,17 +171,70 @@ impl DkimSigner {
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
let key = rsa_key_parse(private_key.as_bytes())?;
Ok(DkimSigner::RsaSha256(build_dkim1_signer(
self.dkim1.push(Dkim1Signer::RsaSha256(build_dkim1_signer(
domain, signature, key,
)))
)));
}
DkimSignature::Dkim2Ed25519Sha256(_) | DkimSignature::Dkim2RsaSha256(_) => {
todo!()
DkimSignature::Dkim2Ed25519Sha256(signature) => {
let private_key = signature
.private_key
.secret()
.await
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
let private_key = simple_pem_parse(&private_key).ok_or_else(|| {
trc::DkimEvent::BuildError
.reason("Failed to parse ED25519 private key PEM")
.details("Invalid PEM format")
})?;
let key =
Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key).map_err(|err| {
trc::DkimEvent::BuildError
.reason(err)
.details("Failed to build ED25519 key")
})?;
self.dkim2 = Some(match self.dkim2.take() {
None => Dkim2Signer::from_key(key)
.domain(domain)
.selector(signature.selector)
.flags(map_dkim2_flags(signature.flags)),
Some(signer) => signer
.additional_key(key, signature.selector)
.flags(map_dkim2_flags(signature.flags)),
});
}
DkimSignature::Dkim2RsaSha256(signature) => {
let private_key = signature
.private_key
.secret()
.await
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
let key = rsa_key_parse(private_key.as_bytes())?;
self.dkim2 = Some(match self.dkim2.take() {
None => Dkim2Signer::from_key(key)
.domain(domain)
.selector(signature.selector)
.flags(map_dkim2_flags(signature.flags)),
Some(signer) => signer
.additional_key(key, signature.selector)
.flags(map_dkim2_flags(signature.flags)),
});
}
}
Ok(())
}
}
fn map_dkim2_flags(flags: Map<enums::Dkim2Flag>) -> impl Iterator<Item = Flag> {
flags.into_inner().into_iter().map(|flag| match flag {
Dkim2Flag::Donotmodify => Flag::DoNotModify,
Dkim2Flag::Donotexplode => Flag::DoNotExplode,
Dkim2Flag::Feedback => Flag::Feedback,
})
}
pub fn rsa_key_parse(private_key: &[u8]) -> trc::Result<RsaKey<Sha256>> {
PrivatePkcs1KeyDer::from_pem_slice(private_key)
.map(PrivateKeyDer::Pkcs1)
@@ -309,8 +365,16 @@ impl VerifyStrategy {
}
}
impl CacheItemWeight for DkimSigner {
impl CacheItemWeight for Dkim1Signer {
fn weight(&self) -> u64 {
std::mem::size_of::<Self>() as u64
}
}
impl CacheItemWeight for DkimSigners {
fn weight(&self) -> u64 {
(std::mem::size_of::<Self>()
+ self.dkim1.len() * std::mem::size_of::<Dkim1Signer>()
+ std::mem::size_of::<Dkim2Signer<Dkim2Done>>()) as u64
}
}

View File

@@ -18,7 +18,7 @@ use crate::{
scripts::Scripting,
spamfilter::{IpResolver, SpamClassifier, SpamFilterConfig},
},
smtp::auth::DkimSigner,
smtp::auth::DkimSigners,
},
ipc::TrainTaskController,
network::security::BlockedIps,
@@ -193,7 +193,7 @@ pub struct Caches {
pub tenants: Cache<u32, Arc<TenantCache>>,
pub lists: Cache<u32, Arc<MailingListCache>>,
pub dkim_signers: Cache<u32, Arc<[DkimSigner]>>,
pub dkim_signers: Cache<u32, Arc<DkimSigners>>,
pub dns_txt: CacheWithTtl<Box<str>, Txt>,
pub dns_mx: CacheWithTtl<Box<str>, RecordSet<MX>>,

View File

@@ -10,7 +10,7 @@ use crate::{
config::{
mailstore::spamfilter::SpamClassifier,
smtp::{
auth::DkimSigner,
auth::DkimSigners,
queue::{
ConnectionStrategy, DEFAULT_QUEUE_NAME, MxConfig, QueueExpiry, QueueName,
QueueStrategy, RequireOptional, RoutingStrategy, TlsStrategy, VirtualQueue,
@@ -188,7 +188,7 @@ impl Server {
&self,
domain: &str,
session_id: u64,
) -> trc::Result<Option<Arc<[DkimSigner]>>> {
) -> trc::Result<Option<Arc<DkimSigners>>> {
if let Some(signers) = self.dkim_signers(domain).await? {
Ok(Some(signers))
} else {