Skip to main content

dynamo_runtime/
tls_utils.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared TLS utilities for the Dynamo runtime.
5//!
6//! Provides helpers for loading PEM certificates and building rustls
7//! `ServerConfig` / `ClientConfig` objects for transport-layer security.
8
9use std::{
10    fmt,
11    path::{Path, PathBuf},
12    sync::{Arc, Mutex},
13    time::Duration,
14};
15
16use anyhow::{Context, Result};
17use arc_swap::ArcSwap;
18use rustls::server::{ClientHello, ResolvesServerCert};
19use rustls::sign::CertifiedKey;
20use rustls::{ClientConfig, RootCertStore, ServerConfig, SignatureScheme};
21use rustls_pemfile::{certs, private_key};
22
23/// TLS handshake timeout, configurable via `DYN_TCP_TLS_HANDSHAKE_TIMEOUT_SECS` (default: 3s).
24pub fn handshake_timeout() -> std::time::Duration {
25    use crate::config::environment_names::tcp_response_stream::tls as env;
26    let secs = std::env::var(env::DYN_TCP_TLS_HANDSHAKE_TIMEOUT_SECS)
27        .ok()
28        .and_then(|v| v.parse::<u64>().ok())
29        .unwrap_or(3);
30    std::time::Duration::from_secs(secs)
31}
32
33/// Build a rustls `ServerConfig` from PEM certificate and key files.
34///
35/// The certificate is served through a `ReloadingCertifiedKey`, so a rotated
36/// cert/key on disk (in-place rewrite or an atomic symlink swap) is picked up
37/// automatically on the next handshake without restarting the process. The
38/// initial load is validated eagerly: an invalid cert/key path
39/// fails here rather than starting a server that cannot serve TLS.
40///
41/// When `client_ca_cert_path` is `Some`, the server requires clients to present
42/// a certificate signed by that CA (mutual TLS); an unauthenticated client is
43/// rejected at the handshake. When `None`, client certificates are not
44/// requested.
45pub fn server_tls_config(
46    cert_path: &Path,
47    key_path: &Path,
48    client_ca_cert_path: Option<&Path>,
49) -> Result<ServerConfig> {
50    let resolver = Arc::new(ReloadingCertifiedKey::new(cert_path, key_path)?);
51
52    let provider = Arc::new(rustls::crypto::ring::default_provider());
53    let builder = ServerConfig::builder_with_provider(provider.clone())
54        .with_safe_default_protocol_versions()
55        .context("configuring TLS protocol versions")?;
56
57    let config = if let Some(ca_path) = client_ca_cert_path {
58        let ca_pem = std::fs::read(ca_path)
59            .with_context(|| format!("reading client CA cert: {}", ca_path.display()))?;
60        let ca_certs = certs(&mut ca_pem.as_slice())
61            .collect::<Result<Vec<_>, _>>()
62            .context("parsing client CA certificate PEM")?;
63        let mut client_roots = RootCertStore::empty();
64        for cert in ca_certs {
65            client_roots
66                .add(cert)
67                .context("adding client CA certificate to root store")?;
68        }
69        if client_roots.is_empty() {
70            anyhow::bail!(
71                "client CA certificate store is empty after parsing {}; \
72                 ensure the file contains at least one valid PEM certificate",
73                ca_path.display()
74            );
75        }
76        let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
77            Arc::new(client_roots),
78            provider,
79        )
80        .build()
81        .context("building client certificate verifier")?;
82        builder
83            .with_client_cert_verifier(verifier)
84            .with_cert_resolver(resolver)
85    } else {
86        builder.with_no_client_auth().with_cert_resolver(resolver)
87    };
88
89    Ok(config)
90}
91
92/// Build a server `ServerConfig` for a TCP plane from optional cert/key/client-CA
93/// paths, with the validation and misconfiguration diagnostics shared by the
94/// request-plane and response-stream servers. `plane` labels the log lines
95/// (e.g. `"TCP request plane"` / `"TCP server"`).
96///
97/// Returns `Ok(None)` for the plaintext case and fails closed on partial or
98/// invalid configuration (cert without key, a client CA without a server
99/// cert/key). When a client CA is supplied, the resulting config enforces mTLS.
100pub fn server_tls_acceptor_config(
101    plane: &str,
102    cert: Option<&Path>,
103    key: Option<&Path>,
104    client_ca: Option<&Path>,
105) -> Result<Option<ServerConfig>> {
106    use crate::config::environment_names::tcp_response_stream::tls as env;
107    match (cert, key) {
108        (Some(cert), Some(key)) => {
109            let config = server_tls_config(cert, key, client_ca)
110                .with_context(|| format!("building {plane} TLS config from cert/key/client CA"))?;
111            if client_ca.is_some() {
112                tracing::info!(
113                    plane,
114                    "TLS enabled with mutual authentication (client certificates required)"
115                );
116                // Every component also dials peers as a client. Enforcing client
117                // certs here while presenting no identity of our own means our
118                // outbound handshakes to other mTLS peers would fail.
119                let has_client_identity = std::env::var(env::DYN_TCP_TLS_CLIENT_CERT_PATH).is_ok()
120                    && std::env::var(env::DYN_TCP_TLS_CLIENT_KEY_PATH).is_ok();
121                if !has_client_identity {
122                    tracing::warn!(
123                        plane,
124                        client_ca_var = env::DYN_TCP_TLS_CLIENT_CA_CERT_PATH,
125                        client_cert_var = env::DYN_TCP_TLS_CLIENT_CERT_PATH,
126                        client_key_var = env::DYN_TCP_TLS_CLIENT_KEY_PATH,
127                        "server enforces client certificates but no client identity is configured; outbound connections to peers that also enforce mTLS will fail the handshake",
128                    );
129                }
130            } else {
131                tracing::info!(plane, "TLS enabled");
132            }
133            // Applies to both TLS and mTLS: if the client side has no way to
134            // verify this server, peers dialing it fail the handshake with an
135            // opaque error.
136            let client_trust_set = std::env::var(env::DYN_TCP_TLS_CA_CERT_PATH).is_ok()
137                || crate::config::env_is_truthy(env::DYN_TCP_TLS_INSECURE);
138            if !client_trust_set {
139                tracing::warn!(
140                    plane,
141                    ca_var = env::DYN_TCP_TLS_CA_CERT_PATH,
142                    insecure_var = env::DYN_TCP_TLS_INSECURE,
143                    "server has TLS enabled but no client trust is configured; peers cannot verify this server",
144                );
145            }
146            Ok(Some(config))
147        }
148        (Some(_), None) | (None, Some(_)) => anyhow::bail!(
149            "both {} and {} must be set to enable {plane} TLS",
150            env::DYN_TCP_TLS_CERT_PATH,
151            env::DYN_TCP_TLS_KEY_PATH,
152        ),
153        (None, None) if client_ca.is_some() => anyhow::bail!(
154            "{} requires {} and {} to also be set",
155            env::DYN_TCP_TLS_CLIENT_CA_CERT_PATH,
156            env::DYN_TCP_TLS_CERT_PATH,
157            env::DYN_TCP_TLS_KEY_PATH,
158        ),
159        (None, None) => {
160            let client_trust_set = std::env::var(env::DYN_TCP_TLS_CA_CERT_PATH).is_ok()
161                || crate::config::env_is_truthy(env::DYN_TCP_TLS_INSECURE);
162            if client_trust_set {
163                tracing::warn!(
164                    plane,
165                    cert_var = env::DYN_TCP_TLS_CERT_PATH,
166                    key_var = env::DYN_TCP_TLS_KEY_PATH,
167                    "server is running in plaintext but client TLS env vars are set; set the server cert/key to enable TLS, or unset the client vars",
168                );
169            }
170            Ok(None)
171        }
172    }
173}
174
175/// Build a rustls `ClientConfig` for outbound TLS connections.
176///
177/// - `ca_cert_path`: trust this CA for verifying the server certificate.
178///   When `None`, the root store is empty — supply a CA cert or use `insecure`.
179/// - `insecure`: skip certificate verification entirely. **Dev/test only.**
180/// - `client_cert_path` + `client_key_path`: when both are `Some`, the client
181///   presents this certificate to the server (mutual TLS). The identity is
182///   served through a `ReloadingCertifiedKey`, so a rotated client cert/key on
183///   disk is picked up without a process restart. Both must be set together.
184pub fn client_tls_config(
185    ca_cert_path: Option<&Path>,
186    insecure: bool,
187    client_cert_path: Option<&Path>,
188    client_key_path: Option<&Path>,
189) -> Result<ClientConfig> {
190    if client_cert_path.is_some() != client_key_path.is_some() {
191        anyhow::bail!("client cert and key paths must both be set or both be unset");
192    }
193
194    let provider = Arc::new(rustls::crypto::ring::default_provider());
195
196    if insecure {
197        tracing::info!("TLS: certificate verification disabled (insecure mode)");
198        let builder = ClientConfig::builder_with_provider(provider)
199            .with_safe_default_protocol_versions()
200            .context("configuring TLS protocol versions")?
201            .dangerous()
202            .with_custom_certificate_verifier(Arc::new(NoVerifier));
203        let config = match (client_cert_path, client_key_path) {
204            (Some(cp), Some(kp)) => {
205                builder.with_client_cert_resolver(Arc::new(ReloadingCertifiedKey::new(cp, kp)?))
206            }
207            _ => builder.with_no_client_auth(),
208        };
209        return Ok(config);
210    }
211
212    let mut root_store = RootCertStore::empty();
213    if let Some(ca_path) = ca_cert_path {
214        let ca_pem = std::fs::read(ca_path)
215            .with_context(|| format!("reading CA cert: {}", ca_path.display()))?;
216        let ca_certs = certs(&mut ca_pem.as_slice())
217            .collect::<Result<Vec<_>, _>>()
218            .context("parsing CA certificate PEM")?;
219        for cert in ca_certs {
220            root_store
221                .add(cert)
222                .context("adding CA certificate to root store")?;
223        }
224        if root_store.is_empty() {
225            anyhow::bail!(
226                "CA certificate store is empty after parsing {}; \
227                 ensure the file contains at least one valid PEM certificate",
228                ca_path.display()
229            );
230        }
231    }
232    // When no CA cert is provided, the root store is empty — the caller must
233    // supply a CA cert or use `insecure = true`. This is intentional: in
234    // cluster deployments, certs are issued by an internal CA and system roots
235    // are not relevant.
236
237    let builder = ClientConfig::builder_with_provider(provider)
238        .with_safe_default_protocol_versions()
239        .context("configuring TLS protocol versions")?
240        .with_root_certificates(root_store);
241    let config = match (client_cert_path, client_key_path) {
242        (Some(cp), Some(kp)) => {
243            builder.with_client_cert_resolver(Arc::new(ReloadingCertifiedKey::new(cp, kp)?))
244        }
245        _ => builder.with_no_client_auth(),
246    };
247
248    Ok(config)
249}
250
251/// Load a leaf certificate chain + private key from PEM bytes into a rustls
252/// [`CertifiedKey`], validating that the certificate and key match.
253fn load_certified_key(cert_pem: &[u8], key_pem: &[u8]) -> Result<CertifiedKey> {
254    let mut cert_reader = cert_pem;
255    let cert_chain = certs(&mut cert_reader)
256        .collect::<Result<Vec<_>, _>>()
257        .context("parsing certificate PEM")?;
258    if cert_chain.is_empty() {
259        anyhow::bail!("no certificates found in PEM");
260    }
261
262    let mut key_reader = key_pem;
263    let key = private_key(&mut key_reader)
264        .context("parsing private key PEM")?
265        .context("no private key found in PEM")?;
266    let signing_key =
267        rustls::crypto::ring::sign::any_supported_type(&key).context("loading TLS private key")?;
268
269    let certified_key = CertifiedKey::new(cert_chain, signing_key);
270    certified_key
271        .keys_match()
272        .context("TLS certificate and private key do not match")?;
273    Ok(certified_key)
274}
275
276/// Content fingerprint of a loaded identity. Change is detected by hashing the
277/// file *contents* (blake3) rather than mtime, so atomic symlink swaps (where a
278/// mounted directory of certs is rotated by relinking) are handled reliably.
279#[derive(Debug, Eq, PartialEq)]
280struct IdentityFingerprint {
281    content_hash: [u8; 32],
282}
283
284impl IdentityFingerprint {
285    fn from_loaded(cert_pem: &[u8], key_pem: &[u8]) -> Self {
286        let mut hasher = blake3::Hasher::new();
287        hasher.update(cert_pem);
288        hasher.update(&[0]); // domain separator between cert and key
289        hasher.update(key_pem);
290        Self {
291            content_hash: *hasher.finalize().as_bytes(),
292        }
293    }
294}
295
296struct LoadedIdentity {
297    fingerprint: IdentityFingerprint,
298    certified_key: Arc<CertifiedKey>,
299}
300
301/// Shared reloadable identity. The current identity lives in an [`ArcSwap`]
302/// read lock-free on the handshake path; a background thread owns the filesystem
303/// reads and swaps in a new identity when the on-disk contents change.
304struct ReloadingState {
305    cert_path: PathBuf,
306    key_path: PathBuf,
307    current: ArcSwap<CertifiedKey>,
308    /// Fingerprint of the last successfully loaded identity. Only the background
309    /// reloader (and tests) touch this, so it never contends with handshakes.
310    fingerprint: Mutex<IdentityFingerprint>,
311}
312
313impl ReloadingState {
314    fn load(cert_path: &Path, key_path: &Path) -> Result<LoadedIdentity> {
315        let cert_pem = std::fs::read(cert_path)
316            .with_context(|| format!("reading cert: {}", cert_path.display()))?;
317        let key_pem = std::fs::read(key_path)
318            .with_context(|| format!("reading key: {}", key_path.display()))?;
319        let certified_key = load_certified_key(&cert_pem, &key_pem)?;
320        let fingerprint = IdentityFingerprint::from_loaded(&cert_pem, &key_pem);
321        Ok(LoadedIdentity {
322            fingerprint,
323            certified_key: Arc::new(certified_key),
324        })
325    }
326
327    /// Re-read the identity from disk and swap it in if the contents changed.
328    /// Runs off the handshake path (background thread / tests). A failed read
329    /// leaves the last valid identity in place and propagates the error so the
330    /// caller can back off.
331    fn refresh(&self) -> Result<()> {
332        let reloaded = Self::load(&self.cert_path, &self.key_path)?;
333        let mut fingerprint = self
334            .fingerprint
335            .lock()
336            .unwrap_or_else(|poisoned| poisoned.into_inner());
337        if *fingerprint != reloaded.fingerprint {
338            let cert_count = reloaded.certified_key.cert.len();
339            self.current.store(reloaded.certified_key);
340            *fingerprint = reloaded.fingerprint;
341            tracing::info!(
342                cert_path = %self.cert_path.display(),
343                cert_count,
344                "Reloaded rotated TLS certificate and key from disk"
345            );
346        }
347        Ok(())
348    }
349
350    /// Spawn a background thread that periodically refreshes the identity. It
351    /// holds only a `Weak` reference, so it exits once the resolver is dropped.
352    /// A plain OS thread (not a Tokio task) keeps the filesystem reads off every
353    /// async runtime worker and avoids depending on a runtime being present when
354    /// the resolver is built.
355    fn spawn_reloader(state: &Arc<Self>) {
356        let weak = Arc::downgrade(state);
357        let spawned = std::thread::Builder::new()
358            .name("tls-cert-reloader".to_string())
359            .spawn(move || {
360                let mut interval = ReloadingCertifiedKey::RELOAD_CHECK_INTERVAL;
361                let mut consecutive_failures: u32 = 0;
362                loop {
363                    std::thread::sleep(interval);
364                    let Some(state) = weak.upgrade() else {
365                        break; // resolver dropped; stop reloading
366                    };
367                    match state.refresh() {
368                        Ok(()) => {
369                            if consecutive_failures > 0 {
370                                tracing::info!(
371                                    cert_path = %state.cert_path.display(),
372                                    failed_attempts = consecutive_failures,
373                                    "Recovered: reloaded TLS certificate after earlier failures"
374                                );
375                            }
376                            consecutive_failures = 0;
377                            interval = ReloadingCertifiedKey::RELOAD_CHECK_INTERVAL;
378                        }
379                        Err(error) => {
380                            consecutive_failures = consecutive_failures.saturating_add(1);
381                            // Exponential backoff from FAILURE_RETRY_INTERVAL, capped
382                            // at the normal check interval, so a persistently broken
383                            // file doesn't hammer the filesystem or the logs.
384                            let backoff = 2u32.saturating_pow((consecutive_failures - 1).min(16));
385                            interval = (ReloadingCertifiedKey::FAILURE_RETRY_INTERVAL * backoff)
386                                .min(ReloadingCertifiedKey::RELOAD_CHECK_INTERVAL);
387                            // Warn once when the failure begins; drop to debug while
388                            // it persists so a permanently broken file isn't logged
389                            // on every retry.
390                            if consecutive_failures == 1 {
391                                tracing::warn!(
392                                    cert_path = %state.cert_path.display(),
393                                    error = %format!("{error:#}"),
394                                    "Failed to reload rotated TLS certificate; keeping the last valid identity"
395                                );
396                            } else {
397                                tracing::debug!(
398                                    cert_path = %state.cert_path.display(),
399                                    error = %format!("{error:#}"),
400                                    attempt = consecutive_failures,
401                                    "TLS certificate reload still failing; retrying with backoff"
402                                );
403                            }
404                        }
405                    }
406                }
407            });
408        if let Err(error) = spawned {
409            tracing::warn!(
410                error = %error,
411                "Failed to spawn TLS certificate reloader thread; certificate hot-reload is disabled for this identity"
412            );
413        }
414    }
415}
416
417/// A rustls certificate resolver whose served identity is refreshed from disk by
418/// a background thread, so a rotated cert/key (in-place rewrite or atomic
419/// symlink swap) is picked up without a process restart.
420///
421/// `resolve()` only reads the current identity from an [`ArcSwap`] — it performs
422/// no filesystem I/O and never blocks, so it is safe to call from the
423/// `tokio-rustls` handshake poll. A failed reload keeps the last valid identity.
424///
425/// The same type serves as both a [`ResolvesServerCert`] (server leaf cert) and
426/// a [`rustls::client::ResolvesClientCert`] (mTLS client identity).
427pub(crate) struct ReloadingCertifiedKey {
428    state: Arc<ReloadingState>,
429}
430
431impl fmt::Debug for ReloadingCertifiedKey {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        f.debug_struct("ReloadingCertifiedKey")
434            .field("cert_path", &self.state.cert_path)
435            .field("key_path", &self.state.key_path)
436            .finish_non_exhaustive()
437    }
438}
439
440impl ReloadingCertifiedKey {
441    const RELOAD_CHECK_INTERVAL: Duration = Duration::from_secs(30);
442    const FAILURE_RETRY_INTERVAL: Duration = Duration::from_secs(1);
443
444    fn new(cert_path: &Path, key_path: &Path) -> Result<Self> {
445        let loaded = ReloadingState::load(cert_path, key_path)?;
446        let state = Arc::new(ReloadingState {
447            cert_path: cert_path.to_path_buf(),
448            key_path: key_path.to_path_buf(),
449            current: ArcSwap::from(loaded.certified_key),
450            fingerprint: Mutex::new(loaded.fingerprint),
451        });
452        ReloadingState::spawn_reloader(&state);
453        Ok(Self { state })
454    }
455
456    fn resolve_key(&self) -> Arc<CertifiedKey> {
457        self.state.current.load_full()
458    }
459
460    /// Test-only: perform one synchronous reload cycle (what the background
461    /// thread does on each tick) so tests can drive rotation deterministically.
462    #[cfg(test)]
463    fn reload_now(&self) -> Result<()> {
464        self.state.refresh()
465    }
466}
467
468impl ResolvesServerCert for ReloadingCertifiedKey {
469    fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
470        Some(self.resolve_key())
471    }
472}
473
474impl rustls::client::ResolvesClientCert for ReloadingCertifiedKey {
475    fn resolve(
476        &self,
477        _root_hint_subjects: &[&[u8]],
478        _sigschemes: &[SignatureScheme],
479    ) -> Option<Arc<CertifiedKey>> {
480        Some(self.resolve_key())
481    }
482
483    fn has_certs(&self) -> bool {
484        true
485    }
486}
487
488#[cfg(test)]
489pub(crate) mod test_certs {
490    use std::io::Write;
491
492    use rcgen::{
493        BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair,
494    };
495    use tempfile::NamedTempFile;
496
497    pub(crate) type IdentityFiles = (NamedTempFile, NamedTempFile);
498    pub(crate) type MtlsChainFiles = (
499        NamedTempFile,
500        NamedTempFile,
501        NamedTempFile,
502        NamedTempFile,
503        NamedTempFile,
504    );
505
506    fn write_pem(contents: &str) -> NamedTempFile {
507        let mut file = NamedTempFile::new().unwrap();
508        file.write_all(contents.as_bytes()).unwrap();
509        file
510    }
511
512    pub(crate) fn self_signed_pem() -> (String, String) {
513        let key_pair = KeyPair::generate().unwrap();
514        let cert = CertificateParams::new(vec!["localhost".to_string()])
515            .unwrap()
516            .self_signed(&key_pair)
517            .unwrap();
518        (cert.pem(), key_pair.serialize_pem())
519    }
520
521    pub(crate) fn self_signed_pair() -> IdentityFiles {
522        let (cert, key) = self_signed_pem();
523        (write_pem(&cert), write_pem(&key))
524    }
525
526    pub(crate) fn mtls_chain() -> MtlsChainFiles {
527        let ca_key = KeyPair::generate().unwrap();
528        let mut ca_params = CertificateParams::new(Vec::new()).unwrap();
529        ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
530        ca_params
531            .distinguished_name
532            .push(DnType::CommonName, "Dynamo Test CA");
533        let ca_cert = ca_params.self_signed(&ca_key).unwrap();
534
535        let server_key = KeyPair::generate().unwrap();
536        let mut server_params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
537        server_params
538            .extended_key_usages
539            .push(ExtendedKeyUsagePurpose::ServerAuth);
540        let server_cert = server_params
541            .signed_by(&server_key, &ca_cert, &ca_key)
542            .unwrap();
543
544        let client_key = KeyPair::generate().unwrap();
545        let mut client_params = CertificateParams::new(vec!["dynamo-client".to_string()]).unwrap();
546        client_params
547            .extended_key_usages
548            .push(ExtendedKeyUsagePurpose::ClientAuth);
549        let client_cert = client_params
550            .signed_by(&client_key, &ca_cert, &ca_key)
551            .unwrap();
552
553        (
554            write_pem(&ca_cert.pem()),
555            write_pem(&server_cert.pem()),
556            write_pem(&server_key.serialize_pem()),
557            write_pem(&client_cert.pem()),
558            write_pem(&client_key.serialize_pem()),
559        )
560    }
561}
562
563/// Certificate verifier that accepts any certificate.
564/// **Only for development/testing. Never use in production.**
565#[derive(Debug)]
566struct NoVerifier;
567
568impl rustls::client::danger::ServerCertVerifier for NoVerifier {
569    fn verify_server_cert(
570        &self,
571        _end_entity: &rustls::pki_types::CertificateDer<'_>,
572        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
573        _server_name: &rustls::pki_types::ServerName<'_>,
574        _ocsp_response: &[u8],
575        _now: rustls::pki_types::UnixTime,
576    ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
577        Ok(rustls::client::danger::ServerCertVerified::assertion())
578    }
579
580    fn verify_tls12_signature(
581        &self,
582        _message: &[u8],
583        _cert: &rustls::pki_types::CertificateDer<'_>,
584        _dss: &rustls::DigitallySignedStruct,
585    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
586        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
587    }
588
589    fn verify_tls13_signature(
590        &self,
591        _message: &[u8],
592        _cert: &rustls::pki_types::CertificateDer<'_>,
593        _dss: &rustls::DigitallySignedStruct,
594    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
595        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
596    }
597
598    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
599        rustls::crypto::ring::default_provider()
600            .signature_verification_algorithms
601            .supported_schemes()
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use tempfile::NamedTempFile;
609
610    #[test]
611    fn server_config_roundtrip() {
612        let (cert, key) = test_certs::self_signed_pair();
613        server_tls_config(cert.path(), key.path(), None).unwrap();
614    }
615
616    #[test]
617    fn server_config_with_mtls() {
618        // A client CA turns on client-certificate verification (mTLS).
619        let (cert, key) = test_certs::self_signed_pair();
620        server_tls_config(cert.path(), key.path(), Some(cert.path())).unwrap();
621    }
622
623    #[test]
624    fn server_config_mtls_empty_client_ca_errors() {
625        let (cert, key) = test_certs::self_signed_pair();
626        let empty = NamedTempFile::new().unwrap();
627        assert!(
628            server_tls_config(cert.path(), key.path(), Some(empty.path()))
629                .unwrap_err()
630                .to_string()
631                .contains("client CA certificate store is empty")
632        );
633    }
634
635    #[test]
636    fn server_config_bad_paths() {
637        let missing = std::path::Path::new("/nonexistent/x.pem");
638        assert!(
639            server_tls_config(missing, missing, None)
640                .unwrap_err()
641                .to_string()
642                .contains("reading cert")
643        );
644        let (cert, _) = test_certs::self_signed_pair();
645        assert!(
646            server_tls_config(cert.path(), missing, None)
647                .unwrap_err()
648                .to_string()
649                .contains("reading key")
650        );
651    }
652
653    #[test]
654    fn certified_key_reloads_rotated_files() {
655        let (cert1, key1) = test_certs::self_signed_pem();
656        let cert_file = NamedTempFile::new().unwrap();
657        let key_file = NamedTempFile::new().unwrap();
658        std::fs::write(cert_file.path(), &cert1).unwrap();
659        std::fs::write(key_file.path(), &key1).unwrap();
660
661        let resolver = ReloadingCertifiedKey::new(cert_file.path(), key_file.path()).unwrap();
662        let before = resolver.resolve_key().cert[0].clone();
663
664        // Rotate the file contents in place and force a re-check.
665        let (cert2, key2) = test_certs::self_signed_pem();
666        std::fs::write(cert_file.path(), &cert2).unwrap();
667        std::fs::write(key_file.path(), &key2).unwrap();
668        resolver.reload_now().unwrap();
669
670        let after = resolver.resolve_key().cert[0].clone();
671        assert_ne!(
672            before, after,
673            "resolver should serve the rotated certificate after the contents change"
674        );
675    }
676
677    #[test]
678    fn certified_key_reloads_symlinked_generation() {
679        use std::os::unix::fs::symlink;
680
681        // Mimic a symlink-based cert rotation: the mounted paths are symlinks
682        // into a per-generation directory, rotated by an atomic rename over the
683        // link.
684        let dir = tempfile::tempdir().unwrap();
685        let (c1, k1) = test_certs::self_signed_pem();
686        let gen1 = dir.path().join("gen1");
687        std::fs::create_dir(&gen1).unwrap();
688        std::fs::write(gen1.join("tls.crt"), &c1).unwrap();
689        std::fs::write(gen1.join("tls.key"), &k1).unwrap();
690
691        let cert_link = dir.path().join("tls.crt");
692        let key_link = dir.path().join("tls.key");
693        symlink(gen1.join("tls.crt"), &cert_link).unwrap();
694        symlink(gen1.join("tls.key"), &key_link).unwrap();
695
696        let resolver = ReloadingCertifiedKey::new(&cert_link, &key_link).unwrap();
697        let before = resolver.resolve_key().cert[0].clone();
698
699        let (c2, k2) = test_certs::self_signed_pem();
700        let gen2 = dir.path().join("gen2");
701        std::fs::create_dir(&gen2).unwrap();
702        std::fs::write(gen2.join("tls.crt"), &c2).unwrap();
703        std::fs::write(gen2.join("tls.key"), &k2).unwrap();
704        // Atomic symlink swap: create new links then rename over the live ones.
705        let cert_tmp = dir.path().join("tls.crt.tmp");
706        let key_tmp = dir.path().join("tls.key.tmp");
707        symlink(gen2.join("tls.crt"), &cert_tmp).unwrap();
708        symlink(gen2.join("tls.key"), &key_tmp).unwrap();
709        std::fs::rename(&cert_tmp, &cert_link).unwrap();
710        std::fs::rename(&key_tmp, &key_link).unwrap();
711        resolver.reload_now().unwrap();
712
713        let after = resolver.resolve_key().cert[0].clone();
714        assert_ne!(
715            before, after,
716            "resolver should follow the swapped symlink to the new generation"
717        );
718    }
719
720    #[test]
721    fn certified_key_keeps_previous_on_corrupt_reload() {
722        let (c1, k1) = test_certs::self_signed_pem();
723        let cert_file = NamedTempFile::new().unwrap();
724        let key_file = NamedTempFile::new().unwrap();
725        std::fs::write(cert_file.path(), &c1).unwrap();
726        std::fs::write(key_file.path(), &k1).unwrap();
727        let resolver = ReloadingCertifiedKey::new(cert_file.path(), key_file.path()).unwrap();
728        let before = resolver.resolve_key().cert[0].clone();
729
730        // Simulate a partial write mid-rotation.
731        std::fs::write(cert_file.path(), b"not a valid pem").unwrap();
732        let reload_result = resolver.reload_now();
733
734        assert!(
735            reload_result.is_err(),
736            "a corrupt reload should surface an error to the caller"
737        );
738        let after = resolver.resolve_key().cert[0].clone();
739        assert_eq!(
740            before, after,
741            "a failed reload must keep serving the previously loaded certificate"
742        );
743    }
744
745    #[test]
746    fn client_config_insecure() {
747        client_tls_config(None, true, None, None).unwrap();
748    }
749
750    #[test]
751    fn client_config_with_ca() {
752        let (cert, _) = test_certs::self_signed_pair();
753        client_tls_config(Some(cert.path()), false, None, None).unwrap();
754    }
755
756    #[test]
757    fn client_config_with_mtls() {
758        // A client cert/key pair is presented as the client identity (mTLS).
759        let (cert, key) = test_certs::self_signed_pair();
760        client_tls_config(
761            Some(cert.path()),
762            false,
763            Some(cert.path()),
764            Some(key.path()),
765        )
766        .unwrap();
767    }
768
769    #[test]
770    fn client_config_mtls_insecure() {
771        // Client identity is also honored in insecure (no server verification) mode.
772        let (cert, key) = test_certs::self_signed_pair();
773        client_tls_config(None, true, Some(cert.path()), Some(key.path())).unwrap();
774    }
775
776    #[test]
777    fn client_config_partial_mtls_errors() {
778        // Cert without key (or vice versa) is rejected.
779        let (cert, _) = test_certs::self_signed_pair();
780        assert!(client_tls_config(Some(cert.path()), false, Some(cert.path()), None).is_err());
781    }
782
783    #[test]
784    fn client_config_empty_ca_errors() {
785        let empty = NamedTempFile::new().unwrap();
786        assert!(
787            client_tls_config(Some(empty.path()), false, None, None)
788                .unwrap_err()
789                .to_string()
790                .contains("CA certificate store is empty")
791        );
792    }
793
794    #[test]
795    fn client_config_missing_ca_errors() {
796        assert!(
797            client_tls_config(
798                Some(std::path::Path::new("/nonexistent/ca.pem")),
799                false,
800                None,
801                None
802            )
803            .unwrap_err()
804            .to_string()
805            .contains("reading CA cert")
806        );
807    }
808}