Skip to main content

max / goingson

Unify TLS on a single rustls stack (OS-native trust) Move HTTP (reqwest), SMTP (lettre) and IMAP off native-tls onto one rustls stack that validates against the OS-native trust store via rustls-platform-verifier, with a single ring crypto provider installed at startup. Previously the binary linked two TLS stacks — native-tls for the app's own requests and rustls (via tauri-plugin-updater) for OTA checks. Both already chained to the OS root store, but carrying two stacks + two crypto providers was needless; the updater's path is now the app's path. - reqwest/lettre features native-tls -> rustls (rustls-no-provider / tokio1-rustls + rustls-platform-verifier + ring) - rewrite email/imap_client.rs from tokio-native-tls to tokio-rustls with the platform verifier - install ring::default_provider() in the desktop and mobile entry points - drop the android-only vendored OpenSSL (native-tls's only consumer) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 16:50 UTC
Commit: 29e1783bb13aa7be53ff0478e4ac789540d0a70e
Parent: de8610d
6 files changed, +47 insertions, -128 deletions
M Cargo.lock +9 -115
@@ -934,7 +934,7 @@ dependencies = [
934 934 "bitflags 2.13.1",
935 935 "core-foundation 0.10.1",
936 936 "core-graphics-types",
937 - "foreign-types 0.5.0",
937 + "foreign-types",
938 938 "libc",
939 939 ]
940 940
@@ -1731,21 +1731,12 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
1731 1731
1732 1732 [[package]]
1733 1733 name = "foreign-types"
1734 - version = "0.3.2"
1735 - source = "registry+https://github.com/rust-lang/crates.io-index"
1736 - checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
1737 - dependencies = [
1738 - "foreign-types-shared 0.1.1",
1739 - ]
1740 -
1741 - [[package]]
1742 - name = "foreign-types"
1743 1734 version = "0.5.0"
1744 1735 source = "registry+https://github.com/rust-lang/crates.io-index"
1745 1736 checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
1746 1737 dependencies = [
1747 1738 "foreign-types-macros",
1748 - "foreign-types-shared 0.3.1",
1739 + "foreign-types-shared",
1749 1740 ]
1750 1741
1751 1742 [[package]]
@@ -1761,12 +1752,6 @@ dependencies = [
1761 1752
1762 1753 [[package]]
1763 1754 name = "foreign-types-shared"
1764 - version = "0.1.1"
1765 - source = "registry+https://github.com/rust-lang/crates.io-index"
1766 - checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
1767 -
1768 - [[package]]
1769 - name = "foreign-types-shared"
1770 1755 version = "0.3.1"
1771 1756 source = "registry+https://github.com/rust-lang/crates.io-index"
1772 1757 checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
@@ -2260,11 +2245,12 @@ dependencies = [
2260 2245 "notify",
2261 2246 "notify-debouncer-mini",
2262 2247 "open",
2263 - "openssl",
2264 2248 "pter",
2265 2249 "rand 0.10.2",
2266 2250 "reqwest",
2267 2251 "rfd",
2252 + "rustls",
2253 + "rustls-platform-verifier",
2268 2254 "serde",
2269 2255 "serde_json",
2270 2256 "sha2 0.11.0",
@@ -2281,7 +2267,7 @@ dependencies = [
2281 2267 "tempfile",
2282 2268 "thiserror 2.0.18",
2283 2269 "tokio",
2284 - "tokio-native-tls",
2270 + "tokio-rustls",
2285 2271 "tokio-util",
2286 2272 "toml 1.1.3+spec-1.1.0",
2287 2273 "tracing",
@@ -2608,22 +2594,6 @@ dependencies = [
2608 2594 ]
2609 2595
2610 2596 [[package]]
2611 - name = "hyper-tls"
2612 - version = "0.6.0"
2613 - source = "registry+https://github.com/rust-lang/crates.io-index"
2614 - checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
2615 - dependencies = [
2616 - "bytes",
2617 - "http-body-util",
2618 - "hyper",
2619 - "hyper-util",
2620 - "native-tls",
2621 - "tokio",
2622 - "tokio-native-tls",
2623 - "tower-service",
2624 - ]
2625 -
2626 - [[package]]
2627 2597 name = "hyper-util"
2628 2598 version = "0.1.20"
2629 2599 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3238,13 +3208,14 @@ dependencies = [
3238 3208 "httpdate",
3239 3209 "idna",
3240 3210 "mime",
3241 - "native-tls",
3242 3211 "nom 8.0.0",
3243 3212 "percent-encoding",
3244 3213 "quoted_printable",
3214 + "rustls",
3215 + "rustls-platform-verifier",
3245 3216 "socket2",
3246 3217 "tokio",
3247 - "tokio-native-tls",
3218 + "tokio-rustls",
3248 3219 "url",
3249 3220 ]
3250 3221
@@ -3605,23 +3576,6 @@ dependencies = [
3605 3576 ]
3606 3577
3607 3578 [[package]]
3608 - name = "native-tls"
3609 - version = "0.2.18"
3610 - source = "registry+https://github.com/rust-lang/crates.io-index"
3611 - checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
3612 - dependencies = [
3613 - "libc",
3614 - "log",
3615 - "openssl",
3616 - "openssl-probe",
3617 - "openssl-sys",
3618 - "schannel",
3619 - "security-framework",
3620 - "security-framework-sys",
3621 - "tempfile",
3622 - ]
3623 -
3624 - [[package]]
3625 3579 name = "ndk"
3626 3580 version = "0.9.0"
3627 3581 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4042,60 +3996,12 @@ dependencies = [
4042 3996 ]
4043 3997
4044 3998 [[package]]
4045 - name = "openssl"
4046 - version = "0.10.76"
4047 - source = "registry+https://github.com/rust-lang/crates.io-index"
4048 - checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
4049 - dependencies = [
4050 - "bitflags 2.13.1",
4051 - "cfg-if",
4052 - "foreign-types 0.3.2",
4053 - "libc",
4054 - "once_cell",
4055 - "openssl-macros",
4056 - "openssl-sys",
4057 - ]
4058 -
4059 - [[package]]
4060 - name = "openssl-macros"
4061 - version = "0.1.1"
4062 - source = "registry+https://github.com/rust-lang/crates.io-index"
4063 - checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
4064 - dependencies = [
4065 - "proc-macro2",
4066 - "quote",
4067 - "syn 2.0.117",
4068 - ]
4069 -
4070 - [[package]]
4071 3999 name = "openssl-probe"
4072 4000 version = "0.2.1"
4073 4001 source = "registry+https://github.com/rust-lang/crates.io-index"
4074 4002 checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
4075 4003
4076 4004 [[package]]
4077 - name = "openssl-src"
4078 - version = "300.5.5+3.5.5"
4079 - source = "registry+https://github.com/rust-lang/crates.io-index"
4080 - checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
4081 - dependencies = [
4082 - "cc",
4083 - ]
4084 -
4085 - [[package]]
4086 - name = "openssl-sys"
4087 - version = "0.9.112"
4088 - source = "registry+https://github.com/rust-lang/crates.io-index"
4089 - checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
4090 - dependencies = [
4091 - "cc",
4092 - "libc",
4093 - "openssl-src",
4094 - "pkg-config",
4095 - "vcpkg",
4096 - ]
4097 -
4098 - [[package]]
4099 4005 name = "option-ext"
4100 4006 version = "0.2.0"
4101 4007 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5122,13 +5028,11 @@ dependencies = [
5122 5028 "http-body-util",
5123 5029 "hyper",
5124 5030 "hyper-rustls",
5125 - "hyper-tls",
5126 5031 "hyper-util",
5127 5032 "js-sys",
5128 5033 "log",
5129 5034 "mime",
5130 5035 "mime_guess",
5131 - "native-tls",
5132 5036 "percent-encoding",
5133 5037 "pin-project-lite",
5134 5038 "rustls",
@@ -5139,7 +5043,6 @@ dependencies = [
5139 5043 "serde_urlencoded",
5140 5044 "sync_wrapper",
5141 5045 "tokio",
5142 - "tokio-native-tls",
5143 5046 "tokio-rustls",
5144 5047 "tokio-util",
5145 5048 "tower",
@@ -5249,6 +5152,7 @@ version = "0.23.37"
5249 5152 source = "registry+https://github.com/rust-lang/crates.io-index"
5250 5153 checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
5251 5154 dependencies = [
5155 + "log",
5252 5156 "once_cell",
5253 5157 "ring",
5254 5158 "rustls-pki-types",
@@ -7009,16 +6913,6 @@ dependencies = [
7009 6913 ]
7010 6914
7011 6915 [[package]]
7012 - name = "tokio-native-tls"
7013 - version = "0.3.1"
7014 - source = "registry+https://github.com/rust-lang/crates.io-index"
7015 - checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
7016 - dependencies = [
7017 - "native-tls",
7018 - "tokio",
7019 - ]
7020 -
7021 - [[package]]
7022 6916 name = "tokio-rustls"
7023 6917 version = "0.26.4"
7024 6918 source = "registry+https://github.com/rust-lang/crates.io-index"
M Cargo.toml +10 -3
@@ -35,14 +35,21 @@ argon2 = "0.5"
35 35
36 36 # Email integration
37 37 async-imap = { version = "0.11", default-features = false, features = ["runtime-tokio"] }
38 - tokio-native-tls = "0.3"
39 38 tokio-util = "0.7"
40 39 futures-util = { version = "0.3", default-features = false, features = ["std", "async-await"] }
41 40 mailparse = "0.16"
42 - lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-native-tls", "smtp-transport", "builder"] }
41 + lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-rustls", "rustls-platform-verifier", "ring", "smtp-transport", "builder"] }
42 +
43 + # TLS — one rustls stack across HTTP (reqwest), SMTP (lettre) and IMAP, all validating
44 + # against the OS-native trust store via rustls-platform-verifier, with a single ring
45 + # crypto provider installed at startup. Matches the trust source tauri-plugin-updater
46 + # already uses, so the binary carries one TLS stack instead of native-tls + rustls.
47 + rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "logging", "ring"] }
48 + tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
49 + rustls-platform-verifier = "0.6"
43 50
44 51 # HTTP client
45 - reqwest = { version = "0.13", default-features = false, features = ["json", "native-tls", "form", "multipart"] }
52 + reqwest = { version = "0.13", default-features = false, features = ["json", "rustls-no-provider", "form", "multipart"] }
46 53
47 54 # Security / OAuth
48 55 rand = "0.10"
@@ -43,12 +43,16 @@ uuid = { workspace = true }
43 43
44 44 # Email
45 45 async-imap = { workspace = true }
46 - tokio-native-tls = { workspace = true }
47 46 tokio-util = { workspace = true }
48 47 futures-util = { workspace = true }
49 48 mailparse = { workspace = true }
50 49 lettre = { workspace = true }
51 50
51 + # TLS (rustls stack; ring provider installed at startup)
52 + rustls = { workspace = true }
53 + tokio-rustls = { workspace = true }
54 + rustls-platform-verifier = { workspace = true }
55 +
52 56 reqwest = { workspace = true }
53 57 thiserror = { workspace = true }
54 58
@@ -107,10 +111,8 @@ workspace = true
107 111 features = ["image-png", "tray-icon"]
108 112
109 113 # Android: bundle SQLite (system SQLite not accessible via NDK)
110 - # and vendor OpenSSL (native-tls deps need it for cross-compilation)
111 114 [target.'cfg(target_os = "android")'.dependencies]
112 115 libsqlite3-sys = { version = "0.37", features = ["bundled"] }
113 - openssl = { version = "0.10", features = ["vendored"] }
114 116
115 117 [dev-dependencies]
116 118 tempfile = "3"
@@ -5,13 +5,15 @@ use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
5 5 use chrono::{DateTime, Utc};
6 6 use futures_util::StreamExt;
7 7 use goingson_core::{EmailAccount, FolderSyncState};
8 + use rustls_platform_verifier::BuilderVerifierExt;
9 + use std::sync::Arc;
8 10 use tokio::net::TcpStream;
9 - use tokio_native_tls::TlsConnector;
10 - use tokio_native_tls::native_tls::TlsConnector as NativeTlsConnector;
11 + use tokio_rustls::TlsConnector;
12 + use tokio_rustls::rustls::{ClientConfig, pki_types::ServerName};
11 13
12 14 use super::mime_parse::{build_parsed_email, mime_nesting_within_limit};
13 15
14 - type ImapSession = async_imap::Session<tokio_native_tls::TlsStream<TcpStream>>;
16 + type ImapSession = async_imap::Session<tokio_rustls::client::TlsStream<TcpStream>>;
15 17
16 18 /// Maximum email size to download (25 MB). Emails exceeding this are skipped during sync.
17 19 const MAX_EMAIL_SIZE: u32 = 25 * 1024 * 1024;
@@ -112,12 +114,18 @@ impl ImapClient {
112 114 .map_err(|_| format!("Connection to {} timed out after 30s", addr))?
113 115 .map_err(|e| format!("Connection error: {}", e))?;
114 116
115 - let tls_connector =
116 - NativeTlsConnector::new().map_err(|e| format!("TLS setup error: {}", e))?;
117 - let tls = TlsConnector::from(tls_connector);
117 + // rustls config validating against the OS-native trust store (same trust source
118 + // as reqwest/lettre/updater). Uses the process-default ring crypto provider.
119 + let config = ClientConfig::builder()
120 + .with_platform_verifier()
121 + .map_err(|e| format!("TLS setup error: {}", e))?
122 + .with_no_client_auth();
123 + let tls = TlsConnector::from(Arc::new(config));
124 + let server_name = ServerName::try_from(self.server.clone())
125 + .map_err(|e| format!("Invalid IMAP server name {}: {}", self.server, e))?;
118 126 let tls_stream = tokio::time::timeout(
119 127 std::time::Duration::from_secs(30),
120 - tls.connect(&self.server, tcp_stream),
128 + tls.connect(server_name, tcp_stream),
121 129 )
122 130 .await
123 131 .map_err(|_| format!("TLS handshake with {} timed out after 30s", self.server))?
@@ -364,6 +364,9 @@ pub fn build_mobile_app() -> tauri::Builder<tauri::Wry> {
364 364 #[cfg(mobile)]
365 365 #[tauri::mobile_entry_point]
366 366 fn main() {
367 + // Install the ring crypto provider before any TLS client builds a config.
368 + let _ = rustls::crypto::ring::default_provider().install_default();
369 +
367 370 tracing_subscriber::fmt::init();
368 371
369 372 build_mobile_app()
@@ -104,6 +104,11 @@ fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
104 104 }
105 105
106 106 fn main() {
107 + // Install the process-wide rustls crypto provider (ring) before any TLS client
108 + // (reqwest, lettre, IMAP, or the updater) builds a config. Everything validates
109 + // against the OS-native trust store via this one provider.
110 + let _ = rustls::crypto::ring::default_provider().install_default();
111 +
107 112 // Initialize structured logging with tracing
108 113 tracing_subscriber::registry()
109 114 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| {