Skip to main content

furnace/tls/
mod.rs

1//! TLS for the HTTP and gRPC servers (feature `tls`): `--tls-cert` and `--tls-key`, and with
2//! `--tls-client-ca` mutual TLS, where clients must present a certificate signed by that CA.
3//!
4//! Certificates are reloaded without a restart: the files are checked every
5//! [`RELOAD_INTERVAL`], and new connections use the new certificate once it loads (connections
6//! already open keep theirs). A file that fails to load is logged and the previous
7//! certificate stays in use, so a half-written renewal does not take the server down.
8//!
9//! rustls runs the protocol, with the pure-Rust crypto provider of the rustls-pure crate
10//! (RustCrypto primitives, TLS 1.3, ECDSA and Ed25519 keys).
11
12use std::io;
13use std::net::SocketAddr;
14use std::path::{Path, PathBuf};
15use std::pin::Pin;
16use std::sync::{Arc, Mutex, RwLock};
17use std::task::{Context, Poll};
18use std::time::{Duration, SystemTime};
19
20use rustls::pki_types::pem::PemObject;
21use rustls::pki_types::{CertificateDer, PrivateKeyDer};
22use rustls::server::WebPkiClientVerifier;
23use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
24use tokio::net::{TcpListener, TcpStream};
25use tokio::sync::mpsc;
26use tokio_rustls::TlsAcceptor;
27use tokio_rustls::server::TlsStream;
28use tracing::{debug, info, warn};
29
30pub use crate::api::TlsSettings;
31
32/// How often the certificate files are checked for changes
33pub const RELOAD_INTERVAL: Duration = Duration::from_secs(10);
34/// A client that has not finished its handshake by then is dropped
35const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
36/// Handshaken connections waiting for the server to take them
37const ACCEPT_QUEUE: usize = 256;
38/// Handshakes in flight at once, per listener. Past it the listener stops accepting until one
39/// ends, and further connections wait in the operating system's backlog, so clients that open
40/// connections and never send a ClientHello cannot hold more than this many tasks and sockets
41/// (each for up to 10 seconds, the handshake timeout). `--max-concurrent-requests` bounds requests, which
42/// come after the handshake.
43pub const MAX_HANDSHAKES: usize = 1024;
44
45/// Why the certificate, key or client CA cannot be used. The underlying error is the
46/// [`source`](std::error::Error::source), boxed so that furnace's API does not depend on the
47/// TLS library's error types.
48#[derive(Debug)]
49#[non_exhaustive]
50pub enum Error {
51    /// A certificate file (`--tls-cert` or `--tls-client-ca`) cannot be read as PEM
52    Certificate {
53        /// The file
54        path: PathBuf,
55        /// What went wrong
56        source: BoxError,
57    },
58    /// A certificate file holds no certificate
59    NoCertificate {
60        /// The file
61        path: PathBuf,
62    },
63    /// `--tls-key` holds no PEM private key
64    PrivateKey {
65        /// The file
66        path: PathBuf,
67        /// What went wrong
68        source: BoxError,
69    },
70    /// A client CA certificate cannot be used to verify clients
71    ClientCa {
72        /// The `--tls-client-ca` file
73        path: PathBuf,
74        /// What went wrong
75        source: BoxError,
76    },
77    /// The certificate and key do not make a server configuration: the key is not one the
78    /// server takes (RSA, an unsupported curve) or does not match the certificate
79    CertificateAndKey {
80        /// `--tls-cert`
81        cert: PathBuf,
82        /// `--tls-key`
83        key: PathBuf,
84        /// What went wrong
85        source: BoxError,
86    },
87    /// The TLS library refused the protocol versions; a bug rather than a configuration error
88    Setup(BoxError),
89}
90
91impl std::fmt::Display for Error {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            Self::Certificate { path, source } | Self::ClientCa { path, source } => {
95                write!(f, "{}: {source}", path.display())
96            }
97            Self::NoCertificate { path } => write!(f, "{}: no PEM certificate", path.display()),
98            Self::PrivateKey { path, source } => {
99                write!(f, "{}: no PEM private key ({source})", path.display())
100            }
101            Self::CertificateAndKey { cert, key, source } => {
102                write!(f, "{} and {}: {source}", cert.display(), key.display())
103            }
104            Self::Setup(source) => write!(f, "TLS setup: {source}"),
105        }
106    }
107}
108
109impl std::error::Error for Error {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            Self::Certificate { source, .. }
113            | Self::PrivateKey { source, .. }
114            | Self::ClientCa { source, .. }
115            | Self::CertificateAndKey { source, .. }
116            | Self::Setup(source) => Some(&**source),
117            Self::NoCertificate { .. } => None,
118        }
119    }
120}
121
122/// The error inside an [`Error`]
123pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
124
125/// Which protocols a listener offers through ALPN
126#[derive(Debug, Clone, Copy, PartialEq)]
127pub enum Protocols {
128    /// HTTP/2 and HTTP/1.1, for the HTTP API
129    Http,
130    /// HTTP/2 only, for gRPC
131    Grpc,
132}
133
134/// The current rustls configurations, replaced when the files change
135pub struct TlsConfigs {
136    settings: TlsSettings,
137    http: RwLock<Arc<rustls::ServerConfig>>,
138    grpc: RwLock<Arc<rustls::ServerConfig>>,
139    /// The files' newest modification time at the last load or reload attempt
140    loaded: Mutex<Option<SystemTime>>,
141}
142
143impl TlsConfigs {
144    /// Load the certificate, key and client CA, or say what is wrong with them
145    pub fn load(settings: TlsSettings) -> Result<Arc<Self>, Error> {
146        let (http, grpc) = build(&settings)?;
147        let loaded = newest_change(&settings);
148        Ok(Arc::new(Self {
149            settings,
150            http: RwLock::new(http),
151            grpc: RwLock::new(grpc),
152            loaded: Mutex::new(loaded),
153        }))
154    }
155
156    /// Whether clients must present a certificate (`--tls-client-ca`)
157    pub fn mutual(&self) -> bool {
158        self.settings.client_ca.is_some()
159    }
160
161    fn current(&self, protocols: Protocols) -> Arc<rustls::ServerConfig> {
162        let config = match protocols {
163            Protocols::Http => &self.http,
164            Protocols::Grpc => &self.grpc,
165        };
166        config.read().unwrap_or_else(|e| e.into_inner()).clone()
167    }
168
169    /// Reload if a file changed since the last load; returns whether it did. On error the
170    /// previous configuration stays.
171    pub fn reload_if_changed(&self) -> Result<bool, Error> {
172        // One lock for the check and the update, held through the reload, so two callers
173        // cannot both see the change and load it twice
174        let mut loaded = self.loaded.lock().unwrap_or_else(|e| e.into_inner());
175        let changed = newest_change(&self.settings);
176        if changed == *loaded {
177            return Ok(false);
178        }
179        // Record the attempt either way, so a bad file is reported once, not every interval
180        *loaded = changed;
181        let (http, grpc) = build(&self.settings)?;
182        *self.http.write().unwrap_or_else(|e| e.into_inner()) = http;
183        *self.grpc.write().unwrap_or_else(|e| e.into_inner()) = grpc;
184        Ok(true)
185    }
186
187    /// Check the files every [`RELOAD_INTERVAL`] for as long as the server runs
188    pub fn watch(self: &Arc<Self>) {
189        let configs = Arc::downgrade(self);
190        tokio::spawn(async move {
191            let mut interval = tokio::time::interval(RELOAD_INTERVAL);
192            interval.tick().await;
193            loop {
194                interval.tick().await;
195                let Some(configs) = configs.upgrade() else {
196                    return;
197                };
198                match configs.reload_if_changed() {
199                    Ok(true) => info!("🔐 Reloaded the TLS certificate"),
200                    Ok(false) => {}
201                    Err(e) => warn!("Keeping the previous TLS certificate: {e}"),
202                }
203            }
204        });
205    }
206}
207
208/// The latest modification time among the files, `None` if one cannot be read
209fn newest_change(settings: &TlsSettings) -> Option<SystemTime> {
210    let mut newest = None;
211    for path in [&settings.cert, &settings.key]
212        .into_iter()
213        .chain(settings.client_ca.as_ref())
214    {
215        let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
216        newest = newest.max(Some(modified));
217    }
218    newest
219}
220
221fn build(
222    settings: &TlsSettings,
223) -> Result<(Arc<rustls::ServerConfig>, Arc<rustls::ServerConfig>), Error> {
224    let certs = read_certs(&settings.cert)?;
225    let key = PrivateKeyDer::from_pem_file(&settings.key).map_err(|e| Error::PrivateKey {
226        path: settings.key.clone(),
227        source: e.into(),
228    })?;
229    let provider = Arc::new(rustls_pure::provider());
230    let builder = rustls::ServerConfig::builder_with_provider(provider.clone())
231        .with_protocol_versions(&[&rustls::version::TLS13])
232        .map_err(|e| Error::Setup(e.into()))?;
233    let builder = match &settings.client_ca {
234        None => builder.with_no_client_auth(),
235        Some(ca) => {
236            let client_ca = |e: BoxError| Error::ClientCa {
237                path: ca.clone(),
238                source: e,
239            };
240            let mut roots = rustls::RootCertStore::empty();
241            for cert in read_certs(ca)? {
242                roots.add(cert).map_err(|e| client_ca(e.into()))?;
243            }
244            let verifier = WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
245                .build()
246                .map_err(|e| client_ca(e.into()))?;
247            builder.with_client_cert_verifier(verifier)
248        }
249    };
250    let config = builder
251        .with_single_cert(certs, key)
252        .map_err(|e| Error::CertificateAndKey {
253            cert: settings.cert.clone(),
254            key: settings.key.clone(),
255            source: e.into(),
256        })?;
257    let mut http = config.clone();
258    http.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
259    let mut grpc = config;
260    grpc.alpn_protocols = vec![b"h2".to_vec()];
261    Ok((Arc::new(http), Arc::new(grpc)))
262}
263
264fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, Error> {
265    let certs = CertificateDer::pem_file_iter(path)
266        .and_then(|certs| certs.collect::<Result<Vec<_>, _>>())
267        .map_err(|e| Error::Certificate {
268            path: path.to_path_buf(),
269            source: e.into(),
270        })?;
271    if certs.is_empty() {
272        return Err(Error::NoCertificate {
273            path: path.to_path_buf(),
274        });
275    }
276    Ok(certs)
277}
278
279/// A TLS connection, with the peer's address
280pub struct TlsConnection {
281    stream: TlsStream<TcpStream>,
282    local: Option<SocketAddr>,
283    peer: SocketAddr,
284}
285
286impl TlsConnection {
287    /// The client's address
288    pub fn peer(&self) -> SocketAddr {
289        self.peer
290    }
291
292    /// The address the client connected to
293    pub fn local(&self) -> Option<SocketAddr> {
294        self.local
295    }
296}
297
298impl AsyncRead for TlsConnection {
299    fn poll_read(
300        mut self: Pin<&mut Self>,
301        cx: &mut Context<'_>,
302        buf: &mut ReadBuf<'_>,
303    ) -> Poll<io::Result<()>> {
304        Pin::new(&mut self.stream).poll_read(cx, buf)
305    }
306}
307
308impl AsyncWrite for TlsConnection {
309    fn poll_write(
310        mut self: Pin<&mut Self>,
311        cx: &mut Context<'_>,
312        buf: &[u8],
313    ) -> Poll<io::Result<usize>> {
314        Pin::new(&mut self.stream).poll_write(cx, buf)
315    }
316
317    fn poll_write_vectored(
318        mut self: Pin<&mut Self>,
319        cx: &mut Context<'_>,
320        bufs: &[io::IoSlice<'_>],
321    ) -> Poll<io::Result<usize>> {
322        Pin::new(&mut self.stream).poll_write_vectored(cx, bufs)
323    }
324
325    fn is_write_vectored(&self) -> bool {
326        self.stream.is_write_vectored()
327    }
328
329    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
330        Pin::new(&mut self.stream).poll_flush(cx)
331    }
332
333    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
334        Pin::new(&mut self.stream).poll_shutdown(cx)
335    }
336}
337
338#[cfg(feature = "grpc")]
339impl tonic::transport::server::Connected for TlsConnection {
340    type ConnectInfo = tonic::transport::server::TcpConnectInfo;
341
342    fn connect_info(&self) -> Self::ConnectInfo {
343        tonic::transport::server::TcpConnectInfo {
344            local_addr: self.local(),
345            remote_addr: Some(self.peer()),
346        }
347    }
348}
349
350/// Accept TCP connections on `listener` and run their TLS handshakes concurrently, at most
351/// [`MAX_HANDSHAKES`] at once and each under a timeout; completed connections arrive on the
352/// returned channel. Stops when the receiver is dropped.
353pub fn accept(
354    listener: TcpListener,
355    configs: Arc<TlsConfigs>,
356    protocols: Protocols,
357) -> mpsc::Receiver<TlsConnection> {
358    accept_bounded(listener, configs, protocols, MAX_HANDSHAKES)
359}
360
361fn accept_bounded(
362    listener: TcpListener,
363    configs: Arc<TlsConfigs>,
364    protocols: Protocols,
365    max_handshakes: usize,
366) -> mpsc::Receiver<TlsConnection> {
367    let (tx, rx) = mpsc::channel(ACCEPT_QUEUE);
368    let local = listener.local_addr().ok();
369    let handshakes = Arc::new(tokio::sync::Semaphore::new(max_handshakes));
370    tokio::spawn(async move {
371        loop {
372            // A slot first, so a full set of handshakes stops accepting
373            let handshake = tokio::select! {
374                permit = handshakes.clone().acquire_owned() => match permit {
375                    Ok(permit) => permit,
376                    Err(_) => return,
377                },
378                () = tx.closed() => return,
379            };
380            let (tcp, peer) = tokio::select! {
381                accepted = listener.accept() => match accepted {
382                    Ok(accepted) => accepted,
383                    Err(e) => {
384                        // E.g. out of file descriptors: wait instead of spinning
385                        warn!("Accepting a connection failed: {e}");
386                        tokio::time::sleep(Duration::from_millis(50)).await;
387                        continue;
388                    }
389                },
390                () = tx.closed() => return,
391            };
392            let _ = tcp.set_nodelay(true);
393            let acceptor = TlsAcceptor::from(configs.current(protocols));
394            let tx = tx.clone();
395            tokio::spawn(async move {
396                let handshaken =
397                    tokio::time::timeout(HANDSHAKE_TIMEOUT, acceptor.accept(tcp)).await;
398                drop(handshake);
399                match handshaken {
400                    Ok(Ok(stream)) => {
401                        let _ = tx
402                            .send(TlsConnection {
403                                stream,
404                                local,
405                                peer,
406                            })
407                            .await;
408                    }
409                    Ok(Err(e)) => debug!("TLS handshake with {peer} failed: {e}"),
410                    Err(_) => debug!("TLS handshake with {peer} timed out"),
411                }
412            });
413        }
414    });
415    rx
416}
417
418/// [`accept`] as a listener for `axum::serve`
419pub struct TlsListener {
420    connections: mpsc::Receiver<TlsConnection>,
421    local: SocketAddr,
422}
423
424impl TlsListener {
425    /// Accept TLS connections on `listener` with the HTTP protocols, as [`accept`] does
426    pub fn new(listener: TcpListener, configs: Arc<TlsConfigs>) -> io::Result<Self> {
427        let local = listener.local_addr()?;
428        Ok(Self {
429            connections: accept(listener, configs, Protocols::Http),
430            local,
431        })
432    }
433}
434
435impl axum::serve::Listener for TlsListener {
436    type Io = TlsConnection;
437    type Addr = SocketAddr;
438
439    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
440        match self.connections.recv().await {
441            Some(connection) => {
442                let peer = connection.peer;
443                (connection, peer)
444            }
445            // The accept loop only ends when this receiver is gone
446            None => std::future::pending().await,
447        }
448    }
449
450    fn local_addr(&self) -> io::Result<Self::Addr> {
451        Ok(self.local)
452    }
453}
454
455/// Paths given for TLS, for messages
456pub fn describe(settings: &TlsSettings) -> String {
457    let mut text = format!("{}", settings.cert.display());
458    if let Some(ca) = &settings.client_ca {
459        text.push_str(&format!(", clients verified against {}", ca.display()));
460    }
461    text
462}
463
464#[cfg(test)]
465pub(crate) mod tests {
466    use super::*;
467    use crate::api::tests::router;
468    use crate::api::{ClientAddr, ServerConfig};
469    use crate::model::{DummyModel, Model};
470    use std::path::PathBuf;
471    use tokio::io::{AsyncReadExt, AsyncWriteExt};
472
473    const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/tls");
474
475    fn fixture(name: &str) -> PathBuf {
476        Path::new(FIXTURES).join(name)
477    }
478
479    /// The first certificate in fixture `name`. A failure names the file only: no certificate,
480    /// nor anything read from one, goes into test output
481    fn fixture_cert(name: &str) -> CertificateDer<'static> {
482        match read_certs(&fixture(name)) {
483            Ok(mut certs) if !certs.is_empty() => certs.remove(0),
484            _ => panic!("no certificate in the fixture {name}"),
485        }
486    }
487
488    pub(crate) fn settings(server: &str, client_ca: bool) -> TlsSettings {
489        TlsSettings {
490            cert: fixture(&format!("{server}.crt")),
491            key: fixture(&format!("{server}.key")),
492            client_ca: client_ca.then(|| fixture("ca.crt")),
493        }
494    }
495
496    /// An HTTPS server for a dummy model on a free port
497    async fn serve(configs: Arc<TlsConfigs>) -> SocketAddr {
498        let model = Model::new(
499            Box::new(DummyModel::new("test_model".into(), vec![4], vec![2])),
500            "dummy",
501            0,
502        );
503        let app = router(model, &ServerConfig::default()).unwrap();
504        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
505        let addr = listener.local_addr().unwrap();
506        let listener = TlsListener::new(listener, configs).unwrap();
507        tokio::spawn(async move {
508            axum::serve(
509                listener,
510                app.into_make_service_with_connect_info::<ClientAddr>(),
511            )
512            .await
513        });
514        addr
515    }
516
517    /// A client trusting the test CA, with the named client certificate if any
518    fn client(certificate: Option<&str>) -> tokio_rustls::TlsConnector {
519        tokio_rustls::TlsConnector::from(Arc::new(client_config(certificate, b"http/1.1")))
520    }
521
522    /// A client configuration trusting the test CA, offering `alpn`
523    pub(crate) fn client_config(certificate: Option<&str>, alpn: &[u8]) -> rustls::ClientConfig {
524        let mut roots = rustls::RootCertStore::empty();
525        roots.add(fixture_cert("ca.crt")).unwrap();
526        let builder =
527            rustls::ClientConfig::builder_with_provider(Arc::new(rustls_pure::provider()))
528                .with_protocol_versions(&[&rustls::version::TLS13])
529                .unwrap()
530                .with_root_certificates(roots);
531        let mut config = match certificate {
532            None => builder.with_no_client_auth(),
533            Some(name) => builder
534                .with_client_auth_cert(
535                    read_certs(&fixture(&format!("{name}.crt"))).unwrap(),
536                    PrivateKeyDer::from_pem_file(fixture(&format!("{name}.key"))).unwrap(),
537                )
538                .unwrap(),
539        };
540        config.alpn_protocols = vec![alpn.to_vec()];
541        config
542    }
543
544    /// `GET /healthz` over TLS: the status line and the server's certificate, or the error
545    async fn get_healthz(
546        addr: SocketAddr,
547        connector: &tokio_rustls::TlsConnector,
548    ) -> io::Result<(String, CertificateDer<'static>)> {
549        let tcp = TcpStream::connect(addr).await?;
550        let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
551        let mut tls = connector.connect(name, tcp).await?;
552        let certificate = tls
553            .get_ref()
554            .1
555            .peer_certificates()
556            .and_then(|chain| chain.first())
557            .cloned()
558            .ok_or_else(|| io::Error::other("the server sent no certificate"))?;
559        tls.write_all(b"GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
560            .await?;
561        let mut response = String::new();
562        tls.read_to_string(&mut response).await?;
563        let status = response.lines().next().unwrap_or_default().to_string();
564        Ok((status, certificate))
565    }
566
567    #[tokio::test]
568    async fn https_with_each_key_type() {
569        for server in ["server-p256", "server-p384", "server-ed25519"] {
570            let addr = serve(TlsConfigs::load(settings(server, false)).unwrap()).await;
571            let (status, certificate) = get_healthz(addr, &client(None)).await.unwrap();
572            assert_eq!(status, "HTTP/1.1 200 OK", "{server}");
573            let expected = fixture_cert(&format!("{server}.crt"));
574            assert!(
575                certificate == expected,
576                "{server}: another certificate was served"
577            );
578        }
579    }
580
581    #[tokio::test]
582    async fn handshakes_in_flight_are_bounded() {
583        let configs = TlsConfigs::load(settings("server-p256", false)).unwrap();
584        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
585        let addr = listener.local_addr().unwrap();
586        let mut connections = accept_bounded(listener, configs, Protocols::Http, 1);
587        // A client that connects and never says hello holds the only handshake...
588        let silent = TcpStream::connect(addr).await.unwrap();
589        let handshake = tokio::spawn(async move {
590            let tcp = TcpStream::connect(addr).await?;
591            let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
592            client(None).connect(name, tcp).await
593        });
594        // ...so the next one waits (a slow machine only makes this wait longer)
595        tokio::time::sleep(Duration::from_millis(100)).await;
596        assert!(!handshake.is_finished());
597        // and goes through once the silent client leaves
598        drop(silent);
599        let handshaken = tokio::time::timeout(Duration::from_secs(10), handshake)
600            .await
601            .expect("the handshake goes through")
602            .unwrap();
603        assert!(handshaken.is_ok(), "{handshaken:?}");
604        assert!(connections.recv().await.is_some());
605    }
606
607    #[tokio::test]
608    async fn plain_http_gets_no_answer() {
609        let addr = serve(TlsConfigs::load(settings("server-p256", false)).unwrap()).await;
610        let mut tcp = TcpStream::connect(addr).await.unwrap();
611        tcp.write_all(b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n")
612            .await
613            .unwrap();
614        let mut response = Vec::new();
615        let _ = tcp.read_to_end(&mut response).await;
616        assert!(!String::from_utf8_lossy(&response).contains("200 OK"));
617    }
618
619    #[tokio::test]
620    async fn mutual_tls_needs_a_certificate_from_the_ca() {
621        let configs = TlsConfigs::load(settings("server-p256", true)).unwrap();
622        assert!(configs.mutual());
623        let addr = serve(configs).await;
624        // One client certificate per key type the provider verifies
625        for client_name in ["client", "client-p384", "client-ed25519"] {
626            let (status, _) = get_healthz(addr, &client(Some(client_name))).await.unwrap();
627            assert_eq!(status, "HTTP/1.1 200 OK", "{client_name}");
628        }
629        // In TLS 1.3 the server checks the client's certificate after the client has finished
630        // its side of the handshake, so the refusal shows when the request goes unanswered.
631        // Only the status line is reported, not the certificate the server sent
632        for (client_name, presented) in [("no certificate", None), ("stranger", Some("stranger"))] {
633            let status = get_healthz(addr, &client(presented))
634                .await
635                .map(|(status, _)| status);
636            assert!(
637                !matches!(&status, Ok(status) if status.contains("200")),
638                "{client_name}: {status:?}"
639            );
640        }
641    }
642
643    #[tokio::test]
644    async fn certificates_are_reloaded_and_bad_files_keep_the_old_one() {
645        let dir = tempfile::tempdir().unwrap();
646        let (cert, key) = (dir.path().join("tls.crt"), dir.path().join("tls.key"));
647        // Each install gets a later timestamp, even on file systems with coarse ones
648        let mut stamp = SystemTime::now();
649        let mut touch = |path: &Path| {
650            stamp += Duration::from_secs(60);
651            let file = std::fs::File::options().write(true).open(path).unwrap();
652            file.set_modified(stamp).unwrap();
653        };
654        let mut install = |server: &str| {
655            std::fs::copy(fixture(&format!("{server}.crt")), &cert).unwrap();
656            std::fs::copy(fixture(&format!("{server}.key")), &key).unwrap();
657            touch(&cert);
658            touch(&key);
659        };
660        install("server-p256");
661        let configs = TlsConfigs::load(TlsSettings {
662            cert: cert.clone(),
663            key: key.clone(),
664            client_ca: None,
665        })
666        .unwrap();
667        let addr = serve(configs.clone()).await;
668        let p256 = fixture_cert("server-p256.crt");
669        let p384 = fixture_cert("server-p384.crt");
670        let served = || async { get_healthz(addr, &client(None)).await.unwrap().1 };
671        assert!(served().await == p256, "the first certificate is served");
672
673        assert!(matches!(configs.reload_if_changed(), Ok(false)));
674        install("server-p384");
675        assert!(matches!(configs.reload_if_changed(), Ok(true)));
676        assert!(served().await == p384, "the renewed certificate is served");
677
678        // A half-written renewal: the error is reported once and the certificate stays
679        std::fs::write(&key, "not a key").unwrap();
680        let file = std::fs::File::options().write(true).open(&key).unwrap();
681        file.set_modified(SystemTime::now() + Duration::from_secs(3600))
682            .unwrap();
683        assert!(matches!(
684            configs.reload_if_changed(),
685            Err(Error::PrivateKey { .. })
686        ));
687        assert!(matches!(configs.reload_if_changed(), Ok(false)));
688        assert!(served().await == p384, "the renewed certificate stays");
689    }
690
691    #[test]
692    fn key_formats() {
693        let load = |key: &str| {
694            build(&TlsSettings {
695                cert: fixture("server-p256.crt"),
696                key: fixture(key),
697                client_ca: None,
698            })
699        };
700        assert!(load("server-p256.key").is_ok());
701        assert!(load("server-p256-sec1.key").is_ok());
702        let error = build(&settings("server-rsa", false)).err().unwrap();
703        assert!(matches!(error, Error::CertificateAndKey { .. }), "{error}");
704        let error = error.to_string();
705        assert!(error.contains("RSA keys are not supported"), "{error}");
706        assert!(error.contains("ECDSA"), "{error}");
707        let error = load("server-p521.key").err().unwrap().to_string();
708        // P-521's object identifier
709        assert!(
710            error.contains("unsupported EC key curve 1.3.132.0.35"),
711            "{error}"
712        );
713
714        let missing = load("nope.key").err().unwrap().to_string();
715        assert!(missing.contains("nope.key"), "{missing}");
716    }
717}