DANE: Verify DNSSEC is supported by the resolver before attempting to validate TLSA records

This commit is contained in:
Maurus Decimus
2026-06-23 11:09:43 +02:00
parent df164e07c8
commit 46f03f7dad
3 changed files with 36 additions and 144 deletions

View File

@@ -194,66 +194,6 @@ pub fn rsa_key_parse(private_key: &[u8]) -> trc::Result<RsaKey<Sha256>> {
})
}
/*impl ArcSealer {
pub fn new(selector: String, domain: String, signature: DkimSignature) -> trc::Result<Self> {
let mut errors = vec![];
if !signature.validate(&mut errors) {
return Err(trc::DkimEvent::BuildError
.reason("DKIM signature validation failed")
.details(
errors
.into_iter()
.map(|v| trc::Value::from(v.to_string()))
.collect::<Vec<_>>(),
));
}
match signature {
DkimSignature::Dkim1Ed25519Sha256(signature) => {
let private_key = simple_pem_parse(&signature.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")
})?;
Ok(ArcSealer::Ed25519Sha256(build_dkim1_sealer(
domain, selector, signature, key,
)))
}
DkimSignature::Dkim1RsaSha256(signature) => {
let key = PrivatePkcs1KeyDer::from_pem_slice(signature.private_key.as_bytes())
.map(PrivateKeyDer::Pkcs1)
.or_else(|_| {
PrivatePkcs8KeyDer::from_pem_slice(signature.private_key.as_bytes())
.map(PrivateKeyDer::Pkcs8)
})
.map_err(|err| {
trc::DkimEvent::BuildError
.reason(err)
.details("Failed to build RSA key")
})
.and_then(|key| {
RsaKey::<Sha256>::from_key_der(key).map_err(|err| {
trc::DkimEvent::BuildError
.reason(err)
.details("Failed to build RSA key")
})
})?;
Ok(ArcSealer::RsaSha256(build_dkim1_sealer(
domain, selector, signature, key,
)))
}
}
}
}*/
pub fn simple_pem_parse(contents: &str) -> Option<Vec<u8>> {
let mut contents = contents.as_bytes().iter().copied();
let mut base64 = vec![];
@@ -338,59 +278,6 @@ fn build_dkim1_signer<T: SigningKey>(
signer
}
/*fn build_dkim1_sealer<T: SigningKey<Hasher = Sha256>>(
domain: String,
selector: String,
mut signature: Dkim1Signature,
key: T,
) -> mail_auth::arc::ArcSealer<T, Done> {
if !signature
.headers
.iter()
.any(|h| h.eq_ignore_ascii_case("DKIM-Signature"))
{
signature
.headers
.push_unchecked("DKIM-Signature".to_string());
}
let mut sealer = mail_auth::arc::ArcSealer::from_key(key)
.domain(domain)
.selector(selector)
.headers(signature.headers);
match signature.canonicalization {
enums::DkimCanonicalization::RelaxedRelaxed => {
sealer = sealer
.body_canonicalization(Canonicalization::Relaxed)
.header_canonicalization(Canonicalization::Relaxed);
}
enums::DkimCanonicalization::SimpleSimple => {
sealer = sealer
.body_canonicalization(Canonicalization::Simple)
.header_canonicalization(Canonicalization::Simple);
}
enums::DkimCanonicalization::RelaxedSimple => {
sealer = sealer
.body_canonicalization(Canonicalization::Simple)
.header_canonicalization(Canonicalization::Relaxed);
}
enums::DkimCanonicalization::SimpleRelaxed => {
sealer = sealer
.body_canonicalization(Canonicalization::Relaxed)
.header_canonicalization(Canonicalization::Simple);
}
}
if let Some(expire) = signature.expire {
sealer = sealer.expiration(expire.into_inner().as_secs());
}
sealer
}
*/
impl<'x> TryFrom<expr::Variable<'x>> for VerifyStrategy {
type Error = ();

View File

@@ -14,8 +14,11 @@ use self::{
auth::MailAuthConfig, queue::QueueConfig, report::ReportConfig, resolver::Resolvers,
session::SessionConfig,
};
use crate::expr::Expression;
use registry::{schema::structs::Rate, types::id::ObjectId};
use crate::{config::smtp::queue::RequireOptional, expr::Expression};
use registry::{
schema::{properties::ObjectType, structs::Rate},
types::id::ObjectId,
};
use store::registry::bootstrap::Bootstrap;
#[derive(Clone)]
@@ -49,12 +52,31 @@ pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9;
impl SmtpConfig {
pub async fn parse(bp: &mut Bootstrap) -> Self {
Self {
let config = Self {
session: SessionConfig::parse(bp).await,
queue: QueueConfig::parse(bp).await,
resolvers: Resolvers::parse(bp).await,
mail_auth: MailAuthConfig::parse(bp).await,
report: ReportConfig::parse(bp).await,
};
if !config.resolvers.dnssec_available
&& config
.queue
.tls_strategy
.values()
.any(|t| !matches!(t.dane, RequireOptional::Disable))
{
bp.build_warning(
ObjectType::DnsResolver.singleton(),
concat!(
"The configured DNS resolver cannot validate DNSSEC. ",
"DANE has been disabled to avoid deferring mail. ",
"Configure a DNSSEC-validating resolver to enable DANE."
),
);
}
config
}
}

View File

@@ -13,14 +13,13 @@ use mail_auth::{
ResolverConfig, ResolverOpts,
},
net::runtime::TokioRuntimeProvider,
proto::rr::{Name, RecordType},
system_conf::read_system_conf,
},
};
use registry::schema::{
enums::{DnsResolverProtocol, MtaRequiredOrOptional, PolicyEnforcement},
enums::{DnsResolverProtocol, PolicyEnforcement},
prelude::ObjectType,
structs::{DnsResolver, MtaSts, MtaTlsStrategy, SystemSettings},
structs::{DnsResolver, MtaSts, SystemSettings},
};
use serde::{Deserialize, Serialize};
use std::{
@@ -223,40 +222,24 @@ impl Resolvers {
.expect("Failed to build DNSSEC resolver"),
};
let uses_dane = bp
.list_infallible::<MtaTlsStrategy>()
.await
.iter()
.any(|obj| obj.object.dane != MtaRequiredOrOptional::Disable);
let dnssec_available = if uses_dane && !cfg!(any(test, feature = "test_mode")) {
let available = dnssec_capable(&dnssec.resolver).await;
if !available {
bp.build_warning(
ObjectType::DnsResolver.singleton(),
concat!(
"The configured DNS resolver cannot validate DNSSEC. ",
"DANE has been disabled to avoid deferring mail. ",
"Configure a DNSSEC-validating resolver to enable DANE."
),
);
}
available
} else {
true
};
Resolvers {
dns: MessageAuthenticator::new(resolver_config, opts).unwrap(),
#[cfg(not(feature = "test_mode"))]
dnssec_available: dnssec_capable(&dnssec.resolver).await,
#[cfg(feature = "test_mode")]
dnssec_available: true,
dnssec,
dnssec_available,
}
}
}
#[cfg(not(feature = "test_mode"))]
async fn dnssec_capable(resolver: &TokioResolver) -> bool {
resolver
.lookup(Name::root(), RecordType::DNSKEY)
.lookup(
hickory_proto::rr::Name::root(),
hickory_proto::rr::RecordType::DNSKEY,
)
.await
.is_ok_and(|lookup| {
lookup