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::pem::PemObject;
27use rustls::pki_types::{CertificateDer, PrivateKeyDer};
28use rustls::server::{ClientHello, ResolvesServerCert};
29use rustls::sign::CertifiedKey;
30use rustls::ServerConfig;
31use tokio::sync::mpsc;
32use tokio_rustls::TlsAcceptor;
33use tracing::{error, info, warn};
34
35use crate::acme::challenge::{ChallengeSolver, ACME_TLS_ALPN};
36use crate::acme::ManagedCerts;
37use crate::config::TlsConfig;
38use crate::stream::sni::SniPattern;
39
40/// What the ACME subsystem hands the TLS layer: the live managed-cert map and
41/// the TLS-ALPN-01 challenge solver. `None` everywhere ACME is not configured.
42#[derive(Clone, Debug)]
43pub struct AcmeHooks {
44    pub certs: ManagedCerts,
45    pub solver: Arc<dyn ChallengeSolver>,
46}
47
48/// Where one certificate slot's `CertifiedKey` comes from.
49#[derive(Debug)]
50enum CertSlot {
51    /// Loaded from `cert_path`/`key_path` at (re)build time.
52    File(Arc<CertifiedKey>),
53    /// Looked up in `AcmeHooks::certs` on every ClientHello (so a renewal is a
54    /// map swap, not a `ServerConfig` rebuild).
55    Managed(String),
56}
57
58/// Resolves the server certificate: a ClientHello offering exactly ALPN
59/// `acme-tls/1` is a CA validation and gets the pending challenge cert (or is
60/// refused); otherwise SNI hostname (exact or single-label wildcard) selects a
61/// slot, falling back to the default.
62#[derive(Debug)]
63struct SniCertResolver {
64    certs: Vec<(SniPattern, CertSlot)>,
65    default: CertSlot,
66    acme: Option<AcmeHooks>,
67}
68
69impl SniCertResolver {
70    fn slot_key(&self, slot: &CertSlot) -> Option<Arc<CertifiedKey>> {
71        match slot {
72            CertSlot::File(ck) => Some(ck.clone()),
73            CertSlot::Managed(id) => self
74                .acme
75                .as_ref()?
76                .certs
77                .load()
78                .get(id)
79                .map(|c| c.key.clone()),
80        }
81    }
82}
83
84impl ResolvesServerCert for SniCertResolver {
85    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
86        if let Some(hooks) = &self.acme {
87            let only_acme = client_hello
88                .alpn()
89                .map(|mut alpn| alpn.next() == Some(ACME_TLS_ALPN) && alpn.next().is_none())
90                .unwrap_or(false);
91            if only_acme {
92                // RFC 8737 §3: no pending challenge for this name ⇒ abort.
93                return hooks.solver.challenge_cert(client_hello.server_name()?);
94            }
95        }
96        if let Some(name) = client_hello.server_name() {
97            for (pattern, slot) in &self.certs {
98                if pattern.matches(name) {
99                    return self.slot_key(slot);
100                }
101            }
102        }
103        self.slot_key(&self.default)
104    }
105}
106
107/// Loads a cert chain + key into a validated [`CertifiedKey`] (same load +
108/// key-match check `with_single_cert` performs).
109fn certified_key(
110    chain: Vec<CertificateDer<'static>>,
111    key: PrivateKeyDer<'static>,
112    provider: &rustls::crypto::CryptoProvider,
113) -> Result<CertifiedKey, TlsError> {
114    CertifiedKey::from_der(chain, key, provider).map_err(|e| TlsError::RustlsConfig(e.to_string()))
115}
116
117/// A live TLS `ServerConfig` that can be atomically swapped for cert rotation.
118/// New connections read the current config; in-flight ones are unaffected.
119pub type SharedTlsConfig = Arc<ArcSwap<ServerConfig>>;
120
121/// Failure loading a cert/key or building the rustls config. Every variant
122/// carries enough detail (the offending path or message) for a fail-fast
123/// startup error.
124#[derive(Debug, thiserror::Error)]
125pub enum TlsError {
126    #[error("failed to read TLS certificate '{0}': {1}")]
127    CertRead(String, String),
128    #[error("TLS certificate file '{0}' contained no certificates")]
129    NoCerts(String),
130    #[error("failed to read TLS private key '{0}': {1}")]
131    KeyRead(String, String),
132    #[error("TLS private key file '{0}' contained no private key")]
133    NoKey(String),
134    #[error("unsupported TLS min_version '{0}' (expected \"1.2\" or \"1.3\")")]
135    BadMinVersion(String),
136    #[error("failed to build TLS server config: {0}")]
137    RustlsConfig(String),
138    #[error("failed to read client-CA bundle '{0}': {1}")]
139    ClientCaRead(String, String),
140    #[error("client-CA bundle '{0}' contained no certificates")]
141    NoClientCaCerts(String),
142    #[error("failed to build client certificate verifier: {0}")]
143    ClientVerifier(String),
144    #[error("TLS slot '{0}' has no certificate source (set cert_path/key_path, or acme)")]
145    MissingCertSource(String),
146    #[error(
147        "TLS slot '{0}' is ACME-managed but no ACME runtime was provided (is `acme:` configured?)"
148    )]
149    AcmeNotWired(String),
150}
151
152/// The file pair of a file-based slot. ACME-managed slots are handled by the
153/// resolver (see `CertSlot`); calling this on one is a wiring bug surfaced as
154/// `MissingCertSource`.
155fn file_pair<'a>(
156    cert: &'a Option<String>,
157    key: &'a Option<String>,
158    what: &str,
159) -> Result<(&'a str, &'a str), TlsError> {
160    match (cert, key) {
161        (Some(c), Some(k)) => Ok((c.as_str(), k.as_str())),
162        _ => Err(TlsError::MissingCertSource(what.to_string())),
163    }
164}
165
166/// Installs the process-level rustls **ring** `CryptoProvider` exactly once.
167///
168/// Both the ring and aws-lc-rs backends end up compiled in through the
169/// dependency tree; without a pinned default, rustls config builders panic on
170/// the ambiguity. Calling this more than once (from the data plane, admin, and
171/// the outbound client) is safe — `install_default` returning `Err` when a
172/// provider is already installed is a harmless no-op.
173pub fn install_crypto_provider() {
174    static INSTALL_PROVIDER: std::sync::Once = std::sync::Once::new();
175    INSTALL_PROVIDER.call_once(|| {
176        let _ = rustls::crypto::ring::default_provider().install_default();
177    });
178}
179
180/// Loads the PEM certificate chain at `path`. Errors if the file is missing or
181/// contains no certificates.
182pub fn load_cert_chain(path: &str) -> Result<Vec<CertificateDer<'static>>, TlsError> {
183    let data =
184        std::fs::read(path).map_err(|e| TlsError::CertRead(path.to_string(), e.to_string()))?;
185    let certs = CertificateDer::pem_slice_iter(&data)
186        .collect::<Result<Vec<_>, _>>()
187        .map_err(|e| TlsError::CertRead(path.to_string(), e.to_string()))?;
188    if certs.is_empty() {
189        return Err(TlsError::NoCerts(path.to_string()));
190    }
191    Ok(certs)
192}
193
194/// Loads the PEM private key at `path` (PKCS#8, PKCS#1, or SEC1). Errors if the
195/// file is missing or contains no private key.
196pub fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, TlsError> {
197    let data =
198        std::fs::read(path).map_err(|e| TlsError::KeyRead(path.to_string(), e.to_string()))?;
199    match PrivateKeyDer::pem_slice_iter(&data).next() {
200        Some(Ok(key)) => Ok(key),
201        Some(Err(e)) => Err(TlsError::KeyRead(path.to_string(), e.to_string())),
202        None => Err(TlsError::NoKey(path.to_string())),
203    }
204}
205
206/// Loads a PEM CA bundle at `path` into a [`RootCertStore`] for verifying
207/// **client** certificates (mTLS). Errors if the file is missing or yields no
208/// usable certificates.
209fn load_client_ca_roots(path: &str) -> Result<rustls::RootCertStore, TlsError> {
210    let data =
211        std::fs::read(path).map_err(|e| TlsError::ClientCaRead(path.to_string(), e.to_string()))?;
212    let certs = CertificateDer::pem_slice_iter(&data)
213        .collect::<Result<Vec<_>, _>>()
214        .map_err(|e| TlsError::ClientCaRead(path.to_string(), e.to_string()))?;
215    let mut roots = rustls::RootCertStore::empty();
216    let (added, _skipped) = roots.add_parsable_certificates(certs);
217    if added == 0 {
218        return Err(TlsError::NoClientCaCerts(path.to_string()));
219    }
220    Ok(roots)
221}
222
223/// Builds a rustls [`ServerConfig`] from `tls`, enforcing `min_version` and
224/// advertising ALPN `h2`+`http/1.1` when `http2_enabled` (else `http/1.1`
225/// only). Uses an explicit ring provider so the version floor is honored
226/// regardless of global-provider install ordering.
227pub fn build_server_config(
228    tls: &TlsConfig,
229    http2_enabled: bool,
230    acme: Option<&AcmeHooks>,
231) -> Result<Arc<ServerConfig>, TlsError> {
232    install_crypto_provider();
233
234    let versions: &[&'static rustls::SupportedProtocolVersion] = match tls.min_version.as_str() {
235        "1.2" => &[&rustls::version::TLS13, &rustls::version::TLS12],
236        "1.3" => &[&rustls::version::TLS13],
237        other => return Err(TlsError::BadMinVersion(other.to_string())),
238    };
239
240    let provider = Arc::new(rustls::crypto::ring::default_provider());
241    let builder = ServerConfig::builder_with_provider(provider.clone())
242        .with_protocol_versions(versions)
243        .map_err(|e| TlsError::RustlsConfig(e.to_string()))?;
244
245    // mTLS: when a client-CA bundle is configured, verify client certs against
246    // it (required by default; optional when `client_cert_required` is false).
247    let builder = match &tls.client_ca_path {
248        Some(ca_path) => {
249            let roots = load_client_ca_roots(ca_path)?;
250            let vbuilder = rustls::server::WebPkiClientVerifier::builder_with_provider(
251                Arc::new(roots),
252                provider.clone(),
253            );
254            let vbuilder = if tls.client_cert_required {
255                vbuilder
256            } else {
257                vbuilder.allow_unauthenticated()
258            };
259            let verifier = vbuilder
260                .build()
261                .map_err(|e| TlsError::ClientVerifier(e.to_string()))?;
262            builder.with_client_cert_verifier(verifier)
263        }
264        None => builder.with_no_client_auth(),
265    };
266
267    // Certificate selection: each slot is either a file-based cert loaded now,
268    // or (when ACME-managed) a lookup key resolved against `AcmeHooks::certs`
269    // on every ClientHello.
270    fn slot(
271        what: &str,
272        cert: &Option<String>,
273        key: &Option<String>,
274        acme_slot: &Option<crate::config::AcmeSlot>,
275        domains_for_id: Vec<String>,
276        acme: Option<&AcmeHooks>,
277        provider: &Arc<rustls::crypto::CryptoProvider>,
278    ) -> Result<CertSlot, TlsError> {
279        if acme_slot.is_some() {
280            if acme.is_none() {
281                return Err(TlsError::AcmeNotWired(what.to_string()));
282            }
283            let (id, _) = crate::acme::CertId::from_domains(&domains_for_id)
284                .map_err(|e| TlsError::RustlsConfig(e.to_string()))?;
285            return Ok(CertSlot::Managed(id.as_str().to_string()));
286        }
287        let (c, k) = file_pair(cert, key, what)?;
288        Ok(CertSlot::File(Arc::new(certified_key(
289            load_cert_chain(c)?,
290            load_private_key(k)?,
291            provider,
292        )?)))
293    }
294
295    let default = slot(
296        "default",
297        &tls.cert_path,
298        &tls.key_path,
299        &tls.acme,
300        tls.acme
301            .as_ref()
302            .map(|s| s.domains.clone())
303            .unwrap_or_default(),
304        acme,
305        &provider,
306    )?;
307    let mut certs = Vec::with_capacity(tls.sni_certs.len());
308    for sc in &tls.sni_certs {
309        let domains = sc
310            .acme_domains()
311            .map_err(TlsError::RustlsConfig)?
312            .unwrap_or_default();
313        certs.push((
314            SniPattern::parse(&sc.server_name),
315            slot(
316                &sc.server_name,
317                &sc.cert_path,
318                &sc.key_path,
319                &sc.acme,
320                domains,
321                acme,
322                &provider,
323            )?,
324        ));
325    }
326
327    // Always the resolver — it is what `with_single_cert` builds internally
328    // (`AlwaysResolvesChain`), so a single file-based cert behaves identically.
329    let mut config = builder.with_cert_resolver(Arc::new(SniCertResolver {
330        certs,
331        default,
332        acme: acme.cloned(),
333    }));
334
335    config.alpn_protocols = if http2_enabled {
336        // h2 first so a client offering both prefers HTTP/2.
337        vec![b"h2".to_vec(), b"http/1.1".to_vec()]
338    } else {
339        vec![b"http/1.1".to_vec()]
340    };
341    if acme.is_some() {
342        // Advertised last: browsers offering h2/http1.1 never pick it, and
343        // rustls would otherwise abort a validator's acme-tls/1-only hello with
344        // no_application_protocol before the resolver could answer.
345        config.alpn_protocols.push(ACME_TLS_ALPN.to_vec());
346    }
347
348    Ok(Arc::new(config))
349}
350
351/// Builds a [`TlsAcceptor`] ready to wrap accepted TCP streams. Fail-fast: any
352/// cert/key/config error surfaces here at startup.
353///
354/// The listeners use [`build_reloadable`] + [`current_acceptor`] so certs can
355/// hot-reload; this one-shot form is kept for tests and simple embedding.
356#[allow(dead_code)]
357pub fn build_acceptor(tls: &TlsConfig, http2_enabled: bool) -> Result<TlsAcceptor, TlsError> {
358    Ok(TlsAcceptor::from(build_server_config(
359        tls,
360        http2_enabled,
361        None,
362    )?))
363}
364
365/// Builds a hot-reloadable TLS config: the initial `ServerConfig` wrapped in an
366/// [`ArcSwap`] so [`spawn_cert_watcher`] can swap it in on cert rotation.
367/// Fail-fast: a bad cert/key at startup surfaces here.
368pub fn build_reloadable(
369    tls: &TlsConfig,
370    http2_enabled: bool,
371    acme: Option<&AcmeHooks>,
372) -> Result<SharedTlsConfig, TlsError> {
373    Ok(Arc::new(ArcSwap::new(build_server_config(
374        tls,
375        http2_enabled,
376        acme,
377    )?)))
378}
379
380/// A [`TlsAcceptor`] over the **current** config. Call this per connection (it's
381/// an atomic load + `Arc` clone) so reloads take effect for new connections.
382pub fn current_acceptor(shared: &SharedTlsConfig) -> TlsAcceptor {
383    TlsAcceptor::from(shared.load_full())
384}
385
386/// True when the finished handshake negotiated `acme-tls/1`: the connection
387/// was a CA validation and must be closed without serving anything.
388pub fn negotiated_acme_challenge<IO>(stream: &tokio_rustls::server::TlsStream<IO>) -> bool {
389    stream.get_ref().1.alpn_protocol() == Some(ACME_TLS_ALPN)
390}
391
392/// Verified identity of an mTLS client, read from its leaf certificate.
393#[derive(Debug, Clone, PartialEq)]
394pub struct ClientCertIdentity {
395    /// Lowercase-hex SHA-256 fingerprint of the leaf certificate (stable id).
396    pub fingerprint: String,
397    /// Subject Common Name, if present.
398    pub subject_cn: Option<String>,
399    /// Subject Alternative Name DNS entries.
400    pub san_dns: Vec<String>,
401}
402
403/// The verified client identity on an mTLS connection, or `None` if the client
404/// presented no certificate (anonymous client in optional mode, or mTLS not
405/// enabled). Read after the handshake.
406pub fn client_cert_identity<IO>(
407    stream: &tokio_rustls::server::TlsStream<IO>,
408) -> Option<ClientCertIdentity> {
409    let leaf = stream.get_ref().1.peer_certificates()?.first()?;
410    let digest = ring::digest::digest(&ring::digest::SHA256, leaf.as_ref());
411    let fingerprint = digest
412        .as_ref()
413        .iter()
414        .map(|b| format!("{:02x}", b))
415        .collect();
416    let (subject_cn, san_dns) = parse_client_identity(leaf.as_ref());
417    Some(ClientCertIdentity {
418        fingerprint,
419        subject_cn,
420        san_dns,
421    })
422}
423
424/// Extracts the subject CN and SAN DNS names from a DER-encoded certificate.
425/// Panic-free: any parse error yields `(None, empty)`.
426fn parse_client_identity(der: &[u8]) -> (Option<String>, Vec<String>) {
427    use x509_parser::prelude::*;
428    let cert = match X509Certificate::from_der(der) {
429        Ok((_rem, cert)) => cert,
430        Err(_) => return (None, Vec::new()),
431    };
432    let subject_cn = cert
433        .subject()
434        .iter_common_name()
435        .filter_map(|a| a.as_str().ok())
436        .next()
437        .map(String::from);
438    let san_dns = match cert.subject_alternative_name() {
439        Ok(Some(ext)) => ext
440            .value
441            .general_names
442            .iter()
443            .filter_map(|gn| match gn {
444                GeneralName::DNSName(name) => Some((*name).to_string()),
445                _ => None,
446            })
447            .collect(),
448        _ => Vec::new(),
449    };
450    (subject_cn, san_dns)
451}
452
453/// Watches the cert/key files and hot-reloads `shared` when they change.
454///
455/// Mirrors [`crate::hot_reload::watch_config`]: an OS thread runs a `notify`
456/// watcher on each unique parent directory of the cert/key paths (so
457/// Kubernetes' atomic secret symlink swap is caught, not just direct writes),
458/// forwarding events to a debounced (500 ms) async loop. On each change the
459/// `ServerConfig` is rebuilt and atomically stored; a bad/partial cert during
460/// rotation is logged and the current config is **kept** (never crash or drop
461/// TLS mid-rotation). `label` names the listener in logs (e.g. `"data-plane"`).
462pub fn spawn_cert_watcher(
463    tls: TlsConfig,
464    http2_enabled: bool,
465    shared: SharedTlsConfig,
466    label: &'static str,
467    acme: Option<AcmeHooks>,
468) {
469    let (tx, mut rx) = mpsc::channel::<()>(1);
470
471    // Unique parent directories of every cert/key file (default + per-SNI), so
472    // rotating any of them triggers a reload. ACME-managed slots have no file
473    // paths to watch.
474    let mut paths: Vec<&String> = Vec::new();
475    paths.extend(tls.cert_path.iter());
476    paths.extend(tls.key_path.iter());
477    for sc in &tls.sni_certs {
478        paths.extend(sc.cert_path.iter());
479        paths.extend(sc.key_path.iter());
480    }
481    if paths.is_empty() {
482        info!(
483            "{} has no file-based certificates; cert watcher not started",
484            label
485        );
486        return;
487    }
488    let mut dirs: Vec<PathBuf> = Vec::new();
489    for path in paths {
490        let dir = PathBuf::from(path)
491            .parent()
492            .map(PathBuf::from)
493            .unwrap_or_else(|| PathBuf::from("."));
494        if !dirs.contains(&dir) {
495            dirs.push(dir);
496        }
497    }
498
499    std::thread::spawn(move || {
500        let mut watcher =
501            match notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
502                if let Ok(event) = res {
503                    if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
504                        let _ = tx.blocking_send(());
505                    }
506                }
507            }) {
508                Ok(w) => w,
509                Err(e) => {
510                    error!("{} cert watcher failed to start: {}", label, e);
511                    return;
512                }
513            };
514
515        for dir in &dirs {
516            if let Err(e) = watcher.watch(dir, RecursiveMode::Recursive) {
517                error!("{} cert watcher failed to watch {:?}: {}", label, dir, e);
518            }
519        }
520        info!("{} TLS certificate watcher started on {:?}", label, dirs);
521
522        // Keep the watcher alive for the process lifetime.
523        loop {
524            std::thread::sleep(Duration::from_secs(3600));
525        }
526    });
527
528    tokio::spawn(async move {
529        loop {
530            if rx.recv().await.is_none() {
531                break;
532            }
533            // Debounce: coalesce a burst of filesystem events.
534            tokio::time::sleep(Duration::from_millis(500)).await;
535            while rx.try_recv().is_ok() {}
536
537            match build_server_config(&tls, http2_enabled, acme.as_ref()) {
538                Ok(config) => {
539                    shared.store(config);
540                    info!("{} TLS certificate reloaded", label);
541                }
542                Err(e) => warn!(
543                    "{} TLS certificate reload failed (keeping current): {}",
544                    label, e
545                ),
546            }
547        }
548    });
549}
550
551/// Serves a single (already-handshaked, `TokioIo`-wrapped) connection.
552///
553/// When `http2_enabled`, the hyper-util **auto** builder sniffs the first bytes
554/// and dispatches to HTTP/1.1 or HTTP/2 — covering ALPN-negotiated h2 over TLS,
555/// h2c prior-knowledge over plaintext, and HTTP/1.1, in one call. Otherwise it
556/// serves HTTP/1.1 only. Connection-level errors are logged, not propagated, so
557/// one bad connection never affects the accept loop.
558///
559/// Both paths enable connection **upgrades** (`with_upgrades` /
560/// `serve_connection_with_upgrades`) so a handler that returns `101 Switching
561/// Protocols` (HTTP/1.1) or `200` (HTTP/2 extended CONNECT) can hand the raw
562/// stream to a WebSocket relay (see [`crate::server::websocket`]). The h2 arm
563/// also enables the RFC 8441 extended CONNECT protocol
564/// (`SETTINGS_ENABLE_CONNECT_PROTOCOL`) so clients can open WebSockets over
565/// HTTP/2; h1/h2 auto-detection is preserved.
566///
567/// The listeners drive connections via [`build_connection`] + a graceful-shutdown
568/// watcher; this await-and-log form is kept for tests and simple embedding.
569#[allow(dead_code)]
570pub async fn serve_connection<I, S, B>(io: I, service: S, http2_enabled: bool)
571where
572    I: Read + Write + Unpin + Send + 'static,
573    S: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
574    S::Future: Send + 'static,
575    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
576    B: Body + Send + 'static,
577    B::Data: Send,
578    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
579{
580    if let Err(err) = build_connection(io, service, http2_enabled).await {
581        error!("Connection error: {}", err);
582    }
583}
584
585/// Builds (but does not drive) a connection future for `io`, ready to be either
586/// `.await`ed directly or handed to a graceful-shutdown watcher.
587///
588/// Both protocol paths go through the hyper-util **auto** builder so they share
589/// one connection type that implements
590/// [`GracefulConnection`](hyper_util::server::graceful::GracefulConnection):
591/// `http1_only()` for strict HTTP/1.1, or h2 (auto-detected, extended CONNECT
592/// enabled) otherwise. `.into_owned()` detaches the connection from the builder
593/// so it is `'static` and can be spawned. See [`serve_connection`] for the
594/// simple await-and-log path.
595pub fn build_connection<I, S, B>(
596    io: I,
597    service: S,
598    http2_enabled: bool,
599) -> auto::UpgradeableConnection<'static, I, S, TokioExecutor>
600where
601    I: Read + Write + Unpin + Send + 'static,
602    S: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
603    S::Future: Send + 'static,
604    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
605    B: Body + Send + 'static,
606    B::Data: Send,
607    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
608{
609    if http2_enabled {
610        auto::Builder::new(TokioExecutor::new())
611            .http2()
612            .enable_connect_protocol()
613            .serve_connection_with_upgrades(io, service)
614            .into_owned()
615    } else {
616        auto::Builder::new(TokioExecutor::new())
617            .http1_only()
618            .serve_connection_with_upgrades(io, service)
619            .into_owned()
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::config::TlsConfig;
627
628    /// Writes a fresh self-signed cert+key to unique temp paths and returns a
629    /// `TlsConfig` pointing at them.
630    fn self_signed(
631        tag: &str,
632        min_version: &str,
633    ) -> (TlsConfig, std::path::PathBuf, std::path::PathBuf) {
634        let certified = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
635        let dir = std::env::temp_dir();
636        let pid = std::process::id();
637        let cert_path = dir.join(format!("featherbit_{}_{}.crt", tag, pid));
638        let key_path = dir.join(format!("featherbit_{}_{}.key", tag, pid));
639        std::fs::write(&cert_path, certified.cert.pem()).unwrap();
640        std::fs::write(&key_path, certified.signing_key.serialize_pem()).unwrap();
641        let tls = TlsConfig {
642            cert_path: Some(cert_path.to_string_lossy().into_owned()),
643            key_path: Some(key_path.to_string_lossy().into_owned()),
644            min_version: min_version.to_string(),
645            client_ca_path: None,
646            client_cert_required: true,
647            sni_certs: Vec::new(),
648            acme: None,
649        };
650        (tls, cert_path, key_path)
651    }
652
653    #[test]
654    fn test_install_crypto_provider_idempotent() {
655        install_crypto_provider();
656        install_crypto_provider();
657    }
658
659    #[test]
660    fn test_load_cert_and_key() {
661        let (tls, cert, key) = self_signed("load", "1.2");
662        assert!(!load_cert_chain(tls.cert_path.as_deref().unwrap())
663            .unwrap()
664            .is_empty());
665        load_private_key(tls.key_path.as_deref().unwrap()).unwrap();
666        let _ = std::fs::remove_file(cert);
667        let _ = std::fs::remove_file(key);
668    }
669
670    #[test]
671    fn test_load_cert_missing_file() {
672        let err = load_cert_chain("does-not-exist.pem").unwrap_err();
673        assert!(matches!(err, TlsError::CertRead(_, _)));
674    }
675
676    #[test]
677    fn test_load_key_no_key_in_pem() {
678        // A cert-only file has no private key.
679        let (tls, cert, key) = self_signed("nokey", "1.2");
680        let err = load_private_key(tls.cert_path.as_deref().unwrap()).unwrap_err();
681        assert!(matches!(err, TlsError::NoKey(_)));
682        let _ = std::fs::remove_file(cert);
683        let _ = std::fs::remove_file(key);
684    }
685
686    #[test]
687    fn test_alpn_reflects_http2_flag() {
688        let (tls, cert, key) = self_signed("alpn", "1.2");
689
690        let with_h2 = build_server_config(&tls, true, None).unwrap();
691        assert_eq!(
692            with_h2.alpn_protocols,
693            vec![b"h2".to_vec(), b"http/1.1".to_vec()]
694        );
695
696        let without_h2 = build_server_config(&tls, false, None).unwrap();
697        assert_eq!(without_h2.alpn_protocols, vec![b"http/1.1".to_vec()]);
698
699        let _ = std::fs::remove_file(cert);
700        let _ = std::fs::remove_file(key);
701    }
702
703    #[test]
704    fn test_min_version_1_3_ok_and_bad_rejected() {
705        let (mut tls, cert, key) = self_signed("minver", "1.3");
706        build_server_config(&tls, true, None).unwrap();
707
708        tls.min_version = "sslv3".to_string();
709        let err = build_server_config(&tls, true, None).unwrap_err();
710        assert!(matches!(err, TlsError::BadMinVersion(v) if v == "sslv3"));
711
712        let _ = std::fs::remove_file(cert);
713        let _ = std::fs::remove_file(key);
714    }
715
716    /// End-to-end over a real socket: build an acceptor, serve one connection
717    /// with the auto builder, and hit it with an HTTPS client — exercising the
718    /// full TLS handshake + protocol negotiation path.
719    #[tokio::test]
720    async fn test_tls_round_trip() {
721        use bytes::Bytes;
722        use http_body_util::Full;
723        use hyper::service::service_fn;
724        use hyper_util::rt::TokioIo;
725        use tokio::net::TcpListener;
726
727        let (tls, cert, key) = self_signed("rt", "1.2");
728        let acceptor = build_acceptor(&tls, true).unwrap();
729
730        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
731        let addr = listener.local_addr().unwrap();
732
733        tokio::spawn(async move {
734            let (stream, _) = listener.accept().await.unwrap();
735            let tls_stream = match acceptor.accept(stream).await {
736                Ok(s) => s,
737                Err(_) => return,
738            };
739            let service = service_fn(|_req| async {
740                Ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from_static(b"ok"))))
741            });
742            serve_connection(TokioIo::new(tls_stream), service, true).await;
743        });
744
745        let client = reqwest::Client::builder()
746            .danger_accept_invalid_certs(true)
747            .use_rustls_tls() // reliably offers the h2 ALPN protocol
748            .build()
749            .unwrap();
750        let resp = client
751            .get(format!("https://{}/", addr))
752            .send()
753            .await
754            .unwrap();
755        assert_eq!(resp.status(), 200);
756        // ALPN advertised h2 first, so a modern HTTPS client negotiates HTTP/2.
757        assert_eq!(resp.version(), reqwest::Version::HTTP_2);
758        assert_eq!(resp.text().await.unwrap(), "ok");
759
760        let _ = std::fs::remove_file(cert);
761        let _ = std::fs::remove_file(key);
762    }
763
764    /// Generates a fresh self-signed cert and writes it to the given paths
765    /// (overwriting) — each call produces a distinct cert (new key/serial).
766    fn write_fresh_cert(cert_path: &std::path::Path, key_path: &std::path::Path) {
767        let certified = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
768        std::fs::write(cert_path, certified.cert.pem()).unwrap();
769        std::fs::write(key_path, certified.signing_key.serialize_pem()).unwrap();
770    }
771
772    /// A client cert verifier that records the presented leaf certificate and
773    /// accepts everything (test-only).
774    #[derive(Debug)]
775    struct CapturingVerifier {
776        captured: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>,
777        provider: rustls::crypto::CryptoProvider,
778    }
779
780    impl rustls::client::danger::ServerCertVerifier for CapturingVerifier {
781        fn verify_server_cert(
782            &self,
783            end_entity: &rustls::pki_types::CertificateDer<'_>,
784            _intermediates: &[rustls::pki_types::CertificateDer<'_>],
785            _server_name: &rustls::pki_types::ServerName<'_>,
786            _ocsp_response: &[u8],
787            _now: rustls::pki_types::UnixTime,
788        ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
789            *self.captured.lock().unwrap() = Some(end_entity.as_ref().to_vec());
790            Ok(rustls::client::danger::ServerCertVerified::assertion())
791        }
792        fn verify_tls12_signature(
793            &self,
794            message: &[u8],
795            cert: &rustls::pki_types::CertificateDer<'_>,
796            dss: &rustls::DigitallySignedStruct,
797        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
798            rustls::crypto::verify_tls12_signature(
799                message,
800                cert,
801                dss,
802                &self.provider.signature_verification_algorithms,
803            )
804        }
805        fn verify_tls13_signature(
806            &self,
807            message: &[u8],
808            cert: &rustls::pki_types::CertificateDer<'_>,
809            dss: &rustls::DigitallySignedStruct,
810        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
811            match rustls::crypto::verify_tls13_signature(
812                message,
813                cert,
814                dss,
815                &self.provider.signature_verification_algorithms,
816            ) {
817                Ok(v) => Ok(v),
818                // rustls-webpki's `EndEntityCert` parse is strict about unknown
819                // *critical* X.509 extensions and fails before the signature is
820                // even checked — which is exactly what RFC 8737's critical
821                // `acmeIdentifier` extension on a TLS-ALPN-01 challenge cert
822                // triggers. Fall back to verifying against the raw
823                // SubjectPublicKeyInfo (same key, same signature, no extension
824                // gate); production code never verifies signatures over ACME
825                // certificates, only this test client does.
826                Err(_) => {
827                    let spki = crate::acme::leaf_spki(cert.as_ref())
828                        .map_err(|e| rustls::Error::General(e.to_string()))?;
829                    rustls::crypto::verify_tls13_signature_with_raw_key(
830                        message,
831                        &rustls::pki_types::SubjectPublicKeyInfoDer::from(spki),
832                        dss,
833                        &self.provider.signature_verification_algorithms,
834                    )
835                }
836            }
837        }
838        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
839            self.provider
840                .signature_verification_algorithms
841                .supported_schemes()
842        }
843    }
844
845    /// Binds a listener that serves the *current* config per connection and
846    /// returns its address.
847    async fn spawn_reload_server(shared: SharedTlsConfig) -> std::net::SocketAddr {
848        use tokio::net::TcpListener;
849        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
850        let addr = listener.local_addr().unwrap();
851        tokio::spawn(async move {
852            while let Ok((stream, _)) = listener.accept().await {
853                let acceptor = current_acceptor(&shared);
854                tokio::spawn(async move {
855                    let _ = acceptor.accept(stream).await; // complete handshake, then drop
856                });
857            }
858        });
859        addr
860    }
861
862    /// Connects to `addr` (SNI `localhost`) and returns the presented leaf cert.
863    async fn served_leaf_cert(addr: std::net::SocketAddr) -> Vec<u8> {
864        served_leaf_cert_sni(addr, "localhost").await
865    }
866
867    /// Connects to `addr` with the given SNI and returns the presented leaf cert.
868    async fn served_leaf_cert_sni(addr: std::net::SocketAddr, sni: &str) -> Vec<u8> {
869        use tokio::net::TcpStream;
870        install_crypto_provider();
871        let captured = std::sync::Arc::new(std::sync::Mutex::new(None));
872        let verifier = std::sync::Arc::new(CapturingVerifier {
873            captured: captured.clone(),
874            provider: rustls::crypto::ring::default_provider(),
875        });
876        let config = rustls::ClientConfig::builder()
877            .dangerous()
878            .with_custom_certificate_verifier(verifier)
879            .with_no_client_auth();
880        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
881        let tcp = TcpStream::connect(addr).await.unwrap();
882        let name = rustls::pki_types::ServerName::try_from(sni.to_string()).unwrap();
883        let _ = connector.connect(name, tcp).await.unwrap();
884        let leaf = captured.lock().unwrap().clone();
885        leaf.expect("no server certificate captured")
886    }
887
888    #[tokio::test]
889    async fn test_cert_hot_reload_swaps_served_cert() {
890        let dir = std::env::temp_dir();
891        let pid = std::process::id();
892        let cert = dir.join(format!("featherbit_reload_{}.crt", pid));
893        let key = dir.join(format!("featherbit_reload_{}.key", pid));
894        write_fresh_cert(&cert, &key);
895        let tls = TlsConfig {
896            cert_path: Some(cert.to_string_lossy().into_owned()),
897            key_path: Some(key.to_string_lossy().into_owned()),
898            min_version: "1.2".to_string(),
899            client_ca_path: None,
900            client_cert_required: true,
901            sni_certs: Vec::new(),
902            acme: None,
903        };
904
905        let shared = build_reloadable(&tls, false, None).unwrap();
906        let addr = spawn_reload_server(shared.clone()).await;
907
908        let leaf_a = served_leaf_cert(addr).await;
909
910        // Rotate: overwrite the files with a new cert and swap it in (the
911        // deterministic path the watcher also takes).
912        write_fresh_cert(&cert, &key);
913        shared.store(build_server_config(&tls, false, None).unwrap());
914
915        let leaf_b = served_leaf_cert(addr).await;
916
917        assert_ne!(
918            leaf_a, leaf_b,
919            "served certificate should change after reload"
920        );
921
922        let _ = std::fs::remove_file(&cert);
923        let _ = std::fs::remove_file(&key);
924    }
925
926    #[tokio::test]
927    async fn test_cert_hot_reload_via_file_watcher() {
928        let dir = std::env::temp_dir();
929        let pid = std::process::id();
930        let cert = dir.join(format!("featherbit_watch_{}.crt", pid));
931        let key = dir.join(format!("featherbit_watch_{}.key", pid));
932        write_fresh_cert(&cert, &key);
933        let tls = TlsConfig {
934            cert_path: Some(cert.to_string_lossy().into_owned()),
935            key_path: Some(key.to_string_lossy().into_owned()),
936            min_version: "1.2".to_string(),
937            client_ca_path: None,
938            client_cert_required: true,
939            sni_certs: Vec::new(),
940            acme: None,
941        };
942
943        let shared = build_reloadable(&tls, false, None).unwrap();
944        spawn_cert_watcher(tls.clone(), false, shared.clone(), "test", None);
945        let addr = spawn_reload_server(shared.clone()).await;
946
947        let leaf_a = served_leaf_cert(addr).await;
948
949        // Overwrite the cert files; the watcher should reload within a few
950        // hundred ms (500ms debounce + notify latency).
951        write_fresh_cert(&cert, &key);
952
953        let mut leaf_b = leaf_a.clone();
954        for _ in 0..50 {
955            tokio::time::sleep(Duration::from_millis(200)).await;
956            leaf_b = served_leaf_cert(addr).await;
957            if leaf_b != leaf_a {
958                break;
959            }
960        }
961        assert_ne!(
962            leaf_a, leaf_b,
963            "file watcher should hot-reload the certificate"
964        );
965
966        let _ = std::fs::remove_file(&cert);
967        let _ = std::fs::remove_file(&key);
968    }
969
970    // ---- mTLS (client-certificate authentication) ----
971
972    fn gen_ca() -> (rcgen::Certificate, rcgen::KeyPair) {
973        let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
974        params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
975        let key = rcgen::KeyPair::generate().unwrap();
976        let cert = params.self_signed(&key).unwrap();
977        (cert, key)
978    }
979
980    fn gen_signed(
981        cn: &str,
982        ca_cert: &rcgen::Certificate,
983        ca_key: &rcgen::KeyPair,
984    ) -> (rcgen::Certificate, rcgen::KeyPair) {
985        // SAN = [cn], and an explicit subject CN so identity extraction has both.
986        let mut params = rcgen::CertificateParams::new(vec![cn.to_string()]).unwrap();
987        params
988            .distinguished_name
989            .push(rcgen::DnType::CommonName, cn);
990        let key = rcgen::KeyPair::generate().unwrap();
991        // rcgen 0.14: signing goes through an `Issuer` built from the CA cert + key.
992        let issuer = rcgen::Issuer::from_ca_cert_der(ca_cert.der(), ca_key).unwrap();
993        let cert = params.signed_by(&key, &issuer).unwrap();
994        (cert, key)
995    }
996
997    fn sha256_hex(bytes: &[u8]) -> String {
998        ring::digest::digest(&ring::digest::SHA256, bytes)
999            .as_ref()
1000            .iter()
1001            .map(|b| format!("{:02x}", b))
1002            .collect()
1003    }
1004
1005    /// Writes a self-signed server cert + CA bundle to temp files and returns a
1006    /// `TlsConfig` with mTLS configured, plus the CA cert/key for signing
1007    /// clients and the temp paths for cleanup.
1008    fn mtls_config(
1009        tag: &str,
1010        required: bool,
1011    ) -> (
1012        TlsConfig,
1013        rcgen::Certificate,
1014        rcgen::KeyPair,
1015        Vec<std::path::PathBuf>,
1016    ) {
1017        let (ca_cert, ca_key) = gen_ca();
1018        let server = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
1019        let dir = std::env::temp_dir();
1020        let pid = std::process::id();
1021        let scert = dir.join(format!("fb_mtls_{}_{}.crt", tag, pid));
1022        let skey = dir.join(format!("fb_mtls_{}_{}.key", tag, pid));
1023        let ca = dir.join(format!("fb_mtls_{}_{}_ca.crt", tag, pid));
1024        std::fs::write(&scert, server.cert.pem()).unwrap();
1025        std::fs::write(&skey, server.signing_key.serialize_pem()).unwrap();
1026        std::fs::write(&ca, ca_cert.pem()).unwrap();
1027        let tls = TlsConfig {
1028            cert_path: Some(scert.to_string_lossy().into_owned()),
1029            key_path: Some(skey.to_string_lossy().into_owned()),
1030            min_version: "1.2".to_string(),
1031            client_ca_path: Some(ca.to_string_lossy().into_owned()),
1032            client_cert_required: required,
1033            sni_certs: Vec::new(),
1034            acme: None,
1035        };
1036        (tls, ca_cert, ca_key, vec![scert, skey, ca])
1037    }
1038
1039    /// Spawns a TLS server that captures the client-cert identity of each
1040    /// accepted connection into the returned shared slot.
1041    async fn spawn_mtls_server(
1042        tls: &TlsConfig,
1043    ) -> (
1044        std::net::SocketAddr,
1045        std::sync::Arc<std::sync::Mutex<Option<ClientCertIdentity>>>,
1046    ) {
1047        use tokio::net::TcpListener;
1048        let acceptor = build_acceptor(tls, false).unwrap();
1049        let captured = std::sync::Arc::new(std::sync::Mutex::new(None));
1050        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1051        let addr = listener.local_addr().unwrap();
1052        let cap = captured.clone();
1053        tokio::spawn(async move {
1054            use tokio::io::AsyncWriteExt;
1055            while let Ok((stream, _)) = listener.accept().await {
1056                let acceptor = acceptor.clone();
1057                let cap = cap.clone();
1058                tokio::spawn(async move {
1059                    if let Ok(mut tls_stream) = acceptor.accept(stream).await {
1060                        *cap.lock().unwrap() = client_cert_identity(&tls_stream);
1061                        // Write a byte so an accepted client reads data (vs. a
1062                        // rejected one reading the handshake alert).
1063                        let _ = tls_stream.write_all(b"1").await;
1064                        let _ = tls_stream.flush().await;
1065                        tokio::time::sleep(Duration::from_millis(50)).await;
1066                    }
1067                });
1068            }
1069        });
1070        (addr, captured)
1071    }
1072
1073    /// Connects a TLS client, optionally presenting `(chain, key)`. Returns
1074    /// whether the connection was **accepted** — not just that the client-side
1075    /// handshake future resolved. In TLS 1.3 a server rejecting a missing client
1076    /// cert lets the client's `connect()` resolve `Ok`, then surfaces the alert
1077    /// on the first read; so we do a post-handshake read and treat a read error
1078    /// (the alert) as rejection, and a clean EOF/read as acceptance.
1079    async fn mtls_client_connects(
1080        addr: std::net::SocketAddr,
1081        client: Option<(
1082            Vec<rustls::pki_types::CertificateDer<'static>>,
1083            rustls::pki_types::PrivateKeyDer<'static>,
1084        )>,
1085    ) -> bool {
1086        use tokio::io::AsyncReadExt;
1087        use tokio::net::TcpStream;
1088        install_crypto_provider();
1089        let verifier = std::sync::Arc::new(CapturingVerifier {
1090            captured: std::sync::Arc::new(std::sync::Mutex::new(None)),
1091            provider: rustls::crypto::ring::default_provider(),
1092        });
1093        let builder = rustls::ClientConfig::builder()
1094            .dangerous()
1095            .with_custom_certificate_verifier(verifier);
1096        let config = match client {
1097            Some((chain, key)) => builder.with_client_auth_cert(chain, key).unwrap(),
1098            None => builder.with_no_client_auth(),
1099        };
1100        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
1101        let tcp = TcpStream::connect(addr).await.unwrap();
1102        let name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
1103        let mut tls = match connector.connect(name, tcp).await {
1104            Ok(t) => t,
1105            Err(_) => return false,
1106        };
1107        let mut buf = [0u8; 1];
1108        // Ok(0) = clean EOF (accepted, then server dropped); Ok(n) = data.
1109        // Err = the server's rejection alert.
1110        tls.read(&mut buf).await.is_ok()
1111    }
1112
1113    fn client_material(
1114        cert: &rcgen::Certificate,
1115        key: &rcgen::KeyPair,
1116    ) -> (
1117        Vec<rustls::pki_types::CertificateDer<'static>>,
1118        rustls::pki_types::PrivateKeyDer<'static>,
1119    ) {
1120        let chain = vec![cert.der().clone()];
1121        let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(key.serialize_der().into());
1122        (chain, key_der)
1123    }
1124
1125    #[test]
1126    fn test_parse_client_identity() {
1127        let mut params = rcgen::CertificateParams::new(vec![
1128            "a.example.com".to_string(),
1129            "b.example.com".to_string(),
1130        ])
1131        .unwrap();
1132        params
1133            .distinguished_name
1134            .push(rcgen::DnType::CommonName, "svc-a");
1135        let key = rcgen::KeyPair::generate().unwrap();
1136        let cert = params.self_signed(&key).unwrap();
1137
1138        let (cn, san) = parse_client_identity(cert.der().as_ref());
1139        assert_eq!(cn.as_deref(), Some("svc-a"));
1140        assert_eq!(
1141            san,
1142            vec!["a.example.com".to_string(), "b.example.com".to_string()]
1143        );
1144
1145        // Malformed DER never panics.
1146        let (cn, san) = parse_client_identity(&[0xff; 16]);
1147        assert_eq!(cn, None);
1148        assert!(san.is_empty());
1149    }
1150
1151    #[test]
1152    fn test_mtls_config_builds_and_rejects_empty_ca() {
1153        let (tls, _ca_cert, _ca_key, paths) = mtls_config("cfg", true);
1154        // Required and optional both build.
1155        build_server_config(&tls, false, None).unwrap();
1156        let mut optional = tls.clone();
1157        optional.client_cert_required = false;
1158        build_server_config(&optional, false, None).unwrap();
1159
1160        // A client-CA path with no certs is an error.
1161        let empty = std::env::temp_dir().join(format!("fb_mtls_empty_{}.pem", std::process::id()));
1162        std::fs::write(&empty, b"not a certificate").unwrap();
1163        let mut bad = tls;
1164        bad.client_ca_path = Some(empty.to_string_lossy().into_owned());
1165        assert!(matches!(
1166            build_server_config(&bad, false, None),
1167            Err(TlsError::NoClientCaCerts(_))
1168        ));
1169
1170        let _ = std::fs::remove_file(&empty);
1171        for p in paths {
1172            let _ = std::fs::remove_file(p);
1173        }
1174    }
1175
1176    #[tokio::test]
1177    async fn test_mtls_required_enforces_client_cert() {
1178        let (tls, ca_cert, ca_key, paths) = mtls_config("req", true);
1179        let (client_cert, client_key) = gen_signed("client-1", &ca_cert, &ca_key);
1180        let expected_fp = sha256_hex(client_cert.der().as_ref());
1181
1182        let (addr, captured) = spawn_mtls_server(&tls).await;
1183
1184        // Client presenting a CA-signed cert connects, and the server sees its
1185        // identity (fingerprint + CN + SAN).
1186        let ok = mtls_client_connects(addr, Some(client_material(&client_cert, &client_key))).await;
1187        assert!(ok, "client with a valid cert should connect");
1188        let mut id = None;
1189        for _ in 0..20 {
1190            tokio::time::sleep(Duration::from_millis(25)).await;
1191            id = captured.lock().unwrap().clone();
1192            if id.is_some() {
1193                break;
1194            }
1195        }
1196        let id = id.expect("server should capture client identity");
1197        assert_eq!(id.fingerprint, expected_fp);
1198        assert_eq!(id.subject_cn.as_deref(), Some("client-1"));
1199        assert!(id.san_dns.iter().any(|s| s == "client-1"));
1200
1201        // Client without a cert is rejected at the handshake.
1202        let ok = mtls_client_connects(addr, None).await;
1203        assert!(!ok, "client without a cert must be rejected when required");
1204
1205        for p in paths {
1206            let _ = std::fs::remove_file(p);
1207        }
1208    }
1209
1210    #[tokio::test]
1211    async fn test_mtls_optional_allows_anonymous() {
1212        let (tls, _ca_cert, _ca_key, paths) = mtls_config("opt", false);
1213        let (addr, captured) = spawn_mtls_server(&tls).await;
1214
1215        // With the cert optional, a client without one still connects.
1216        let ok = mtls_client_connects(addr, None).await;
1217        assert!(ok, "anonymous client should connect in optional mode");
1218        tokio::time::sleep(Duration::from_millis(75)).await;
1219        assert_eq!(captured.lock().unwrap().clone(), None);
1220
1221        for p in paths {
1222            let _ = std::fs::remove_file(p);
1223        }
1224    }
1225
1226    // ---- SNI multi-certificate termination ----
1227
1228    /// Writes a self-signed cert+key (for `sans`) to temp files, returning
1229    /// `(cert_path, key_path, leaf_der)`.
1230    fn write_named_cert(tag: &str, sans: Vec<String>) -> (String, String, Vec<u8>) {
1231        let certified = rcgen::generate_simple_self_signed(sans).unwrap();
1232        let dir = std::env::temp_dir();
1233        let pid = std::process::id();
1234        let cert = dir.join(format!("fb_sni_{}_{}.crt", tag, pid));
1235        let key = dir.join(format!("fb_sni_{}_{}.key", tag, pid));
1236        std::fs::write(&cert, certified.cert.pem()).unwrap();
1237        std::fs::write(&key, certified.signing_key.serialize_pem()).unwrap();
1238        let leaf = certified.cert.der().as_ref().to_vec();
1239        (
1240            cert.to_string_lossy().into_owned(),
1241            key.to_string_lossy().into_owned(),
1242            leaf,
1243        )
1244    }
1245
1246    #[tokio::test]
1247    async fn test_sni_multicert_selects_by_hostname() {
1248        use crate::config::SniCert;
1249
1250        let (def_cert, def_key, def_leaf) = write_named_cert("def", vec!["localhost".to_string()]);
1251        let (a_cert, a_key, a_leaf) = write_named_cert("a", vec!["a.example.com".to_string()]);
1252        let (w_cert, w_key, w_leaf) =
1253            write_named_cert("wild", vec!["x.tenant.example.com".to_string()]);
1254
1255        let tls = TlsConfig {
1256            cert_path: Some(def_cert.clone()),
1257            key_path: Some(def_key.clone()),
1258            min_version: "1.2".to_string(),
1259            client_ca_path: None,
1260            client_cert_required: true,
1261            sni_certs: vec![
1262                SniCert {
1263                    server_name: "a.example.com".to_string(),
1264                    cert_path: Some(a_cert.clone()),
1265                    key_path: Some(a_key.clone()),
1266                    acme: None,
1267                },
1268                SniCert {
1269                    server_name: "*.tenant.example.com".to_string(),
1270                    cert_path: Some(w_cert.clone()),
1271                    key_path: Some(w_key.clone()),
1272                    acme: None,
1273                },
1274            ],
1275            acme: None,
1276        };
1277
1278        let shared = build_reloadable(&tls, false, None).unwrap();
1279        let addr = spawn_reload_server(shared).await;
1280
1281        // Exact match, wildcard match, and default fallback each get their cert.
1282        assert_eq!(served_leaf_cert_sni(addr, "a.example.com").await, a_leaf);
1283        assert_eq!(
1284            served_leaf_cert_sni(addr, "x.tenant.example.com").await,
1285            w_leaf
1286        );
1287        assert_eq!(
1288            served_leaf_cert_sni(addr, "unmatched.example.com").await,
1289            def_leaf
1290        );
1291        // Distinct certs, so the assertions above are meaningful.
1292        assert_ne!(a_leaf, def_leaf);
1293        assert_ne!(w_leaf, def_leaf);
1294
1295        for p in [def_cert, def_key, a_cert, a_key, w_cert, w_key] {
1296            let _ = std::fs::remove_file(p);
1297        }
1298    }
1299
1300    // ---- ACME: managed slots + TLS-ALPN-01 ----
1301
1302    #[derive(Debug)]
1303    struct FakeSolver(std::sync::Mutex<std::collections::HashMap<String, Arc<CertifiedKey>>>);
1304
1305    impl crate::acme::challenge::ChallengeSolver for FakeSolver {
1306        fn challenge_cert(&self, server_name: &str) -> Option<Arc<CertifiedKey>> {
1307            self.0.lock().unwrap().get(server_name).cloned()
1308        }
1309    }
1310
1311    fn managed_tls(domains: &[&str]) -> TlsConfig {
1312        TlsConfig {
1313            cert_path: None,
1314            key_path: None,
1315            acme: Some(crate::config::AcmeSlot {
1316                domains: domains.iter().map(|d| d.to_string()).collect(),
1317            }),
1318            min_version: "1.2".to_string(),
1319            client_ca_path: None,
1320            client_cert_required: true,
1321            sni_certs: Vec::new(),
1322        }
1323    }
1324
1325    fn hooks_with_placeholder(
1326        domains: &[&str],
1327    ) -> (crate::server::tls::AcmeHooks, crate::acme::CertId, Vec<u8>) {
1328        let domains: Vec<String> = domains.iter().map(|d| d.to_string()).collect();
1329        let (id, norm) = crate::acme::CertId::from_domains(&domains).unwrap();
1330        let (key, leaf) = crate::acme::placeholder_cert(&norm).unwrap();
1331        let certs = crate::acme::new_managed_certs();
1332        crate::acme::publish(
1333            &certs,
1334            &id,
1335            crate::acme::ManagedCert {
1336                key,
1337                leaf_der: leaf.clone(),
1338                state: crate::acme::CertState::Placeholder,
1339                meta: crate::acme::CertMeta::default(),
1340                domains: norm,
1341            },
1342        );
1343        let solver: Arc<dyn crate::acme::challenge::ChallengeSolver> =
1344            Arc::new(FakeSolver(std::sync::Mutex::new(Default::default())));
1345        (AcmeHooks { certs, solver }, id, leaf)
1346    }
1347
1348    /// Like `served_leaf_cert_sni` but offering the given ALPN list; `Err` when
1349    /// the handshake is refused.
1350    async fn handshake_with_alpn(
1351        addr: std::net::SocketAddr,
1352        sni: &str,
1353        alpn: Vec<Vec<u8>>,
1354    ) -> Result<(Vec<u8>, Option<Vec<u8>>), String> {
1355        use tokio::net::TcpStream;
1356        install_crypto_provider();
1357        let captured = std::sync::Arc::new(std::sync::Mutex::new(None));
1358        let verifier = std::sync::Arc::new(CapturingVerifier {
1359            captured: captured.clone(),
1360            provider: rustls::crypto::ring::default_provider(),
1361        });
1362        let mut config = rustls::ClientConfig::builder()
1363            .dangerous()
1364            .with_custom_certificate_verifier(verifier)
1365            .with_no_client_auth();
1366        config.alpn_protocols = alpn;
1367        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
1368        let tcp = TcpStream::connect(addr).await.unwrap();
1369        let name = rustls::pki_types::ServerName::try_from(sni.to_string()).unwrap();
1370        let stream = connector
1371            .connect(name, tcp)
1372            .await
1373            .map_err(|e| e.to_string())?;
1374        let negotiated = stream.get_ref().1.alpn_protocol().map(|p| p.to_vec());
1375        let leaf = captured.lock().unwrap().clone().ok_or("no cert")?;
1376        Ok((leaf, negotiated))
1377    }
1378
1379    #[tokio::test]
1380    async fn test_managed_slot_serves_placeholder_then_swapped_cert_without_rebuild() {
1381        let (hooks, id, placeholder_leaf) = hooks_with_placeholder(&["m.example.com"]);
1382        let tls = managed_tls(&["m.example.com"]);
1383        let shared = build_reloadable(&tls, false, Some(&hooks)).unwrap();
1384        let addr = spawn_reload_server(shared).await;
1385
1386        assert_eq!(
1387            served_leaf_cert_sni(addr, "m.example.com").await,
1388            placeholder_leaf
1389        );
1390        // No SNI / unknown SNI falls back to the managed default too.
1391        assert_eq!(
1392            served_leaf_cert_sni(addr, "other.example.com").await,
1393            placeholder_leaf
1394        );
1395
1396        // "Issue": publish a new cert into the map — no ServerConfig rebuild.
1397        let issued = rcgen::generate_simple_self_signed(vec!["m.example.com".to_string()]).unwrap();
1398        let (key, leaf) = crate::acme::load_certified_key(
1399            &issued.cert.pem(),
1400            &issued.signing_key.serialize_pem(),
1401        )
1402        .unwrap();
1403        crate::acme::update(&hooks.certs, &id, |c| {
1404            c.key = key;
1405            c.leaf_der = leaf.clone();
1406            c.state = crate::acme::CertState::Issued;
1407        });
1408        assert_eq!(served_leaf_cert_sni(addr, "m.example.com").await, leaf);
1409    }
1410
1411    #[tokio::test]
1412    async fn test_acme_tls_alpn_serves_challenge_cert_and_refuses_without_one() {
1413        // `hooks_with_placeholder` installs an empty FakeSolver; build a second
1414        // hooks value sharing the same cert map but with a pending challenge.
1415        let (empty_hooks, _, placeholder_leaf) = hooks_with_placeholder(&["m.example.com"]);
1416        let challenge =
1417            crate::acme::challenge::build_challenge_cert("m.example.com", "ka").unwrap();
1418        let challenge_leaf = challenge.end_entity_cert().unwrap().as_ref().to_vec();
1419        let pending = Arc::new(FakeSolver(std::sync::Mutex::new(Default::default())));
1420        pending
1421            .0
1422            .lock()
1423            .unwrap()
1424            .insert("m.example.com".to_string(), challenge.clone());
1425        let pending_hooks = AcmeHooks {
1426            certs: empty_hooks.certs.clone(),
1427            solver: pending,
1428        };
1429        let tls = managed_tls(&["m.example.com"]);
1430
1431        let addr_pending =
1432            spawn_reload_server(build_reloadable(&tls, true, Some(&pending_hooks)).unwrap()).await;
1433        let addr_empty =
1434            spawn_reload_server(build_reloadable(&tls, true, Some(&empty_hooks)).unwrap()).await;
1435
1436        // Validator: gets the challenge cert and negotiates acme-tls/1.
1437        let (leaf, alpn) =
1438            handshake_with_alpn(addr_pending, "m.example.com", vec![b"acme-tls/1".to_vec()])
1439                .await
1440                .unwrap();
1441        assert_eq!(leaf, challenge_leaf);
1442        assert_eq!(alpn.as_deref(), Some(&b"acme-tls/1"[..]));
1443
1444        // A normal client on the same listener is unaffected.
1445        let (leaf, alpn) = handshake_with_alpn(
1446            addr_pending,
1447            "m.example.com",
1448            vec![b"h2".to_vec(), b"http/1.1".to_vec()],
1449        )
1450        .await
1451        .unwrap();
1452        assert_eq!(leaf, placeholder_leaf);
1453        assert_eq!(alpn.as_deref(), Some(&b"h2"[..]));
1454
1455        // No pending challenge for the name ⇒ handshake refused (RFC 8737 §3).
1456        assert!(
1457            handshake_with_alpn(addr_empty, "m.example.com", vec![b"acme-tls/1".to_vec()])
1458                .await
1459                .is_err()
1460        );
1461    }
1462
1463    /// Accepts TLS and mirrors `server::listener`'s post-handshake branch: a
1464    /// connection that negotiated `acme-tls/1` is dropped without being served,
1465    /// anything else gets a fixed HTTP/1.1 response.
1466    async fn spawn_acme_aware_server(shared: SharedTlsConfig) -> std::net::SocketAddr {
1467        use tokio::io::AsyncWriteExt;
1468        use tokio::net::TcpListener;
1469        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1470        let addr = listener.local_addr().unwrap();
1471        tokio::spawn(async move {
1472            while let Ok((stream, _)) = listener.accept().await {
1473                let acceptor = current_acceptor(&shared);
1474                tokio::spawn(async move {
1475                    let Ok(mut tls) = acceptor.accept(stream).await else {
1476                        return;
1477                    };
1478                    if negotiated_acme_challenge(&tls) {
1479                        return;
1480                    }
1481                    let _ = tls
1482                        .write_all(
1483                            b"HTTP/1.1 200 OK
1484content-length: 2
1485
1486ok",
1487                        )
1488                        .await;
1489                    let _ = tls.shutdown().await;
1490                });
1491            }
1492        });
1493        addr
1494    }
1495
1496    /// Handshakes with `alpn`, sends a request, and returns whatever comes
1497    /// back (empty when the server closed without answering).
1498    async fn read_response_over_alpn(
1499        addr: std::net::SocketAddr,
1500        sni: &str,
1501        alpn: Vec<Vec<u8>>,
1502    ) -> Vec<u8> {
1503        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1504        use tokio::net::TcpStream;
1505        install_crypto_provider();
1506        let verifier = std::sync::Arc::new(CapturingVerifier {
1507            captured: std::sync::Arc::new(std::sync::Mutex::new(None)),
1508            provider: rustls::crypto::ring::default_provider(),
1509        });
1510        let mut config = rustls::ClientConfig::builder()
1511            .dangerous()
1512            .with_custom_certificate_verifier(verifier)
1513            .with_no_client_auth();
1514        config.alpn_protocols = alpn;
1515        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
1516        let tcp = TcpStream::connect(addr).await.unwrap();
1517        let name = rustls::pki_types::ServerName::try_from(sni.to_string()).unwrap();
1518        let mut stream = connector.connect(name, tcp).await.unwrap();
1519        let _ = stream
1520            .write_all(
1521                format!(
1522                    "GET / HTTP/1.1
1523host: {sni}
1524
1525"
1526                )
1527                .as_bytes(),
1528            )
1529            .await;
1530        let mut buf = Vec::new();
1531        let _ = tokio::time::timeout(
1532            std::time::Duration::from_secs(5),
1533            stream.read_to_end(&mut buf),
1534        )
1535        .await
1536        .expect("server neither answered nor closed the connection");
1537        buf
1538    }
1539
1540    /// RFC 8737 §3: the validator only needs the handshake. A connection that
1541    /// negotiated `acme-tls/1` must be closed without anything being served on
1542    /// it, while a normal client on the same listener is answered as usual.
1543    #[tokio::test]
1544    async fn test_acme_challenge_connection_is_closed_without_a_response() {
1545        let (empty_hooks, _, _) = hooks_with_placeholder(&["m.example.com"]);
1546        let challenge =
1547            crate::acme::challenge::build_challenge_cert("m.example.com", "ka").unwrap();
1548        let pending = Arc::new(FakeSolver(std::sync::Mutex::new(Default::default())));
1549        pending
1550            .0
1551            .lock()
1552            .unwrap()
1553            .insert("m.example.com".to_string(), challenge);
1554        let hooks = AcmeHooks {
1555            certs: empty_hooks.certs.clone(),
1556            solver: pending,
1557        };
1558        let tls = managed_tls(&["m.example.com"]);
1559        let addr =
1560            spawn_acme_aware_server(build_reloadable(&tls, false, Some(&hooks)).unwrap()).await;
1561
1562        let answered =
1563            read_response_over_alpn(addr, "m.example.com", vec![b"http/1.1".to_vec()]).await;
1564        assert!(
1565            answered.starts_with(b"HTTP/1.1 200"),
1566            "{:?}",
1567            String::from_utf8_lossy(&answered)
1568        );
1569
1570        let validation =
1571            read_response_over_alpn(addr, "m.example.com", vec![b"acme-tls/1".to_vec()]).await;
1572        assert!(
1573            validation.is_empty(),
1574            "acme-tls/1 connections must be closed without a response, got {:?}",
1575            String::from_utf8_lossy(&validation)
1576        );
1577    }
1578
1579    #[tokio::test]
1580    async fn test_acme_alpn_not_advertised_without_acme() {
1581        let (tls, cert, key) = self_signed("noacme", "1.2");
1582        let shared = build_reloadable(&tls, true, None).unwrap();
1583        let addr = spawn_reload_server(shared).await;
1584        // Client offering only acme-tls/1 against a non-ACME listener: rustls
1585        // refuses (no overlap), which is the pre-existing behavior.
1586        assert!(
1587            handshake_with_alpn(addr, "localhost", vec![b"acme-tls/1".to_vec()])
1588                .await
1589                .is_err()
1590        );
1591        let _ = std::fs::remove_file(cert);
1592        let _ = std::fs::remove_file(key);
1593    }
1594
1595    #[test]
1596    fn test_managed_slot_without_hooks_is_a_wiring_error() {
1597        let tls = managed_tls(&["m.example.com"]);
1598        let err = build_server_config(&tls, true, None).unwrap_err();
1599        assert!(matches!(err, TlsError::AcmeNotWired(_)), "{err}");
1600    }
1601}