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