feat: TLS 1.3 session resumption

This commit is contained in:
link2xt
2024-11-07 19:22:19 +00:00
committed by l
parent 460d2f3c2a
commit eb1bd1d200
7 changed files with 179 additions and 24 deletions

View File

@@ -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<RwLock<Option<Iroh>>>,
@@ -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()),

View File

@@ -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<dyn SessionStream> = 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<dyn SessionStream> = 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<dyn SessionStream> = 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<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);

View File

@@ -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<impl SessionStream + 'static> {
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)
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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<impl SessionStream + 'a> {
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<dyn SessionStream> = 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<HashMap<(u16, String), Arc<dyn ClientSessionStore>>>,
}
// 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<dyn ClientSessionStore> {
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<impl SessionStream + 'a> {
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 <https://www.rfc-editor.org/rfc/rfc8446#section-2.2>.
//
// Obsolete TLS 1.2 mechanisms defined in RFC 5246
// and RFC 5077 have worse security
// and are not worth increasing
// attack surface: <https://words.filippo.io/we-need-to-talk-about-session-tickets/>.
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?;

View File

@@ -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<dyn SessionBufStream> = 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<dyn SessionBufStream> = 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<Box<dyn SessionBufStream>> {
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<dyn SessionBufStream> = Box::new(buffered_stream);
@@ -286,6 +313,7 @@ async fn connect_starttls(
addr: SocketAddr,
host: &str,
strict_tls: bool,
tls_session_store: &TlsSessionStore,
) -> Result<Box<dyn SessionBufStream>> {
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<dyn SessionBufStream> = Box::new(buffered_stream);