-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use public suffix list for DMARC relaxed alignment verification (fixes …
…#37)
- Loading branch information
Showing
5 changed files
with
59 additions
and
274 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
[package] | ||
name = "mail-auth" | ||
description = "DKIM, ARC, SPF and DMARC library for Rust" | ||
version = "0.4.3" | ||
version = "0.5.0" | ||
edition = "2021" | ||
authors = [ "Stalwart Labs <[email protected]>"] | ||
license = "Apache-2.0 OR MIT" | ||
|
@@ -29,7 +29,7 @@ lru-cache = "0.1.2" | |
mail-parser = { version = "0.9", features = ["ludicrous_mode", "full_encoding"] } | ||
mail-builder = { version = "0.3", features = ["ludicrous_mode"] } | ||
parking_lot = "0.12.0" | ||
quick-xml = "0.34" | ||
quick-xml = "0.36" | ||
ring = { version = "0.17", optional = true } | ||
rsa = { version = "0.9.6", optional = true } | ||
rustls-pemfile = { version = "2", optional = true } | ||
|
@@ -44,3 +44,4 @@ rand = { version = "0.8.5", optional = true } | |
[dev-dependencies] | ||
tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] } | ||
rustls-pemfile = "2" | ||
psl = "2.1.55" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,39 +18,40 @@ use crate::{ | |
use super::{Alignment, Dmarc, URI}; | ||
|
||
impl Resolver { | ||
/// Verifies the DMARC policy of an RFC5322.From domain | ||
/// Verifies the DMARC policy of an RFC5321.MailFrom domain | ||
pub async fn verify_dmarc( | ||
&self, | ||
message: &AuthenticatedMessage<'_>, | ||
dkim_output: &[DkimOutput<'_>], | ||
mail_from_domain: &str, | ||
rfc5321_mail_from_domain: &str, | ||
spf_output: &SpfOutput, | ||
domain_suffix_fn: impl Fn(&str) -> &str, | ||
) -> DmarcOutput { | ||
// Extract RFC5322.From | ||
let mut from_domain = ""; | ||
// Extract RFC5322.From domain | ||
let mut rfc5322_from_domain = ""; | ||
for from in &message.from { | ||
if let Some((_, domain)) = from.rsplit_once('@') { | ||
if from_domain.is_empty() { | ||
from_domain = domain; | ||
} else if from_domain != domain { | ||
if rfc5322_from_domain.is_empty() { | ||
rfc5322_from_domain = domain; | ||
} else if rfc5322_from_domain != domain { | ||
// Multi-valued RFC5322.From header fields with multiple | ||
// domains MUST be exempt from DMARC checking. | ||
return DmarcOutput::default(); | ||
} | ||
} | ||
} | ||
if from_domain.is_empty() { | ||
if rfc5322_from_domain.is_empty() { | ||
return DmarcOutput::default(); | ||
} | ||
|
||
// Obtain DMARC policy | ||
let dmarc = match self.dmarc_tree_walk(from_domain).await { | ||
let dmarc = match self.dmarc_tree_walk(rfc5322_from_domain).await { | ||
Ok(Some(dmarc)) => dmarc, | ||
Ok(None) => return DmarcOutput::default().with_domain(from_domain), | ||
Ok(None) => return DmarcOutput::default().with_domain(rfc5322_from_domain), | ||
Err(err) => { | ||
let err = DmarcResult::from(err); | ||
return DmarcOutput::default() | ||
.with_domain(from_domain) | ||
.with_domain(rfc5322_from_domain) | ||
.with_dkim_result(err.clone()) | ||
.with_spf_result(err); | ||
} | ||
|
@@ -59,21 +60,22 @@ impl Resolver { | |
let mut output = DmarcOutput { | ||
spf_result: DmarcResult::None, | ||
dkim_result: DmarcResult::None, | ||
domain: from_domain.to_string(), | ||
domain: rfc5322_from_domain.to_string(), | ||
policy: dmarc.p, | ||
record: None, | ||
}; | ||
|
||
let has_dkim_pass = dkim_output.iter().any(|o| o.result == DkimResult::Pass); | ||
if spf_output.result == SpfResult::Pass || has_dkim_pass { | ||
// Check SPF alignment | ||
let from_subdomain = format!(".{from_domain}"); | ||
let from_subdomain = format!(".{}", domain_suffix_fn(rfc5322_from_domain)); | ||
if spf_output.result == SpfResult::Pass { | ||
output.spf_result = if mail_from_domain == from_domain { | ||
output.spf_result = if rfc5321_mail_from_domain == rfc5322_from_domain { | ||
DmarcResult::Pass | ||
} else if dmarc.aspf == Alignment::Relaxed | ||
&& mail_from_domain.ends_with(&from_subdomain) | ||
|| from_domain.ends_with(&format!(".{mail_from_domain}")) | ||
&& rfc5321_mail_from_domain.ends_with(&from_subdomain) | ||
|| rfc5322_from_domain | ||
.ends_with(&format!(".{}", domain_suffix_fn(rfc5321_mail_from_domain))) | ||
{ | ||
output.policy = dmarc.sp; | ||
DmarcResult::Pass | ||
|
@@ -85,15 +87,18 @@ impl Resolver { | |
// Check DKIM alignment | ||
if has_dkim_pass { | ||
output.dkim_result = if dkim_output.iter().any(|o| { | ||
o.result == DkimResult::Pass && o.signature.as_ref().unwrap().d.eq(from_domain) | ||
o.result == DkimResult::Pass | ||
&& o.signature.as_ref().unwrap().d.eq(rfc5322_from_domain) | ||
}) { | ||
DmarcResult::Pass | ||
} else if dmarc.adkim == Alignment::Relaxed | ||
&& dkim_output.iter().any(|o| { | ||
o.result == DkimResult::Pass | ||
&& (o.signature.as_ref().unwrap().d.ends_with(&from_subdomain) | ||
|| from_domain | ||
.ends_with(&format!(".{}", o.signature.as_ref().unwrap().d))) | ||
|| rfc5322_from_domain.ends_with(&format!( | ||
".{}", | ||
domain_suffix_fn(&o.signature.as_ref().unwrap().d) | ||
))) | ||
}) | ||
{ | ||
output.policy = dmarc.sp; | ||
|
@@ -102,8 +107,10 @@ impl Resolver { | |
if dkim_output.iter().any(|o| { | ||
o.result == DkimResult::Pass | ||
&& (o.signature.as_ref().unwrap().d.ends_with(&from_subdomain) | ||
|| from_domain | ||
.ends_with(&format!(".{}", o.signature.as_ref().unwrap().d))) | ||
|| rfc5322_from_domain.ends_with(&format!( | ||
".{}", | ||
domain_suffix_fn(&o.signature.as_ref().unwrap().d) | ||
))) | ||
}) { | ||
output.policy = dmarc.sp; | ||
} | ||
|
@@ -208,7 +215,7 @@ mod test { | |
dmarc_dns, | ||
dmarc, | ||
message, | ||
mail_from_domain, | ||
rfc5321_mail_from_domain, | ||
signature_domain, | ||
dkim, | ||
spf, | ||
|
@@ -296,6 +303,22 @@ mod test { | |
DmarcResult::Pass, | ||
Policy::Quarantine, | ||
), | ||
// Relaxed - Pass with tree walk and different subdomains | ||
( | ||
"_dmarc.c.example.org.", | ||
concat!( | ||
"v=DMARC1; p=reject; sp=quarantine; np=None; aspf=r; adkim=r; fo=1;", | ||
"rua=mailto:[email protected]" | ||
), | ||
"From: [email protected]\r\n\r\n", | ||
"z.example.org", | ||
"z.example.org", | ||
DkimResult::Pass, | ||
SpfResult::Pass, | ||
DmarcResult::Pass, | ||
DmarcResult::Pass, | ||
Policy::Quarantine, | ||
), | ||
// Failed mechanisms | ||
( | ||
"_dmarc.example.org.", | ||
|
@@ -333,12 +356,18 @@ mod test { | |
}; | ||
let spf = SpfOutput { | ||
result: spf, | ||
domain: mail_from_domain.to_string(), | ||
domain: rfc5321_mail_from_domain.to_string(), | ||
report: None, | ||
explanation: None, | ||
}; | ||
let result = resolver | ||
.verify_dmarc(&auth_message, &[dkim], mail_from_domain, &spf) | ||
.verify_dmarc( | ||
&auth_message, | ||
&[dkim], | ||
rfc5321_mail_from_domain, | ||
&spf, | ||
|d| psl::domain_str(d).unwrap_or(d), | ||
) | ||
.await; | ||
assert_eq!(result.dkim_result, expect_dkim); | ||
assert_eq!(result.spf_result, expect_spf); | ||
|
Oops, something went wrong.