deltachat/net/
tls.rs

1//! TLS support.
2use parking_lot::Mutex;
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use anyhow::Result;
7
8use crate::net::session::SessionStream;
9
10use tokio_rustls::rustls::client::ClientSessionStore;
11
12pub async fn wrap_tls<'a>(
13    strict_tls: bool,
14    hostname: &str,
15    port: u16,
16    alpn: &str,
17    stream: impl SessionStream + 'static,
18    tls_session_store: &TlsSessionStore,
19) -> Result<impl SessionStream + 'a> {
20    if strict_tls {
21        let tls_stream = wrap_rustls(hostname, port, alpn, stream, tls_session_store).await?;
22        let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
23        Ok(boxed_stream)
24    } else {
25        // We use native_tls because it accepts 1024-bit RSA keys.
26        // Rustls does not support them even if
27        // certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
28        let alpns = if alpn.is_empty() {
29            Box::from([])
30        } else {
31            Box::from([alpn])
32        };
33        let tls = async_native_tls::TlsConnector::new()
34            .min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
35            .request_alpns(&alpns)
36            .danger_accept_invalid_hostnames(true)
37            .danger_accept_invalid_certs(true);
38        let tls_stream = tls.connect(hostname, stream).await?;
39        let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
40        Ok(boxed_stream)
41    }
42}
43
44/// Map to store TLS session tickets.
45///
46/// Tickets are separated by port and ALPN
47/// to avoid trying to use Postfix ticket for Dovecot and vice versa.
48/// Doing so would not be a security issue,
49/// but wastes the ticket and the opportunity to resume TLS session unnecessarily.
50/// Rustls takes care of separating tickets that belong to different domain names.
51#[derive(Debug)]
52pub(crate) struct TlsSessionStore {
53    sessions: Mutex<HashMap<(u16, String), Arc<dyn ClientSessionStore>>>,
54}
55
56// This is the default for TLS session store
57// as of Rustls version 0.23.16,
58// but we want to create multiple caches
59// to separate them by port and ALPN.
60const TLS_CACHE_SIZE: usize = 256;
61
62impl TlsSessionStore {
63    /// Creates a new TLS session store.
64    ///
65    /// One such store should be created per profile
66    /// to keep TLS sessions independent.
67    pub fn new() -> Self {
68        Self {
69            sessions: Default::default(),
70        }
71    }
72
73    /// Returns session store for given port and ALPN.
74    ///
75    /// Rustls additionally separates sessions by hostname.
76    pub fn get(&self, port: u16, alpn: &str) -> Arc<dyn ClientSessionStore> {
77        Arc::clone(
78            self.sessions
79                .lock()
80                .entry((port, alpn.to_string()))
81                .or_insert_with(|| {
82                    Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new(
83                        TLS_CACHE_SIZE,
84                    ))
85                }),
86        )
87    }
88}
89
90pub async fn wrap_rustls<'a>(
91    hostname: &str,
92    port: u16,
93    alpn: &str,
94    stream: impl SessionStream + 'a,
95    tls_session_store: &TlsSessionStore,
96) -> Result<impl SessionStream + 'a> {
97    let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
98    root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
99
100    let mut config = tokio_rustls::rustls::ClientConfig::builder()
101        .with_root_certificates(root_cert_store)
102        .with_no_client_auth();
103    config.alpn_protocols = if alpn.is_empty() {
104        vec![]
105    } else {
106        vec![alpn.as_bytes().to_vec()]
107    };
108
109    // Enable TLS 1.3 session resumption
110    // as defined in <https://www.rfc-editor.org/rfc/rfc8446#section-2.2>.
111    //
112    // Obsolete TLS 1.2 mechanisms defined in RFC 5246
113    // and RFC 5077 have worse security
114    // and are not worth increasing
115    // attack surface: <https://words.filippo.io/we-need-to-talk-about-session-tickets/>.
116    let resumption_store = tls_session_store.get(port, alpn);
117    let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store)
118        .tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled);
119    config.resumption = resumption;
120
121    let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
122    let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned();
123    let tls_stream = tls.connect(name, stream).await?;
124    Ok(tls_stream)
125}