From b112a13f4ed489aa15d9731b35e5a1ed9467a4f7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 9 Nov 2024 03:20:18 +0000 Subject: [PATCH 1/4] refactor: use rustls reexported from tokio_rustls --- Cargo.lock | 1 - Cargo.toml | 1 - src/net/tls.rs | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f21d8fd91..66d22336c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1368,7 +1368,6 @@ dependencies = [ "regex", "rusqlite", "rust-hsluv", - "rustls", "rustls-pki-types", "sanitize-filename", "serde", diff --git a/Cargo.toml b/Cargo.toml index b22863e063..94d763ecce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,6 @@ regex = { workspace = true } rusqlite = { workspace = true, features = ["sqlcipher"] } rust-hsluv = "0.1" rustls-pki-types = "1.10.0" -rustls = { version = "0.23.14", default-features = false } sanitize-filename = { workspace = true } serde_json = { workspace = true } serde_urlencoded = "0.7.1" diff --git a/src/net/tls.rs b/src/net/tls.rs index 183ad75319..e8a62d11ab 100644 --- a/src/net/tls.rs +++ b/src/net/tls.rs @@ -35,10 +35,10 @@ pub async fn wrap_rustls( alpn: &[&str], stream: impl SessionStream, ) -> Result { - let mut root_cert_store = rustls::RootCertStore::empty(); + let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty(); root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let mut config = rustls::ClientConfig::builder() + let mut config = tokio_rustls::rustls::ClientConfig::builder() .with_root_certificates(root_cert_store) .with_no_client_auth(); config.alpn_protocols = alpn.iter().map(|s| s.as_bytes().to_vec()).collect(); From a8fcd497abb4f30507a25d201b646819c6139032 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 9 Nov 2024 20:54:06 +0000 Subject: [PATCH 2/4] refactor: pass ALPN around as &str --- src/imap/client.rs | 10 +++++----- src/net.rs | 2 +- src/net/http.rs | 4 ++-- src/net/proxy.rs | 2 +- src/net/tls.rs | 17 +++++++++++++---- src/smtp/connect.rs | 12 ++++++------ 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/imap/client.rs b/src/imap/client.rs index 9bcc448909..9883a43b02 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -38,12 +38,12 @@ impl DerefMut for Client { } /// Converts port number to ALPN list. -fn alpn(port: u16) -> &'static [&'static str] { +fn alpn(port: u16) -> &'static str { if port == 993 { // Do not request ALPN on standard port. - &[] + "" } else { - &["imap"] + "imap" } } @@ -242,7 +242,7 @@ impl Client { let buffered_tcp_stream = client.into_inner(); let tcp_stream = buffered_tcp_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream) + let tls_stream = wrap_tls(strict_tls, host, "", tcp_stream) .await .context("STARTTLS upgrade failed")?; @@ -315,7 +315,7 @@ impl Client { let buffered_proxy_stream = client.into_inner(); let proxy_stream = buffered_proxy_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream) + let tls_stream = wrap_tls(strict_tls, hostname, "", proxy_stream) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); diff --git a/src/net.rs b/src/net.rs index 350967ddef..ebc2058a04 100644 --- a/src/net.rs +++ b/src/net.rs @@ -127,7 +127,7 @@ pub(crate) async fn connect_tls_inner( addr: SocketAddr, host: &str, strict_tls: bool, - alpn: &[&str], + alpn: &str, ) -> Result { let tcp_stream = connect_tcp_inner(addr).await?; let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?; diff --git a/src/net/http.rs b/src/net/http.rs index 94e85f68c1..c71bbb5be9 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -72,11 +72,11 @@ where let proxy_stream = proxy_config .connect(context, host, port, load_cache) .await?; - let tls_stream = wrap_rustls(host, &[], proxy_stream).await?; + let tls_stream = wrap_rustls(host, "", proxy_stream).await?; Box::new(tls_stream) } else { let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; - let tls_stream = wrap_rustls(host, &[], tcp_stream).await?; + let tls_stream = wrap_rustls(host, "", tcp_stream).await?; Box::new(tls_stream) } } diff --git a/src/net/proxy.rs b/src/net/proxy.rs index f0a9c32c42..f6a9d1d1a0 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -425,7 +425,7 @@ impl ProxyConfig { load_cache, ) .await?; - let tls_stream = wrap_rustls(&https_config.host, &[], tcp_stream).await?; + let tls_stream = wrap_rustls(&https_config.host, "", tcp_stream).await?; let auth = if let Some((username, password)) = &https_config.user_password { Some((username.as_str(), password.as_str())) } else { diff --git a/src/net/tls.rs b/src/net/tls.rs index e8a62d11ab..50a81c0ee1 100644 --- a/src/net/tls.rs +++ b/src/net/tls.rs @@ -8,7 +8,7 @@ use crate::net::session::SessionStream; pub async fn wrap_tls( strict_tls: bool, hostname: &str, - alpn: &[&str], + alpn: &str, stream: impl SessionStream + 'static, ) -> Result { if strict_tls { @@ -19,9 +19,14 @@ pub async fn wrap_tls( // We use native_tls because it accepts 1024-bit RSA keys. // Rustls does not support them even if // certificate checks are disabled: . + let alpns = if alpn.is_empty() { + Box::from([]) + } else { + Box::from([alpn]) + }; let tls = async_native_tls::TlsConnector::new() .min_protocol_version(Some(async_native_tls::Protocol::Tlsv12)) - .request_alpns(alpn) + .request_alpns(&alpns) .danger_accept_invalid_hostnames(true) .danger_accept_invalid_certs(true); let tls_stream = tls.connect(hostname, stream).await?; @@ -32,7 +37,7 @@ pub async fn wrap_tls( pub async fn wrap_rustls( hostname: &str, - alpn: &[&str], + alpn: &str, stream: impl SessionStream, ) -> Result { let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty(); @@ -41,7 +46,11 @@ pub async fn wrap_rustls( let mut config = tokio_rustls::rustls::ClientConfig::builder() .with_root_certificates(root_cert_store) .with_no_client_auth(); - config.alpn_protocols = alpn.iter().map(|s| s.as_bytes().to_vec()).collect(); + config.alpn_protocols = if alpn.is_empty() { + vec![] + } else { + vec![alpn.as_bytes().to_vec()] + }; let tls = tokio_rustls::TlsConnector::from(Arc::new(config)); let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned(); diff --git a/src/smtp/connect.rs b/src/smtp/connect.rs index 5652d14272..9b8b969366 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -18,13 +18,13 @@ use crate::net::{ use crate::oauth2::get_oauth2_access_token; use crate::tools::time; -/// Converts port number to ALPN list. -fn alpn(port: u16) -> &'static [&'static str] { +/// Converts port number to ALPN. +fn alpn(port: u16) -> &'static str { if port == 465 { // Do not request ALPN on standard port. - &[] + "" } else { - &["smtp"] + "smtp" } } @@ -248,7 +248,7 @@ async fn connect_starttls_proxy( skip_smtp_greeting(&mut buffered_stream).await?; let transport = new_smtp_transport(buffered_stream).await?; let tcp_stream = transport.starttls().await?.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream) + let tls_stream = wrap_tls(strict_tls, hostname, "", tcp_stream) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufStream::new(tls_stream); @@ -293,7 +293,7 @@ async fn connect_starttls( skip_smtp_greeting(&mut buffered_stream).await?; let transport = new_smtp_transport(buffered_stream).await?; let tcp_stream = transport.starttls().await?.into_inner(); - let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream) + let tls_stream = wrap_tls(strict_tls, host, "", tcp_stream) .await .context("STARTTLS upgrade failed")?; From 7657955068ee93e86573be83170be5fb81514fc5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 10 Nov 2024 00:11:11 +0000 Subject: [PATCH 3/4] feat(deltachat-repl): allow to send messages when disconnected If scheduler is not connected, use `send_msg_sync` to create a dedicated connection and send a single message. This allows easily testing SMTP connection establishment. --- deltachat-repl/src/cmdline.rs | 17 ++++++++++++++++- src/context.rs | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 2f636ee867..e9afe1d07f 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -922,7 +922,22 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let msg = format!("{arg1} {arg2}"); - chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?; + if context.is_running().await { + chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?; + } else { + // Send message over a dedicated SMTP connection + // and measure time. + // + // This can be used to benchmark SMTP connection establishment. + let time_start = std::time::SystemTime::now(); + + let mut msg = Message::new_text(msg); + chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg) + .await?; + + let time_needed = time_start.elapsed().unwrap_or_default(); + println!("Sent message in {time_needed:?}."); + } } "sendempty" => { ensure!(sel_chat.is_some(), "No chat selected."); diff --git a/src/context.rs b/src/context.rs index ba1784fdfe..2dfdb2665d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -479,6 +479,11 @@ impl Context { self.scheduler.stop(self).await; } + /// Returns true if IO scheduler is running. + pub async fn is_running(&self) -> bool { + self.scheduler.is_running().await + } + /// Restarts the IO scheduler if it was running before /// when it is not running this is an no-op pub async fn restart_io_if_running(&self) { From c0067f87fa3a3aca040317068ecf238f38f850f0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 7 Nov 2024 19:22:19 +0000 Subject: [PATCH 4/4] feat: TLS 1.3 session resumption --- src/imap/client.rs | 6 +++--- src/net.rs | 2 +- src/net/http.rs | 4 ++-- src/net/proxy.rs | 3 ++- src/net/tls.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- src/smtp/connect.rs | 6 +++--- 6 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/imap/client.rs b/src/imap/client.rs index 9883a43b02..c62a979c87 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -242,7 +242,7 @@ impl Client { let buffered_tcp_stream = client.into_inner(); let tcp_stream = buffered_tcp_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, host, "", tcp_stream) + let tls_stream = wrap_tls(strict_tls, host, addr.port(), "", tcp_stream) .await .context("STARTTLS upgrade failed")?; @@ -262,7 +262,7 @@ impl Client { let proxy_stream = proxy_config .connect(context, domain, port, strict_tls) .await?; - let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?; + let tls_stream = wrap_tls(strict_tls, domain, port, alpn(port), proxy_stream).await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); @@ -315,7 +315,7 @@ impl Client { let buffered_proxy_stream = client.into_inner(); let proxy_stream = buffered_proxy_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, "", proxy_stream) + let tls_stream = wrap_tls(strict_tls, hostname, port, "", proxy_stream) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); diff --git a/src/net.rs b/src/net.rs index ebc2058a04..dfe983b275 100644 --- a/src/net.rs +++ b/src/net.rs @@ -130,7 +130,7 @@ pub(crate) async fn connect_tls_inner( alpn: &str, ) -> Result { let tcp_stream = connect_tcp_inner(addr).await?; - let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?; + let tls_stream = wrap_tls(strict_tls, host, addr.port(), alpn, tcp_stream).await?; Ok(tls_stream) } diff --git a/src/net/http.rs b/src/net/http.rs index c71bbb5be9..382963a3aa 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -72,11 +72,11 @@ where let proxy_stream = proxy_config .connect(context, host, port, load_cache) .await?; - let tls_stream = wrap_rustls(host, "", proxy_stream).await?; + let tls_stream = wrap_rustls(host, port, "", proxy_stream).await?; Box::new(tls_stream) } else { let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; - let tls_stream = wrap_rustls(host, "", tcp_stream).await?; + let tls_stream = wrap_rustls(host, port, "", tcp_stream).await?; Box::new(tls_stream) } } diff --git a/src/net/proxy.rs b/src/net/proxy.rs index f6a9d1d1a0..aa90c79e7c 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -425,7 +425,8 @@ impl ProxyConfig { load_cache, ) .await?; - let tls_stream = wrap_rustls(&https_config.host, "", tcp_stream).await?; + let tls_stream = + wrap_rustls(&https_config.host, https_config.port, "", tcp_stream).await?; let auth = if let Some((username, password)) = &https_config.user_password { Some((username.as_str(), password.as_str())) } else { diff --git a/src/net/tls.rs b/src/net/tls.rs index 50a81c0ee1..b19b44cee3 100644 --- a/src/net/tls.rs +++ b/src/net/tls.rs @@ -1,18 +1,24 @@ //! TLS support. +use std::collections::HashMap; use std::sync::Arc; use anyhow::Result; +use once_cell::sync::Lazy; +use parking_lot::Mutex; use crate::net::session::SessionStream; +use tokio_rustls::rustls::client::ClientSessionStore; + pub async fn wrap_tls( strict_tls: bool, hostname: &str, + port: u16, alpn: &str, stream: impl SessionStream + 'static, ) -> Result { if strict_tls { - let tls_stream = wrap_rustls(hostname, alpn, stream).await?; + let tls_stream = wrap_rustls(hostname, port, alpn, stream).await?; let boxed_stream: Box = Box::new(tls_stream); Ok(boxed_stream) } else { @@ -35,8 +41,20 @@ pub async fn wrap_tls( } } +type SessionMap = HashMap<(u16, String), Arc>; + +/// Map to store TLS session tickets. +/// +/// Tickets are separated by port and ALPN +/// to avoid trying to use Postfix ticket for Dovecot and vice versa. +/// Doing so would not be a security issue, +/// but wastes the ticket and the opportunity to resume TLS session unnecessarily. +/// Rustls takes care of separating tickets that belong to different domain names. +static RESUMPTION_STORE: Lazy> = Lazy::new(Default::default); + pub async fn wrap_rustls( hostname: &str, + port: u16, alpn: &str, stream: impl SessionStream, ) -> Result { @@ -52,6 +70,31 @@ pub async fn wrap_rustls( vec![alpn.as_bytes().to_vec()] }; + // Enable TLS 1.3 session resumption + // as defined in . + // + // Obsolete TLS 1.2 mechanisms defined in RFC 5246 + // and RFC 5077 have worse security + // and are not worth increasing + // attack surface: . + let resumption_store = Arc::clone( + RESUMPTION_STORE + .lock() + .entry((port, alpn.to_string())) + .or_insert_with(|| { + // This is the default as of Rustls version 0.23.16, + // but we want to create multiple caches + // to separate them by port and ALPN. + Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new( + 256, + )) + }), + ); + + let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store) + .tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled); + config.resumption = resumption; + let tls = tokio_rustls::TlsConnector::from(Arc::new(config)); let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned(); let tls_stream = tls.connect(name, stream).await?; diff --git a/src/smtp/connect.rs b/src/smtp/connect.rs index 9b8b969366..eee2b8af87 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -225,7 +225,7 @@ async fn connect_secure_proxy( let proxy_stream = proxy_config .connect(context, hostname, port, strict_tls) .await?; - let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?; + let tls_stream = wrap_tls(strict_tls, hostname, port, alpn(port), proxy_stream).await?; let mut buffered_stream = BufStream::new(tls_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); @@ -248,7 +248,7 @@ async fn connect_starttls_proxy( skip_smtp_greeting(&mut buffered_stream).await?; let transport = new_smtp_transport(buffered_stream).await?; let tcp_stream = transport.starttls().await?.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, "", tcp_stream) + let tls_stream = wrap_tls(strict_tls, hostname, port, "", tcp_stream) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufStream::new(tls_stream); @@ -293,7 +293,7 @@ async fn connect_starttls( skip_smtp_greeting(&mut buffered_stream).await?; let transport = new_smtp_transport(buffered_stream).await?; let tcp_stream = transport.starttls().await?.into_inner(); - let tls_stream = wrap_tls(strict_tls, host, "", tcp_stream) + let tls_stream = wrap_tls(strict_tls, host, addr.port(), "", tcp_stream) .await .context("STARTTLS upgrade failed")?;