Skip to main content

featherbit/server/
tls.rs

1//! TLS termination and protocol (HTTP/1.1 vs HTTP/2) selection for the
2//! listeners.
3//!
4//! This is the single place that knows how to load a PEM cert/key, build a
5//! rustls [`ServerConfig`] (with ALPN and a `min_version` floor), turn it into
6//! a [`tokio_rustls::TlsAcceptor`], and serve a connection over the negotiated
7//! protocol. Both the data-plane listener (`server::listener`) and the optional
8//! Admin API TLS reuse [`build_acceptor`] + [`serve_connection`], so cert
9//! handling lives in exactly one module.
10//!
11//! Everything is pinned to the rustls **ring** provider, matching the rest of
12//! the dependency tree (see [`install_crypto_provider`]).
13
14use std::path::PathBuf;
15use std::sync::Arc;
16use std::time::Duration;
17
18use arc_swap::ArcSwap;
19use hyper::body::{Body, Incoming};
20use hyper::rt::{Read, Write};
21use hyper::service::Service;
22use hyper::{Request, Response};
23use hyper_util::rt::TokioExecutor;
24use hyper_util::server::conn::auto;
25use notify::{Event, EventKind, RecursiveMode, Watcher};
26use rustls::pki_types::{CertificateDer, PrivateKeyDer};
27use rustls::server::{ClientHello, ResolvesServerCert};
28use rustls::sign::CertifiedKey;
29use rustls::ServerConfig;
30use tokio::sync::mpsc;
31use tokio_rustls::TlsAcceptor;
32use tracing::{error, info, warn};
33
34use crate::config::TlsConfig;
35use crate::stream::sni::SniPattern;
36
37/// Resolves the server certificate by ClientHello SNI hostname (exact or
38/// single-label wildcard), falling back to the default cert. Enables
39/// multi-domain TLS termination on one listener.
40#[derive(Debug)]
41struct SniCertResolver {
42    certs: Vec<(SniPattern, Arc<CertifiedKey>)>,
43    default: Arc<CertifiedKey>,
44}
45
46impl ResolvesServerCert for SniCertResolver {
47    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
48        if let Some(name) = client_hello.server_name() {
49            for (pattern, ck) in &self.certs {
50                if pattern.matches(name) {
51                    return Some(ck.clone());
52                }
53            }
54        }
55        Some(self.default.clone())
56    }
57}
58
59/// Loads a cert chain + key into a validated [`CertifiedKey`] (same load +
60/// key-match check `with_single_cert` performs).
61fn certified_key(
62    chain: Vec<CertificateDer<'static>>,
63    key: PrivateKeyDer<'static>,
64    provider: &rustls::crypto::CryptoProvider,
65) -> Result<CertifiedKey, TlsError> {
66    CertifiedKey::from_der(chain, key, provider).map_err(|e| TlsError::RustlsConfig(e.to_string()))
67}
68
69/// A live TLS `ServerConfig` that can be atomically swapped for cert rotation.
70/// New connections read the current config; in-flight ones are unaffected.
71pub type SharedTlsConfig = Arc<ArcSwap<ServerConfig>>;
72
73/// Failure loading a cert/key or building the rustls config. Every variant
74/// carries enough detail (the offending path or message) for a fail-fast
75/// startup error.
76#[derive(Debug, thiserror::Error)]
77pub enum TlsError {
78    #[error("failed to read TLS certificate '{0}': {1}")]
79    CertRead(String, String),
80    #[error("TLS certificate file '{0}' contained no certificates")]
81    NoCerts(String),
82    #[error("failed to read TLS private key '{0}': {1}")]
83    KeyRead(String, String),
84    #[error("TLS private key file '{0}' contained no private key")]
85    NoKey(String),
86    #[error("unsupported TLS min_version '{0}' (expected \"1.2\" or \"1.3\")")]
87    BadMinVersion(String),
88    #[error("failed to build TLS server config: {0}")]
89    RustlsConfig(String),
90    #[error("failed to read client-CA bundle '{0}': {1}")]
91    ClientCaRead(String, String),
92    #[error("client-CA bundle '{0}' contained no certificates")]
93    NoClientCaCerts(String),
94    #[error("failed to build client certificate verifier: {0}")]
95    ClientVerifier(String),
96}
97
98/// Installs the process-level rustls **ring** `CryptoProvider` exactly once.
99///
100/// Both the ring and aws-lc-rs backends end up compiled in through the
101/// dependency tree; without a pinned default, rustls config builders panic on
102/// the ambiguity. Calling this more than once (from the data plane, admin, and
103/// the outbound client) is safe — `install_default` returning `Err` when a
104/// provider is already installed is a harmless no-op.
105pub fn install_crypto_provider() {
106    static INSTALL_PROVIDER: std::sync::Once = std::sync::Once::new();
107    INSTALL_PROVIDER.call_once(|| {
108        let _ = rustls::crypto::ring::default_provider().install_default();
109    });
110}
111
112/// Loads the PEM certificate chain at `path`. Errors if the file is missing or
113/// contains no certificates.
114pub fn load_cert_chain(path: &str) -> Result<Vec<CertificateDer<'static>>, TlsError> {
115    let data =
116        std::fs::read(path).map_err(|e| TlsError::CertRead(path.to_string(), e.to_string()))?;
117    let mut reader = std::io::BufReader::new(&data[..]);
118    let certs = rustls_pemfile::certs(&mut reader)
119        .collect::<Result<Vec<_>, _>>()
120        .map_err(|e| TlsError::CertRead(path.to_string(), e.to_string()))?;
121    if certs.is_empty() {
122        return Err(TlsError::NoCerts(path.to_string()));
123    }
124    Ok(certs)
125}
126
127/// Loads the PEM private key at `path` (PKCS#8, PKCS#1, or SEC1). Errors if the
128/// file is missing or contains no private key.
129pub fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, TlsError> {
130    let data =
131        std::fs::read(path).map_err(|e| TlsError::KeyRead(path.to_string(), e.to_string()))?;
132    let mut reader = std::io::BufReader::new(&data[..]);
133    rustls_pemfile::private_key(&mut reader)
134        .map_err(|e| TlsError::KeyRead(path.to_string(), e.to_string()))?
135        .ok_or_else(|| TlsError::NoKey(path.to_string()))
136}
137
138/// Loads a PEM CA bundle at `path` into a [`RootCertStore`] for verifying
139/// **client** certificates (mTLS). Errors if the file is missing or yields no
140/// usable certificates.
141fn load_client_ca_roots(path: &str) -> Result<rustls::RootCertStore, TlsError> {
142    let data =
143        std::fs::read(path).map_err(|e| TlsError::ClientCaRead(path.to_string(), e.to_string()))?;
144    let mut reader = std::io::BufReader::new(&data[..]);
145    let certs = rustls_pemfile::certs(&mut reader)
146        .collect::<Result<Vec<_>, _>>()
147        .map_err(|e| TlsError::ClientCaRead(path.to_string(), e.to_string()))?;
148    let mut roots = rustls::RootCertStore::empty();
149    let (added, _skipped) = roots.add_parsable_certificates(certs);
150    if added == 0 {
151        return Err(TlsError::NoClientCaCerts(path.to_string()));
152    }
153    Ok(roots)
154}
155
156/// Builds a rustls [`ServerConfig`] from `tls`, enforcing `min_version` and
157/// advertising ALPN `h2`+`http/1.1` when `http2_enabled` (else `http/1.1`
158/// only). Uses an explicit ring provider so the version floor is honored
159/// regardless of global-provider install ordering.
160pub fn build_server_config(
161    tls: &TlsConfig,
162    http2_enabled: bool,
163) -> Result<Arc<ServerConfig>, TlsError> {
164    install_crypto_provider();
165
166    let versions: &[&'static rustls::SupportedProtocolVersion] = match tls.min_version.as_str() {
167        "1.2" => &[&rustls::version::TLS13, &rustls::version::TLS12],
168        "1.3" => &[&rustls::version::TLS13],
169        other => return Err(TlsError::BadMinVersion(other.to_string())),
170    };
171
172    let chain = load_cert_chain(&tls.cert_path)?;
173    let key = load_private_key(&tls.key_path)?;
174
175    let provider = Arc::new(rustls::crypto::ring::default_provider());
176    let builder = ServerConfig::builder_with_provider(provider.clone())
177        .with_protocol_versions(versions)
178        .map_err(|e| TlsError::RustlsConfig(e.to_string()))?;
179
180    // mTLS: when a client-CA bundle is configured, verify client certs against
181    // it (required by default; optional when `client_cert_required` is false).
182    let builder = match &tls.client_ca_path {
183        Some(ca_path) => {
184            let roots = load_client_ca_roots(ca_path)?;
185            let vbuilder = rustls::server::WebPkiClientVerifier::builder_with_provider(
186                Arc::new(roots),
187                provider.clone(),
188            );
189            let vbuilder = if tls.client_cert_required {
190                vbuilder
191            } else {
192                vbuilder.allow_unauthenticated()
193            };
194            let verifier = vbuilder
195                .build()
196                .map_err(|e| TlsError::ClientVerifier(e.to_string()))?;
197            builder.with_client_cert_verifier(verifier)
198        }
199        None => builder.with_no_client_auth(),
200    };
201
202    // Certificate selection: a single cert, or an SNI resolver that presents a
203    // per-hostname cert (falling back to the default `cert_path`/`key_path`).
204    let mut config = if tls.sni_certs.is_empty() {
205        builder
206            .with_single_cert(chain, key)
207            .map_err(|e| TlsError::RustlsConfig(e.to_string()))?
208    } else {
209        let default = Arc::new(certified_key(chain, key, &provider)?);
210        let mut certs = Vec::with_capacity(tls.sni_certs.len());
211        for sc in &tls.sni_certs {
212            let c = load_cert_chain(&sc.cert_path)?;
213            let k = load_private_key(&sc.key_path)?;
214            certs.push((
215                SniPattern::parse(&sc.server_name),
216                Arc::new(certified_key(c, k, &provider)?),
217            ));
218        }
219        builder.with_cert_resolver(Arc::new(SniCertResolver { certs, default }))
220    };
221
222    config.alpn_protocols = if http2_enabled {
223        // h2 first so a client offering both prefers HTTP/2.
224        vec![b"h2".to_vec(), b"http/1.1".to_vec()]
225    } else {
226        vec![b"http/1.1".to_vec()]
227    };
228
229    Ok(Arc::new(config))
230}
231
232/// Builds a [`TlsAcceptor`] ready to wrap accepted TCP streams. Fail-fast: any
233/// cert/key/config error surfaces here at startup.
234///
235/// The listeners use [`build_reloadable`] + [`current_acceptor`] so certs can
236/// hot-reload; this one-shot form is kept for tests and simple embedding.
237#[allow(dead_code)]
238pub fn build_acceptor(tls: &TlsConfig, http2_enabled: bool) -> Result<TlsAcceptor, TlsError> {
239    Ok(TlsAcceptor::from(build_server_config(tls, http2_enabled)?))
240}
241
242/// Builds a hot-reloadable TLS config: the initial `ServerConfig` wrapped in an
243/// [`ArcSwap`] so [`spawn_cert_watcher`] can swap it in on cert rotation.
244/// Fail-fast: a bad cert/key at startup surfaces here.
245pub fn build_reloadable(tls: &TlsConfig, http2_enabled: bool) -> Result<SharedTlsConfig, TlsError> {
246    Ok(Arc::new(ArcSwap::new(build_server_config(
247        tls,
248        http2_enabled,
249    )?)))
250}
251
252/// A [`TlsAcceptor`] over the **current** config. Call this per connection (it's
253/// an atomic load + `Arc` clone) so reloads take effect for new connections.
254pub fn current_acceptor(shared: &SharedTlsConfig) -> TlsAcceptor {
255    TlsAcceptor::from(shared.load_full())
256}
257
258/// Verified identity of an mTLS client, read from its leaf certificate.
259#[derive(Debug, Clone, PartialEq)]
260pub struct ClientCertIdentity {
261    /// Lowercase-hex SHA-256 fingerprint of the leaf certificate (stable id).
262    pub fingerprint: String,
263    /// Subject Common Name, if present.
264    pub subject_cn: Option<String>,
265    /// Subject Alternative Name DNS entries.
266    pub san_dns: Vec<String>,
267}
268
269/// The verified client identity on an mTLS connection, or `None` if the client
270/// presented no certificate (anonymous client in optional mode, or mTLS not
271/// enabled). Read after the handshake.
272pub fn client_cert_identity<IO>(
273    stream: &tokio_rustls::server::TlsStream<IO>,
274) -> Option<ClientCertIdentity> {
275    let leaf = stream.get_ref().1.peer_certificates()?.first()?;
276    let digest = ring::digest::digest(&ring::digest::SHA256, leaf.as_ref());
277    let fingerprint = digest
278        .as_ref()
279        .iter()
280        .map(|b| format!("{:02x}", b))
281        .collect();
282    let (subject_cn, san_dns) = parse_client_identity(leaf.as_ref());
283    Some(ClientCertIdentity {
284        fingerprint,
285        subject_cn,
286        san_dns,
287    })
288}
289
290/// Extracts the subject CN and SAN DNS names from a DER-encoded certificate.
291/// Panic-free: any parse error yields `(None, empty)`.
292fn parse_client_identity(der: &[u8]) -> (Option<String>, Vec<String>) {
293    use x509_parser::prelude::*;
294    let cert = match X509Certificate::from_der(der) {
295        Ok((_rem, cert)) => cert,
296        Err(_) => return (None, Vec::new()),
297    };
298    let subject_cn = cert
299        .subject()
300        .iter_common_name()
301        .filter_map(|a| a.as_str().ok())
302        .next()
303        .map(String::from);
304    let san_dns = match cert.subject_alternative_name() {
305        Ok(Some(ext)) => ext
306            .value
307            .general_names
308            .iter()
309            .filter_map(|gn| match gn {
310                GeneralName::DNSName(name) => Some((*name).to_string()),
311                _ => None,
312            })
313            .collect(),
314        _ => Vec::new(),
315    };
316    (subject_cn, san_dns)
317}
318
319/// Watches the cert/key files and hot-reloads `shared` when they change.
320///
321/// Mirrors [`crate::hot_reload::watch_config`]: an OS thread runs a `notify`
322/// watcher on each unique parent directory of the cert/key paths (so
323/// Kubernetes' atomic secret symlink swap is caught, not just direct writes),
324/// forwarding events to a debounced (500 ms) async loop. On each change the
325/// `ServerConfig` is rebuilt and atomically stored; a bad/partial cert during
326/// rotation is logged and the current config is **kept** (never crash or drop
327/// TLS mid-rotation). `label` names the listener in logs (e.g. `"data-plane"`).
328pub fn spawn_cert_watcher(
329    tls: TlsConfig,
330    http2_enabled: bool,
331    shared: SharedTlsConfig,
332    label: &'static str,
333) {
334    let (tx, mut rx) = mpsc::channel::<()>(1);
335
336    // Unique parent directories of every cert/key file (default + per-SNI), so
337    // rotating any of them triggers a reload.
338    let mut paths: Vec<&String> = vec![&tls.cert_path, &tls.key_path];
339    for sc in &tls.sni_certs {
340        paths.push(&sc.cert_path);
341        paths.push(&sc.key_path);
342    }
343    let mut dirs: Vec<PathBuf> = Vec::new();
344    for path in paths {
345        let dir = PathBuf::from(path)
346            .parent()
347            .map(PathBuf::from)
348            .unwrap_or_else(|| PathBuf::from("."));
349        if !dirs.contains(&dir) {
350            dirs.push(dir);
351        }
352    }
353
354    std::thread::spawn(move || {
355        let mut watcher =
356            match notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
357                if let Ok(event) = res {
358                    if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
359                        let _ = tx.blocking_send(());
360                    }
361                }
362            }) {
363                Ok(w) => w,
364                Err(e) => {
365                    error!("{} cert watcher failed to start: {}", label, e);
366                    return;
367                }
368            };
369
370        for dir in &dirs {
371            if let Err(e) = watcher.watch(dir, RecursiveMode::Recursive) {
372                error!("{} cert watcher failed to watch {:?}: {}", label, dir, e);
373            }
374        }
375        info!("{} TLS certificate watcher started on {:?}", label, dirs);
376
377        // Keep the watcher alive for the process lifetime.
378        loop {
379            std::thread::sleep(Duration::from_secs(3600));
380        }
381    });
382
383    tokio::spawn(async move {
384        loop {
385            if rx.recv().await.is_none() {
386                break;
387            }
388            // Debounce: coalesce a burst of filesystem events.
389            tokio::time::sleep(Duration::from_millis(500)).await;
390            while rx.try_recv().is_ok() {}
391
392            match build_server_config(&tls, http2_enabled) {
393                Ok(config) => {
394                    shared.store(config);
395                    info!("{} TLS certificate reloaded", label);
396                }
397                Err(e) => warn!(
398                    "{} TLS certificate reload failed (keeping current): {}",
399                    label, e
400                ),
401            }
402        }
403    });
404}
405
406/// Serves a single (already-handshaked, `TokioIo`-wrapped) connection.
407///
408/// When `http2_enabled`, the hyper-util **auto** builder sniffs the first bytes
409/// and dispatches to HTTP/1.1 or HTTP/2 — covering ALPN-negotiated h2 over TLS,
410/// h2c prior-knowledge over plaintext, and HTTP/1.1, in one call. Otherwise it
411/// serves HTTP/1.1 only. Connection-level errors are logged, not propagated, so
412/// one bad connection never affects the accept loop.
413///
414/// Both paths enable connection **upgrades** (`with_upgrades` /
415/// `serve_connection_with_upgrades`) so a handler that returns `101 Switching
416/// Protocols` (HTTP/1.1) or `200` (HTTP/2 extended CONNECT) can hand the raw
417/// stream to a WebSocket relay (see [`crate::server::websocket`]). The h2 arm
418/// also enables the RFC 8441 extended CONNECT protocol
419/// (`SETTINGS_ENABLE_CONNECT_PROTOCOL`) so clients can open WebSockets over
420/// HTTP/2; h1/h2 auto-detection is preserved.
421///
422/// The listeners drive connections via [`build_connection`] + a graceful-shutdown
423/// watcher; this await-and-log form is kept for tests and simple embedding.
424#[allow(dead_code)]
425pub async fn serve_connection<I, S, B>(io: I, service: S, http2_enabled: bool)
426where
427    I: Read + Write + Unpin + Send + 'static,
428    S: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
429    S::Future: Send + 'static,
430    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
431    B: Body + Send + 'static,
432    B::Data: Send,
433    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
434{
435    if let Err(err) = build_connection(io, service, http2_enabled).await {
436        error!("Connection error: {}", err);
437    }
438}
439
440/// Builds (but does not drive) a connection future for `io`, ready to be either
441/// `.await`ed directly or handed to a graceful-shutdown watcher.
442///
443/// Both protocol paths go through the hyper-util **auto** builder so they share
444/// one connection type that implements
445/// [`GracefulConnection`](hyper_util::server::graceful::GracefulConnection):
446/// `http1_only()` for strict HTTP/1.1, or h2 (auto-detected, extended CONNECT
447/// enabled) otherwise. `.into_owned()` detaches the connection from the builder
448/// so it is `'static` and can be spawned. See [`serve_connection`] for the
449/// simple await-and-log path.
450pub fn build_connection<I, S, B>(
451    io: I,
452    service: S,
453    http2_enabled: bool,
454) -> auto::UpgradeableConnection<'static, I, S, TokioExecutor>
455where
456    I: Read + Write + Unpin + Send + 'static,
457    S: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
458    S::Future: Send + 'static,
459    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
460    B: Body + Send + 'static,
461    B::Data: Send,
462    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
463{
464    if http2_enabled {
465        auto::Builder::new(TokioExecutor::new())
466            .http2()
467            .enable_connect_protocol()
468            .serve_connection_with_upgrades(io, service)
469            .into_owned()
470    } else {
471        auto::Builder::new(TokioExecutor::new())
472            .http1_only()
473            .serve_connection_with_upgrades(io, service)
474            .into_owned()
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::config::TlsConfig;
482
483    /// Writes a fresh self-signed cert+key to unique temp paths and returns a
484    /// `TlsConfig` pointing at them.
485    fn self_signed(
486        tag: &str,
487        min_version: &str,
488    ) -> (TlsConfig, std::path::PathBuf, std::path::PathBuf) {
489        let certified = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
490        let dir = std::env::temp_dir();
491        let pid = std::process::id();
492        let cert_path = dir.join(format!("featherbit_{}_{}.crt", tag, pid));
493        let key_path = dir.join(format!("featherbit_{}_{}.key", tag, pid));
494        std::fs::write(&cert_path, certified.cert.pem()).unwrap();
495        std::fs::write(&key_path, certified.key_pair.serialize_pem()).unwrap();
496        let tls = TlsConfig {
497            cert_path: cert_path.to_string_lossy().into_owned(),
498            key_path: key_path.to_string_lossy().into_owned(),
499            min_version: min_version.to_string(),
500            client_ca_path: None,
501            client_cert_required: true,
502            sni_certs: Vec::new(),
503        };
504        (tls, cert_path, key_path)
505    }
506
507    #[test]
508    fn test_install_crypto_provider_idempotent() {
509        install_crypto_provider();
510        install_crypto_provider();
511    }
512
513    #[test]
514    fn test_load_cert_and_key() {
515        let (tls, cert, key) = self_signed("load", "1.2");
516        assert!(!load_cert_chain(&tls.cert_path).unwrap().is_empty());
517        load_private_key(&tls.key_path).unwrap();
518        let _ = std::fs::remove_file(cert);
519        let _ = std::fs::remove_file(key);
520    }
521
522    #[test]
523    fn test_load_cert_missing_file() {
524        let err = load_cert_chain("does-not-exist.pem").unwrap_err();
525        assert!(matches!(err, TlsError::CertRead(_, _)));
526    }
527
528    #[test]
529    fn test_load_key_no_key_in_pem() {
530        // A cert-only file has no private key.
531        let (tls, cert, key) = self_signed("nokey", "1.2");
532        let err = load_private_key(&tls.cert_path).unwrap_err();
533        assert!(matches!(err, TlsError::NoKey(_)));
534        let _ = std::fs::remove_file(cert);
535        let _ = std::fs::remove_file(key);
536    }
537
538    #[test]
539    fn test_alpn_reflects_http2_flag() {
540        let (tls, cert, key) = self_signed("alpn", "1.2");
541
542        let with_h2 = build_server_config(&tls, true).unwrap();
543        assert_eq!(
544            with_h2.alpn_protocols,
545            vec![b"h2".to_vec(), b"http/1.1".to_vec()]
546        );
547
548        let without_h2 = build_server_config(&tls, false).unwrap();
549        assert_eq!(without_h2.alpn_protocols, vec![b"http/1.1".to_vec()]);
550
551        let _ = std::fs::remove_file(cert);
552        let _ = std::fs::remove_file(key);
553    }
554
555    #[test]
556    fn test_min_version_1_3_ok_and_bad_rejected() {
557        let (mut tls, cert, key) = self_signed("minver", "1.3");
558        build_server_config(&tls, true).unwrap();
559
560        tls.min_version = "sslv3".to_string();
561        let err = build_server_config(&tls, true).unwrap_err();
562        assert!(matches!(err, TlsError::BadMinVersion(v) if v == "sslv3"));
563
564        let _ = std::fs::remove_file(cert);
565        let _ = std::fs::remove_file(key);
566    }
567
568    /// End-to-end over a real socket: build an acceptor, serve one connection
569    /// with the auto builder, and hit it with an HTTPS client — exercising the
570    /// full TLS handshake + protocol negotiation path.
571    #[tokio::test]
572    async fn test_tls_round_trip() {
573        use bytes::Bytes;
574        use http_body_util::Full;
575        use hyper::service::service_fn;
576        use hyper_util::rt::TokioIo;
577        use tokio::net::TcpListener;
578
579        let (tls, cert, key) = self_signed("rt", "1.2");
580        let acceptor = build_acceptor(&tls, true).unwrap();
581
582        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
583        let addr = listener.local_addr().unwrap();
584
585        tokio::spawn(async move {
586            let (stream, _) = listener.accept().await.unwrap();
587            let tls_stream = match acceptor.accept(stream).await {
588                Ok(s) => s,
589                Err(_) => return,
590            };
591            let service = service_fn(|_req| async {
592                Ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from_static(b"ok"))))
593            });
594            serve_connection(TokioIo::new(tls_stream), service, true).await;
595        });
596
597        let client = reqwest::Client::builder()
598            .danger_accept_invalid_certs(true)
599            .use_rustls_tls() // reliably offers the h2 ALPN protocol
600            .build()
601            .unwrap();
602        let resp = client
603            .get(format!("https://{}/", addr))
604            .send()
605            .await
606            .unwrap();
607        assert_eq!(resp.status(), 200);
608        // ALPN advertised h2 first, so a modern HTTPS client negotiates HTTP/2.
609        assert_eq!(resp.version(), reqwest::Version::HTTP_2);
610        assert_eq!(resp.text().await.unwrap(), "ok");
611
612        let _ = std::fs::remove_file(cert);
613        let _ = std::fs::remove_file(key);
614    }
615
616    /// Generates a fresh self-signed cert and writes it to the given paths
617    /// (overwriting) — each call produces a distinct cert (new key/serial).
618    fn write_fresh_cert(cert_path: &std::path::Path, key_path: &std::path::Path) {
619        let certified = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
620        std::fs::write(cert_path, certified.cert.pem()).unwrap();
621        std::fs::write(key_path, certified.key_pair.serialize_pem()).unwrap();
622    }
623
624    /// A client cert verifier that records the presented leaf certificate and
625    /// accepts everything (test-only).
626    #[derive(Debug)]
627    struct CapturingVerifier {
628        captured: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>,
629        provider: rustls::crypto::CryptoProvider,
630    }
631
632    impl rustls::client::danger::ServerCertVerifier for CapturingVerifier {
633        fn verify_server_cert(
634            &self,
635            end_entity: &rustls::pki_types::CertificateDer<'_>,
636            _intermediates: &[rustls::pki_types::CertificateDer<'_>],
637            _server_name: &rustls::pki_types::ServerName<'_>,
638            _ocsp_response: &[u8],
639            _now: rustls::pki_types::UnixTime,
640        ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
641            *self.captured.lock().unwrap() = Some(end_entity.as_ref().to_vec());
642            Ok(rustls::client::danger::ServerCertVerified::assertion())
643        }
644        fn verify_tls12_signature(
645            &self,
646            message: &[u8],
647            cert: &rustls::pki_types::CertificateDer<'_>,
648            dss: &rustls::DigitallySignedStruct,
649        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
650            rustls::crypto::verify_tls12_signature(
651                message,
652                cert,
653                dss,
654                &self.provider.signature_verification_algorithms,
655            )
656        }
657        fn verify_tls13_signature(
658            &self,
659            message: &[u8],
660            cert: &rustls::pki_types::CertificateDer<'_>,
661            dss: &rustls::DigitallySignedStruct,
662        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
663            rustls::crypto::verify_tls13_signature(
664                message,
665                cert,
666                dss,
667                &self.provider.signature_verification_algorithms,
668            )
669        }
670        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
671            self.provider
672                .signature_verification_algorithms
673                .supported_schemes()
674        }
675    }
676
677    /// Binds a listener that serves the *current* config per connection and
678    /// returns its address.
679    async fn spawn_reload_server(shared: SharedTlsConfig) -> std::net::SocketAddr {
680        use tokio::net::TcpListener;
681        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
682        let addr = listener.local_addr().unwrap();
683        tokio::spawn(async move {
684            while let Ok((stream, _)) = listener.accept().await {
685                let acceptor = current_acceptor(&shared);
686                tokio::spawn(async move {
687                    let _ = acceptor.accept(stream).await; // complete handshake, then drop
688                });
689            }
690        });
691        addr
692    }
693
694    /// Connects to `addr` (SNI `localhost`) and returns the presented leaf cert.
695    async fn served_leaf_cert(addr: std::net::SocketAddr) -> Vec<u8> {
696        served_leaf_cert_sni(addr, "localhost").await
697    }
698
699    /// Connects to `addr` with the given SNI and returns the presented leaf cert.
700    async fn served_leaf_cert_sni(addr: std::net::SocketAddr, sni: &str) -> Vec<u8> {
701        use tokio::net::TcpStream;
702        install_crypto_provider();
703        let captured = std::sync::Arc::new(std::sync::Mutex::new(None));
704        let verifier = std::sync::Arc::new(CapturingVerifier {
705            captured: captured.clone(),
706            provider: rustls::crypto::ring::default_provider(),
707        });
708        let config = rustls::ClientConfig::builder()
709            .dangerous()
710            .with_custom_certificate_verifier(verifier)
711            .with_no_client_auth();
712        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
713        let tcp = TcpStream::connect(addr).await.unwrap();
714        let name = rustls::pki_types::ServerName::try_from(sni.to_string()).unwrap();
715        let _ = connector.connect(name, tcp).await.unwrap();
716        let leaf = captured.lock().unwrap().clone();
717        leaf.expect("no server certificate captured")
718    }
719
720    #[tokio::test]
721    async fn test_cert_hot_reload_swaps_served_cert() {
722        let dir = std::env::temp_dir();
723        let pid = std::process::id();
724        let cert = dir.join(format!("featherbit_reload_{}.crt", pid));
725        let key = dir.join(format!("featherbit_reload_{}.key", pid));
726        write_fresh_cert(&cert, &key);
727        let tls = TlsConfig {
728            cert_path: cert.to_string_lossy().into_owned(),
729            key_path: key.to_string_lossy().into_owned(),
730            min_version: "1.2".to_string(),
731            client_ca_path: None,
732            client_cert_required: true,
733            sni_certs: Vec::new(),
734        };
735
736        let shared = build_reloadable(&tls, false).unwrap();
737        let addr = spawn_reload_server(shared.clone()).await;
738
739        let leaf_a = served_leaf_cert(addr).await;
740
741        // Rotate: overwrite the files with a new cert and swap it in (the
742        // deterministic path the watcher also takes).
743        write_fresh_cert(&cert, &key);
744        shared.store(build_server_config(&tls, false).unwrap());
745
746        let leaf_b = served_leaf_cert(addr).await;
747
748        assert_ne!(
749            leaf_a, leaf_b,
750            "served certificate should change after reload"
751        );
752
753        let _ = std::fs::remove_file(&cert);
754        let _ = std::fs::remove_file(&key);
755    }
756
757    #[tokio::test]
758    async fn test_cert_hot_reload_via_file_watcher() {
759        let dir = std::env::temp_dir();
760        let pid = std::process::id();
761        let cert = dir.join(format!("featherbit_watch_{}.crt", pid));
762        let key = dir.join(format!("featherbit_watch_{}.key", pid));
763        write_fresh_cert(&cert, &key);
764        let tls = TlsConfig {
765            cert_path: cert.to_string_lossy().into_owned(),
766            key_path: key.to_string_lossy().into_owned(),
767            min_version: "1.2".to_string(),
768            client_ca_path: None,
769            client_cert_required: true,
770            sni_certs: Vec::new(),
771        };
772
773        let shared = build_reloadable(&tls, false).unwrap();
774        spawn_cert_watcher(tls.clone(), false, shared.clone(), "test");
775        let addr = spawn_reload_server(shared.clone()).await;
776
777        let leaf_a = served_leaf_cert(addr).await;
778
779        // Overwrite the cert files; the watcher should reload within a few
780        // hundred ms (500ms debounce + notify latency).
781        write_fresh_cert(&cert, &key);
782
783        let mut leaf_b = leaf_a.clone();
784        for _ in 0..50 {
785            tokio::time::sleep(Duration::from_millis(200)).await;
786            leaf_b = served_leaf_cert(addr).await;
787            if leaf_b != leaf_a {
788                break;
789            }
790        }
791        assert_ne!(
792            leaf_a, leaf_b,
793            "file watcher should hot-reload the certificate"
794        );
795
796        let _ = std::fs::remove_file(&cert);
797        let _ = std::fs::remove_file(&key);
798    }
799
800    // ---- mTLS (client-certificate authentication) ----
801
802    fn gen_ca() -> (rcgen::Certificate, rcgen::KeyPair) {
803        let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
804        params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
805        let key = rcgen::KeyPair::generate().unwrap();
806        let cert = params.self_signed(&key).unwrap();
807        (cert, key)
808    }
809
810    fn gen_signed(
811        cn: &str,
812        ca_cert: &rcgen::Certificate,
813        ca_key: &rcgen::KeyPair,
814    ) -> (rcgen::Certificate, rcgen::KeyPair) {
815        // SAN = [cn], and an explicit subject CN so identity extraction has both.
816        let mut params = rcgen::CertificateParams::new(vec![cn.to_string()]).unwrap();
817        params
818            .distinguished_name
819            .push(rcgen::DnType::CommonName, cn);
820        let key = rcgen::KeyPair::generate().unwrap();
821        let cert = params.signed_by(&key, ca_cert, ca_key).unwrap();
822        (cert, key)
823    }
824
825    fn sha256_hex(bytes: &[u8]) -> String {
826        ring::digest::digest(&ring::digest::SHA256, bytes)
827            .as_ref()
828            .iter()
829            .map(|b| format!("{:02x}", b))
830            .collect()
831    }
832
833    /// Writes a self-signed server cert + CA bundle to temp files and returns a
834    /// `TlsConfig` with mTLS configured, plus the CA cert/key for signing
835    /// clients and the temp paths for cleanup.
836    fn mtls_config(
837        tag: &str,
838        required: bool,
839    ) -> (
840        TlsConfig,
841        rcgen::Certificate,
842        rcgen::KeyPair,
843        Vec<std::path::PathBuf>,
844    ) {
845        let (ca_cert, ca_key) = gen_ca();
846        let server = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
847        let dir = std::env::temp_dir();
848        let pid = std::process::id();
849        let scert = dir.join(format!("fb_mtls_{}_{}.crt", tag, pid));
850        let skey = dir.join(format!("fb_mtls_{}_{}.key", tag, pid));
851        let ca = dir.join(format!("fb_mtls_{}_{}_ca.crt", tag, pid));
852        std::fs::write(&scert, server.cert.pem()).unwrap();
853        std::fs::write(&skey, server.key_pair.serialize_pem()).unwrap();
854        std::fs::write(&ca, ca_cert.pem()).unwrap();
855        let tls = TlsConfig {
856            cert_path: scert.to_string_lossy().into_owned(),
857            key_path: skey.to_string_lossy().into_owned(),
858            min_version: "1.2".to_string(),
859            client_ca_path: Some(ca.to_string_lossy().into_owned()),
860            client_cert_required: required,
861            sni_certs: Vec::new(),
862        };
863        (tls, ca_cert, ca_key, vec![scert, skey, ca])
864    }
865
866    /// Spawns a TLS server that captures the client-cert identity of each
867    /// accepted connection into the returned shared slot.
868    async fn spawn_mtls_server(
869        tls: &TlsConfig,
870    ) -> (
871        std::net::SocketAddr,
872        std::sync::Arc<std::sync::Mutex<Option<ClientCertIdentity>>>,
873    ) {
874        use tokio::net::TcpListener;
875        let acceptor = build_acceptor(tls, false).unwrap();
876        let captured = std::sync::Arc::new(std::sync::Mutex::new(None));
877        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
878        let addr = listener.local_addr().unwrap();
879        let cap = captured.clone();
880        tokio::spawn(async move {
881            use tokio::io::AsyncWriteExt;
882            while let Ok((stream, _)) = listener.accept().await {
883                let acceptor = acceptor.clone();
884                let cap = cap.clone();
885                tokio::spawn(async move {
886                    if let Ok(mut tls_stream) = acceptor.accept(stream).await {
887                        *cap.lock().unwrap() = client_cert_identity(&tls_stream);
888                        // Write a byte so an accepted client reads data (vs. a
889                        // rejected one reading the handshake alert).
890                        let _ = tls_stream.write_all(b"1").await;
891                        let _ = tls_stream.flush().await;
892                        tokio::time::sleep(Duration::from_millis(50)).await;
893                    }
894                });
895            }
896        });
897        (addr, captured)
898    }
899
900    /// Connects a TLS client, optionally presenting `(chain, key)`. Returns
901    /// whether the connection was **accepted** — not just that the client-side
902    /// handshake future resolved. In TLS 1.3 a server rejecting a missing client
903    /// cert lets the client's `connect()` resolve `Ok`, then surfaces the alert
904    /// on the first read; so we do a post-handshake read and treat a read error
905    /// (the alert) as rejection, and a clean EOF/read as acceptance.
906    async fn mtls_client_connects(
907        addr: std::net::SocketAddr,
908        client: Option<(
909            Vec<rustls::pki_types::CertificateDer<'static>>,
910            rustls::pki_types::PrivateKeyDer<'static>,
911        )>,
912    ) -> bool {
913        use tokio::io::AsyncReadExt;
914        use tokio::net::TcpStream;
915        install_crypto_provider();
916        let verifier = std::sync::Arc::new(CapturingVerifier {
917            captured: std::sync::Arc::new(std::sync::Mutex::new(None)),
918            provider: rustls::crypto::ring::default_provider(),
919        });
920        let builder = rustls::ClientConfig::builder()
921            .dangerous()
922            .with_custom_certificate_verifier(verifier);
923        let config = match client {
924            Some((chain, key)) => builder.with_client_auth_cert(chain, key).unwrap(),
925            None => builder.with_no_client_auth(),
926        };
927        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
928        let tcp = TcpStream::connect(addr).await.unwrap();
929        let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
930        let mut tls = match connector.connect(name, tcp).await {
931            Ok(t) => t,
932            Err(_) => return false,
933        };
934        let mut buf = [0u8; 1];
935        // Ok(0) = clean EOF (accepted, then server dropped); Ok(n) = data.
936        // Err = the server's rejection alert.
937        tls.read(&mut buf).await.is_ok()
938    }
939
940    fn client_material(
941        cert: &rcgen::Certificate,
942        key: &rcgen::KeyPair,
943    ) -> (
944        Vec<rustls::pki_types::CertificateDer<'static>>,
945        rustls::pki_types::PrivateKeyDer<'static>,
946    ) {
947        let chain = vec![cert.der().clone()];
948        let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(key.serialize_der().into());
949        (chain, key_der)
950    }
951
952    #[test]
953    fn test_parse_client_identity() {
954        let mut params = rcgen::CertificateParams::new(vec![
955            "a.example.com".to_string(),
956            "b.example.com".to_string(),
957        ])
958        .unwrap();
959        params
960            .distinguished_name
961            .push(rcgen::DnType::CommonName, "svc-a");
962        let key = rcgen::KeyPair::generate().unwrap();
963        let cert = params.self_signed(&key).unwrap();
964
965        let (cn, san) = parse_client_identity(cert.der().as_ref());
966        assert_eq!(cn.as_deref(), Some("svc-a"));
967        assert_eq!(
968            san,
969            vec!["a.example.com".to_string(), "b.example.com".to_string()]
970        );
971
972        // Malformed DER never panics.
973        let (cn, san) = parse_client_identity(&[0xff; 16]);
974        assert_eq!(cn, None);
975        assert!(san.is_empty());
976    }
977
978    #[test]
979    fn test_mtls_config_builds_and_rejects_empty_ca() {
980        let (tls, _ca_cert, _ca_key, paths) = mtls_config("cfg", true);
981        // Required and optional both build.
982        build_server_config(&tls, false).unwrap();
983        let mut optional = tls.clone();
984        optional.client_cert_required = false;
985        build_server_config(&optional, false).unwrap();
986
987        // A client-CA path with no certs is an error.
988        let empty = std::env::temp_dir().join(format!("fb_mtls_empty_{}.pem", std::process::id()));
989        std::fs::write(&empty, b"not a certificate").unwrap();
990        let mut bad = tls;
991        bad.client_ca_path = Some(empty.to_string_lossy().into_owned());
992        assert!(matches!(
993            build_server_config(&bad, false),
994            Err(TlsError::NoClientCaCerts(_))
995        ));
996
997        let _ = std::fs::remove_file(&empty);
998        for p in paths {
999            let _ = std::fs::remove_file(p);
1000        }
1001    }
1002
1003    #[tokio::test]
1004    async fn test_mtls_required_enforces_client_cert() {
1005        let (tls, ca_cert, ca_key, paths) = mtls_config("req", true);
1006        let (client_cert, client_key) = gen_signed("client-1", &ca_cert, &ca_key);
1007        let expected_fp = sha256_hex(client_cert.der().as_ref());
1008
1009        let (addr, captured) = spawn_mtls_server(&tls).await;
1010
1011        // Client presenting a CA-signed cert connects, and the server sees its
1012        // identity (fingerprint + CN + SAN).
1013        let ok = mtls_client_connects(addr, Some(client_material(&client_cert, &client_key))).await;
1014        assert!(ok, "client with a valid cert should connect");
1015        let mut id = None;
1016        for _ in 0..20 {
1017            tokio::time::sleep(Duration::from_millis(25)).await;
1018            id = captured.lock().unwrap().clone();
1019            if id.is_some() {
1020                break;
1021            }
1022        }
1023        let id = id.expect("server should capture client identity");
1024        assert_eq!(id.fingerprint, expected_fp);
1025        assert_eq!(id.subject_cn.as_deref(), Some("client-1"));
1026        assert!(id.san_dns.iter().any(|s| s == "client-1"));
1027
1028        // Client without a cert is rejected at the handshake.
1029        let ok = mtls_client_connects(addr, None).await;
1030        assert!(!ok, "client without a cert must be rejected when required");
1031
1032        for p in paths {
1033            let _ = std::fs::remove_file(p);
1034        }
1035    }
1036
1037    #[tokio::test]
1038    async fn test_mtls_optional_allows_anonymous() {
1039        let (tls, _ca_cert, _ca_key, paths) = mtls_config("opt", false);
1040        let (addr, captured) = spawn_mtls_server(&tls).await;
1041
1042        // With the cert optional, a client without one still connects.
1043        let ok = mtls_client_connects(addr, None).await;
1044        assert!(ok, "anonymous client should connect in optional mode");
1045        tokio::time::sleep(Duration::from_millis(75)).await;
1046        assert_eq!(captured.lock().unwrap().clone(), None);
1047
1048        for p in paths {
1049            let _ = std::fs::remove_file(p);
1050        }
1051    }
1052
1053    // ---- SNI multi-certificate termination ----
1054
1055    /// Writes a self-signed cert+key (for `sans`) to temp files, returning
1056    /// `(cert_path, key_path, leaf_der)`.
1057    fn write_named_cert(tag: &str, sans: Vec<String>) -> (String, String, Vec<u8>) {
1058        let certified = rcgen::generate_simple_self_signed(sans).unwrap();
1059        let dir = std::env::temp_dir();
1060        let pid = std::process::id();
1061        let cert = dir.join(format!("fb_sni_{}_{}.crt", tag, pid));
1062        let key = dir.join(format!("fb_sni_{}_{}.key", tag, pid));
1063        std::fs::write(&cert, certified.cert.pem()).unwrap();
1064        std::fs::write(&key, certified.key_pair.serialize_pem()).unwrap();
1065        let leaf = certified.cert.der().as_ref().to_vec();
1066        (
1067            cert.to_string_lossy().into_owned(),
1068            key.to_string_lossy().into_owned(),
1069            leaf,
1070        )
1071    }
1072
1073    #[tokio::test]
1074    async fn test_sni_multicert_selects_by_hostname() {
1075        use crate::config::SniCert;
1076
1077        let (def_cert, def_key, def_leaf) = write_named_cert("def", vec!["localhost".to_string()]);
1078        let (a_cert, a_key, a_leaf) = write_named_cert("a", vec!["a.example.com".to_string()]);
1079        let (w_cert, w_key, w_leaf) =
1080            write_named_cert("wild", vec!["x.tenant.example.com".to_string()]);
1081
1082        let tls = TlsConfig {
1083            cert_path: def_cert.clone(),
1084            key_path: def_key.clone(),
1085            min_version: "1.2".to_string(),
1086            client_ca_path: None,
1087            client_cert_required: true,
1088            sni_certs: vec![
1089                SniCert {
1090                    server_name: "a.example.com".to_string(),
1091                    cert_path: a_cert.clone(),
1092                    key_path: a_key.clone(),
1093                },
1094                SniCert {
1095                    server_name: "*.tenant.example.com".to_string(),
1096                    cert_path: w_cert.clone(),
1097                    key_path: w_key.clone(),
1098                },
1099            ],
1100        };
1101
1102        let shared = build_reloadable(&tls, false).unwrap();
1103        let addr = spawn_reload_server(shared).await;
1104
1105        // Exact match, wildcard match, and default fallback each get their cert.
1106        assert_eq!(served_leaf_cert_sni(addr, "a.example.com").await, a_leaf);
1107        assert_eq!(
1108            served_leaf_cert_sni(addr, "x.tenant.example.com").await,
1109            w_leaf
1110        );
1111        assert_eq!(
1112            served_leaf_cert_sni(addr, "unmatched.example.com").await,
1113            def_leaf
1114        );
1115        // Distinct certs, so the assertions above are meaningful.
1116        assert_ne!(a_leaf, def_leaf);
1117        assert_ne!(w_leaf, def_leaf);
1118
1119        for p in [def_cert, def_key, a_cert, a_key, w_cert, w_key] {
1120            let _ = std::fs::remove_file(p);
1121        }
1122    }
1123}