diff --git a/src/context.rs b/src/context.rs index 57c14e01f..767db0d7f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -30,6 +30,7 @@ use crate::log::{info, warn}; use crate::logged_debug_assert; use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam}; use crate::message::{self, Message, MessageState, MsgId}; +use crate::net::tls::TlsSessionStore; use crate::param::{Param, Params}; use crate::peer_channels::Iroh; use crate::push::PushSubscriber; @@ -297,6 +298,9 @@ pub struct InnerContext { /// True if account has subscribed to push notifications via IMAP. pub(crate) push_subscribed: AtomicBool, + /// TLS session resumption cache. + pub(crate) tls_session_store: TlsSessionStore, + /// Iroh for realtime peer channels. pub(crate) iroh: Arc>>, @@ -475,6 +479,7 @@ impl Context { debug_logging: std::sync::RwLock::new(None), push_subscriber, push_subscribed: AtomicBool::new(false), + tls_session_store: TlsSessionStore::new(), iroh: Arc::new(RwLock::new(None)), self_fingerprint: OnceLock::new(), connectivities: parking_lot::Mutex::new(Vec::new()), diff --git a/src/imap/client.rs b/src/imap/client.rs index 6a6b14f97..bd5b13659 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -210,7 +210,15 @@ impl Client { let account_id = context.get_id(); let events = context.events.clone(); let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?; - let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + addr.port(), + alpn(addr.port()), + logging_stream, + &context.tls_session_store, + ) + .await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); @@ -262,9 +270,16 @@ 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) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + host, + addr.port(), + "", + tcp_stream, + &context.tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let client = Client::new(session_stream); @@ -281,7 +296,15 @@ 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, + &context.tls_session_store, + ) + .await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); @@ -334,9 +357,16 @@ 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) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + port, + "", + proxy_stream, + &context.tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let client = Client::new(session_stream); diff --git a/src/net.rs b/src/net.rs index 5830677cc..4471a492d 100644 --- a/src/net.rs +++ b/src/net.rs @@ -12,6 +12,7 @@ use tokio_io_timeout::TimeoutStream; use crate::context::Context; use crate::net::session::SessionStream; +use crate::net::tls::TlsSessionStore; use crate::sql::Sql; use crate::tools::time; @@ -128,9 +129,18 @@ pub(crate) async fn connect_tls_inner( host: &str, strict_tls: bool, alpn: &str, + tls_session_store: &TlsSessionStore, ) -> 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, + tls_session_store, + ) + .await?; Ok(tls_stream) } diff --git a/src/net/http.rs b/src/net/http.rs index ff8212af8..da06c6039 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -76,11 +76,13 @@ 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, &context.tls_session_store).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, &context.tls_session_store).await?; Box::new(tls_stream) } } diff --git a/src/net/proxy.rs b/src/net/proxy.rs index 14eb3f430..c59692ccc 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -429,7 +429,14 @@ 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, + &context.tls_session_store, + ) + .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 b4fe4d544..fce4abcb9 100644 --- a/src/net/tls.rs +++ b/src/net/tls.rs @@ -1,18 +1,24 @@ //! TLS support. +use parking_lot::Mutex; +use std::collections::HashMap; use std::sync::Arc; use anyhow::Result; use crate::net::session::SessionStream; +use tokio_rustls::rustls::client::ClientSessionStore; + pub async fn wrap_tls<'a>( strict_tls: bool, hostname: &str, + port: u16, alpn: &str, stream: impl SessionStream + 'static, + tls_session_store: &TlsSessionStore, ) -> Result { if strict_tls { - let tls_stream = wrap_rustls(hostname, alpn, stream).await?; + let tls_stream = wrap_rustls(hostname, port, alpn, stream, tls_session_store).await?; let boxed_stream: Box = Box::new(tls_stream); Ok(boxed_stream) } else { @@ -35,10 +41,58 @@ pub async fn wrap_tls<'a>( } } +/// 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. +#[derive(Debug)] +pub(crate) struct TlsSessionStore { + sessions: Mutex>>, +} + +// This is the default for TLS session store +// as of Rustls version 0.23.16, +// but we want to create multiple caches +// to separate them by port and ALPN. +const TLS_CACHE_SIZE: usize = 256; + +impl TlsSessionStore { + /// Creates a new TLS session store. + /// + /// One such store should be created per profile + /// to keep TLS sessions independent. + pub fn new() -> Self { + Self { + sessions: Default::default(), + } + } + + /// Returns session store for given port and ALPN. + /// + /// Rustls additionally separates sessions by hostname. + pub fn get(&self, port: u16, alpn: &str) -> Arc { + Arc::clone( + self.sessions + .lock() + .entry((port, alpn.to_string())) + .or_insert_with(|| { + Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new( + TLS_CACHE_SIZE, + )) + }), + ) + } +} + pub async fn wrap_rustls<'a>( hostname: &str, + port: u16, alpn: &str, stream: impl SessionStream + 'a, + tls_session_store: &TlsSessionStore, ) -> Result { let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty(); root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); @@ -52,6 +106,18 @@ pub async fn wrap_rustls<'a>( 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 = tls_session_store.get(port, alpn); + 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 c1c93d481..f7b35d657 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -12,7 +12,7 @@ use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionBufStream; -use crate::net::tls::wrap_tls; +use crate::net::tls::{TlsSessionStore, wrap_tls}; use crate::net::{ connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history, }; @@ -109,8 +109,12 @@ async fn connection_attempt( "Attempting SMTP connection to {host} ({resolved_addr})." ); let res = match security { - ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await, - ConnectionSecurity::Starttls => connect_starttls(resolved_addr, host, strict_tls).await, + ConnectionSecurity::Tls => { + connect_secure(resolved_addr, host, strict_tls, &context.tls_session_store).await + } + ConnectionSecurity::Starttls => { + connect_starttls(resolved_addr, host, strict_tls, &context.tls_session_store).await + } ConnectionSecurity::Plain => connect_insecure(resolved_addr).await, }; match res { @@ -226,7 +230,15 @@ 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, + &context.tls_session_store, + ) + .await?; let mut buffered_stream = BufStream::new(tls_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); @@ -249,9 +261,16 @@ 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) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + port, + "", + tcp_stream, + &context.tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufStream::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); Ok(session_stream) @@ -274,8 +293,16 @@ async fn connect_secure( addr: SocketAddr, hostname: &str, strict_tls: bool, + tls_session_store: &TlsSessionStore, ) -> Result> { - let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?; + let tls_stream = connect_tls_inner( + addr, + hostname, + strict_tls, + alpn(addr.port()), + tls_session_store, + ) + .await?; let mut buffered_stream = BufStream::new(tls_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); @@ -286,6 +313,7 @@ async fn connect_starttls( addr: SocketAddr, host: &str, strict_tls: bool, + tls_session_store: &TlsSessionStore, ) -> Result> { let tcp_stream = connect_tcp_inner(addr).await?; @@ -294,9 +322,16 @@ 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) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + host, + addr.port(), + "", + tcp_stream, + tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufStream::new(tls_stream); let session_stream: Box = Box::new(buffered_stream);