Skip to main content

saluki_tls/
lib.rs

1//! Transport Layer Security (TLS) configuration and helpers.
2
3#[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
36/// Tracks if the default cryptography provider for `rustls` has been set.
37static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
38
39/// Default root certificate store to use for TLS when one isn't explicitly provided.
40static 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
45// Various defaults for TLS configuration.
46const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
47const TLS12_PLUS_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
48const TLS13_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13];
49
50/// Minimum TLS protocol version to use for client connections.
51#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
52pub enum TlsMinimumVersion {
53    /// TLS 1.2 or newer.
54    #[default]
55    Tls12,
56
57    /// TLS 1.3 or newer.
58    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/// A certificate verifier that accepts all server certificates without validation.
71///
72/// This is inherently insecure and should only be used for local/development connections where the
73/// server's identity is already established through other means (for example, connecting via Unix domain socket
74/// to a local process).
75#[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        // Open is attempted exactly once per path. Both success and failure are cached so that
124        // repeated builds for the same path do not re-open the file or re-emit warnings.
125        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
236/// A TLS client configuration builder.
237///
238/// Exposes various options for configuring a client's TLS configuration that would otherwise be cumbersome to
239/// configure, and provides sane defaults for many common options.
240///
241/// # Missing
242///
243/// - ability to configure client authentication
244pub 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    /// Enables logging of TLS key material to the given file path.
264    ///
265    /// TLS key material will be logged to the given file path in the [NSS Key Log][nss_key_log]
266    /// format, which can be used for debugging TLS issues, as well as decrypting captured
267    /// TLS traffic in tools such as Wireshark.
268    ///
269    /// Newly created files are created with owner read/write permissions on Unix.
270    /// Existing file permissions are preserved.
271    ///
272    /// [nss_key_log]: https://nss-crypto.org/reference/security/nss/legacy/key_log_format/index.html
273    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    /// Sets the maximum number of TLS 1.2 sessions to cache.
279    ///
280    /// Defaults to 8.
281    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    /// Sets the root certificate store to use for the client.
287    ///
288    /// Defaults to the "default" root certificate store initialized from the platform. (See [`load_platform_root_certificates`].)
289    pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
290        self.root_cert_store = Some(store);
291        self
292    }
293
294    /// Sets the minimum TLS protocol version to allow for client connections.
295    ///
296    /// Defaults to TLS 1.2.
297    pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
298        self.min_tls_version = version;
299        self
300    }
301
302    /// Disables server certificate verification entirely.
303    ///
304    /// This is inherently insecure and should only be used for local/development connections where
305    /// the server's identity is already established through other means (for example, connecting via Unix
306    /// domain socket to a local process).
307    pub fn danger_accept_invalid_certs(mut self) -> Self {
308        self.danger_accept_invalid_certs = true;
309        self
310    }
311
312    /// Builds the client TLS configuration.
313    ///
314    /// # Errors
315    ///
316    /// If the default root cert store (see [`load_platform_root_certificates`]) hasn't been initialized, and a root
317    /// cert store hasn't been provided, or if the resulting configuration isn't FIPS compliant, an error will be
318    /// returned.
319    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        // One unfortunate thing is that by creating `config` above, it assigns the default value for `Resumption` before
361        // we reset it down here... which means the big, beefy default one gets allocated and then immediately thrown
362        // away.
363        config.resumption = Resumption::in_memory_sessions(max_tls12_resumption_sessions);
364
365        // Do our final check that this configuration is FIPS compliant.
366        #[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
375/// Initializes the default TLS cryptography provider used by `rustls`.
376///
377/// This explicitly sets the [AWS-LC][aws_lc] provider as the default provider for all future TLS configurations, which
378/// provides the ability to run in FIPS mode for FIPS-compliant builds.
379///
380/// This is the only supported cryptography provider in Saluki.
381///
382/// # Errors
383///
384/// If the default cryptography provider has already been set, an error will be returned.
385///
386/// [aws_lc]: https://github.com/aws/aws-lc-rs
387pub 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    // Set the process-wide default `CryptoProvider` to AWS-LC.
393    //
394    // This locks in AWS-LC as the default provider for all future TLS configurations, regardless of whether they use
395    // the configuration builders here or not. (The main caveat is that it's only relevant if `rustls` is being used.)
396    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    // With the process-wide default having been set, mark it as having been set.
401    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
408/// Initializes the default root certificate store from the platform's native certificate store.
409///
410/// ## Environment Variables
411///
412/// | Environment Variable | Description                                                                           |
413/// |----------------------|---------------------------------------------------------------------------------------|
414/// | SSL_CERT_FILE        | File containing an arbitrary number of certificates in PEM format.                    |
415/// | SSL_CERT_DIR         | Directory utilizing the hierarchy and naming convention used by OpenSSL's `c_rehash`. |
416///
417/// If **either** (or **both**) are set, certificates are only loaded from the locations specified via environment
418/// variables and not the platform- native certificate store.
419///
420/// ## Certificate Validity
421///
422/// All certificates are expected to be in PEM format. A file may contain multiple certificates.
423///
424/// Example:
425///
426/// ```text
427/// -----BEGIN CERTIFICATE-----
428/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
429/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
430/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
431/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
432/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
433/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
434/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
435/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
436/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
437/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
438/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
439/// /q4AaOeMSQ+2b1tbFfLn
440/// -----END CERTIFICATE-----
441/// -----BEGIN CERTIFICATE-----
442/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
443/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
444/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
445/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
446/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
447/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
448/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
449/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
450/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
451/// YyRIHN8wfdVoOw==
452/// -----END CERTIFICATE-----
453///
454/// ```
455///
456/// For reasons of compatibility, an attempt is made to skip invalid sections of a certificate file but this means it's
457/// also possible for a malformed certificate to be skipped.
458///
459/// If a certificate isn't loaded, and no error is reported, check if:
460///
461/// 1. the certificate is in PEM format (see example above)
462/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
463/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
464/// 4. there is a line break after the certificate.
465///
466/// ## Errors
467///
468/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
469/// returned. Missing files or directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and treated as
470/// "no certificates available" rather than as a load failure.
471///
472/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
473pub 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    // The reason it should be impossible is that we intentionally only set it _here_, and we do so after acquiring the
486    // mutex, and only then do we make sure that it hasn't been set before proceeding to try to set it.
487    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
494/// Builds a `RootCertStore` from the platform's native certificate store.
495///
496/// Behaves identically to [`load_platform_root_certificates`] with respect to which certificates are loaded, but
497/// returns the constructed store instead of writing it into the process-wide default.
498///
499/// # Errors
500///
501/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
502/// returned. Otherwise, even if some certificates failed to parse, the store is returned with whatever certificates were
503/// successfully added. Missing files or directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and do
504/// not produce an error.
505pub 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    // Drop "not found" IO errors before evaluating success or failure: a missing `SSL_CERT_FILE` or `SSL_CERT_DIR`
511    // should look like "no certificates available" rather than a load failure, since callers may simply not have set
512    // those env vars on this host.
513    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    // For whatever certificates we _did_ get back, try and add them to the root certificate store.
521    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        // When we don't manage to add any certificates, it either means that:
531        // - we found no certificates to add
532        // - we hit an error when loading the certificates
533        // - we hit an error when trying to add the certificates to our root certificate store
534        //
535        // We only consider this operation to have truly failed if there were errors during the initial loading of the
536        // certificates.
537        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        // FIPS builds soft-skip TLS key logging instead of failing, so a leftover key log file path does not
681        // prevent TLS client construction.
682        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        // The default no-op `KeyLog` remains in place: invoking it must not panic and must not produce a file.
689        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}