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#[derive(Debug)]
491struct NoVerifier;
492
493impl rustls::client::danger::ServerCertVerifier for NoVerifier {
494 fn verify_server_cert(
495 &self,
496 _end_entity: &rustls::pki_types::CertificateDer<'_>,
497 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
498 _server_name: &rustls::pki_types::ServerName<'_>,
499 _ocsp_response: &[u8],
500 _now: rustls::pki_types::UnixTime,
501 ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
502 Ok(rustls::client::danger::ServerCertVerified::assertion())
503 }
504
505 fn verify_tls12_signature(
506 &self,
507 _message: &[u8],
508 _cert: &rustls::pki_types::CertificateDer<'_>,
509 _dss: &rustls::DigitallySignedStruct,
510 ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
511 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
512 }
513
514 fn verify_tls13_signature(
515 &self,
516 _message: &[u8],
517 _cert: &rustls::pki_types::CertificateDer<'_>,
518 _dss: &rustls::DigitallySignedStruct,
519 ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
520 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
521 }
522
523 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
524 rustls::crypto::ring::default_provider()
525 .signature_verification_algorithms
526 .supported_schemes()
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use std::io::Write;
534 use tempfile::NamedTempFile;
535
536 fn make_cert_files() -> (NamedTempFile, NamedTempFile) {
537 let key_pair = rcgen::KeyPair::generate().unwrap();
538 let cert = rcgen::CertificateParams::new(vec!["localhost".to_string()])
539 .unwrap()
540 .self_signed(&key_pair)
541 .unwrap();
542 let mut cert_file = NamedTempFile::new().unwrap();
543 cert_file.write_all(cert.pem().as_bytes()).unwrap();
544 let mut key_file = NamedTempFile::new().unwrap();
545 key_file
546 .write_all(key_pair.serialize_pem().as_bytes())
547 .unwrap();
548 (cert_file, key_file)
549 }
550
551 #[test]
552 fn server_config_roundtrip() {
553 let (cert, key) = make_cert_files();
554 server_tls_config(cert.path(), key.path(), None).unwrap();
555 }
556
557 #[test]
558 fn server_config_with_mtls() {
559 let (cert, key) = make_cert_files();
561 server_tls_config(cert.path(), key.path(), Some(cert.path())).unwrap();
562 }
563
564 #[test]
565 fn server_config_mtls_empty_client_ca_errors() {
566 let (cert, key) = make_cert_files();
567 let empty = NamedTempFile::new().unwrap();
568 assert!(
569 server_tls_config(cert.path(), key.path(), Some(empty.path()))
570 .unwrap_err()
571 .to_string()
572 .contains("client CA certificate store is empty")
573 );
574 }
575
576 #[test]
577 fn server_config_bad_paths() {
578 let missing = std::path::Path::new("/nonexistent/x.pem");
579 assert!(
580 server_tls_config(missing, missing, None)
581 .unwrap_err()
582 .to_string()
583 .contains("reading cert")
584 );
585 let (cert, _) = make_cert_files();
586 assert!(
587 server_tls_config(cert.path(), missing, None)
588 .unwrap_err()
589 .to_string()
590 .contains("reading key")
591 );
592 }
593
594 fn make_cert_pem() -> (String, String) {
595 let key_pair = rcgen::KeyPair::generate().unwrap();
596 let cert = rcgen::CertificateParams::new(vec!["localhost".to_string()])
597 .unwrap()
598 .self_signed(&key_pair)
599 .unwrap();
600 (cert.pem(), key_pair.serialize_pem())
601 }
602
603 #[test]
604 fn certified_key_reloads_rotated_files() {
605 let (cert1, key1) = make_cert_pem();
606 let cert_file = NamedTempFile::new().unwrap();
607 let key_file = NamedTempFile::new().unwrap();
608 std::fs::write(cert_file.path(), &cert1).unwrap();
609 std::fs::write(key_file.path(), &key1).unwrap();
610
611 let resolver = ReloadingCertifiedKey::new(cert_file.path(), key_file.path()).unwrap();
612 let before = resolver.resolve_key().cert[0].clone();
613
614 let (cert2, key2) = make_cert_pem();
616 std::fs::write(cert_file.path(), &cert2).unwrap();
617 std::fs::write(key_file.path(), &key2).unwrap();
618 resolver.reload_now().unwrap();
619
620 let after = resolver.resolve_key().cert[0].clone();
621 assert_ne!(
622 before, after,
623 "resolver should serve the rotated certificate after the contents change"
624 );
625 }
626
627 #[test]
628 fn certified_key_reloads_symlinked_generation() {
629 use std::os::unix::fs::symlink;
630
631 let dir = tempfile::tempdir().unwrap();
635 let (c1, k1) = make_cert_pem();
636 let gen1 = dir.path().join("gen1");
637 std::fs::create_dir(&gen1).unwrap();
638 std::fs::write(gen1.join("tls.crt"), &c1).unwrap();
639 std::fs::write(gen1.join("tls.key"), &k1).unwrap();
640
641 let cert_link = dir.path().join("tls.crt");
642 let key_link = dir.path().join("tls.key");
643 symlink(gen1.join("tls.crt"), &cert_link).unwrap();
644 symlink(gen1.join("tls.key"), &key_link).unwrap();
645
646 let resolver = ReloadingCertifiedKey::new(&cert_link, &key_link).unwrap();
647 let before = resolver.resolve_key().cert[0].clone();
648
649 let (c2, k2) = make_cert_pem();
650 let gen2 = dir.path().join("gen2");
651 std::fs::create_dir(&gen2).unwrap();
652 std::fs::write(gen2.join("tls.crt"), &c2).unwrap();
653 std::fs::write(gen2.join("tls.key"), &k2).unwrap();
654 let cert_tmp = dir.path().join("tls.crt.tmp");
656 let key_tmp = dir.path().join("tls.key.tmp");
657 symlink(gen2.join("tls.crt"), &cert_tmp).unwrap();
658 symlink(gen2.join("tls.key"), &key_tmp).unwrap();
659 std::fs::rename(&cert_tmp, &cert_link).unwrap();
660 std::fs::rename(&key_tmp, &key_link).unwrap();
661 resolver.reload_now().unwrap();
662
663 let after = resolver.resolve_key().cert[0].clone();
664 assert_ne!(
665 before, after,
666 "resolver should follow the swapped symlink to the new generation"
667 );
668 }
669
670 #[test]
671 fn certified_key_keeps_previous_on_corrupt_reload() {
672 let (c1, k1) = make_cert_pem();
673 let cert_file = NamedTempFile::new().unwrap();
674 let key_file = NamedTempFile::new().unwrap();
675 std::fs::write(cert_file.path(), &c1).unwrap();
676 std::fs::write(key_file.path(), &k1).unwrap();
677 let resolver = ReloadingCertifiedKey::new(cert_file.path(), key_file.path()).unwrap();
678 let before = resolver.resolve_key().cert[0].clone();
679
680 std::fs::write(cert_file.path(), b"not a valid pem").unwrap();
682 let reload_result = resolver.reload_now();
683
684 assert!(
685 reload_result.is_err(),
686 "a corrupt reload should surface an error to the caller"
687 );
688 let after = resolver.resolve_key().cert[0].clone();
689 assert_eq!(
690 before, after,
691 "a failed reload must keep serving the previously loaded certificate"
692 );
693 }
694
695 #[test]
696 fn client_config_insecure() {
697 client_tls_config(None, true, None, None).unwrap();
698 }
699
700 #[test]
701 fn client_config_with_ca() {
702 let (cert, _) = make_cert_files();
703 client_tls_config(Some(cert.path()), false, None, None).unwrap();
704 }
705
706 #[test]
707 fn client_config_with_mtls() {
708 let (cert, key) = make_cert_files();
710 client_tls_config(
711 Some(cert.path()),
712 false,
713 Some(cert.path()),
714 Some(key.path()),
715 )
716 .unwrap();
717 }
718
719 #[test]
720 fn client_config_mtls_insecure() {
721 let (cert, key) = make_cert_files();
723 client_tls_config(None, true, Some(cert.path()), Some(key.path())).unwrap();
724 }
725
726 #[test]
727 fn client_config_partial_mtls_errors() {
728 let (cert, _) = make_cert_files();
730 assert!(client_tls_config(Some(cert.path()), false, Some(cert.path()), None).is_err());
731 }
732
733 #[test]
734 fn client_config_empty_ca_errors() {
735 let empty = NamedTempFile::new().unwrap();
736 assert!(
737 client_tls_config(Some(empty.path()), false, None, None)
738 .unwrap_err()
739 .to_string()
740 .contains("CA certificate store is empty")
741 );
742 }
743
744 #[test]
745 fn client_config_missing_ca_errors() {
746 assert!(
747 client_tls_config(
748 Some(std::path::Path::new("/nonexistent/ca.pem")),
749 false,
750 None,
751 None
752 )
753 .unwrap_err()
754 .to_string()
755 .contains("reading CA cert")
756 );
757 }
758}