DANE: Verify DNSSEC is supported by the resolver before attempting to validate TLSA records
This commit is contained in:
@@ -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>> {
|
pub fn simple_pem_parse(contents: &str) -> Option<Vec<u8>> {
|
||||||
let mut contents = contents.as_bytes().iter().copied();
|
let mut contents = contents.as_bytes().iter().copied();
|
||||||
let mut base64 = vec![];
|
let mut base64 = vec![];
|
||||||
@@ -338,59 +278,6 @@ fn build_dkim1_signer<T: SigningKey>(
|
|||||||
signer
|
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 {
|
impl<'x> TryFrom<expr::Variable<'x>> for VerifyStrategy {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ use self::{
|
|||||||
auth::MailAuthConfig, queue::QueueConfig, report::ReportConfig, resolver::Resolvers,
|
auth::MailAuthConfig, queue::QueueConfig, report::ReportConfig, resolver::Resolvers,
|
||||||
session::SessionConfig,
|
session::SessionConfig,
|
||||||
};
|
};
|
||||||
use crate::expr::Expression;
|
use crate::{config::smtp::queue::RequireOptional, expr::Expression};
|
||||||
use registry::{schema::structs::Rate, types::id::ObjectId};
|
use registry::{
|
||||||
|
schema::{properties::ObjectType, structs::Rate},
|
||||||
|
types::id::ObjectId,
|
||||||
|
};
|
||||||
use store::registry::bootstrap::Bootstrap;
|
use store::registry::bootstrap::Bootstrap;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -49,12 +52,31 @@ pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9;
|
|||||||
|
|
||||||
impl SmtpConfig {
|
impl SmtpConfig {
|
||||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||||
Self {
|
let config = Self {
|
||||||
session: SessionConfig::parse(bp).await,
|
session: SessionConfig::parse(bp).await,
|
||||||
queue: QueueConfig::parse(bp).await,
|
queue: QueueConfig::parse(bp).await,
|
||||||
resolvers: Resolvers::parse(bp).await,
|
resolvers: Resolvers::parse(bp).await,
|
||||||
mail_auth: MailAuthConfig::parse(bp).await,
|
mail_auth: MailAuthConfig::parse(bp).await,
|
||||||
report: ReportConfig::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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ use mail_auth::{
|
|||||||
ResolverConfig, ResolverOpts,
|
ResolverConfig, ResolverOpts,
|
||||||
},
|
},
|
||||||
net::runtime::TokioRuntimeProvider,
|
net::runtime::TokioRuntimeProvider,
|
||||||
proto::rr::{Name, RecordType},
|
|
||||||
system_conf::read_system_conf,
|
system_conf::read_system_conf,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use registry::schema::{
|
use registry::schema::{
|
||||||
enums::{DnsResolverProtocol, MtaRequiredOrOptional, PolicyEnforcement},
|
enums::{DnsResolverProtocol, PolicyEnforcement},
|
||||||
prelude::ObjectType,
|
prelude::ObjectType,
|
||||||
structs::{DnsResolver, MtaSts, MtaTlsStrategy, SystemSettings},
|
structs::{DnsResolver, MtaSts, SystemSettings},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -223,40 +222,24 @@ impl Resolvers {
|
|||||||
.expect("Failed to build DNSSEC resolver"),
|
.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 {
|
Resolvers {
|
||||||
dns: MessageAuthenticator::new(resolver_config, opts).unwrap(),
|
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,
|
||||||
dnssec_available,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "test_mode"))]
|
||||||
async fn dnssec_capable(resolver: &TokioResolver) -> bool {
|
async fn dnssec_capable(resolver: &TokioResolver) -> bool {
|
||||||
resolver
|
resolver
|
||||||
.lookup(Name::root(), RecordType::DNSKEY)
|
.lookup(
|
||||||
|
hickory_proto::rr::Name::root(),
|
||||||
|
hickory_proto::rr::RecordType::DNSKEY,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok_and(|lookup| {
|
.is_ok_and(|lookup| {
|
||||||
lookup
|
lookup
|
||||||
|
|||||||
Reference in New Issue
Block a user