1#[cfg(all(unix, not(feature = "fips")))]
4use std::os::unix::fs::OpenOptionsExt;
5#[cfg(not(feature = "fips"))]
6use std::{
7 fmt::{Debug, Formatter},
8 fs::{File, OpenOptions},
9 io::{self, Write},
10 path::Path,
11};
12use std::{
13 path::PathBuf,
14 sync::{Arc, Mutex, OnceLock},
15};
16
17#[cfg(not(feature = "fips"))]
18use rustls::KeyLog;
19use rustls::{
20 client::{
21 danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
22 Resumption,
23 },
24 crypto::CryptoProvider,
25 pki_types::{CertificateDer, ServerName, UnixTime},
26 version::{TLS12, TLS13},
27 ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme, SupportedProtocolVersion,
28};
29#[cfg(not(feature = "fips"))]
30use saluki_common::collections::FastHashMap;
31use saluki_error::{generic_error, GenericError};
32use tracing::debug;
33#[cfg(not(feature = "fips"))]
34use tracing::warn;
35
36static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
38
39static DEFAULT_ROOT_CERT_STORE_MUTEX: Mutex<()> = Mutex::new(());
41static DEFAULT_ROOT_CERT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
42#[cfg(not(feature = "fips"))]
43static KEY_LOG_FILES: OnceLock<Mutex<FastHashMap<PathBuf, Option<Arc<NssKeyLogFile>>>>> = OnceLock::new();
44
45const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
47const TLS12_PLUS_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
48const TLS13_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13];
49
50#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
52pub enum TlsMinimumVersion {
53 #[default]
55 Tls12,
56
57 Tls13,
59}
60
61impl TlsMinimumVersion {
62 const fn protocol_versions(self) -> &'static [&'static SupportedProtocolVersion] {
63 match self {
64 Self::Tls12 => TLS12_PLUS_PROTOCOL_VERSIONS,
65 Self::Tls13 => TLS13_PROTOCOL_VERSIONS,
66 }
67 }
68}
69
70#[derive(Debug)]
76struct AcceptAllServerCertVerifier {
77 provider: Arc<CryptoProvider>,
78}
79
80impl ServerCertVerifier for AcceptAllServerCertVerifier {
81 fn verify_server_cert(
82 &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
83 _ocsp_response: &[u8], _now: UnixTime,
84 ) -> Result<ServerCertVerified, rustls::Error> {
85 Ok(ServerCertVerified::assertion())
86 }
87
88 fn verify_tls12_signature(
89 &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
90 ) -> Result<HandshakeSignatureValid, rustls::Error> {
91 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
92 }
93
94 fn verify_tls13_signature(
95 &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
96 ) -> Result<HandshakeSignatureValid, rustls::Error> {
97 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
98 }
99
100 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
101 self.provider.signature_verification_algorithms.supported_schemes()
102 }
103}
104
105#[cfg(not(feature = "fips"))]
106struct NssKeyLogFile {
107 path: PathBuf,
108 file: Mutex<File>,
109}
110
111#[cfg(not(feature = "fips"))]
112impl NssKeyLogFile {
113 fn open_shared<P: Into<PathBuf>>(path: P) -> Option<Arc<Self>> {
114 let path = path.into();
115 let mut key_log_files = match KEY_LOG_FILES.get_or_init(|| Mutex::new(FastHashMap::default())).lock() {
116 Ok(key_log_files) => key_log_files,
117 Err(_) => {
118 warn!("Failed to acquire TLS key log file registry lock; TLS key logging disabled.");
119 return None;
120 }
121 };
122
123 if let Some(cached) = key_log_files.get(&path) {
126 return cached.clone();
127 }
128
129 let key_log_file = match open_key_log_file(&path) {
130 Ok(file) => {
131 warn!(
132 path = %path.display(),
133 "TLS key logging enabled; TLS session secrets will be written to disk."
134 );
135 Some(Arc::new(Self {
136 path: path.clone(),
137 file: Mutex::new(file),
138 }))
139 }
140 Err(e) => {
141 warn!(
142 path = %path.display(),
143 error = %e,
144 "Failed to open TLS key log file for appending; TLS key logging disabled."
145 );
146 None
147 }
148 };
149
150 key_log_files.insert(path, key_log_file.clone());
151 key_log_file
152 }
153}
154
155#[cfg(not(feature = "fips"))]
156impl Debug for NssKeyLogFile {
157 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("NssKeyLogFile").field("path", &self.path).finish()
159 }
160}
161
162#[cfg(feature = "fips")]
163static FIPS_KEY_LOG_WARNED_PATHS: OnceLock<Mutex<saluki_common::collections::FastHashSet<PathBuf>>> = OnceLock::new();
164
165#[cfg(feature = "fips")]
166fn fips_key_log_warn_once(path: PathBuf) {
167 let warned =
168 FIPS_KEY_LOG_WARNED_PATHS.get_or_init(|| Mutex::new(saluki_common::collections::FastHashSet::default()));
169 let Ok(mut warned) = warned.lock() else {
170 return;
171 };
172 if warned.insert(path.clone()) {
173 tracing::warn!(
174 path = %path.display(),
175 "FIPS build: TLS key logging is disabled because exporting TLS secrets is not FIPS-compliant."
176 );
177 }
178}
179
180#[cfg(not(feature = "fips"))]
181impl KeyLog for NssKeyLogFile {
182 fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) {
183 let line = match build_nss_key_log_line(label, client_random, secret) {
184 Ok(line) => line,
185 Err(e) => {
186 debug!(path = %self.path.display(), error = %e, "Failed to format TLS key log line.");
187 return;
188 }
189 };
190
191 match self.file.lock() {
192 Ok(mut file) => {
193 if let Err(e) = file.write_all(&line) {
194 debug!(path = %self.path.display(), error = %e, "Failed to write TLS key log line.");
195 }
196 }
197 Err(_) => {
198 debug!(path = %self.path.display(), "TLS key log file lock poisoned; dropping TLS key log line.");
199 }
200 }
201 }
202}
203
204#[cfg(not(feature = "fips"))]
205fn open_key_log_file(path: &Path) -> io::Result<File> {
206 let mut options = OpenOptions::new();
207 options.write(true).create(true).append(true);
208
209 #[cfg(unix)]
210 options.mode(0o600);
211
212 options.open(path)
213}
214
215#[cfg(not(feature = "fips"))]
216fn build_nss_key_log_line(label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<Vec<u8>> {
217 let mut line = Vec::new();
218 write!(line, "{label} ")?;
219 write_hex(&mut line, client_random)?;
220 write!(line, " ")?;
221 write_hex(&mut line, secret)?;
222 writeln!(line)?;
223
224 Ok(line)
225}
226
227#[cfg(not(feature = "fips"))]
228fn write_hex(writer: &mut impl Write, bytes: &[u8]) -> io::Result<()> {
229 for byte in bytes {
230 write!(writer, "{byte:02x}")?;
231 }
232
233 Ok(())
234}
235
236pub struct ClientTLSConfigBuilder {
245 key_log_file_path: Option<PathBuf>,
246 max_tls12_resumption_sessions: Option<usize>,
247 min_tls_version: TlsMinimumVersion,
248 root_cert_store: Option<RootCertStore>,
249 danger_accept_invalid_certs: bool,
250}
251
252impl ClientTLSConfigBuilder {
253 pub fn new() -> Self {
254 Self {
255 key_log_file_path: None,
256 max_tls12_resumption_sessions: None,
257 min_tls_version: TlsMinimumVersion::default(),
258 root_cert_store: None,
259 danger_accept_invalid_certs: false,
260 }
261 }
262
263 pub fn with_key_log_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
274 self.key_log_file_path = Some(path.into());
275 self
276 }
277
278 pub fn with_max_tls12_resumption_sessions(mut self, max: usize) -> Self {
282 self.max_tls12_resumption_sessions = Some(max);
283 self
284 }
285
286 pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
290 self.root_cert_store = Some(store);
291 self
292 }
293
294 pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
298 self.min_tls_version = version;
299 self
300 }
301
302 pub fn danger_accept_invalid_certs(mut self) -> Self {
308 self.danger_accept_invalid_certs = true;
309 self
310 }
311
312 pub fn build(self) -> Result<ClientConfig, GenericError> {
320 let max_tls12_resumption_sessions = self
321 .max_tls12_resumption_sessions
322 .unwrap_or(DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS);
323 let protocol_versions = self.min_tls_version.protocol_versions();
324
325 let mut config = if self.danger_accept_invalid_certs {
326 let crypto_provider = CryptoProvider::get_default()
327 .map(Arc::clone)
328 .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
329 let verifier = Arc::new(AcceptAllServerCertVerifier {
330 provider: crypto_provider,
331 });
332
333 ClientConfig::builder_with_protocol_versions(protocol_versions)
334 .dangerous()
335 .with_custom_certificate_verifier(verifier)
336 .with_no_client_auth()
337 } else {
338 let root_cert_store = self.root_cert_store.map(Arc::new).map(Ok).unwrap_or_else(|| {
339 DEFAULT_ROOT_CERT_STORE
340 .get()
341 .map(Arc::clone)
342 .ok_or(generic_error!("Default TLS root certificate store not initialized."))
343 })?;
344
345 ClientConfig::builder_with_protocol_versions(protocol_versions)
346 .with_root_certificates(root_cert_store)
347 .with_no_client_auth()
348 };
349
350 if let Some(path) = self.key_log_file_path {
351 #[cfg(feature = "fips")]
352 fips_key_log_warn_once(path);
353
354 #[cfg(not(feature = "fips"))]
355 if let Some(key_log) = NssKeyLogFile::open_shared(path) {
356 config.key_log = key_log;
357 }
358 }
359
360 config.resumption = Resumption::in_memory_sessions(max_tls12_resumption_sessions);
364
365 #[cfg(feature = "fips")]
367 if !config.fips() {
368 return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
369 }
370
371 Ok(config)
372 }
373}
374
375pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
388 if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
389 return Err(generic_error!("Default TLS cryptography provider already initialized."));
390 }
391
392 rustls::crypto::aws_lc_rs::default_provider()
397 .install_default()
398 .map_err(|_| generic_error!("Failed to install AWS-LC as default cryptography provider. This is likely due to a conflicting provider already being installed."))?;
399
400 DEFAULT_CRYPTO_PROVIDER_SET
402 .set(())
403 .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
404
405 Ok(())
406}
407
408pub fn load_platform_root_certificates() -> Result<(), GenericError> {
474 let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
475 .lock()
476 .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
477 if DEFAULT_ROOT_CERT_STORE.get().is_some() {
478 return Err(generic_error!(
479 "Default TLS root certificate store already initialized."
480 ));
481 }
482
483 let root_cert_store = load_platform_root_certificates_inner()?;
484
485 DEFAULT_ROOT_CERT_STORE
488 .set(Arc::new(root_cert_store))
489 .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
490
491 Ok(())
492}
493
494pub fn load_platform_root_certificates_inner() -> Result<RootCertStore, GenericError> {
506 let mut root_cert_store = RootCertStore::empty();
507
508 let mut result = rustls_native_certs::load_native_certs();
509
510 result.errors.retain(|err| {
514 !matches!(
515 &err.kind,
516 rustls_native_certs::ErrorKind::Io { inner, .. } if inner.kind() == std::io::ErrorKind::NotFound,
517 )
518 });
519
520 let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
522 if failed == 0 && added > 0 {
523 debug!(
524 "Added {} certificates from environment to the default root certificate store.",
525 added
526 );
527 } else if failed > 0 && added > 0 {
528 debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
529 } else {
530 if !result.errors.is_empty() {
538 let joined_errors = result
539 .errors
540 .iter()
541 .map(|e| e.to_string())
542 .collect::<Vec<_>>()
543 .join(", ");
544
545 return Err(generic_error!(
546 "Failed to load certificates from platform's native certificate store: {}",
547 joined_errors
548 ));
549 }
550 }
551
552 Ok(root_cert_store)
553}
554
555#[cfg(test)]
556mod tests {
557 #[cfg(not(feature = "fips"))]
558 use std::fs;
559 #[cfg(all(unix, not(feature = "fips")))]
560 use std::os::unix::fs::PermissionsExt;
561
562 use rustls::{ProtocolVersion, RootCertStore};
563
564 #[cfg(not(feature = "fips"))]
565 use super::{build_nss_key_log_line, open_key_log_file};
566 use super::{ClientTLSConfigBuilder, TlsMinimumVersion};
567
568 #[test]
569 fn tls12_minimum_enables_tls12_and_tls13() {
570 let versions = TlsMinimumVersion::Tls12.protocol_versions();
571
572 assert_eq!(versions.len(), 2);
573 assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
574 assert_eq!(versions[1].version, ProtocolVersion::TLSv1_2);
575 }
576
577 #[test]
578 fn tls13_minimum_enables_tls13_only() {
579 let versions = TlsMinimumVersion::Tls13.protocol_versions();
580
581 assert_eq!(versions.len(), 1);
582 assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
583 }
584
585 #[test]
586 #[cfg(not(feature = "fips"))]
587 fn nss_key_log_lines_are_written_in_hex_format() {
588 let output =
589 build_nss_key_log_line("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]).expect("key log line should build");
590
591 assert_eq!(output, b"CLIENT_RANDOM abcd 0123\n");
592 }
593
594 #[test]
595 #[cfg(not(feature = "fips"))]
596 fn client_config_uses_configured_key_log_file() {
597 let _ = super::initialize_default_crypto_provider();
598 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
599 let key_log_path = tempdir.path().join("sslkeylogfile");
600
601 let config = ClientTLSConfigBuilder::new()
602 .with_root_cert_store(RootCertStore::empty())
603 .with_key_log_file(&key_log_path)
604 .build()
605 .expect("client TLS config should build");
606
607 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
608
609 let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
610 assert_eq!(contents, "CLIENT_RANDOM abcd 0123\n");
611 }
612
613 #[test]
614 #[cfg(not(feature = "fips"))]
615 fn client_config_ignores_unwritable_key_log_file() {
616 let _ = super::initialize_default_crypto_provider();
617 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
618 let key_log_path = tempdir.path().join("missing").join("sslkeylogfile");
619
620 let config = ClientTLSConfigBuilder::new()
621 .with_root_cert_store(RootCertStore::empty())
622 .with_key_log_file(&key_log_path)
623 .build()
624 .expect("client TLS config should build even when the key log file cannot be opened");
625
626 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
627
628 assert!(!key_log_path.exists());
629 }
630
631 #[test]
632 #[cfg(not(feature = "fips"))]
633 fn client_configs_append_to_shared_key_log_file() {
634 let _ = super::initialize_default_crypto_provider();
635 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
636 let key_log_path = tempdir.path().join("shared-sslkeylogfile");
637
638 let first_config = ClientTLSConfigBuilder::new()
639 .with_root_cert_store(RootCertStore::empty())
640 .with_key_log_file(&key_log_path)
641 .build()
642 .expect("first client TLS config should build");
643 let second_config = ClientTLSConfigBuilder::new()
644 .with_root_cert_store(RootCertStore::empty())
645 .with_key_log_file(&key_log_path)
646 .build()
647 .expect("second client TLS config should build");
648
649 first_config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
650 second_config.key_log.log("CLIENT_RANDOM", &[0xef, 0x01], &[0x45, 0x67]);
651
652 let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
653 assert_eq!(contents, "CLIENT_RANDOM abcd 0123\nCLIENT_RANDOM ef01 4567\n");
654 }
655
656 #[cfg(all(unix, not(feature = "fips")))]
657 #[test]
658 fn key_log_file_is_created_with_owner_only_permissions() {
659 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
660 let key_log_path = tempdir.path().join("sslkeylogfile");
661
662 let file = open_key_log_file(&key_log_path).expect("key log file should open");
663 drop(file);
664
665 let mode = fs::metadata(&key_log_path)
666 .expect("key log file metadata should be readable")
667 .permissions()
668 .mode()
669 & 0o777;
670 assert_eq!(mode, 0o600);
671 }
672
673 #[test]
674 #[cfg(feature = "fips")]
675 fn key_log_file_ignored_in_fips_mode() {
676 let _ = super::initialize_default_crypto_provider();
677 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
678 let key_log_path = tempdir.path().join("fips-sslkeylogfile");
679
680 let config = ClientTLSConfigBuilder::new()
683 .with_root_cert_store(RootCertStore::empty())
684 .with_key_log_file(&key_log_path)
685 .build()
686 .expect("TLS config should build in FIPS mode even when a key log file is configured");
687
688 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
690
691 assert!(!key_log_path.exists(), "FIPS builds must not create a TLS key log file");
692 }
693}