From 94b33187a05aa10dac3231583cfce40d46056254 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:07:37 +0200 Subject: [PATCH] DKIM2 tests --- Cargo.lock | 401 +++++++++---- crates/common/Cargo.toml | 6 +- crates/dav-proto/Cargo.toml | 2 +- crates/http/Cargo.toml | 4 +- crates/jmap/Cargo.toml | 2 +- crates/registry/Cargo.toml | 2 +- crates/smtp/Cargo.toml | 2 +- crates/smtp/src/inbound/dkim.rs | 173 +++++- crates/smtp/src/inbound/milter/protocol.rs | 20 +- crates/spam-filter/Cargo.toml | 2 +- crates/trc/Cargo.toml | 2 +- crates/utils/Cargo.toml | 4 +- crates/utils/src/cache.rs | 14 +- tests/Cargo.toml | 4 +- tests/src/smtp/inbound/dkim2.rs | 633 +++++++++++++++++++++ tests/src/smtp/inbound/mod.rs | 1 + 16 files changed, 1124 insertions(+), 148 deletions(-) create mode 100644 tests/src/smtp/inbound/dkim2.rs diff --git a/Cargo.lock b/Cargo.lock index 4b983026..0067227f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy", + "zerocopy 0.8.52", ] [[package]] @@ -122,15 +122,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -167,9 +167,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "ascii-canvas" @@ -221,15 +221,15 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" +checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd" dependencies = [ - "filetime", "futures-core", "libc", "portable-atomic", "rustc-hash", + "rustix 0.38.44", "tokio", "tokio-stream", "xattr", @@ -281,7 +281,7 @@ dependencies = [ "memchr", "pin-project", "portable-atomic", - "rand 0.10.1", + "rand 0.10.2", "regex", "rustls-native-certs", "rustls-pki-types", @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -401,14 +401,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -770,7 +771,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "hybrid-array", + "hybrid-array 0.4.13", "zeroize", ] @@ -789,7 +790,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" dependencies = [ - "hybrid-array", + "hybrid-array 0.4.13", ] [[package]] @@ -917,9 +918,9 @@ dependencies = [ [[package]] name = "buffered-reader" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db26bf1f092fd5e05b5ab3be2f290915aeb6f3f20c4e9f86ce0f07f336c2412f" +checksum = "c7531a07caf83a3fa3b47a233b437ee7910cccfb688555d224338295a0a64fcc" dependencies = [ "libc", ] @@ -1319,7 +1320,7 @@ dependencies = [ "proxy-header", "psl", "pwhash", - "quick-xml 0.40.1", + "quick-xml 0.41.0", "quick_cache", "rasn", "rasn-cms", @@ -1641,7 +1642,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "hybrid-array", + "hybrid-array 0.4.13", + "rand_core 0.10.1", ] [[package]] @@ -1729,8 +1731,8 @@ dependencies = [ "crypto-bigint", "elliptic-curve", "rand_core 0.6.4", - "sha3", - "signature", + "sha3 0.10.9", + "signature 2.2.0", "subtle", "zeroize", ] @@ -1849,7 +1851,7 @@ dependencies = [ "hashify", "hyper", "mail-parser", - "quick-xml 0.40.1", + "quick-xml 0.41.0", "rkyv", "serde", "serde_json", @@ -2170,7 +2172,7 @@ dependencies = [ "pkcs8", "rfc6979", "sha2 0.10.9", - "signature", + "signature 2.2.0", "zeroize", ] @@ -2218,7 +2220,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature", + "signature 2.2.0", "spki", ] @@ -2247,7 +2249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", - "signature", + "signature 2.2.0", ] [[package]] @@ -2488,7 +2490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.10.1", + "rand 0.10.2", "web-time", ] @@ -2509,16 +2511,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2626,7 +2618,7 @@ dependencies = [ "foundationdb-sys", "foundationdb-tuple", "futures", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_bytes", "serde_json", @@ -2817,7 +2809,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-link 0.2.1", ] @@ -3072,7 +3064,7 @@ dependencies = [ "jni 0.22.4", "lru-cache", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "rustls", "rustls-pki-types", "rustls-platform-verifier 0.7.0", @@ -3099,7 +3091,7 @@ dependencies = [ "jni 0.22.4", "once_cell", "prefix-trie", - "rand 0.10.1", + "rand 0.10.2", "ring", "rustls-pki-types", "thiserror 2.0.18", @@ -3126,7 +3118,7 @@ dependencies = [ "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "resolv-conf", "rustls", "smallvec", @@ -3230,7 +3222,7 @@ dependencies = [ "mime", "percent-encoding", "pkcs8", - "quick-xml 0.40.1", + "quick-xml 0.41.0", "registry", "rkyv", "rsa", @@ -3334,19 +3326,30 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af" dependencies = [ "typenum", ] +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "ctutils", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.10.1" @@ -3730,7 +3733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ "block-padding 0.4.2", - "hybrid-array", + "hybrid-array 0.4.13", ] [[package]] @@ -4034,9 +4037,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -4073,7 +4076,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "signature", + "signature 2.2.0", "simple_asn1", "zeroize", ] @@ -4087,6 +4090,26 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + [[package]] name = "keyed-set" version = "1.1.0" @@ -4134,7 +4157,7 @@ dependencies = [ "petgraph", "regex", "regex-syntax", - "sha3", + "sha3 0.10.9", "string_cache", "term", "unicode-xid", @@ -4248,9 +4271,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -4299,6 +4322,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4390,6 +4419,8 @@ dependencies = [ [[package]] name = "mail-auth" version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2a3c0ef3c8f70fc3a2ce043453605b4d5e9501f5ff2318aac92ab8d59fb78b" dependencies = [ "aws-lc-rs", "flate2", @@ -4643,6 +4674,47 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", + "hybrid-array 0.4.13", + "module-lattice", + "shake", + "signature 3.0.0", + "zeroize", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array 0.4.13", + "kem", + "module-lattice", + "rand_core 0.10.1", + "sha3 0.11.0", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array 0.4.13", + "num-traits", + "zeroize", +] + [[package]] name = "moka" version = "0.12.15" @@ -4893,9 +4965,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" dependencies = [ "num-integer", "num-traits", @@ -5632,7 +5704,7 @@ dependencies = [ "hmac 0.13.0", "md-5 0.11.0", "memchr", - "rand 0.10.1", + "rand 0.10.2", "sha2 0.11.0", "stringprep", ] @@ -5677,7 +5749,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.52", ] [[package]] @@ -5844,9 +5916,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.214" +version = "2.1.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9124f6ba3f3aea75e4918fc86c76adfba8c9d289ab36f926f0b6ce4987a9e6a9" +checksum = "9826c2fe4dade07e9da1af2e18427257877bc8bc27aa810f6fbb52d907a480cc" dependencies = [ "psl-types", ] @@ -5939,13 +6011,13 @@ dependencies = [ [[package]] name = "quick_cache" -version = "0.6.23" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3db184a8b66cfe87f0263a1de147a6b554c864d1767c6f7fa4eb0e5497b565" +checksum = "403c1a912fec895cafb223201e368234842acb9220aaf08ab042ae89ba5f135c" dependencies = [ - "ahash", "equivalent", - "hashbrown 0.16.1", + "foldhash 0.2.0", + "hashbrown 0.17.1", "parking_lot", ] @@ -6053,9 +6125,9 @@ checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" [[package]] name = "rancor" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c" dependencies = [ "ptr_meta", ] @@ -6096,9 +6168,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20 0.10.1", "getrandom 0.4.3", @@ -6311,9 +6383,9 @@ dependencies = [ [[package]] name = "redis" -version = "1.2.4" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae41a63fd0b8a5372f82b21e810e09a316f5dd7efd96bf08e678fb240fc1918" +checksum = "2fa6f8e4b491d7a8ef3a9550a4d71969bd0064f46e32b8dbbcc7fc60dad94fed" dependencies = [ "arcstr", "async-lock", @@ -6327,7 +6399,7 @@ dependencies = [ "num-bigint", "percent-encoding", "pin-project-lite", - "rand 0.10.1", + "rand 0.10.2", "rustls", "rustls-native-certs", "ryu", @@ -6429,9 +6501,9 @@ dependencies = [ [[package]] name = "rend" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" dependencies = [ "bytecheck", ] @@ -6572,9 +6644,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +checksum = "815cc8a37159a463064825246cadb07961e25cd9885908606f6d08a98d8f8874" dependencies = [ "bytecheck", "bytes", @@ -6591,9 +6663,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" dependencies = [ "proc-macro2", "quote", @@ -6655,7 +6727,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sha2 0.10.9", - "signature", + "signature 2.2.0", "spki", "subtle", "zeroize", @@ -6748,9 +6820,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -6770,6 +6842,19 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -6779,7 +6864,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -7056,9 +7141,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "sequoia-openpgp" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c847f0f148cf238c3aec88d092fd3c4301c21e906829ea9e415ea7531f7ec094" +checksum = "e421679c8175ef4674d913af7fdb8ad0078c8a2599db633ad0b4d02a06b2cb3c" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -7093,6 +7178,8 @@ dependencies = [ "libc", "md-5 0.10.6", "memsec", + "ml-dsa", + "ml-kem", "num-bigint-dig", "num-integer", "ocb3", @@ -7107,7 +7194,8 @@ dependencies = [ "rsa", "sha1collisiondetection", "sha2 0.10.9", - "sha3", + "sha3 0.10.9", + "slh-dsa", "thiserror 2.0.18", "twofish", "typenum", @@ -7431,7 +7519,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", - "keccak", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", ] [[package]] @@ -7500,6 +7609,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -7555,6 +7673,25 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slh-dsa" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "hmac 0.12.1", + "hybrid-array 0.3.1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "sha3 0.10.9", + "signature 2.2.0", + "typenum", + "zerocopy 0.7.35", +] + [[package]] name = "smallvec" version = "1.15.2" @@ -7720,6 +7857,12 @@ dependencies = [ "der", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlite-wasm-rs" version = "0.5.5" @@ -8095,7 +8238,7 @@ dependencies = [ "nlp", "num_cpus", "pop3", - "quick-xml 0.40.1", + "quick-xml 0.41.0", "rayon", "rcgen", "registry", @@ -8176,9 +8319,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "js-sys", @@ -8197,9 +8340,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -8310,7 +8453,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.10.1", + "rand 0.10.2", "socket2 0.6.4", "tokio", "tokio-util", @@ -8952,9 +9095,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -9083,9 +9226,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -9096,9 +9239,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.75" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -9106,9 +9249,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9116,9 +9259,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -9129,9 +9272,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -9164,9 +9307,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -9436,6 +9579,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -9716,7 +9868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -9742,9 +9894,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576" [[package]] name = "yasna" @@ -9907,7 +10059,7 @@ dependencies = [ "hmac 0.12.1", "rand 0.8.6", "rand_chacha 0.3.1", - "sha3", + "sha3 0.10.9", "zenoh-result", ] @@ -10200,7 +10352,7 @@ dependencies = [ "ringbuffer-spsc", "rsa", "serde", - "sha3", + "sha3 0.10.9", "tokio", "tokio-util", "tracing", @@ -10246,13 +10398,34 @@ dependencies = [ "zenoh-result", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.52", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -10369,9 +10542,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" +checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" [[package]] name = "zmij" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 5ce83ab8..6aef8865 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -18,7 +18,7 @@ imap_proto = { path = "../imap-proto" } sieve-rs = { version = "0.7", features = ["rkyv", "serde"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["generate", "arc"] } +mail-auth = { version = "0.11", features = ["generate", "arc"] } smtp-proto = { version = "0.2", features = ["rkyv"] } dns-update = { version = "0.5" } calcard = { version = "0.3", features = ["rkyv"] } @@ -78,14 +78,14 @@ compact_str = { version = "0.9.0", features = ["rkyv", "serde"] } lz4_flex = { version = "0.13", features = ["frame"], default-features = false } hickory-proto = "0.26" nohash-hasher = "0.2.0" -quick_cache = "0.6.9" +quick_cache = "0.7" rasn = "0.28" rasn-cms = "0.28" rasn-pkix = "0.28" sequoia-openpgp = { version = "2.0", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] } zxcvbn = "3.1.0" pkcs8 = { version = "0.10.2", features = ["alloc", "std"] } -quick-xml = "0.40" +quick-xml = "0.41" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 2fdb038d..7b14fef3 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" trc = { path = "../trc" } types = { path = "../types" } hashify = "0.2.6" -quick-xml = "0.40" +quick-xml = "0.41" calcard = { version = "0.3", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } hyper = "1.6.0" diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index 172c7eb9..35af0dc3 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -23,13 +23,13 @@ registry = { path = "../registry" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["generate", "arc"] } +mail-auth = { version = "0.11", features = ["generate", "arc"] } tokio = { version = "1.47", features = ["rt"] } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1.1", features = ["tokio"] } http-body-util = "0.1.0" async-stream = "0.3.5" -quick-xml = "0.40" +quick-xml = "0.41" serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" x509-parser = "0.18" diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 1d660bbf..1f000bec 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -23,7 +23,7 @@ calcard = { version = "0.3" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["generate", "arc"] } +mail-auth = { version = "0.11", features = ["generate", "arc"] } sieve-rs = { version = "0.7", features = ["rkyv"] } jmap-tools = { version = "0.1", features = ["rkyv"] } serde = { version = "1.0", features = ["derive"]} diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 32133738..2cbfae8c 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -13,7 +13,7 @@ hashify = "0.2.7" ahash = { version = "0.8" } jmap-tools = { version = "0.1" } xxhash-rust = { version = "0.8.5", features = ["xxh3"] } -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.11" } tokio = { version = "1.47", features = ["fs"] } lz4_flex = { version = "0.13", default-features = false } diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index b20bd5ff..34ff326d 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -21,7 +21,7 @@ email = { path = "../email" } registry = { path = "../registry" } spam-filter = { path = "../spam-filter" } trc = { path = "../trc" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["rkyv"] } +mail-auth = { version = "0.11", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } smtp-proto = { version = "0.2", features = ["rkyv", "serde"] } diff --git a/crates/smtp/src/inbound/dkim.rs b/crates/smtp/src/inbound/dkim.rs index 1af02a3b..a13afcd4 100644 --- a/crates/smtp/src/inbound/dkim.rs +++ b/crates/smtp/src/inbound/dkim.rs @@ -85,7 +85,7 @@ impl DkimSign for Server { }); let instance = MessageInstance::from_message(&modified, original.as_ref()); if let Some(instance) = &instance { - instance.write(&mut headers); + instance.write_header(&mut headers); } // Obtain disclosed and undisclosed recipients @@ -104,11 +104,11 @@ impl DkimSign for Server { Ok(signature) => { if envelopes.undisclosed_recipients.is_empty() { // Happy path: no undisclosed recipients, serialize signature straight to blob - signature.write(&mut headers); + signature.write_header(&mut headers); } else { // Undisclosed recipients present, serialize signature to metadata let mut header = Vec::with_capacity(64); - signature.write(&mut header); + signature.write_header(&mut header); params.metadata.push(Metadata::Headers { value: header.into_boxed_slice(), id: u64::MAX, @@ -135,7 +135,7 @@ impl DkimSign for Server { Ok(signature) => { // Serialize signature to metadata let mut header = Vec::with_capacity(64); - signature.write(&mut header); + signature.write_header(&mut header); params.metadata.push(Metadata::Headers { value: header.into_boxed_slice(), id: pos as u64, @@ -251,3 +251,168 @@ impl MessageWrapper { fn sanitize_or_lower(rcpt: &str) -> String { sanitize_email(rcpt).unwrap_or_else(|| rcpt.to_lowercase()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::queue::{Message, QueueId, Recipient}; + use common::config::smtp::queue::QueueName; + use std::net::{IpAddr, Ipv4Addr}; + + fn wrapper(recipients: &[&str]) -> MessageWrapper { + MessageWrapper { + queue_id: 0 as QueueId, + queue_name: QueueName::default(), + is_multi_queue: false, + span_id: 0, + message: Message { + created: 0, + blob_hash: Default::default(), + return_path: "sender@example.com".into(), + recipients: recipients.iter().map(Recipient::new).collect(), + received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + received_via_port: 0, + flags: 0, + env_id: None, + priority: 0, + size: 0, + metadata: Default::default(), + }, + } + } + + fn split(headers: &str, recipients: &[&str]) -> (Vec, Vec) { + let raw = format!("{headers}\r\nSubject: test\r\n\r\nbody\r\n"); + let auth = AuthenticatedMessage::parse(raw.as_bytes()).expect("parse message"); + let message = wrapper(recipients); + let envelopes = message.undisclosed_recipients(&auth); + let mut disclosed = envelopes + .disclosed_recipients + .iter() + .map(|s| s.to_string()) + .collect::>(); + let mut undisclosed = envelopes + .undisclosed_recipients + .iter() + .map(|(_, s)| s.to_string()) + .collect::>(); + disclosed.sort(); + undisclosed.sort(); + (disclosed, undisclosed) + } + + #[test] + fn single_recipient_is_always_disclosed() { + let (disclosed, undisclosed) = split("To: someone-else@example.com", &["bcc@example.org"]); + assert_eq!(disclosed, vec!["bcc@example.org".to_string()]); + assert!(undisclosed.is_empty()); + } + + #[test] + fn all_recipients_disclosed() { + let (disclosed, undisclosed) = split( + "To: alice@example.com, bob@example.com\r\nCc: carol@example.com", + &["alice@example.com", "bob@example.com", "carol@example.com"], + ); + assert_eq!( + disclosed, + vec![ + "alice@example.com".to_string(), + "bob@example.com".to_string(), + "carol@example.com".to_string(), + ] + ); + assert!(undisclosed.is_empty()); + } + + #[test] + fn mixed_disclosed_and_undisclosed() { + let (disclosed, undisclosed) = split( + "To: alice@example.com\r\nCc: bob@example.com", + &[ + "alice@example.com", + "bob@example.com", + "eve@secret.example.org", + "mallory@secret.example.org", + ], + ); + assert_eq!( + disclosed, + vec![ + "alice@example.com".to_string(), + "bob@example.com".to_string() + ] + ); + assert_eq!( + undisclosed, + vec![ + "eve@secret.example.org".to_string(), + "mallory@secret.example.org".to_string(), + ] + ); + } + + #[test] + fn no_to_or_cc_header_all_undisclosed() { + let (disclosed, undisclosed) = split( + "From: sender@example.com", + &["eve@example.org", "mallory@example.org"], + ); + assert!(disclosed.is_empty()); + assert_eq!( + undisclosed, + vec![ + "eve@example.org".to_string(), + "mallory@example.org".to_string() + ] + ); + } + + #[test] + fn group_addresses_are_disclosed() { + let (disclosed, undisclosed) = split( + "To: Team:alice@example.com,bob@example.com;", + &["alice@example.com", "bob@example.com", "eve@example.org"], + ); + assert_eq!( + disclosed, + vec![ + "alice@example.com".to_string(), + "bob@example.com".to_string() + ] + ); + assert_eq!(undisclosed, vec!["eve@example.org".to_string()]); + } + + #[test] + fn header_address_casing_is_ignored() { + let (disclosed, undisclosed) = split( + "To: Alice@Example.COM, BOB@EXAMPLE.com", + &["alice@example.com", "bob@example.com", "eve@example.org"], + ); + assert_eq!( + disclosed, + vec![ + "alice@example.com".to_string(), + "bob@example.com".to_string() + ] + ); + assert_eq!(undisclosed, vec!["eve@example.org".to_string()]); + } + + #[test] + fn display_names_and_brackets_are_ignored() { + let (disclosed, undisclosed) = split( + "To: \"Alice Doe\" \r\nCc: Bob ", + &["alice@example.com", "bob@example.com", "eve@example.org"], + ); + assert_eq!( + disclosed, + vec![ + "alice@example.com".to_string(), + "bob@example.com".to_string() + ] + ); + assert_eq!(undisclosed, vec!["eve@example.org".to_string()]); + } +} diff --git a/crates/smtp/src/inbound/milter/protocol.rs b/crates/smtp/src/inbound/milter/protocol.rs index 896a392b..556fc6b0 100644 --- a/crates/smtp/src/inbound/milter/protocol.rs +++ b/crates/smtp/src/inbound/milter/protocol.rs @@ -50,7 +50,7 @@ pub const SMFIC_UNKNOWN: u8 = b'U'; /* Any unknown command */ impl Command<'_> { fn build(command: u8, len: u32) -> Vec { let mut buf = Vec::with_capacity(len as usize + 1 + std::mem::size_of::()); - buf.extend_from_slice((len + 1).to_be_bytes().as_ref()); + buf.extend_from_slice((len + 1).to_be_bytes().as_slice()); buf.push(command); buf } @@ -95,7 +95,7 @@ impl Command<'_> { buf.extend(hostname); buf.push(0x00); buf.push(family); - buf.extend(port.to_be_bytes().as_ref()); + buf.extend(port.to_be_bytes().as_slice()); buf.extend(address.as_bytes()); buf.push(0x00); buf @@ -168,9 +168,9 @@ impl Command<'_> { } Command::OptionNegotiation(opt) => { let mut buf = Command::build(SMFIC_OPTNEG, 3 * std::mem::size_of::() as u32); - buf.extend(opt.version.to_be_bytes().as_ref()); - buf.extend(opt.actions.to_be_bytes().as_ref()); - buf.extend(opt.protocol.to_be_bytes().as_ref()); + buf.extend(opt.version.to_be_bytes().as_slice()); + buf.extend(opt.actions.to_be_bytes().as_slice()); + buf.extend(opt.protocol.to_be_bytes().as_slice()); buf } Command::Quit => Command::build(SMFIC_QUIT, 0), @@ -417,7 +417,7 @@ impl Response { + std::mem::size_of::() as u32 + 2, ); - buf.extend(index.to_be_bytes().as_ref()); + buf.extend(index.to_be_bytes().as_slice()); buf.extend(name.as_bytes()); buf.push(0x00); buf.extend(value.as_bytes()); @@ -432,7 +432,7 @@ impl Response { + std::mem::size_of::() as u32 + 2, ); - buf.extend(index.to_be_bytes().as_ref()); + buf.extend(index.to_be_bytes().as_slice()); buf.extend(name.as_bytes()); buf.push(0x00); buf.extend(value.as_bytes()); @@ -451,9 +451,9 @@ impl Response { Response::SetSymbols => Command::build(SMFIR_SETSYMLIST, 0), Response::OptionNegotiation(opt) => { let mut buf = Command::build(SMFIC_OPTNEG, 3 * std::mem::size_of::() as u32); - buf.extend(opt.version.to_be_bytes().as_ref()); - buf.extend(opt.actions.to_be_bytes().as_ref()); - buf.extend(opt.protocol.to_be_bytes().as_ref()); + buf.extend(opt.version.to_be_bytes().as_slice()); + buf.extend(opt.actions.to_be_bytes().as_slice()); + buf.extend(opt.protocol.to_be_bytes().as_slice()); buf } } diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index 7bf839ba..208213f2 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -14,7 +14,7 @@ registry = { path = "../registry" } smtp-proto = { version = "0.2", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.11" } tokio = { version = "1.47", features = ["net", "macros"] } psl = "2" hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index cbcbbe71..ca271c39 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] event_macro = { path = "./event-macro" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["arc"] } +mail-auth = { version = "0.11", features = ["arc"] } mail-parser = { version = "0.11", features = ["full_encoding"] } base64 = "0.22.1" serde = "1.0" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 7b95f671..ff6b2539 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -12,7 +12,7 @@ rustls-platform-verifier = "0.7" tokio = { version = "1.47", features = ["net", "macros", "signal"] } tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } serde = { version = "1.0", features = ["derive"]} -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.11" } smtp-proto = { version = "0.2" } ahash = { version = "0.8", features = ["serde"] } chrono = "0.4" @@ -32,7 +32,7 @@ http-body-util = "0.1.0" form_urlencoded = "1.1.0" psl = "2" idna = "1.0" -quick_cache = "0.6.9" +quick_cache = "0.7" fast-float = "0.2.0" rkyv = { version = "0.8.10", features = ["little_endian"] } compact_str = "0.9.0" diff --git a/crates/utils/src/cache.rs b/crates/utils/src/cache.rs index d747afcf..0b3bae3e 100644 --- a/crates/utils/src/cache.rs +++ b/crates/utils/src/cache.rs @@ -19,11 +19,11 @@ use std::{ }; pub struct Cache( - quick_cache::sync::Cache, + quick_cache::sync::Cache, ); pub struct CacheWithTtl( - quick_cache::sync::Cache, CacheItemWeighter>, + quick_cache::sync::Cache, CacheItemWeighter, ahash::RandomState>, ); #[derive(Clone)] @@ -38,10 +38,12 @@ impl Cache { } pub fn new_estimated(estimated_items_capacity: usize, weight_capacity: u64) -> Self { - Self(quick_cache::sync::Cache::with_weighter( + Self(quick_cache::sync::Cache::with( estimated_items_capacity, weight_capacity, CacheItemWeighter, + ahash::RandomState::default(), + DefaultLifecycle::default(), )) } @@ -101,7 +103,7 @@ impl Cache { } #[inline(always)] - pub fn inner(&self) -> &quick_cache::sync::Cache { + pub fn inner(&self) -> &quick_cache::sync::Cache { &self.0 } } @@ -112,10 +114,12 @@ impl CacheWithTtl Self { - Self(quick_cache::sync::Cache::with_weighter( + Self(quick_cache::sync::Cache::with( estimated_items_capacity, weight_capacity, CacheItemWeighter, + ahash::RandomState::default(), + DefaultLifecycle::default(), )) } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index de8e3fc8..12490a7a 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -45,7 +45,7 @@ migration = { path = "../crates/migration", features = ["test_mode", "enterprise trc = { path = "../crates/trc", features = ["enterprise"] } managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] } smtp-proto = { version = "0.2" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["test"] } +mail-auth = { version = "0.11", features = ["test"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = "0.4.4" sieve-rs = { version = "0.7", features = ["rkyv"] } @@ -79,7 +79,7 @@ biscuit = "0.8.0" form_urlencoded = "1.1.0" rkyv = { version = "0.8.10", features = ["little_endian"] } compact_str = "0.9.0" -quick-xml = "0.40" +quick-xml = "0.41" jmap-tools = { version = "0.1" } dns-update = { version = "0.5", features = ["test_provider"] } x509-parser = "0.18" diff --git a/tests/src/smtp/inbound/dkim2.rs b/tests/src/smtp/inbound/dkim2.rs new file mode 100644 index 00000000..74d2a32c --- /dev/null +++ b/tests/src/smtp/inbound/dkim2.rs @@ -0,0 +1,633 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + smtp::{inbound::TestMessage, session::TestSession}, + utils::{account::Account, dns::DnsCache, server::TestServer, server::TestServerBuilder}, +}; +use base64::{Engine, engine::general_purpose::STANDARD}; +use mail_auth::{ + DnssecStatus, MX, + common::{crypto::Ed25519Key, parse::TxtRecordParser, verify::DomainKey}, + dkim2::{Dkim2Signer, Hop}, +}; +use registry::schema::{ + enums::{DkimCanonicalization, DkimRotationStage}, + structs::{ + CertificateManagement, Dkim1Signature, Dkim2Signature, DkimManagement, DkimSignature, + DnsManagement, Domain, DsnReportSettings, Expression, SecretText, SecretTextValue, + SenderAuth, + }, +}; +use std::time::{Duration, Instant}; +use types::id::Id; + +const ED_PRIVATE: &str = concat!( + "-----BEGIN PRIVATE KEY-----\n", + "MC4CAQAwBQYDK2VwBCIEIIOQVf8MDGvvmIkpUbgoqtyUIxjlzRqaBR6aP12tcGGE\n", + "-----END PRIVATE KEY-----\n" +); +const ED_PUBLIC: &str = "hwjviTXyzUXSCWayBqE17s/4NSynQKxw58jayHudRAI="; + +#[tokio::test] +#[serial_test::serial] +async fn dkim2_all_disclosed() { + let (mut local, remote) = build_signer_and_verifier(19040, 19041, false).await; + + let delivered = deliver_and_collect( + &mut local, + &remote, + "john@example.com", + &["alice@foobar.org", "bob@foobar.org"], + &message("To: Alice , Bob \r\n"), + 1, + ) + .await; + + assert_eq!(delivered.len(), 1); + let msg = &delivered[0]; + assert_eq!(msg.recipients, vec!["alice@foobar.org", "bob@foobar.org"]); + assert!( + msg.body.contains("DKIM2-Signature"), + "missing DKIM2 signature: {}", + msg.body + ); + assert!( + without_whitespace(&msg.body).contains("dkim2=pass"), + "verifier did not report dkim2=pass: {}", + msg.body + ); + + // Both disclosed recipients belong to the same shared signature + let stripped = without_whitespace(&msg.body); + assert!(stripped.contains(&rt_token("alice@foobar.org"))); + assert!(stripped.contains(&rt_token("bob@foobar.org"))); +} + +#[tokio::test] +#[serial_test::serial] +async fn dkim2_mixed_recipients_do_not_leak_bcc() { + let (mut local, remote) = build_signer_and_verifier(19042, 19043, false).await; + + let delivered = deliver_and_collect( + &mut local, + &remote, + "john@example.com", + &[ + "alice@foobar.org", + "bob@foobar.org", + "eve@foobar.org", + "mallory@foobar.org", + ], + &message("To: Alice \r\nCc: Bob \r\n"), + 3, + ) + .await; + + assert_eq!(delivered.len(), 3); + + let eve = rt_token("eve@foobar.org"); + let mallory = rt_token("mallory@foobar.org"); + + for msg in &delivered { + let stripped = without_whitespace(&msg.body); + assert!( + stripped.contains("dkim2=pass"), + "verifier did not report dkim2=pass for {:?}: {}", + msg.recipients, + msg.body + ); + + if msg.recipients == vec!["alice@foobar.org", "bob@foobar.org"] { + // Disclosed copy: the two Bcc recipients must not appear anywhere + assert!( + !stripped.contains(&eve) && !stripped.contains(&mallory), + "Bcc recipient leaked into the disclosed signature: {}", + msg.body + ); + assert!(!msg.body.contains("eve@foobar.org")); + assert!(!msg.body.contains("mallory@foobar.org")); + assert!(stripped.contains(&rt_token("alice@foobar.org"))); + assert!(stripped.contains(&rt_token("bob@foobar.org"))); + } else if msg.recipients == vec!["eve@foobar.org"] { + // Bcc copy: only Eve's address is in this signature + assert!(stripped.contains(&eve)); + assert!(!stripped.contains(&mallory)); + assert!(!msg.body.contains("mallory@foobar.org")); + } else if msg.recipients == vec!["mallory@foobar.org"] { + assert!(stripped.contains(&mallory)); + assert!(!stripped.contains(&eve)); + assert!(!msg.body.contains("eve@foobar.org")); + } else { + panic!("unexpected recipient grouping: {:?}", msg.recipients); + } + } +} + +#[tokio::test] +#[serial_test::serial] +async fn dkim2_all_undisclosed_do_not_leak() { + let (mut local, remote) = build_signer_and_verifier(19044, 19045, false).await; + + let rcpts = ["carol@foobar.org", "dave@foobar.org", "frank@foobar.org"]; + let delivered = deliver_and_collect( + &mut local, + &remote, + "john@example.com", + &rcpts, + &message("To: undisclosed-recipients:;\r\n"), + 3, + ) + .await; + + assert_eq!(delivered.len(), 3); + + for msg in &delivered { + assert_eq!(msg.recipients.len(), 1, "expected one recipient per copy"); + let own = msg.recipients[0].as_str(); + let stripped = without_whitespace(&msg.body); + assert!( + stripped.contains("dkim2=pass"), + "verifier did not report dkim2=pass for {own}: {}", + msg.body + ); + assert!(stripped.contains(&rt_token(own))); + + // No other recipient may appear in this copy + for other in rcpts.iter().filter(|r| **r != own) { + assert!( + !stripped.contains(&rt_token(other)), + "recipient {other} leaked into the copy for {own}: {}", + msg.body + ); + assert!( + !msg.body.contains(other), + "recipient {other} leaked into the copy for {own}: {}", + msg.body + ); + } + } +} + +#[tokio::test] +#[serial_test::serial] +async fn dkim1_and_dkim2_signed_together() { + let (mut local, remote) = build_signer_and_verifier(19046, 19047, true).await; + + let delivered = deliver_and_collect( + &mut local, + &remote, + "john@example.com", + &["alice@foobar.org", "bob@foobar.org"], + &message("To: Alice , Bob \r\n"), + 1, + ) + .await; + + assert_eq!(delivered.len(), 1); + let stripped = without_whitespace(&delivered[0].body); + assert!( + delivered[0].body.contains("DKIM-Signature"), + "missing DKIM1 signature: {}", + delivered[0].body + ); + assert!( + delivered[0].body.contains("DKIM2-Signature"), + "missing DKIM2 signature: {}", + delivered[0].body + ); + assert!( + stripped.contains("dkim=pass"), + "DKIM1 did not pass: {}", + delivered[0].body + ); + assert!( + stripped.contains("dkim2=pass"), + "DKIM2 did not pass: {}", + delivered[0].body + ); +} + +#[tokio::test] +#[serial_test::serial] +async fn dkim2_dsn_is_signed() { + let mut local = TestServerBuilder::new("dkim2_dsn_signer") + .await + .with_http_listener(19048) + .await + .disable_services() + .capture_queue() + .build() + .await; + + let admin = local.account("admin"); + admin.mta_allow_relaying().await; + admin.mta_no_auth().await; + admin.mta_all_extensions().await; + admin.mta_disable_spam_filter().await; + admin.mta_add_all_headers().await; + let domain_id = admin.create_signing_domain(false).await; + admin + .registry_create_object(DsnReportSettings { + dkim_sign_domain: expr("'example.com'"), + ..Default::default() + }) + .await; + let _ = domain_id; + admin.reload_settings().await; + local.reload_core(); + local.expect_reload_settings().await; + + // Deliver to a domain with no DNS records: the lookup fails permanently and + // a DSN is generated for the sender. + let mut session = local.new_mta_session(); + session.data.remote_ip_str = "10.0.0.1".into(); + session.eval_session_params().await; + session.ehlo("mx.example.com").await; + session + .send_message( + "john@example.com", + &["bob@does-not-resolve.invalid"], + &message("To: Bob \r\n"), + "250", + ) + .await; + + local + .expect_message_then_deliver() + .await + .try_deliver(local.server.clone()); + + let dsn = local.expect_message().await; + assert!( + dsn.message.return_path.is_empty(), + "expected a DSN (null return path)" + ); + let body = dsn.read_message(&local).await; + assert!( + body.contains("Content-Type: multipart/report"), + "not a DSN: {body}" + ); + assert!( + body.contains("DKIM2-Signature"), + "the generated DSN was not DKIM2 signed: {body}" + ); +} + +#[tokio::test] +#[serial_test::serial] +async fn dkim2_inbound_dsn_validation() { + let mut server = TestServerBuilder::new("dkim2_dsn_receiver") + .await + .with_http_listener(19049) + .await + .disable_services() + .capture_queue() + .build() + .await; + + let admin = server.account("admin"); + admin.mta_allow_relaying().await; + admin.mta_no_auth().await; + admin.mta_all_extensions().await; + admin.mta_disable_spam_filter().await; + admin.mta_add_all_headers().await; + admin.configure_sender_auth("''").await; + admin.reload_settings().await; + server.reload_core(); + server.expect_reload_settings().await; + + // The returned message is signed by us (example.com); the DSN is signed by + // the bouncing domain (foobar.org). Publish both keys. + server + .server + .txt_add("ed._domainkey.example.com", dkim_dns_record(), valid()); + server + .server + .txt_add("ed._domainkey.foobar.org", dkim_dns_record(), valid()); + + let returned = dkim2_sign( + "example.com", + "ed", + RETURNED_PLAIN.as_bytes(), + "john@example.com", + &["bob@foobar.org"], + ); + + // A well-formed, aligned DSN is accepted + let dsn_ok = build_dsn(&returned, true); + let mut session = server.new_mta_session(); + session.data.remote_ip_str = "10.0.0.1".into(); + session.eval_session_params().await; + session.ehlo("mx.foobar.org").await; + session + .send_message( + "<>", + &["john@example.com"], + &String::from_utf8(dsn_ok).unwrap(), + "250", + ) + .await; + + // Tampering the returned message body breaks its signature chain, so the DSN + // is rejected. + let returned_tampered = String::from_utf8(returned) + .unwrap() + .replace("DKIM2-ORIGINAL-BODY-CONTENT", "DKIM2-TAMPERED-BODY-CONTENT") + .into_bytes(); + let dsn_bad = build_dsn(&returned_tampered, true); + session + .send_message( + "<>", + &["john@example.com"], + &String::from_utf8(dsn_bad).unwrap(), + "550", + ) + .await; +} + +impl Account { + async fn create_signing_domain(&self, with_dkim1: bool) -> Id { + let domain_id = self + .registry_create_object(Domain { + name: "example.com".into(), + certificate_management: CertificateManagement::Manual, + dns_management: DnsManagement::Manual, + dkim_management: DkimManagement::Manual, + allow_relaying: true, + ..Default::default() + }) + .await; + + self.registry_create_object(DkimSignature::Dkim2Ed25519Sha256(Dkim2Signature { + stage: DkimRotationStage::Active, + selector: "ed2".to_string(), + domain_id, + private_key: SecretText::Text(SecretTextValue { + secret: ED_PRIVATE.to_string(), + }), + ..Default::default() + })) + .await; + + if with_dkim1 { + self.registry_create_object(DkimSignature::Dkim1Ed25519Sha256(Dkim1Signature { + stage: DkimRotationStage::Active, + selector: "ed1".to_string(), + canonicalization: DkimCanonicalization::RelaxedRelaxed, + domain_id, + private_key: SecretText::Text(SecretTextValue { + secret: ED_PRIVATE.to_string(), + }), + ..Default::default() + })) + .await; + } + + domain_id + } + + async fn configure_sender_auth(&self, dkim_sign_domain: &str) { + self.registry_create_object(SenderAuth { + dmarc_verify: expr("relaxed"), + reverse_ip_verify: expr("relaxed"), + spf_ehlo_verify: expr("relaxed"), + spf_from_verify: expr("relaxed"), + arc_verify: expr("relaxed"), + dkim_sign_domain: expr(dkim_sign_domain), + dkim_verify: expr("relaxed"), + dkim_strict: false, + }) + .await; + } +} + +async fn build_signer_and_verifier( + http_local: u16, + http_remote: u16, + with_dkim1: bool, +) -> (TestServer, TestServer) { + let mut local = TestServerBuilder::new("dkim2_signer") + .await + .with_http_listener(http_local) + .await + .disable_services() + .capture_queue() + .build() + .await; + let mut remote = TestServerBuilder::new("dkim2_verifier") + .await + .with_http_listener(http_remote) + .await + .with_smtp_listener(9925) + .await + .disable_services() + .capture_queue() + .build() + .await; + + // Signer (originating MTA) + let admin = local.account("admin"); + admin.mta_allow_relaying().await; + admin.mta_no_auth().await; + admin.mta_all_extensions().await; + admin.mta_disable_spam_filter().await; + admin.mta_add_all_headers().await; + admin.create_signing_domain(with_dkim1).await; + admin.configure_sender_auth("'example.com'").await; + admin.reload_settings().await; + local.reload_core(); + local.expect_reload_settings().await; + + // Verifier (receiving MTA) + let remote_admin = remote.account("admin"); + remote_admin.mta_allow_relaying().await; + remote_admin.mta_no_auth().await; + remote_admin.mta_all_extensions().await; + remote_admin.mta_disable_spam_filter().await; + remote_admin.mta_add_all_headers().await; + remote_admin.configure_sender_auth("''").await; + remote_admin.reload_settings().await; + remote.reload_core(); + remote.expect_reload_settings().await; + + // Publish the signer's public keys in the verifier's DNS + remote + .server + .txt_add("ed2._domainkey.example.com", dkim_dns_record(), valid()); + if with_dkim1 { + remote + .server + .txt_add("ed1._domainkey.example.com", dkim_dns_record(), valid()); + } + + // Route foobar.org deliveries back to the local (in-process) receiver + local.server.mx_add( + "foobar.org", + vec![MX { + exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), + preference: 10, + }], + DnssecStatus::Secure, + valid(), + ); + local + .server + .ipv4_add("mx.foobar.org", vec!["127.0.0.1".parse().unwrap()], valid()); + + (local, remote) +} + +fn message(to_header: &str) -> String { + format!( + concat!( + "From: John Doe \r\n", + "{}", + "Subject: DKIM2 privacy test\r\n", + "\r\n", + "This is a DKIM2 test message.\r\n", + ), + to_header + ) +} + +fn build_dsn(returned: &[u8], sign_dsn: bool) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: text/plain\r\n\r\n"); + body.extend_from_slice(b"Delivery to bob@foobar.org failed.\r\n"); + body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: message/delivery-status\r\n\r\n"); + body.extend_from_slice(b"Reporting-MTA: dns; foobar.org\r\n\r\n"); + body.extend_from_slice(b"Final-Recipient: rfc822; bob@foobar.org\r\n"); + body.extend_from_slice(b"Action: failed\r\nStatus: 5.1.1\r\n"); + body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: message/rfc822\r\n\r\n"); + body.extend_from_slice(returned); + body.extend_from_slice(b"\r\n--BOUNDARY--\r\n"); + + let mut dsn = Vec::new(); + dsn.extend_from_slice(b"From: postmaster@foobar.org\r\n"); + dsn.extend_from_slice(b"To: john@example.com\r\n"); + dsn.extend_from_slice(b"Subject: Delivery Status Notification (Failure)\r\n"); + dsn.extend_from_slice(b"Date: Sat, 01 Mar 2026 12:05:00 +0000\r\n"); + dsn.extend_from_slice(b"Message-ID: \r\n"); + dsn.extend_from_slice( + b"Content-Type: multipart/report; report-type=delivery-status; boundary=\"BOUNDARY\"\r\n\r\n", + ); + dsn.extend_from_slice(&body); + + if sign_dsn { + dkim2_sign("foobar.org", "ed", &dsn, "<>", &["john@example.com"]) + } else { + dsn + } +} + +fn ed25519_key() -> Ed25519Key { + let der = STANDARD + .decode("MC4CAQAwBQYDK2VwBCIEIIOQVf8MDGvvmIkpUbgoqtyUIxjlzRqaBR6aP12tcGGE") + .unwrap(); + Ed25519Key::from_pkcs8_maybe_unchecked_der(&der).unwrap() +} + +fn dkim2_sign( + domain: &str, + selector: &str, + message: &[u8], + mail_from: &str, + rcpt_to: &[&str], +) -> Vec { + let signed = Dkim2Signer::from_key(ed25519_key()) + .domain(domain) + .selector(selector) + .sign(message, Hop::real(mail_from, rcpt_to)) + .expect("dkim2 sign"); + let mut out = signed.to_header().into_bytes(); + out.extend_from_slice(message); + out +} + +const RETURNED_PLAIN: &str = concat!( + "From: John Doe \r\n", + "To: Bob \r\n", + "Subject: Original message\r\n", + "Date: Sat, 01 Mar 2026 12:00:00 +0000\r\n", + "Message-ID: \r\n", + "\r\n", + "DKIM2-ORIGINAL-BODY-CONTENT\r\n", +); + +struct Delivered { + recipients: Vec, + body: String, +} + +async fn deliver_and_collect( + local: &mut TestServer, + remote: &TestServer, + from: &str, + rcpts: &[&str], + raw: &str, + expected: usize, +) -> Vec { + let mut session = local.new_mta_session(); + session.data.remote_ip_str = "10.0.0.1".into(); + session.eval_session_params().await; + session.ehlo("mx.example.com").await; + session.send_message(from, rcpts, raw, "250").await; + + local + .expect_message_then_deliver() + .await + .try_deliver(local.server.clone()); + + let mut delivered = Vec::new(); + for _ in 0..expected { + let mut waited = 0; + let msg = loop { + if let Some(msg) = remote.read_queued_messages().await.into_iter().next() { + break msg; + } + assert!(waited < 100, "timed out waiting for a delivered message"); + tokio::time::sleep(Duration::from_millis(50)).await; + waited += 1; + }; + let body = msg.read_message(remote).await; + let mut recipients = msg + .message + .recipients + .iter() + .map(|r| r.address().to_string()) + .collect::>(); + recipients.sort(); + let due = remote.message_due(msg.queue_id).await; + msg.clone().remove(&remote.server, due.into()).await; + delivered.push(Delivered { recipients, body }); + } + + delivered +} + +fn rt_token(address: &str) -> String { + STANDARD.encode(format!("<{address}>")) +} + +fn without_whitespace(value: &str) -> String { + value.chars().filter(|c| !c.is_whitespace()).collect() +} + +fn dkim_dns_record() -> DomainKey { + DomainKey::parse(format!("v=DKIM1; k=ed25519; p={ED_PUBLIC}").as_bytes()).unwrap() +} + +fn valid() -> Instant { + Instant::now() + Duration::from_secs(300) +} + +fn expr(value: &str) -> Expression { + Expression { + else_: value.into(), + ..Default::default() + } +} diff --git a/tests/src/smtp/inbound/mod.rs b/tests/src/smtp/inbound/mod.rs index 8f99c5e3..96df5855 100644 --- a/tests/src/smtp/inbound/mod.rs +++ b/tests/src/smtp/inbound/mod.rs @@ -24,6 +24,7 @@ pub mod asn; pub mod auth; pub mod basic; pub mod data; +pub mod dkim2; pub mod dmarc; pub mod ehlo; pub mod limits;