diff --git a/filtermail/src/dkim_verifier.rs b/filtermail/src/dkim_verifier.rs index 00452667..9ce00958 100644 --- a/filtermail/src/dkim_verifier.rs +++ b/filtermail/src/dkim_verifier.rs @@ -14,6 +14,18 @@ use viadkim::verifier::LookupTxt; // "top 1000 relays" is much more than enough, the limit is mostly to prevent DoS attacks. const LRU_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(1000).expect("1000 != 0"); +/// Normalizes a TXT record RDATA by removing irrelevant characters. +/// +/// Some DKIM key records use e.g. LF + WSP line breaks. +/// This is technically not correct, and `viadkim` fails to parse such records, +/// but in practice this is accepted by many implementations. +/// +/// Additionally, removes escaped quotes, as such records as: +/// `"...UL9" "\" \"7vGm..."` proved to still be accepted by e.g. dkimpy or OpenDKIM. +fn normalize_rdata(txt_data: &str) -> String { + txt_data.replace([' ', '\t', '\n', '\r', '"'], "") +} + /// DNS resolver for DKIM TXT records, that caches RDATA in memory. #[derive(Clone)] struct CachedResolver { @@ -38,15 +50,6 @@ impl CachedResolver { }) } - /// Normalizes a TXT record RDATA by removing whitespace characters. - /// - /// Some DKIM key records use e.g. LF + WSP line breaks. - /// This is technically not correct, and `viadkim` fails to parse such records, - /// but in practice this is accepted by many implementations. - fn normalize_rdata(txt_data: &str) -> String { - txt_data.replace([' ', '\t', '\n', '\r'], "") - } - /// Invalidates the cached RDATA for a given selector and domain. /// /// Fails silently. @@ -98,7 +101,10 @@ impl LookupTxt for CachedResolver { .map(|txt| { let rdata_raw = txt.txt_data().concat(); let rdata = String::from_utf8_lossy(&rdata_raw); - Self::normalize_rdata(&rdata).into_bytes() + log::trace!("TXT (concat): {:?}", rdata); + let normalized = normalize_rdata(&rdata); + log::trace!("TXT (normalized): {:?}", normalized); + normalized.into_bytes() }) .collect() }; @@ -124,7 +130,8 @@ impl LookupTxt for MockResolver { fn lookup_txt(&self, _domain: &str) -> Self::Query<'_> { Box::pin(async move { - let txts: Self::Answer = Box::new(std::iter::once(Ok(self.0.clone().into_bytes()))); + let txts: Self::Answer = + Box::new(std::iter::once(Ok(normalize_rdata(&self.0).into_bytes()))); Ok(txts) }) } @@ -256,13 +263,29 @@ impl DkimVerifier { #[cfg(test)] mod tests { use super::*; + use rstest::rstest; + #[rstest] #[tokio::test] - async fn test_dkim_verifier() { - let verifier = DkimVerifier::mock( - r#"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5krC4Xi5Wkr6eMlla38LCFmV645E3FLAgsRl2YJ0SrZ4N2Vw1/yH0mefvtk7HYE7ytV7RQl/er2CkSsaHLJSYLmPCBw5CO6PSsBSXuh6DBqdylh/1t9vVQ9p38fTwn9gU1QvplcpRQL9eepRra1k24VMIaVy2ZZcu3LI9zkPsR7o7TyNaeMhsL8ouWInWc1NSid+p0SgliQuwHIejZhlTPE60JLbJE0OR9I4wmq3377H6z/QrO8XeabCgtmTuzE/hTRyIyNS40jql/99pjlhIcjM2U+P2B0FjwYt7BwLHsgANr74ctlnKY+SdH25rNwVpPmkotaULG5SJCByKBkfCwIDAQAB;s=email;t=s"#.to_string() - ); - let raw_mail = include_bytes!("../test_data/dkim-abjadiyah.eml"); - verifier.verify(raw_mail, "abjadiyah.xyz").await.unwrap(); + #[case::simple_simple_canonicalization( + r#"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5krC4Xi5Wkr6eMlla38LCFmV645E3FLAgsRl2YJ0SrZ4N2Vw1/yH0mefvtk7HYE7ytV7RQl/er2CkSsaHLJSYLmPCBw5CO6PSsBSXuh6DBqdylh/1t9vVQ9p38fTwn9gU1QvplcpRQL9eepRra1k24VMIaVy2ZZcu3LI9zkPsR7o7TyNaeMhsL8ouWInWc1NSid+p0SgliQuwHIejZhlTPE60JLbJE0OR9I4wmq3377H6z/QrO8XeabCgtmTuzE/hTRyIyNS40jql/99pjlhIcjM2U+P2B0FjwYt7BwLHsgANr74ctlnKY+SdH25rNwVpPmkotaULG5SJCByKBkfCwIDAQAB;s=email;t=s"#, + include_bytes!("../test_data/dkim-abjadiyah.eml") + )] + #[case::txt_escaped_quotes( + r#"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1giTh8KDkEchWhrAB6hGnb+V87kTezkt5I3SP7BGNg8wpv0yAuj/SUmnsttYmcEU+zmNAPqxePmCNvmjLYi/c3YyWEBwHcLyZE9OlS9W4enPdsoCuEN3DayzN4JCV3MsXMedCORvLFXmIARDXDLJUSJeqCeQoudXa9GmF1CrCmx70YyTtV0xOIxEzo7z0DkUL9" "7vGmNJCv6EMpi9wccMKKu8NSmOv+DBw1MLIJqChSZMCs8CYZ5i0KT/+Lijtn6B7wyOcAuQsVL+zr7DWYrFdrePe0wGuivfJ3SvUEfUo1SIykl0nvm0iLGhjNmNa1e/tUw4ULXhQ12Qw685+sq7wIDAQAB;s=email;t=s"#, + include_bytes!("../test_data/dkim-privitty.eml") + )] + async fn test_dkim_verifier(#[case] txt: &str, #[case] message: &[u8]) { + let verifier = DkimVerifier::mock(txt.to_string()); + verifier.verify(message, "abjadiyah.xyz").await.unwrap(); + } + + #[rstest] + #[case::escaped_quotes( + r#"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1giTh8KDkEchWhrAB6hGnb+V87kTezkt5I3SP7BGNg8wpv0yAuj/SUmnsttYmcEU+zmNAPqxePmCNvmjLYi/c3YyWEBwHcLyZE9OlS9W4enPdsoCuEN3DayzN4JCV3MsXMedCORvLFXmIARDXDLJUSJeqCeQoudXa9GmF1CrCmx70YyTtV0xOIxEzo7z0DkUL9" "7vGmNJCv6EMpi9wccMKKu8NSmOv+DBw1MLIJqChSZMCs8CYZ5i0KT/+Lijtn6B7wyOcAuQsVL+zr7DWYrFdrePe0wGuivfJ3SvUEfUo1SIykl0nvm0iLGhjNmNa1e/tUw4ULXhQ12Qw685+sq7wIDAQAB;s=email;t=s"#, + r#"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1giTh8KDkEchWhrAB6hGnb+V87kTezkt5I3SP7BGNg8wpv0yAuj/SUmnsttYmcEU+zmNAPqxePmCNvmjLYi/c3YyWEBwHcLyZE9OlS9W4enPdsoCuEN3DayzN4JCV3MsXMedCORvLFXmIARDXDLJUSJeqCeQoudXa9GmF1CrCmx70YyTtV0xOIxEzo7z0DkUL97vGmNJCv6EMpi9wccMKKu8NSmOv+DBw1MLIJqChSZMCs8CYZ5i0KT/+Lijtn6B7wyOcAuQsVL+zr7DWYrFdrePe0wGuivfJ3SvUEfUo1SIykl0nvm0iLGhjNmNa1e/tUw4ULXhQ12Qw685+sq7wIDAQAB;s=email;t=s"# + )] + fn test_normalize_rdata(#[case] input: &str, #[case] expected: &str) { + assert_eq!(normalize_rdata(input), expected); } } diff --git a/filtermail/test_data/dkim-privitty.eml b/filtermail/test_data/dkim-privitty.eml new file mode 100644 index 00000000..c23274cd --- /dev/null +++ b/filtermail/test_data/dkim-privitty.eml @@ -0,0 +1,80 @@ +Received: from chat.privittytech.com (unknown [35.154.144.0]) + by nine.testrun.org (Postfix) with ESMTPS + for ; Sat, 21 Feb 2026 22:17:25 +0100 (CET) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; + d=chat.privittytech.com; s=opendkim; t=1771708644; + bh=Qs0Xoe6dH8cM3t3hv45QSkQQe5TLvyJ5R4OGJL9GYUc=; + h=Content-Type:From:To:Subject:Date:References:from:reply-to: + subject:date:to:cc:resent-date:resent-from:resent-sender:resent-to: + resent-cc:in-reply-to:references:list-id:list-help: + list-unsubscribe:list-subscribe:list-post:list-owner:list-archive: + autocrypt; + b=rsdW71iU79CF0dooc1PnbZsJbWZsnBampamyF3jTWirF8AXBBKeR2JJsxr4km3Vx9 + QoT1s7VAyl/hdUBh2tBWT7Kzvw4KJcgkZ7tV/uRcY7edlLoH8TqATeTkWeNX18rL92 + F6Vv/6krs/z8Rpz2ZaVkBdLQif0fLQ3e96ODOdpgkLFXWsMVTM5rGepPWYht8yA+BF + /sMopzgHhJaBvDqV42Ep75IK4w3opf9fPTAvtpwKbnDmJQmLSDv1So/Rd4xL3Oj6PX + CSO8dfJSzQ+ukyoDqVMddVCUpgVgFHTXMdxxQVTe54tj2JPZan5ql90SHa3lNFTgN8 + hrM8BExznme5Q== +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="18965f9fc8f94f5b_d3ca8c34f6c5d05_4df177c93ae6931" +MIME-Version: 1.0 +From: <8dn75miab@chat.privittytech.com> +To: "hidden-recipients": ; +Subject: [...] +Date: Mon, 16 Feb 2026 06:28:47 +0000 +Message-ID: <0737aaa0-d6f9-4240-9d94-1b73216ca96d@localhost> +References: <0737aaa0-d6f9-4240-9d94-1b73216ca96d@localhost> +Chat-Version: 1.0 + + +--18965f9fc8f94f5b_d3ca8c34f6c5d05_4df177c93ae6931 +Content-Type: application/pgp-encrypted; charset="utf-8" +Content-Description: PGP/MIME version identification +Content-Transfer-Encoding: 7bit + +Version: 1 + +--18965f9fc8f94f5b_d3ca8c34f6c5d05_4df177c93ae6931 +Content-Type: application/octet-stream; name="encrypted.asc"; + charset="utf-8" +Content-Description: OpenPGP encrypted message +Content-Disposition: inline; filename="encrypted.asc"; +Content-Transfer-Encoding: 7bit + +-----BEGIN PGP MESSAGE----- + +wU4DWvqoogTPq7gSAQdAO8msB6rJLzr5isiWx/ZoAHqNnYZTcE4fpjpj8C1+Vhgg +c3o2P2p9T2GfJ6aa6KqbJQW7VPl8zaaylYCN8JQLKqHBTgPP/P3FoTw8jxIBB0CE +/iffxyAThaUNNQyp0vmrW9k8v47ll3TBQjCv6K/bbiB7u1vsYbBfl8FQgjDNhHTV +U+lJQntsqmsn3VmxRjz1vdLD4QGvqJXYHUUyYJ7EetMBHFcmpCLlO14MhvZKw4Gd +yWhEcgv6fsFpBaTk7Y9MXrflbpSR84aCtNqEKWU6FjjFWoRUzWVV42gI0cXectg6 +Ry2p21EfiBsLLaQdMneep7Hpq/GXccLZcFVVc9VCQq80lq/9GkiDSdA/9JD3gGFu +NCDMKkgAQRaKVkfGZ7VhQqpkAZeeI3H/kGjzFElcNUqTH1lRgi7OgJuhIA9VeIWb +ZX677i3tjnNWrAJoIVs+l5I7jWhgE9VMutI4kBA0fxk2QYW++n9TPz2/FbTuFkjZ +ULXQC8F7CaVdUVb1DIAc7hw8EGFX98LKj04ucBuoe1QfJK6LKbhelSmGYGQbBOOG +AZSPs/jgH1EkgtIzxD6rTJTFy/+2Y4eOTJaq0F4ZP32+pOVr3Iq2OuSS3LakfCjk +TPqe9Y1w4q0afQdS2tHlnLGOGrFxoYeRjUnTeeHuH9sSp6G6Lf5751sLVgYUtILo +Kgaac+gCh0rBK4Ve4LksnY6FUzogLYxIt4NkmDIWDvlDc0RnC+moQ/VaICPYlnMl +dJB4mVFKefqqQM61rWTZyqHc8tDJj0D1dqpxkYJT+VH+t0RNp6br8I+wQ91BP595 +Usma3Uv1MjLufMFWcTtd72PAfk13uWF2u2dkKO+KR4MUnXm/B3ZSBAUfGfBaH4SP +mn/dDeNY0c9ya+8l+Nt2lK/3imh3E29z22pnVwTB/PAK1HCxrehw6j/W6cfrylXC +TcqLJW2K542oy5Q6zE5sU6mYRfOQcoR/s3t7y5ynu3s29kIbUhysPN2qT7YMmTWo +hSlMHU5oDrzDNKWkE1FsXpTCGTdSvKhQf/HuzmQBXiCQxLlH+55UJPwbf2H95o1A +cVChQP6vwqBIrB0k1CkJbfqCMPvr7fyNcZNqdk+iyXwPu9vOhE+4k5ybCZ7t/uoY +DHm05/nj5jqIEG2vV0sTogd8dnu6KiVK+wET4mZwOMmAPp/8ROOmZ2NvEK1iPlgL +c92u2XLAgZW+AbI93oJARWI8MGT5CfvJcKvjO1Nr4KIjN2G4uMiE//f2hPM676mq +eeFqFgITGN5ivfwnfdRgEHB63mkNeTVpzrHLNYYH/HVPdjsTqDp0cPCvZxoIDgTj +/9+uJtfgRekpD0LqOfnUg27TBthFksuFaiMW4cvxgNdqOVbo7RjcSvGg2pd80Bu0 +0gv/47tDGl0Hn9BI+XIbwLRos6dSzxgU6XotjfA31lcSFiP99IkWH4sBFRWVIaEX +iPqJOmDWsgWjzzgu9JYQ8cZH4BP74Rv8qCC2klP4HfdQuErekNu7vDPLH3iHWpuX +HcSc6imuNXz45ncwyEsQ+tP4t42SEv0dGRaMpwNIniIiKYAL+dMFNvdqEsKiPRoG +f7xEuY2sZKoBtNw+gXcGlYs+QhSXJtpAdpHg+Bp0AYUQBU9ciYFfk8ZsEbSdpWGP +D0+Xd7bWrgUYN0UM0EdtpbyxnH/bFe3Jg5jdDuS43ifhYTEPEdpmG5pnqNk5Yi0Q +wxN6ntEHbfy3gmhMePOigh09RlwgbdoqurlvlUL2FHEY/Z1P9VI0tHbTXayUcBQk +3aljbg== +=wRZT +-----END PGP MESSAGE----- + + +--18965f9fc8f94f5b_d3ca8c34f6c5d05_4df177c93ae6931-- +