1use 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
23pub 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
33pub 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
92pub 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 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 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
175pub 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 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
251fn 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#[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]); 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
301struct ReloadingState {
305 cert_path: PathBuf,
306 key_path: PathBuf,
307 current: ArcSwap<CertifiedKey>,
308 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 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 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; };
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 let backoff = 2u32.saturating_pow((consecutive_failures - 1).min(16));
385 interval = (ReloadingCertifiedKey::FAILURE_RETRY_INTERVAL * backoff)
386 .min(ReloadingCertifiedKey::RELOAD_CHECK_INTERVAL);
387 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
417pub(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 #[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#[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 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 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 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 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 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 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 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 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}