From af19ac6465bc3f76b61deef15fcdf0a97eaa0904 Mon Sep 17 00:00:00 2001 From: aszlig Date: Wed, 21 May 2025 19:03:55 +0200 Subject: [PATCH] Allow to switch to operating system CA root store So far, we only used the root CA certificates from the "webpki-roots" crate, which makes it very difficult if you want to run a custom CA. In my case I'm running automated tests of full production systems, which also includes an ACME setup that injects a Pebble instance into the system's CA certificates. Since Stalwart doesn't use the system's root CA certificate store, it's very difficult to inject the certificate for the Pebble instance. Given that there's also some interest (issue #247) for doing this in an enterprise environment with intranet CAs, I decided to generalise this far enough that it can not only be used in my downstream setup. Currently, this doesn't fully address the issue, since in the long term this might be something we'd want to configure at runtime, as per @mdecimus's comment[1]: > A slightly more complex approach is required that allows the user to > select at runtime which CA store to use. However, this makes it at least easier to switch to native root CA store by simply recompiling with the "tls-native-roots" feature. [1]: https://github.com/stalwartlabs/stalwart/issues/247#issuecomment-2437039500 Signed-off-by: aszlig Issue: https://github.com/stalwartlabs/stalwart/issues/247 --- Cargo.lock | 237 +++++++++++++++++-- crates/cli/Cargo.toml | 8 +- crates/cli/src/main.rs | 8 +- crates/common/Cargo.toml | 2 +- crates/common/src/enterprise/llm.rs | 2 +- crates/common/src/enterprise/mod.rs | 4 +- crates/common/src/lib.rs | 3 +- crates/common/src/listener/acme/directory.rs | 6 +- crates/common/src/manager/mod.rs | 2 +- crates/common/src/scripts/plugins/http.rs | 2 +- crates/common/src/telemetry/webhooks/mod.rs | 2 +- crates/directory/Cargo.toml | 4 +- crates/directory/src/backend/oidc/lookup.rs | 4 +- crates/jmap/Cargo.toml | 2 +- crates/main/Cargo.toml | 13 +- crates/smtp/Cargo.toml | 8 +- crates/smtp/src/inbound/hooks/client.rs | 2 +- crates/smtp/src/outbound/mta_sts/lookup.rs | 3 +- crates/smtp/src/reporting/tls.rs | 5 +- crates/spam-filter/Cargo.toml | 4 +- crates/spam-filter/src/analysis/url.rs | 2 +- crates/store/Cargo.toml | 2 +- crates/store/src/backend/azure/mod.rs | 2 +- crates/store/src/backend/http/lookup.rs | 2 +- crates/trc/Cargo.toml | 4 +- crates/utils/Cargo.toml | 2 + crates/utils/src/lib.rs | 41 +++- crates/utils/src/suffixlist.rs | 7 +- tests/Cargo.toml | 2 +- tests/src/jmap/auth_oauth.rs | 6 +- tests/src/jmap/mod.rs | 4 +- tests/src/smtp/management/queue.rs | 2 +- 32 files changed, 309 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21c7f726..67fe5739 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,7 +410,7 @@ checksum = "412b79ce053cef36eda52c25664b45ec92a21769488e20d5a8bf0b3c9e1a28cb" dependencies = [ "http 1.2.0", "log", - "rustls 0.23.21", + "rustls 0.23.27", "serde", "serde_json", "url", @@ -1035,6 +1035,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1250,7 +1256,7 @@ dependencies = [ "reqwest 0.12.12", "ring 0.17.8", "rsa", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -1347,6 +1353,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1819,7 +1835,7 @@ dependencies = [ "pwhash", "regex", "reqwest 0.12.12", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pki-types", "scrypt", "serde", @@ -3056,7 +3072,8 @@ dependencies = [ "http 1.2.0", "hyper 1.6.0", "hyper-util", - "rustls 0.23.21", + "rustls 0.23.27", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.1", @@ -3296,7 +3313,7 @@ dependencies = [ "nlp", "parking_lot", "rand 0.9.0", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "store", "tokio", @@ -3620,6 +3637,28 @@ dependencies = [ "utils", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.32" @@ -3978,7 +4017,7 @@ dependencies = [ "base64 0.22.1", "gethostname", "md5", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pki-types", "smtp-proto", "tokio", @@ -4022,7 +4061,7 @@ dependencies = [ "mail-send", "md5", "parking_lot", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "sieve-rs", "store", @@ -4841,7 +4880,7 @@ dependencies = [ "jmap_proto", "mail-parser", "mail-send", - "rustls 0.23.21", + "rustls 0.23.27", "store", "tokio", "tokio-rustls 0.26.1", @@ -5152,7 +5191,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.0", - "rustls 0.23.21", + "rustls 0.23.27", "socket2", "thiserror 2.0.11", "tokio", @@ -5170,7 +5209,7 @@ dependencies = [ "rand 0.8.5", "ring 0.17.8", "rustc-hash 2.1.0", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pki-types", "slab", "thiserror 2.0.11", @@ -5452,7 +5491,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand 0.8.5", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-native-certs 0.7.3", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -5595,7 +5634,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.21", + "rustls 0.23.27", + "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -5960,14 +6000,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -5981,7 +6021,7 @@ dependencies = [ "openssl-probe", "rustls-pemfile 1.0.4", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] @@ -5994,7 +6034,19 @@ dependencies = [ "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.2.0", ] [[package]] @@ -6024,6 +6076,33 @@ dependencies = [ "web-time", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.27", + "rustls-native-certs 0.8.1", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.3", + "security-framework 3.2.0", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -6045,6 +6124,17 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -6169,7 +6259,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -6546,7 +6649,7 @@ dependencies = [ "rayon", "regex", "reqwest 0.12.12", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -6684,6 +6787,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "utils", ] [[package]] @@ -6730,7 +6834,7 @@ dependencies = [ "rocksdb", "rusqlite", "rust-s3", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pki-types", "serde", "serde_json", @@ -6861,7 +6965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6938,7 +7042,7 @@ dependencies = [ "rayon", "reqwest 0.12.12", "ring 0.17.8", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -7144,7 +7248,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.21", + "rustls 0.23.27", "tokio", ] @@ -7610,9 +7714,10 @@ dependencies = [ "regex", "reqwest 0.12.12", "ring 0.17.8", - "rustls 0.23.21", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "smtp-proto", @@ -7817,6 +7922,24 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -7939,6 +8062,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -7966,6 +8098,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -7997,6 +8144,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8009,6 +8162,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8021,6 +8180,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8039,6 +8204,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8051,6 +8222,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8063,6 +8240,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8075,6 +8258,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 878def77..d4239cdb 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,8 +11,8 @@ readme = "README.md" resolver = "2" [dependencies] -jmap-client = { version = "0.3", features = ["async"] } -mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } +jmap-client = { version = "0.3", features = ["async"] } +mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]} tokio = { version = "1.23", features = ["full"] } num_cpus = "1.13.1" @@ -30,3 +30,7 @@ futures = "0.3.28" pwhash = "1.0.0" rand = "0.9.0" mail-auth = { version = "0.6" } +utils.path = "../utils" + +[features] +tls-native-roots = ["utils/tls-native-roots"] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 1ce96eda..da52b2ba 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -85,7 +85,7 @@ fn parse_credentials(credentials: &str) -> Credentials { async fn oauth(url: &str) -> Credentials { let metadata: HashMap = serde_json::from_slice( - &reqwest::Client::builder() + &utils::reqwest_client_builder() .danger_accept_invalid_certs(is_localhost(url)) .build() .unwrap_or_default() @@ -103,7 +103,7 @@ async fn oauth(url: &str) -> Credentials { let mut params: HashMap = HashMap::from_iter([("client_id".to_string(), "Stalwart_CLI".to_string())]); let response: HashMap = serde_json::from_slice( - &reqwest::Client::builder() + &utils::reqwest_client_builder() .danger_accept_invalid_certs(is_localhost(url)) .build() .unwrap_or_default() @@ -137,7 +137,7 @@ async fn oauth(url: &str) -> Credentials { std::io::stdin().lock().lines().next(); let mut response: HashMap = serde_json::from_slice( - &reqwest::Client::builder() + &utils::reqwest_client_builder() .danger_accept_invalid_certs(is_localhost(url)) .build() .unwrap_or_default() @@ -229,7 +229,7 @@ impl Client { }, url ); - let mut request = reqwest::Client::builder() + let mut request = utils::reqwest_client_builder() .danger_accept_invalid_certs(is_localhost(&url)) .timeout(Duration::from_secs(self.timeout.unwrap_or(60))) .build() diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 7c53a1db..edf72dfa 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -32,7 +32,7 @@ tokio = { version = "1.23", features = ["net", "macros"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } futures = "0.3" rcgen = "0.12" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]} +reqwest = { version = "0.12", default-features = false, features = ["http2", "stream"]} serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" base64 = "0.22" diff --git a/crates/common/src/enterprise/llm.rs b/crates/common/src/enterprise/llm.rs index bc370b4b..b54cae27 100644 --- a/crates/common/src/enterprise/llm.rs +++ b/crates/common/src/enterprise/llm.rs @@ -125,7 +125,7 @@ impl AiApiConfig { }; // Send request - let response = reqwest::Client::builder() + let response = utils::reqwest_client_builder() .timeout(self.timeout) .danger_accept_invalid_certs(self.tls_allow_invalid_certs) .build() diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index 5eec0f98..7718bbd8 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -193,7 +193,9 @@ impl Server { let mut logo = None; if let Some(logo_url) = logo_url { - let response = reqwest::get(&logo_url).await.map_err(|err| { + let response = utils::reqwest_client_builder() + .build() + .unwrap_or_default().get(&logo_url).await.map_err(|err| { trc::ResourceEvent::DownloadExternal .into_err() .details("Failed to download logo") diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index c7ffdf0a..0fee0564 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -47,6 +47,8 @@ use utils::{ snowflake::SnowflakeIdGenerator, }; +pub use utils::USER_AGENT; + pub mod addresses; pub mod auth; pub mod config; @@ -63,7 +65,6 @@ pub mod telemetry; pub use psl; -pub static USER_AGENT: &str = concat!("Stalwart/", env!("CARGO_PKG_VERSION"),); pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG_VERSION"),); pub const IPC_CHANNEL_BUFFER: usize = 1024; diff --git a/crates/common/src/listener/acme/directory.rs b/crates/common/src/listener/acme/directory.rs index 6eedc5a9..7fc3c20f 100644 --- a/crates/common/src/listener/acme/directory.rs +++ b/crates/common/src/listener/acme/directory.rs @@ -4,7 +4,6 @@ use std::time::Duration; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use hyper::header::USER_AGENT; use rcgen::{Certificate, CustomExtension, PKCS_ECDSA_P256_SHA256}; use reqwest::header::CONTENT_TYPE; use reqwest::{Method, Response}; @@ -315,7 +314,7 @@ async fn https( body: Option, ) -> trc::Result { let url = url.as_ref(); - let mut builder = reqwest::Client::builder() + let mut builder = utils::reqwest_client_builder() .timeout(Duration::from_secs(30)) .http1_only(); @@ -329,8 +328,7 @@ async fn https( let mut request = builder .build() .map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_http_error(err))? - .request(method, url) - .header(USER_AGENT, crate::USER_AGENT); + .request(method, url); if let Some(body) = body { request = request diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs index 51861a82..a59e74b5 100644 --- a/crates/common/src/manager/mod.rs +++ b/crates/common/src/manager/mod.rs @@ -76,7 +76,7 @@ pub async fn fetch_resource( .await .map_err(|err| format!("Failed to read {path}: {err}")) } else { - let response = reqwest::Client::builder() + let response = utils::reqwest_client_builder() .timeout(timeout) .danger_accept_invalid_certs(is_localhost_url(url)) .user_agent(USER_AGENT) diff --git a/crates/common/src/scripts/plugins/http.rs b/crates/common/src/scripts/plugins/http.rs index 6cba1fe6..8cc3250f 100644 --- a/crates/common/src/scripts/plugins/http.rs +++ b/crates/common/src/scripts/plugins/http.rs @@ -26,7 +26,7 @@ pub async fn exec_header(ctx: PluginContext<'_>) -> trc::Result { return Ok(Variable::from(url.split_once("/?").unwrap().1.to_string())); } - reqwest::Client::builder() + utils::reqwest_client_builder() .user_agent(agent.as_ref()) .timeout(Duration::from_millis(timeout)) .redirect(Policy::none()) diff --git a/crates/common/src/telemetry/webhooks/mod.rs b/crates/common/src/telemetry/webhooks/mod.rs index 6207fd6c..994afae5 100644 --- a/crates/common/src/telemetry/webhooks/mod.rs +++ b/crates/common/src/telemetry/webhooks/mod.rs @@ -150,7 +150,7 @@ async fn post_webhook_events( } // Send request - let response = reqwest::Client::builder() + let response = utils::reqwest_client_builder() .timeout(settings.timeout) .danger_accept_invalid_certs(settings.tls_allow_invalid_certs) .build() diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 3bff0fc5..ad3d8a01 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -11,7 +11,7 @@ store = { path = "../store" } trc = { path = "../trc" } jmap_proto = { path = "../jmap-proto" } smtp-proto = { version = "0.1" } -mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } +mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } mail-builder = { version = "0.4" } tokio = { version = "1.23", features = ["net"] } @@ -34,7 +34,7 @@ futures = "0.3" regex = "1.7.0" serde = { version = "1.0", features = ["derive"]} totp-rs = { version = "5.5.1", features = ["otpauth"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] } +reqwest = { version = "0.12", default-features = false, features = ["http2"] } serde_json = "1.0" base64 = "0.22" diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index 49253dd3..5122ccab 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -36,10 +36,10 @@ impl OpenIdDirectory { QueryBy::Credentials(Credentials::OAuthBearer { token }) => { // Send request #[cfg(feature = "test_mode")] - let client = reqwest::Client::builder().danger_accept_invalid_certs(true); + let client = utils::reqwest_client_builder().danger_accept_invalid_certs(true); #[cfg(not(feature = "test_mode"))] - let client = reqwest::Client::builder(); + let client = utils::reqwest_client_builder(); let client = client .timeout(self.config.endpoint_timeout) diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 25643ac5..1383ae9f 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -38,7 +38,7 @@ p256 = { version = "0.13", features = ["ecdh"] } hkdf = "0.12.3" sha1 = "0.10" sha2 = "0.10" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]} +reqwest = { version = "0.12", default-features = false, features = ["http2"]} tokio-tungstenite = "0.26" tungstenite = "0.26" chrono = "0.4" diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index a5a6160c..ec737cce 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -45,10 +45,11 @@ elastic = ["store/elastic"] s3 = ["store/s3"] redis = ["store/redis"] azure = ["store/azure"] -enterprise = [ "jmap/enterprise", - "smtp/enterprise", - "common/enterprise", - "store/enterprise", - "managesieve/enterprise", - "directory/enterprise", +enterprise = [ "jmap/enterprise", + "smtp/enterprise", + "common/enterprise", + "store/enterprise", + "managesieve/enterprise", + "directory/enterprise", "spam-filter/enterprise" ] +tls-native-roots = ["utils/tls-native-roots"] diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index e7232a5b..5618fcef 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -22,10 +22,10 @@ spam-filter = { path = "../spam-filter" } trc = { path = "../trc" } mail-auth = { version = "0.6" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } -mail-parser = { version = "0.10", features = ["full_encoding"] } -mail-builder = { version = "0.4" } +mail-parser = { version = "0.10", features = ["full_encoding"] } +mail-builder = { version = "0.4" } smtp-proto = { version = "0.1", features = ["serde_support"] } -sieve-rs = { version = "0.6" } +sieve-rs = { version = "0.6" } ahash = { version = "0.8" } rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] } rustls-pemfile = "2.0" @@ -47,7 +47,7 @@ blake3 = "1.3" lru-cache = "0.1.2" rand = "0.9.0" x509-parser = "0.16.0" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] } +reqwest = { version = "0.12", default-features = false, features = ["http2"] } serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" num_cpus = "1.15.0" diff --git a/crates/smtp/src/inbound/hooks/client.rs b/crates/smtp/src/inbound/hooks/client.rs index 2e3a9e17..6bfb40a6 100644 --- a/crates/smtp/src/inbound/hooks/client.rs +++ b/crates/smtp/src/inbound/hooks/client.rs @@ -13,7 +13,7 @@ pub(super) async fn send_mta_hook_request( mta_hook: &MTAHook, request: Request, ) -> Result { - let response = reqwest::Client::builder() + let response = utils::reqwest_client_builder() .timeout(mta_hook.timeout) .danger_accept_invalid_certs(mta_hook.tls_allow_invalid_certs) .build() diff --git a/crates/smtp/src/outbound/mta_sts/lookup.rs b/crates/smtp/src/outbound/mta_sts/lookup.rs index 03c5d251..45aff403 100644 --- a/crates/smtp/src/outbound/mta_sts/lookup.rs +++ b/crates/smtp/src/outbound/mta_sts/lookup.rs @@ -67,8 +67,7 @@ impl MtaStsLookup for Server { // Fetch policy #[cfg(not(feature = "test_mode"))] - let bytes = reqwest::Client::builder() - .user_agent(common::USER_AGENT) + let bytes = utils::reqwest_client_builder() .timeout(timeout) .redirect(reqwest::redirect::Policy::none()) .build()? diff --git a/crates/smtp/src/reporting/tls.rs b/crates/smtp/src/reporting/tls.rs index 1da29ea9..3b504b13 100644 --- a/crates/smtp/src/reporting/tls.rs +++ b/crates/smtp/src/reporting/tls.rs @@ -13,7 +13,7 @@ use common::{ resolver::{Mode, MxPattern}, }, ipc::{TlsEvent, ToHash}, - Server, USER_AGENT, + Server, }; use mail_auth::{ flate2::{write::GzEncoder, Compression}, @@ -145,8 +145,7 @@ impl TlsReporting for Server { for uri in &rua { match uri { ReportUri::Http(uri) => { - if let Ok(client) = reqwest::Client::builder() - .user_agent(USER_AGENT) + if let Ok(client) = utils::reqwest_client_builder() .timeout(Duration::from_secs(2 * 60)) .build() { diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index 4d0e0fa8..aefd5d0f 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -11,7 +11,7 @@ store = { path = "../store" } trc = { path = "../trc" } common = { path = "../common" } smtp-proto = { version = "0.1", features = ["serde_support"] } -mail-parser = { version = "0.10", features = ["full_encoding"] } +mail-parser = { version = "0.10", features = ["full_encoding"] } mail-builder = { version = "0.4" } mail-auth = { version = "0.6" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } @@ -19,7 +19,7 @@ tokio = { version = "1.23", features = ["net", "macros"] } psl = "2" hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } idna = "1.0" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]} +reqwest = { version = "0.12", default-features = false, features = ["http2", "stream"]} decancer = "3.0.1" unicode-security = "0.1.0" infer = "0.16" diff --git a/crates/spam-filter/src/analysis/url.rs b/crates/spam-filter/src/analysis/url.rs index c3d5f828..51a93141 100644 --- a/crates/spam-filter/src/analysis/url.rs +++ b/crates/spam-filter/src/analysis/url.rs @@ -289,7 +289,7 @@ async fn http_get_header( Ok(None) }; } - reqwest::Client::builder() + utils::reqwest_client_builder() .user_agent("Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/118.0") .timeout(timeout) .redirect(Policy::none()) diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index d89d8da1..aa1aef33 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -15,7 +15,7 @@ rust-s3 = { version = "=0.35.0-alpha.2", default-features = false, features = [" azure_core = { version = "0.21.0", optional = true } azure_storage = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true } azure_storage_blobs = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]} +reqwest = { version = "0.12", default-features = false, features = ["http2", "stream"]} tokio = { version = "1.23", features = ["sync", "fs", "io-util"] } r2d2 = { version = "0.8.10", optional = true } futures = { version = "0.3", optional = true } diff --git a/crates/store/src/backend/azure/mod.rs b/crates/store/src/backend/azure/mod.rs index 3b6d5be4..e91d6275 100644 --- a/crates/store/src/backend/azure/mod.rs +++ b/crates/store/src/backend/azure/mod.rs @@ -63,7 +63,7 @@ impl AzureStore { let timeout = config .property_or_default::((&prefix, "timeout"), "30s") .unwrap_or_else(|| Duration::from_secs(30)); - let transport = match reqwest::Client::builder().timeout(timeout).build() { + let transport = match utils::reqwest_client_builder().timeout(timeout).build() { Ok(client) => Arc::new(client), Err(err) => { config.new_build_error( diff --git a/crates/store/src/backend/http/lookup.rs b/crates/store/src/backend/http/lookup.rs index bb5c6104..ce7c6149 100644 --- a/crates/store/src/backend/http/lookup.rs +++ b/crates/store/src/backend/http/lookup.rs @@ -90,7 +90,7 @@ impl HttpStore { async fn try_refresh(&self) -> trc::Result>> { let time = Instant::now(); let agent = BROWSER_USER_AGENTS.choose(&mut rand::rng()).unwrap(); - let response = reqwest::Client::builder() + let response = utils::reqwest_client_builder() .timeout(self.config.timeout) .user_agent(*agent) .build() diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index 13221373..bb7b6fd8 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -7,11 +7,11 @@ resolver = "2" [dependencies] event_macro = { path = "./event-macro" } mail-auth = { version = "0.6" } -mail-parser = { version = "0.10", features = ["full_encoding"] } +mail-parser = { version = "0.10", features = ["full_encoding"] } base64 = "0.22.1" serde = "1.0" serde_json = "1.0.120" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]} +reqwest = { version = "0.12", default-features = false, features = ["http2"]} bincode = "1.3.3" rtrb = "0.3.1" parking_lot = "0.12.3" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index b451e742..cbd981ec 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -23,6 +23,7 @@ ring = { version = "0.17" } base64 = "0.22" serde_json = "1.0" rcgen = "0.13" +rustls-platform-verifier = { version = "0.5.3", optional = true } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]} x509-parser = "0.16.0" pem = "3.0" @@ -40,6 +41,7 @@ privdrop = "0.5.3" [features] test_mode = [] +tls-native-roots = ["dep:rustls-platform-verifier", "reqwest/rustls-tls-native-roots"] [dev-dependencies] tokio = { version = "1.23", features = ["full"] } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index acec2f04..f3c667f3 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -18,10 +18,11 @@ use futures::StreamExt; use reqwest::Response; use rustls::{ client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - ClientConfig, RootCertStore, SignatureScheme, + ClientConfig, SignatureScheme, }; use rustls_pki_types::TrustAnchor; +pub static USER_AGENT: &str = "Stalwart/1.0.0"; pub const BLOB_HASH_LEN: usize = 32; #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] @@ -277,20 +278,28 @@ pub async fn wait_for_shutdown() { } pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig { + #[cfg(feature = "tls-native-roots")] + use rustls_platform_verifier::BuilderVerifierExt; + let config = ClientConfig::builder(); if !allow_invalid_certs { - let mut root_cert_store = RootCertStore::empty(); + #[cfg(feature = "tls-native-roots")] + let config = config.with_platform_verifier(); - root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| TrustAnchor { - subject: ta.subject.clone(), - subject_public_key_info: ta.subject_public_key_info.clone(), - name_constraints: ta.name_constraints.clone(), - })); + #[cfg(not(feature = "tls-native-roots"))] + let config = config.with_root_certificates( + webpki_roots::TLS_SERVER_ROOTS + .iter() + .map(|ta| rustls_pki_types::TrustAnchor { + subject: ta.subject.clone(), + subject_public_key_info: ta.subject_public_key_info.clone(), + name_constraints: ta.name_constraints.clone(), + }) + .collect::(), + ); - config - .with_root_certificates(root_cert_store) - .with_no_client_auth() + config.with_no_client_auth() } else { config .dangerous() @@ -299,6 +308,18 @@ pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig { } } +pub fn reqwest_client_builder() -> reqwest::ClientBuilder { + let builder = reqwest::Client::builder(); + + #[cfg(feature = "tls-native-roots")] + let builder = builder.tls_built_in_native_certs(true); + + #[cfg(not(feature = "tls-native-roots"))] + let builder = builder.tls_built_in_webpki_certs(true); + + builder.user_agent(USER_AGENT) +} + #[derive(Debug)] struct DummyVerifier; diff --git a/crates/utils/src/suffixlist.rs b/crates/utils/src/suffixlist.rs index e0921abe..b53126ae 100644 --- a/crates/utils/src/suffixlist.rs +++ b/crates/utils/src/suffixlist.rs @@ -111,7 +111,12 @@ impl PublicSuffix { for (idx, value) in values.into_iter().enumerate() { let bytes = if value.starts_with("https://") || value.starts_with("http://") { - let result = match reqwest::get(&value).await { + let result = match crate::reqwest_client_builder() + .build() + .unwrap_or_default() + .get(&value) + .await + { Ok(r) => { if r.status().is_success() { r.bytes().await diff --git a/tests/Cargo.toml b/tests/Cargo.toml index b054e41f..1d1762d8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -50,7 +50,7 @@ rayon = { version = "1.5.1" } flate2 = { version = "1.0.17", features = ["zlib"], default-features = false } serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "multipart", "http2"]} +reqwest = { version = "0.12", default-features = false, features = ["multipart", "http2"]} bytes = "1.4.0" futures = "0.3" ece = "2.2" diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index fb6f5fe8..c36654e6 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -406,7 +406,7 @@ async fn post_bytes( auth_token: Option<&str>, params: &AHashMap, ) -> Bytes { - let mut client = reqwest::Client::builder() + let mut client = utils::reqwest_client_builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() @@ -432,7 +432,7 @@ async fn post_json( auth_token: Option<&str>, body: &impl Serialize, ) -> D { - let mut client = reqwest::Client::builder() + let mut client = utils::reqwest_client_builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() @@ -468,7 +468,7 @@ async fn post_with_auth( } async fn get_bytes(url: &str) -> Bytes { - reqwest::Client::builder() + utils::reqwest_client_builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index f44f0781..fbf22556 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -700,7 +700,7 @@ pub async fn jmap_raw_request(body: impl AsRef, username: &str, secret: &st }"#; String::from_utf8( - reqwest::Client::builder() + utils::reqwest_client_builder() .danger_accept_invalid_certs(true) .timeout(Duration::from_millis(1000)) .default_headers(headers) @@ -901,7 +901,7 @@ impl ManagementApi { query: &str, body: Option, ) -> Result { - let mut request = reqwest::Client::builder() + let mut request = utils::reqwest_client_builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index 0a553b74..ed2a88d9 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -489,7 +489,7 @@ async fn manage_queue() { // Test authentication error assert_eq!( - reqwest::Client::builder() + utils::reqwest_client_builder() .timeout(Duration::from_millis(500)) .danger_accept_invalid_certs(true) .build() -- 2.49.0