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};
11use std::{
12 path::{Path, PathBuf},
13 sync::{Arc, Mutex, OnceLock},
14};
15
16#[cfg(not(feature = "fips"))]
17use rustls::KeyLog;
18use rustls::{
19 client::{
20 danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
21 Resumption,
22 },
23 crypto::CryptoProvider,
24 pki_types::{pem::PemObject as _, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
25 server::WebPkiClientVerifier,
26 version::{TLS12, TLS13},
27 ClientConfig, DigitallySignedStruct, RootCertStore, ServerConfig, 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
36#[cfg(any(test, feature = "test-util"))]
37pub mod test_util;
38
39static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
41
42static DEFAULT_ROOT_CERT_STORE_MUTEX: Mutex<()> = Mutex::new(());
44static DEFAULT_ROOT_CERT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
45#[cfg(not(feature = "fips"))]
46static KEY_LOG_FILES: OnceLock<Mutex<FastHashMap<PathBuf, Option<Arc<NssKeyLogFile>>>>> = OnceLock::new();
47
48const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
50const TLS12_PLUS_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
51const TLS13_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13];
52
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55pub enum TlsMinimumVersion {
56 #[default]
58 Tls12,
59
60 Tls13,
62}
63
64impl TlsMinimumVersion {
65 const fn protocol_versions(self) -> &'static [&'static SupportedProtocolVersion] {
66 match self {
67 Self::Tls12 => TLS12_PLUS_PROTOCOL_VERSIONS,
68 Self::Tls13 => TLS13_PROTOCOL_VERSIONS,
69 }
70 }
71}
72
73#[derive(Debug)]
79struct AcceptAllServerCertVerifier {
80 provider: Arc<CryptoProvider>,
81}
82
83impl ServerCertVerifier for AcceptAllServerCertVerifier {
84 fn verify_server_cert(
85 &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
86 _ocsp_response: &[u8], _now: UnixTime,
87 ) -> Result<ServerCertVerified, rustls::Error> {
88 Ok(ServerCertVerified::assertion())
89 }
90
91 fn verify_tls12_signature(
92 &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
93 ) -> Result<HandshakeSignatureValid, rustls::Error> {
94 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
95 }
96
97 fn verify_tls13_signature(
98 &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
99 ) -> Result<HandshakeSignatureValid, rustls::Error> {
100 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
101 }
102
103 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
104 self.provider.signature_verification_algorithms.supported_schemes()
105 }
106}
107
108#[cfg(not(feature = "fips"))]
109struct NssKeyLogFile {
110 path: PathBuf,
111 file: Mutex<File>,
112}
113
114#[cfg(not(feature = "fips"))]
115impl NssKeyLogFile {
116 fn open_shared<P: Into<PathBuf>>(path: P) -> Option<Arc<Self>> {
117 let path = path.into();
118 let mut key_log_files = match KEY_LOG_FILES.get_or_init(|| Mutex::new(FastHashMap::default())).lock() {
119 Ok(key_log_files) => key_log_files,
120 Err(_) => {
121 warn!("Failed to acquire TLS key log file registry lock; TLS key logging disabled.");
122 return None;
123 }
124 };
125
126 if let Some(cached) = key_log_files.get(&path) {
129 return cached.clone();
130 }
131
132 let key_log_file = match open_key_log_file(&path) {
133 Ok(file) => {
134 warn!(
135 path = %path.display(),
136 "TLS key logging enabled; TLS session secrets will be written to disk."
137 );
138 Some(Arc::new(Self {
139 path: path.clone(),
140 file: Mutex::new(file),
141 }))
142 }
143 Err(e) => {
144 warn!(
145 path = %path.display(),
146 error = %e,
147 "Failed to open TLS key log file for appending; TLS key logging disabled."
148 );
149 None
150 }
151 };
152
153 key_log_files.insert(path, key_log_file.clone());
154 key_log_file
155 }
156}
157
158#[cfg(not(feature = "fips"))]
159impl Debug for NssKeyLogFile {
160 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct("NssKeyLogFile").field("path", &self.path).finish()
162 }
163}
164
165#[cfg(feature = "fips")]
166static FIPS_KEY_LOG_WARNED_PATHS: OnceLock<Mutex<saluki_common::collections::FastHashSet<PathBuf>>> = OnceLock::new();
167
168#[cfg(feature = "fips")]
169fn fips_key_log_warn_once(path: PathBuf) {
170 let warned =
171 FIPS_KEY_LOG_WARNED_PATHS.get_or_init(|| Mutex::new(saluki_common::collections::FastHashSet::default()));
172 let Ok(mut warned) = warned.lock() else {
173 return;
174 };
175 if warned.insert(path.clone()) {
176 tracing::warn!(
177 path = %path.display(),
178 "FIPS build: TLS key logging is disabled because exporting TLS secrets is not FIPS-compliant."
179 );
180 }
181}
182
183#[cfg(not(feature = "fips"))]
184impl KeyLog for NssKeyLogFile {
185 fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) {
186 let line = match build_nss_key_log_line(label, client_random, secret) {
187 Ok(line) => line,
188 Err(e) => {
189 debug!(path = %self.path.display(), error = %e, "Failed to format TLS key log line.");
190 return;
191 }
192 };
193
194 match self.file.lock() {
195 Ok(mut file) => {
196 if let Err(e) = file.write_all(&line) {
197 debug!(path = %self.path.display(), error = %e, "Failed to write TLS key log line.");
198 }
199 }
200 Err(_) => {
201 debug!(path = %self.path.display(), "TLS key log file lock poisoned; dropping TLS key log line.");
202 }
203 }
204 }
205}
206
207#[cfg(not(feature = "fips"))]
208fn open_key_log_file(path: &Path) -> io::Result<File> {
209 let mut options = OpenOptions::new();
210 options.write(true).create(true).append(true);
211
212 #[cfg(unix)]
213 options.mode(0o600);
214
215 options.open(path)
216}
217
218#[cfg(not(feature = "fips"))]
219fn build_nss_key_log_line(label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<Vec<u8>> {
220 let mut line = Vec::new();
221 write!(line, "{label} ")?;
222 write_hex(&mut line, client_random)?;
223 write!(line, " ")?;
224 write_hex(&mut line, secret)?;
225 writeln!(line)?;
226
227 Ok(line)
228}
229
230#[cfg(not(feature = "fips"))]
231fn write_hex(writer: &mut impl Write, bytes: &[u8]) -> io::Result<()> {
232 for byte in bytes {
233 write!(writer, "{byte:02x}")?;
234 }
235
236 Ok(())
237}
238
239pub struct ClientTLSConfigBuilder {
248 key_log_file_path: Option<PathBuf>,
249 max_tls12_resumption_sessions: Option<usize>,
250 min_tls_version: TlsMinimumVersion,
251 root_cert_store: Option<RootCertStore>,
252 danger_accept_invalid_certs: bool,
253}
254
255impl ClientTLSConfigBuilder {
256 pub fn new() -> Self {
257 Self {
258 key_log_file_path: None,
259 max_tls12_resumption_sessions: None,
260 min_tls_version: TlsMinimumVersion::default(),
261 root_cert_store: None,
262 danger_accept_invalid_certs: false,
263 }
264 }
265
266 pub fn with_key_log_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
277 self.key_log_file_path = Some(path.into());
278 self
279 }
280
281 pub fn with_max_tls12_resumption_sessions(mut self, max: usize) -> Self {
285 self.max_tls12_resumption_sessions = Some(max);
286 self
287 }
288
289 pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
293 self.root_cert_store = Some(store);
294 self
295 }
296
297 pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
301 self.min_tls_version = version;
302 self
303 }
304
305 pub fn danger_accept_invalid_certs(mut self) -> Self {
311 self.danger_accept_invalid_certs = true;
312 self
313 }
314
315 pub fn build(self) -> Result<ClientConfig, GenericError> {
323 let max_tls12_resumption_sessions = self
324 .max_tls12_resumption_sessions
325 .unwrap_or(DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS);
326 let protocol_versions = self.min_tls_version.protocol_versions();
327
328 let mut config = if self.danger_accept_invalid_certs {
329 let crypto_provider = CryptoProvider::get_default()
330 .map(Arc::clone)
331 .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
332 let verifier = Arc::new(AcceptAllServerCertVerifier {
333 provider: crypto_provider,
334 });
335
336 ClientConfig::builder_with_protocol_versions(protocol_versions)
337 .dangerous()
338 .with_custom_certificate_verifier(verifier)
339 .with_no_client_auth()
340 } else {
341 let root_cert_store = self.root_cert_store.map(Arc::new).map(Ok).unwrap_or_else(|| {
342 DEFAULT_ROOT_CERT_STORE
343 .get()
344 .map(Arc::clone)
345 .ok_or(generic_error!("Default TLS root certificate store not initialized."))
346 })?;
347
348 ClientConfig::builder_with_protocol_versions(protocol_versions)
349 .with_root_certificates(root_cert_store)
350 .with_no_client_auth()
351 };
352
353 if let Some(path) = self.key_log_file_path {
354 #[cfg(feature = "fips")]
355 fips_key_log_warn_once(path);
356
357 #[cfg(not(feature = "fips"))]
358 if let Some(key_log) = NssKeyLogFile::open_shared(path) {
359 config.key_log = key_log;
360 }
361 }
362
363 config.resumption = Resumption::in_memory_sessions(max_tls12_resumption_sessions);
367
368 ensure_client_config_fips_compliant(&config)?;
369
370 Ok(config)
371 }
372}
373
374pub struct ServerTLSConfigBuilder {
385 cert_file: Option<PathBuf>,
386 key_file: Option<PathBuf>,
387 ca_file: Option<PathBuf>,
388}
389
390impl ServerTLSConfigBuilder {
391 pub fn new() -> Self {
393 Self {
394 cert_file: None,
395 key_file: None,
396 ca_file: None,
397 }
398 }
399
400 pub fn with_cert_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
405 self.cert_file = Some(path.into());
406 self
407 }
408
409 pub fn with_key_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
413 self.key_file = Some(path.into());
414 self
415 }
416
417 pub fn with_ca_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
425 self.ca_file = Some(path.into());
426 self
427 }
428
429 pub fn build(self) -> Result<ServerConfig, GenericError> {
436 let cert_file = self
437 .cert_file
438 .ok_or_else(|| generic_error!("No certificate file configured for server TLS."))?;
439 let key_file = self
440 .key_file
441 .ok_or_else(|| generic_error!("No private key file configured for server TLS."))?;
442
443 let cert_bytes = std::fs::read(&cert_file)
445 .map_err(|e| generic_error!("Failed to read certificate file '{}': {}", cert_file.display(), e))?;
446 let cert_chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_bytes)
447 .collect::<Result<Vec<_>, _>>()
448 .map_err(|e| generic_error!("Failed to parse certificate file '{}': {}", cert_file.display(), e))?;
449
450 if cert_chain.is_empty() {
451 return Err(generic_error!(
452 "No PEM-encoded certificates found in certificate file '{}'.",
453 cert_file.display()
454 ));
455 }
456
457 let key_bytes = std::fs::read(&key_file)
459 .map_err(|e| generic_error!("Failed to read private key file '{}': {}", key_file.display(), e))?;
460 let private_key = PrivateKeyDer::from_pem_slice(&key_bytes)
461 .map_err(|e| generic_error!("Failed to parse private key file '{}': {}", key_file.display(), e))?;
462
463 let mut config = if let Some(ca_file) = self.ca_file {
464 build_server_config_with_client_verifier(&ca_file, cert_chain, private_key)?
465 } else {
466 ServerConfig::builder()
467 .with_no_client_auth()
468 .with_single_cert(cert_chain, private_key)
469 .map_err(|e| generic_error!("Failed to build server TLS configuration: {}", e))?
470 };
471
472 ensure_server_config_fips_compliant(&mut config)?;
473
474 Ok(config)
475 }
476}
477
478fn build_server_config_with_client_verifier(
483 ca_file: &Path, cert_chain: Vec<CertificateDer<'static>>, private_key: PrivateKeyDer<'static>,
484) -> Result<ServerConfig, GenericError> {
485 let ca_bytes = std::fs::read(ca_file)
486 .map_err(|e| generic_error!("Failed to read CA certificate file '{}': {}", ca_file.display(), e))?;
487 let mut root_cert_store = RootCertStore::empty();
488 let ca_certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&ca_bytes)
489 .collect::<Result<Vec<_>, _>>()
490 .map_err(|e| generic_error!("Failed to parse CA certificate file '{}': {}", ca_file.display(), e))?;
491
492 if ca_certs.is_empty() {
493 return Err(generic_error!(
494 "No PEM-encoded certificates found in CA file '{}'.",
495 ca_file.display()
496 ));
497 }
498
499 for ca_cert in ca_certs {
500 root_cert_store
501 .add(ca_cert)
502 .map_err(|e| generic_error!("Failed to add CA certificate to root store: {}", e))?;
503 }
504
505 let client_verifier = WebPkiClientVerifier::builder(Arc::new(root_cert_store))
506 .allow_unauthenticated()
507 .build()
508 .map_err(|e| generic_error!("Failed to build client certificate verifier: {}", e))?;
509
510 ServerConfig::builder()
511 .with_client_cert_verifier(client_verifier)
512 .with_single_cert(cert_chain, private_key)
513 .map_err(|e| generic_error!("Failed to build server TLS configuration: {}", e))
514}
515
516pub fn ensure_client_config_fips_compliant(config: &ClientConfig) -> Result<(), GenericError> {
524 #[cfg(feature = "fips")]
525 if !config.fips() {
526 return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
527 }
528
529 #[cfg(not(feature = "fips"))]
530 let _ = config;
531
532 Ok(())
533}
534
535pub fn ensure_server_config_fips_compliant(config: &mut ServerConfig) -> Result<(), GenericError> {
544 #[cfg(feature = "fips")]
545 {
546 config.key_log = Arc::new(rustls::NoKeyLog);
547 config.enable_secret_extraction = false;
548
549 if !config.fips() {
550 return Err(generic_error!("Server TLS configuration is not FIPS compliant."));
551 }
552 }
553
554 #[cfg(not(feature = "fips"))]
555 let _ = config;
556
557 Ok(())
558}
559
560pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
569 if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
570 return Err(generic_error!("Default TLS cryptography provider already initialized."));
571 }
572
573 default_crypto_provider().install_default().map_err(|_| {
574 generic_error!(
575 "Failed to install the default TLS cryptography provider. This is likely due to a conflicting provider already being installed."
576 )
577 })?;
578
579 DEFAULT_CRYPTO_PROVIDER_SET
581 .set(())
582 .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
583
584 Ok(())
585}
586
587#[cfg(not(windows))]
588fn default_crypto_provider() -> CryptoProvider {
589 let provider = rustls::crypto::aws_lc_rs::default_provider();
590
591 #[cfg(feature = "fips")]
592 {
593 let mut provider = provider;
594 provider.cipher_suites.retain(|suite| suite.fips());
595 provider.kx_groups.retain(|group| group.fips());
596 provider
597 }
598
599 #[cfg(not(feature = "fips"))]
600 provider
601}
602
603#[cfg(windows)]
604fn default_crypto_provider() -> CryptoProvider {
605 rustls_cng_crypto::default_provider()
606}
607
608pub fn load_platform_root_certificates() -> Result<(), GenericError> {
675 let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
676 .lock()
677 .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
678 if DEFAULT_ROOT_CERT_STORE.get().is_some() {
679 return Err(generic_error!(
680 "Default TLS root certificate store already initialized."
681 ));
682 }
683
684 let root_cert_store = load_platform_root_certificates_inner()?;
685
686 DEFAULT_ROOT_CERT_STORE
689 .set(Arc::new(root_cert_store))
690 .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
691
692 Ok(())
693}
694
695pub fn load_platform_root_certificates_inner() -> Result<RootCertStore, GenericError> {
707 let mut root_cert_store = RootCertStore::empty();
708
709 let mut result = rustls_native_certs::load_native_certs();
710
711 result.errors.retain(|err| match &err.kind {
718 rustls_native_certs::ErrorKind::Io { inner, path }
719 if matches!(
720 inner.kind(),
721 std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied
722 ) =>
723 {
724 debug!(
725 error = %inner,
726 path = %path.display(),
727 "Skipping missing or unreadable certificate source while loading platform root certificates."
728 );
729 false
730 }
731 _ => true,
732 });
733
734 let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
736 if failed == 0 && added > 0 {
737 debug!(
738 "Added {} certificates from environment to the default root certificate store.",
739 added
740 );
741 } else if failed > 0 && added > 0 {
742 debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
743 } else {
744 if !result.errors.is_empty() {
752 let joined_errors = result
753 .errors
754 .iter()
755 .map(|e| e.to_string())
756 .collect::<Vec<_>>()
757 .join(", ");
758
759 return Err(generic_error!(
760 "Failed to load certificates from platform's native certificate store: {}",
761 joined_errors
762 ));
763 }
764 }
765
766 Ok(root_cert_store)
767}
768
769#[cfg(test)]
770mod tests {
771 use std::fs;
772 #[cfg(all(unix, not(feature = "fips")))]
773 use std::os::unix::fs::PermissionsExt;
774 use std::path::Path;
775 #[cfg(feature = "fips")]
776 use std::sync::{
777 atomic::{AtomicBool, Ordering},
778 Arc,
779 };
780
781 #[cfg(feature = "fips")]
782 use rustls::KeyLog;
783 use rustls::{
784 client::danger::ServerCertVerifier,
785 crypto::CryptoProvider,
786 pki_types::{ServerName, UnixTime},
787 ClientConfig, ProtocolVersion, RootCertStore, ServerConfig,
788 };
789
790 #[cfg(not(feature = "fips"))]
791 use super::build_nss_key_log_line;
792 #[cfg(all(unix, not(feature = "fips")))]
793 use super::open_key_log_file;
794 use super::test_util::SelfSignedCert;
795 use super::{
796 ensure_client_config_fips_compliant, ensure_server_config_fips_compliant, AcceptAllServerCertVerifier,
797 ClientTLSConfigBuilder, ServerTLSConfigBuilder, TlsMinimumVersion,
798 };
799
800 fn client_config_with_key_log(path: &Path) -> ClientConfig {
803 let _ = super::initialize_default_crypto_provider();
804
805 ClientTLSConfigBuilder::new()
806 .with_root_cert_store(RootCertStore::empty())
807 .with_key_log_file(path)
808 .build()
809 .expect("client TLS config should build")
810 }
811
812 fn temp_file_path(file_name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
814 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
815 let path = tempdir.path().join(file_name);
816 (tempdir, path)
817 }
818
819 #[test]
820 fn tls12_minimum_enables_tls12_and_tls13() {
821 let versions = TlsMinimumVersion::Tls12.protocol_versions();
822
823 assert_eq!(versions.len(), 2);
824 assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
825 assert_eq!(versions[1].version, ProtocolVersion::TLSv1_2);
826 }
827
828 #[test]
829 fn tls13_minimum_enables_tls13_only() {
830 let versions = TlsMinimumVersion::Tls13.protocol_versions();
831
832 assert_eq!(versions.len(), 1);
833 assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
834 }
835
836 #[test]
837 fn client_config_fips_validation_accepts_builder_config() {
838 let _ = super::initialize_default_crypto_provider();
839
840 let config = ClientTLSConfigBuilder::new()
841 .with_root_cert_store(RootCertStore::empty())
842 .build()
843 .expect("client TLS config should build");
844
845 ensure_client_config_fips_compliant(&config).expect("client TLS config should pass FIPS validation");
846 }
847
848 #[test]
849 fn accept_all_verifier_accepts_mismatched_server_certificate() {
850 let _ = super::initialize_default_crypto_provider();
854 let provider = CryptoProvider::get_default()
855 .cloned()
856 .expect("default crypto provider should be installed");
857 let verifier = AcceptAllServerCertVerifier { provider };
858
859 let cert = SelfSignedCert::new(["localhost"]);
860 let cert_chain = cert.cert_chain();
861 let server_name = ServerName::try_from("totally.different.example").expect("server name should parse");
862
863 let result = verifier.verify_server_cert(&cert_chain[0], &[], &server_name, &[], UnixTime::now());
864
865 assert!(
866 result.is_ok(),
867 "accept-all verifier must accept a certificate presented for a mismatched server name"
868 );
869 }
870
871 #[test]
872 fn danger_accept_invalid_certs_builds_without_root_cert_store() {
873 let _ = super::initialize_default_crypto_provider();
874
875 let missing_store_error = ClientTLSConfigBuilder::new()
878 .build()
879 .expect_err("client TLS config should fail to build without any root cert store");
880 assert!(
881 missing_store_error
882 .to_string()
883 .contains("root certificate store not initialized"),
884 "unexpected error: {missing_store_error}"
885 );
886
887 ClientTLSConfigBuilder::new()
890 .danger_accept_invalid_certs()
891 .build()
892 .expect("client TLS config should build with an accept-all verifier and no root cert store");
893 }
894
895 #[test]
896 fn server_config_fips_validation_accepts_basic_server_config() {
897 let _ = super::initialize_default_crypto_provider();
898 let cert = SelfSignedCert::localhost();
899 let mut config = ServerConfig::builder()
900 .with_no_client_auth()
901 .with_single_cert(cert.cert_chain(), cert.private_key())
902 .expect("server TLS config should build");
903
904 ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
905 }
906
907 #[cfg(feature = "fips")]
908 #[test]
909 fn server_config_fips_validation_disables_secret_extraction() {
910 #[derive(Debug)]
911 struct TestKeyLog(Arc<AtomicBool>);
912
913 impl KeyLog for TestKeyLog {
914 fn log(&self, _label: &str, _client_random: &[u8], _secret: &[u8]) {
915 self.0.store(true, Ordering::Relaxed);
916 }
917 }
918
919 let _ = super::initialize_default_crypto_provider();
920 let cert = SelfSignedCert::localhost();
921 let key_log_used = Arc::new(AtomicBool::new(false));
922 let mut config = ServerConfig::builder()
923 .with_no_client_auth()
924 .with_single_cert(cert.cert_chain(), cert.private_key())
925 .expect("server TLS config should build");
926 config.key_log = Arc::new(TestKeyLog(Arc::clone(&key_log_used)));
927 config.enable_secret_extraction = true;
928
929 ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
930
931 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
932
933 assert!(!key_log_used.load(Ordering::Relaxed));
934 assert!(!config.enable_secret_extraction);
935 }
936
937 #[cfg(all(feature = "fips", not(windows)))]
942 #[test]
943 fn client_config_fips_validation_rejects_non_fips_config() {
944 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
945 let config = ClientConfig::builder_with_provider(provider)
946 .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
947 .expect("client config builder should accept protocol versions")
948 .with_root_certificates(RootCertStore::empty())
949 .with_no_client_auth();
950
951 let error =
952 ensure_client_config_fips_compliant(&config).expect_err("a non-FIPS client configuration must be rejected");
953 assert!(
954 error.to_string().contains("not FIPS compliant"),
955 "unexpected error: {error}"
956 );
957 }
958
959 #[cfg(all(feature = "fips", not(windows)))]
960 #[test]
961 fn server_config_fips_validation_rejects_non_fips_config() {
962 let cert = SelfSignedCert::localhost();
963 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
964 let mut config = ServerConfig::builder_with_provider(provider)
965 .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
966 .expect("server config builder should accept protocol versions")
967 .with_no_client_auth()
968 .with_single_cert(cert.cert_chain(), cert.private_key())
969 .expect("server TLS config should build");
970
971 let error = ensure_server_config_fips_compliant(&mut config)
972 .expect_err("a non-FIPS server configuration must be rejected");
973 assert!(
974 error.to_string().contains("not FIPS compliant"),
975 "unexpected error: {error}"
976 );
977 }
978
979 #[test]
980 #[cfg(not(feature = "fips"))]
981 fn nss_key_log_lines_are_written_in_hex_format() {
982 let output =
983 build_nss_key_log_line("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]).expect("key log line should build");
984
985 assert_eq!(output, b"CLIENT_RANDOM abcd 0123\n");
986 }
987
988 #[test]
989 #[cfg(not(feature = "fips"))]
990 fn client_config_uses_configured_key_log_file() {
991 let (_tempdir, key_log_path) = temp_file_path("sslkeylogfile");
992
993 let config = client_config_with_key_log(&key_log_path);
994 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
995
996 let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
997 assert_eq!(contents, "CLIENT_RANDOM abcd 0123\n");
998 }
999
1000 #[test]
1001 #[cfg(not(feature = "fips"))]
1002 fn client_config_ignores_unwritable_key_log_file() {
1003 let (_tempdir, key_log_path) = temp_file_path("missing/sslkeylogfile");
1006
1007 let config = client_config_with_key_log(&key_log_path);
1008 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1009
1010 assert!(!key_log_path.exists());
1011 }
1012
1013 #[test]
1014 #[cfg(not(feature = "fips"))]
1015 fn client_configs_append_to_shared_key_log_file() {
1016 let (_tempdir, key_log_path) = temp_file_path("shared-sslkeylogfile");
1017
1018 let first_config = client_config_with_key_log(&key_log_path);
1019 let second_config = client_config_with_key_log(&key_log_path);
1020
1021 first_config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1022 second_config.key_log.log("CLIENT_RANDOM", &[0xef, 0x01], &[0x45, 0x67]);
1023
1024 let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
1025 assert_eq!(contents, "CLIENT_RANDOM abcd 0123\nCLIENT_RANDOM ef01 4567\n");
1026 }
1027
1028 #[cfg(all(unix, not(feature = "fips")))]
1029 #[test]
1030 fn key_log_file_is_created_with_owner_only_permissions() {
1031 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1032 let key_log_path = tempdir.path().join("sslkeylogfile");
1033
1034 let file = open_key_log_file(&key_log_path).expect("key log file should open");
1035 drop(file);
1036
1037 let mode = fs::metadata(&key_log_path)
1038 .expect("key log file metadata should be readable")
1039 .permissions()
1040 .mode()
1041 & 0o777;
1042 assert_eq!(mode, 0o600);
1043 }
1044
1045 #[cfg(windows)]
1046 #[test]
1047 fn windows_default_crypto_provider_builds_client_config() {
1048 let _ = super::initialize_default_crypto_provider();
1049
1050 ClientTLSConfigBuilder::new()
1051 .with_root_cert_store(RootCertStore::empty())
1052 .build()
1053 .expect("Windows CNG-backed TLS config should build");
1054 }
1055
1056 #[test]
1057 #[cfg(feature = "fips")]
1058 fn key_log_file_ignored_in_fips_mode() {
1059 let (_tempdir, key_log_path) = temp_file_path("fips-sslkeylogfile");
1062
1063 let config = client_config_with_key_log(&key_log_path);
1064
1065 config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1067
1068 assert!(!key_log_path.exists(), "FIPS builds must not create a TLS key log file");
1069 }
1070
1071 #[test]
1072 fn server_tls_config_builder_loads_cert_and_key_files() {
1073 let _ = super::initialize_default_crypto_provider();
1074 let cert = SelfSignedCert::localhost();
1075 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1076 let cert_path = tempdir.path().join("cert.pem");
1077 let key_path = tempdir.path().join("key.pem");
1078 cert.write_cert_pem(&cert_path);
1079 cert.write_key_pem(&key_path);
1080
1081 ServerTLSConfigBuilder::new()
1082 .with_cert_file(&cert_path)
1083 .with_key_file(&key_path)
1084 .build()
1085 .expect("server TLS config should build from cert and key files");
1086 }
1087
1088 #[test]
1089 fn server_tls_config_builder_loads_ca_file_for_client_auth() {
1090 let _ = super::initialize_default_crypto_provider();
1091 let server_cert = SelfSignedCert::localhost();
1092 let ca_cert = SelfSignedCert::new(["test-ca"]);
1093 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1094 let cert_path = tempdir.path().join("server-cert.pem");
1095 let key_path = tempdir.path().join("server-key.pem");
1096 let ca_path = tempdir.path().join("ca.pem");
1097 server_cert.write_cert_pem(&cert_path);
1098 server_cert.write_key_pem(&key_path);
1099 ca_cert.write_cert_pem(&ca_path);
1100
1101 ServerTLSConfigBuilder::new()
1102 .with_cert_file(&cert_path)
1103 .with_key_file(&key_path)
1104 .with_ca_file(&ca_path)
1105 .build()
1106 .expect("server TLS config with CA should build");
1107 }
1108
1109 #[test]
1110 fn server_tls_config_builder_errors_without_cert_file() {
1111 let _ = super::initialize_default_crypto_provider();
1112
1113 let error = ServerTLSConfigBuilder::new()
1114 .with_key_file("/tmp/key.pem")
1115 .build()
1116 .expect_err("server TLS config should fail without a cert file");
1117
1118 assert!(
1119 error.to_string().contains("No certificate file"),
1120 "unexpected error: {error}"
1121 );
1122 }
1123
1124 #[test]
1125 fn server_tls_config_builder_errors_without_key_file() {
1126 let _ = super::initialize_default_crypto_provider();
1127
1128 let error = ServerTLSConfigBuilder::new()
1129 .with_cert_file("/tmp/cert.pem")
1130 .build()
1131 .expect_err("server TLS config should fail without a key file");
1132
1133 assert!(
1134 error.to_string().contains("No private key file"),
1135 "unexpected error: {error}"
1136 );
1137 }
1138
1139 #[test]
1140 fn server_tls_config_builder_errors_on_unreadable_cert_file() {
1141 let _ = super::initialize_default_crypto_provider();
1142 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1143 let key_path = tempdir.path().join("key.pem");
1144 let cert_path = tempdir.path().join("nonexistent.pem");
1145 let cert = SelfSignedCert::localhost();
1146 cert.write_key_pem(&key_path);
1147
1148 let error = ServerTLSConfigBuilder::new()
1149 .with_cert_file(&cert_path)
1150 .with_key_file(&key_path)
1151 .build()
1152 .expect_err("server TLS config should fail with unreadable cert file");
1153
1154 assert!(
1155 error.to_string().contains("Failed to read certificate file"),
1156 "unexpected error: {error}"
1157 );
1158 }
1159
1160 #[test]
1161 fn server_tls_config_builder_errors_on_non_pem_cert_file() {
1162 let _ = super::initialize_default_crypto_provider();
1163 let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1164 let cert_path = tempdir.path().join("cert.pem");
1165 let key_path = tempdir.path().join("key.pem");
1166 fs::write(&cert_path, "this is not a certificate").expect("should write garbage file");
1168 let cert = SelfSignedCert::localhost();
1169 cert.write_key_pem(&key_path);
1170
1171 let error = ServerTLSConfigBuilder::new()
1172 .with_cert_file(&cert_path)
1173 .with_key_file(&key_path)
1174 .build()
1175 .expect_err("server TLS config should fail with non-PEM cert file");
1176
1177 assert!(
1178 error.to_string().contains("No PEM-encoded certificates found"),
1179 "unexpected error: {error}"
1180 );
1181 }
1182}