stalwartlabs/mail-auth

Relaxed alignment with differing subdomains not passing

Closed this issue · 3 comments

If a sender passes SPF or DKIM and their domain is z.example.org and the domain of the sender in the From header is a.b.c.example.org under relaxed alignent DMARC should pass because the Organizational Domain example.org aligns.

This test case can be added to the tests in dmarc/verify.rs to illustrate the scenario.

// 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:dmarc-feedback@example.org"
    ),
    "From: hello@a.b.c.example.org\r\n\r\n",
    "z.example.org",
    "z.example.org",
    DkimResult::Pass,
    SpfResult::Pass,
    DmarcResult::Pass,
    DmarcResult::Pass,
    Policy::Quarantine,
),

Currently, the test for relaxed alignment checks if either domain ends with the other. If this condition is met, the domains are considered aligned.

https://github.com/stalwartlabs/mail-auth/blob/main/src/dmarc/verify.rs#L74-L76

However, this test fails if the domains are 2 separate subdomains entirely. Looking at RFC sections 3.1.1 and 3.1.2, it looks like the correct test should be to test if the SPF/DKIM authenticated domain matches the Organizational Domain of the From header domain.

RFC povides guidance on how to extract the Organizational Domain in section 3.2. It looks like there are a couple of crates that provide this functionality:

  1. https://crates.io/crates/publicsuffix
  2. https://crates.io/crates/psl

The simplest approach is to use the psl crate but that comes with the shortcoming of needing to re-compile if the public suffix list changes in the future. The publicsuffix crate would require reading a file from the filesystem but it's unclear to me how you'd want to handle that configuration in this library.

Fixed, the dmarc_verify function now expects a function to extract the domain as a parameter. You can call the psl crate from there:

let dmarc_result = resolver
        .verify_dmarc(
            &authenticated_message,
            &dkim_result,
            "example.org",
            &spf_result,
            |domain| psl::domain_str(domain).unwrap_or(domain),
        )
        .await;

If it works well please let me know and I'll publish v0.5.0.

Looks great. Thanks!