Skip to main content

ullun/
tls.rs

1//! HTTPS (feature `tls`)
2//!
3//! ```no_run
4//! use ullun::prelude::*;
5//! use ullun::tls::TlsConfig;
6//!
7//! # async fn run() {
8//! let tls = TlsConfig::from_pem_files("fullchain.pem", "privkey.pem").unwrap();
9//! App::new()
10//!     .get("/", |req: Request| async move { Ok(Response::text(format!("secure: {}", req.is_secure()))) })
11//!     .tls(tls)
12//!     .tls_reload_every(std::time::Duration::from_secs(3600))   // pick up renewed certificates
13//!     .run("0.0.0.0:8443")
14//!     .await
15//!     .unwrap();
16//! # }
17//! ```
18//!
19//! * TLS 1.2 and 1.3 with rustls (ring provider); ALPN offers `h2` and `http/1.1`,
20//!   so clients that speak HTTP/2 get it automatically.
21//! * **Reload without restart**: [`TlsConfig::reload`] re-reads the certificate
22//!   files; new connections use the new certificate, open ones keep running. A
23//!   renewal that fails to load (half-written file, wrong key) is reported and
24//!   the previous certificate stays in service.
25//! * **Client certificates** (mutual TLS) with [`TlsConfig::client_auth`],
26//!   required or optional; handlers read the presented certificate with
27//!   `Request::client_certificate`.
28//! * A client that does not complete the handshake within
29//!   [`TlsConfig::handshake_timeout`] (default 10 s) is dropped, so half-open
30//!   connections cannot pile up.
31//!
32//! Redirect plain HTTP with [`RequireHttps`](crate::secure::RequireHttps) on a
33//! second listener and add [`Hsts`](crate::secure::Hsts) to the HTTPS app.
34
35use crate::error::{Error, Result};
36use std::path::{Path, PathBuf};
37use std::sync::{Arc, Mutex, MutexGuard};
38use std::time::{Duration, SystemTime};
39use tokio::net::TcpStream;
40use tokio_rustls::rustls::pki_types::pem::PemObject;
41use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
42use tokio_rustls::rustls::server::WebPkiClientVerifier;
43use tokio_rustls::rustls::{RootCertStore, ServerConfig};
44use tokio_rustls::server::TlsStream;
45use tokio_rustls::TlsAcceptor;
46
47struct Source {
48    cert_path: PathBuf,
49    key_path: PathBuf,
50    modified: Option<(SystemTime, SystemTime)>,
51}
52
53struct ClientAuth {
54    roots: RootCertStore,
55    required: bool,
56}
57
58struct State {
59    acceptor: TlsAcceptor,
60    /// kept so that the configuration can be rebuilt (client auth changes)
61    chain_pem: Vec<u8>,
62    key_pem: Vec<u8>,
63    source: Option<Source>,
64}
65
66/// Certificate, key and client-auth policy of an HTTPS server; cheap to clone,
67/// clones share the (reloadable) configuration
68#[derive(Clone)]
69pub struct TlsConfig {
70    state: Arc<Mutex<State>>,
71    client_auth: Arc<Mutex<Option<Arc<ClientAuth>>>>,
72    handshake_timeout: Duration,
73}
74
75fn tls_error(what: &str, detail: impl std::fmt::Display) -> Error {
76    Error::internal(format!("TLS: {}: {}", what, detail))
77}
78
79fn certs_from_pem(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>> {
80    let certs: std::result::Result<Vec<_>, _> = CertificateDer::pem_slice_iter(pem).collect();
81    let certs = certs.map_err(|e| tls_error("invalid certificate PEM", e))?;
82    if certs.is_empty() {
83        return Err(tls_error("no certificate found in the PEM data", "empty"));
84    }
85    Ok(certs)
86}
87
88fn build_server_config(
89    chain_pem: &[u8],
90    key_pem: &[u8],
91    client_auth: Option<&ClientAuth>,
92) -> Result<ServerConfig> {
93    let chain = certs_from_pem(chain_pem)?;
94    let key = PrivateKeyDer::from_pem_slice(key_pem)
95        .map_err(|e| tls_error("no usable private key in the PEM data", e))?;
96
97    let builder = ServerConfig::builder_with_provider(Arc::new(
98        tokio_rustls::rustls::crypto::ring::default_provider(),
99    ))
100    .with_safe_default_protocol_versions()
101    .map_err(|e| tls_error("protocol versions", e))?;
102    let builder = match client_auth {
103        None => builder.with_no_client_auth(),
104        Some(auth) => {
105            let mut verifier = WebPkiClientVerifier::builder_with_provider(
106                Arc::new(auth.roots.clone()),
107                Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()),
108            );
109            if !auth.required {
110                verifier = verifier.allow_unauthenticated();
111            }
112            builder.with_client_cert_verifier(
113                verifier
114                    .build()
115                    .map_err(|e| tls_error("client certificate verifier", e))?,
116            )
117        }
118    };
119    let mut config = builder
120        .with_single_cert(chain, key)
121        .map_err(|e| tls_error("certificate and key do not match", e))?;
122    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
123    Ok(config)
124}
125
126fn modified(path: &Path) -> Option<SystemTime> {
127    std::fs::metadata(path).ok()?.modified().ok()
128}
129
130impl TlsConfig {
131    /// From PEM text: the certificate chain (leaf first) and one private key
132    /// (PKCS#8, PKCS#1 or SEC1)
133    pub fn from_pem(chain_pem: &[u8], key_pem: &[u8]) -> Result<Self> {
134        let config = build_server_config(chain_pem, key_pem, None)?;
135        Ok(Self {
136            state: Arc::new(Mutex::new(State {
137                acceptor: TlsAcceptor::from(Arc::new(config)),
138                chain_pem: chain_pem.to_vec(),
139                key_pem: key_pem.to_vec(),
140                source: None,
141            })),
142            client_auth: Arc::new(Mutex::new(None)),
143            handshake_timeout: Duration::from_secs(10),
144        })
145    }
146
147    /// From PEM files, which [`reload`](TlsConfig::reload) reads again later
148    pub fn from_pem_files(cert_path: impl AsRef<Path>, key_path: impl AsRef<Path>) -> Result<Self> {
149        let (cert_path, key_path) = (
150            cert_path.as_ref().to_path_buf(),
151            key_path.as_ref().to_path_buf(),
152        );
153        let chain = std::fs::read(&cert_path)
154            .map_err(|e| tls_error(&format!("cannot read {}", cert_path.display()), e))?;
155        let key = std::fs::read(&key_path)
156            .map_err(|e| tls_error(&format!("cannot read {}", key_path.display()), e))?;
157        let this = Self::from_pem(&chain, &key)?;
158        this.lock().source = Some(Source {
159            modified: modified(&cert_path).zip(modified(&key_path)),
160            cert_path,
161            key_path,
162        });
163        Ok(this)
164    }
165
166    fn lock(&self) -> MutexGuard<'_, State> {
167        self.state.lock().unwrap_or_else(|e| e.into_inner())
168    }
169
170    fn client_auth_policy(&self) -> Option<Arc<ClientAuth>> {
171        self.client_auth
172            .lock()
173            .unwrap_or_else(|e| e.into_inner())
174            .clone()
175    }
176
177    /// Ask clients for a certificate signed by one of the CAs in `ca_pem`
178    ///
179    /// With `required`, connections without a valid certificate fail the
180    /// handshake. Without it, clients may connect anonymously and handlers
181    /// decide per route (`Request::client_certificate` is `None` then);
182    /// a certificate that is presented but not trusted is always rejected.
183    pub fn client_auth(self, ca_pem: &[u8], required: bool) -> Result<Self> {
184        let mut roots = RootCertStore::empty();
185        for ca in certs_from_pem(ca_pem)? {
186            roots
187                .add(ca)
188                .map_err(|e| tls_error("unusable CA certificate", e))?;
189        }
190        let auth = Arc::new(ClientAuth { roots, required });
191        // build first: a bad CA must not leave a half-applied policy behind
192        let config = {
193            let st = self.lock();
194            build_server_config(&st.chain_pem, &st.key_pem, Some(&auth))?
195        };
196        *self.client_auth.lock().unwrap_or_else(|e| e.into_inner()) = Some(auth);
197        self.lock().acceptor = TlsAcceptor::from(Arc::new(config));
198        Ok(self)
199    }
200
201    /// Time allowed for the TLS handshake (default 10 s)
202    pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
203        self.handshake_timeout = timeout;
204        self
205    }
206
207    /// Read the certificate files again and switch new connections to them
208    ///
209    /// Returns `Ok(true)` when the files had changed and were applied,
210    /// `Ok(false)` when they are unchanged. On error the previous certificate
211    /// stays in use.
212    pub fn reload(&self) -> Result<bool> {
213        let (cert_path, key_path, previous) = {
214            let st = self.lock();
215            let Some(src) = &st.source else {
216                return Ok(false);
217            };
218            (src.cert_path.clone(), src.key_path.clone(), src.modified)
219        };
220        let now = modified(&cert_path).zip(modified(&key_path));
221        if previous.is_some() && now == previous {
222            return Ok(false);
223        }
224        let chain = std::fs::read(&cert_path)
225            .map_err(|e| tls_error(&format!("cannot read {}", cert_path.display()), e))?;
226        let key = std::fs::read(&key_path)
227            .map_err(|e| tls_error(&format!("cannot read {}", key_path.display()), e))?;
228        let auth = self.client_auth_policy();
229        let config = build_server_config(&chain, &key, auth.as_deref())?;
230        let mut st = self.lock();
231        st.acceptor = TlsAcceptor::from(Arc::new(config));
232        st.chain_pem = chain;
233        st.key_pem = key;
234        if let Some(src) = st.source.as_mut() {
235            src.modified = now;
236        }
237        Ok(true)
238    }
239
240    /// Perform the handshake; returns the stream, the DER of the client's
241    /// leaf certificate if it presented one, and whether the client chose HTTP/2
242    pub(crate) async fn accept(
243        &self,
244        stream: TcpStream,
245    ) -> std::io::Result<(TlsStream<TcpStream>, Option<Vec<u8>>, bool)> {
246        let acceptor = self.lock().acceptor.clone();
247        let tls = tokio::time::timeout(self.handshake_timeout, acceptor.accept(stream))
248            .await
249            .map_err(|_| {
250                std::io::Error::new(std::io::ErrorKind::TimedOut, "TLS handshake timed out")
251            })??;
252        let cert = tls
253            .get_ref()
254            .1
255            .peer_certificates()
256            .and_then(|c| c.first())
257            .map(|c| c.as_ref().to_vec());
258        let h2 = tls.get_ref().1.alpn_protocol() == Some(b"h2");
259        Ok((tls, cert, h2))
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn self_signed() -> (String, String) {
268        let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
269        (ck.cert.pem(), ck.signing_key.serialize_pem())
270    }
271
272    #[test]
273    fn valid_pem_builds_a_config() {
274        let (cert, key) = self_signed();
275        assert!(TlsConfig::from_pem(cert.as_bytes(), key.as_bytes()).is_ok());
276    }
277
278    #[test]
279    fn bad_material_is_reported_not_panicked() {
280        let (cert, key) = self_signed();
281        let (_, other_key) = self_signed();
282        let msg = |r: Result<TlsConfig>| r.err().expect("should fail").message().to_string();
283
284        assert!(msg(TlsConfig::from_pem(b"", key.as_bytes())).contains("no certificate"));
285        assert!(msg(TlsConfig::from_pem(b"garbage", key.as_bytes())).contains("TLS"));
286        assert!(msg(TlsConfig::from_pem(cert.as_bytes(), b"")).contains("private key"));
287        assert!(msg(TlsConfig::from_pem(
288            cert.as_bytes(),
289            b"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n"
290        ))
291        .contains("TLS"));
292        assert!(
293            msg(TlsConfig::from_pem(cert.as_bytes(), other_key.as_bytes()))
294                .contains("do not match")
295        );
296        assert!(
297            msg(TlsConfig::from_pem_files("/nope/cert.pem", "/nope/key.pem"))
298                .contains("cannot read")
299        );
300    }
301
302    #[test]
303    fn reload_applies_changes_and_keeps_the_old_certificate_on_errors() {
304        let dir = std::env::temp_dir().join(format!("ullun-tls-unit-{}", std::process::id()));
305        std::fs::create_dir_all(&dir).unwrap();
306        let (cert_path, key_path) = (dir.join("cert.pem"), dir.join("key.pem"));
307        let (c1, k1) = self_signed();
308        std::fs::write(&cert_path, &c1).unwrap();
309        std::fs::write(&key_path, &k1).unwrap();
310
311        let tls = TlsConfig::from_pem_files(&cert_path, &key_path).unwrap();
312        assert!(!tls.reload().unwrap(), "unchanged files are not re-read");
313
314        // a renewal arrives (make sure the modification time differs)
315        std::thread::sleep(Duration::from_millis(1100));
316        let (c2, k2) = self_signed();
317        std::fs::write(&cert_path, &c2).unwrap();
318        std::fs::write(&key_path, &k2).unwrap();
319        assert!(tls.reload().unwrap());
320        assert!(!tls.reload().unwrap());
321
322        // a broken renewal is an error and changes nothing
323        std::thread::sleep(Duration::from_millis(1100));
324        std::fs::write(&cert_path, "half written").unwrap();
325        assert!(tls.reload().is_err());
326        assert!(tls.reload().is_err(), "still broken, still reported");
327        std::fs::remove_dir_all(&dir).ok();
328    }
329
330    #[test]
331    fn configs_from_pem_text_do_not_reload_but_take_client_auth() {
332        let (cert, key) = self_signed();
333        let tls = TlsConfig::from_pem(cert.as_bytes(), key.as_bytes()).unwrap();
334        assert!(!tls.reload().unwrap());
335        // the CA argument must be a certificate
336        assert!(tls.clone().client_auth(b"", true).is_err());
337        assert!(tls.clone().client_auth(key.as_bytes(), true).is_err());
338        assert!(tls.client_auth(cert.as_bytes(), true).is_ok());
339    }
340}