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, 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/// 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        ensure_client_config_fips_compliant(&config)?;
366
367        Ok(config)
368    }
369}
370
371/// Ensures that a client TLS configuration is FIPS compliant.
372///
373/// In FIPS builds, this checks the Rustls FIPS marker on the configuration. In non-FIPS builds, this is a no-op.
374///
375/// # Errors
376///
377/// If FIPS support is enabled and the configuration is not FIPS compliant, an error is returned.
378pub fn ensure_client_config_fips_compliant(config: &ClientConfig) -> Result<(), GenericError> {
379    #[cfg(feature = "fips")]
380    if !config.fips() {
381        return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
382    }
383
384    #[cfg(not(feature = "fips"))]
385    let _ = config;
386
387    Ok(())
388}
389
390/// Ensures that a server TLS configuration is FIPS compliant.
391///
392/// In FIPS builds, this disables operational secret extraction settings and checks the Rustls FIPS marker on the
393/// configuration. In non-FIPS builds, this is a no-op.
394///
395/// # Errors
396///
397/// If FIPS support is enabled and the configuration is not FIPS compliant, an error is returned.
398pub fn ensure_server_config_fips_compliant(config: &mut ServerConfig) -> Result<(), GenericError> {
399    #[cfg(feature = "fips")]
400    {
401        config.key_log = Arc::new(rustls::NoKeyLog);
402        config.enable_secret_extraction = false;
403
404        if !config.fips() {
405            return Err(generic_error!("Server TLS configuration is not FIPS compliant."));
406        }
407    }
408
409    #[cfg(not(feature = "fips"))]
410    let _ = config;
411
412    Ok(())
413}
414
415/// Initializes the default TLS cryptography provider used by `rustls`.
416///
417/// This explicitly sets the platform default provider for all future TLS configurations: CNG on Windows and AWS-LC on
418/// other platforms. FIPS builds configure the selected platform provider for FIPS mode.
419///
420/// # Errors
421///
422/// If the default cryptography provider has already been set, an error will be returned.
423pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
424    if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
425        return Err(generic_error!("Default TLS cryptography provider already initialized."));
426    }
427
428    default_crypto_provider().install_default().map_err(|_| {
429        generic_error!(
430            "Failed to install the default TLS cryptography provider. This is likely due to a conflicting provider already being installed."
431        )
432    })?;
433
434    // With the process-wide default having been set, mark it as having been set.
435    DEFAULT_CRYPTO_PROVIDER_SET
436        .set(())
437        .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
438
439    Ok(())
440}
441
442#[cfg(not(windows))]
443fn default_crypto_provider() -> CryptoProvider {
444    let provider = rustls::crypto::aws_lc_rs::default_provider();
445
446    #[cfg(feature = "fips")]
447    {
448        let mut provider = provider;
449        provider.cipher_suites.retain(|suite| suite.fips());
450        provider.kx_groups.retain(|group| group.fips());
451        provider
452    }
453
454    #[cfg(not(feature = "fips"))]
455    provider
456}
457
458#[cfg(windows)]
459fn default_crypto_provider() -> CryptoProvider {
460    rustls_cng_crypto::default_provider()
461}
462
463/// Initializes the default root certificate store from the platform's native certificate store.
464///
465/// ## Environment Variables
466///
467/// | Environment Variable | Description                                                                           |
468/// |----------------------|---------------------------------------------------------------------------------------|
469/// | SSL_CERT_FILE        | File containing an arbitrary number of certificates in PEM format.                    |
470/// | SSL_CERT_DIR         | Directory utilizing the hierarchy and naming convention used by OpenSSL's `c_rehash`. |
471///
472/// If **either** (or **both**) are set, certificates are only loaded from the locations specified via environment
473/// variables and not the platform- native certificate store.
474///
475/// ## Certificate Validity
476///
477/// All certificates are expected to be in PEM format. A file may contain multiple certificates.
478///
479/// Example:
480///
481/// ```text
482/// -----BEGIN CERTIFICATE-----
483/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
484/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
485/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
486/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
487/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
488/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
489/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
490/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
491/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
492/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
493/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
494/// /q4AaOeMSQ+2b1tbFfLn
495/// -----END CERTIFICATE-----
496/// -----BEGIN CERTIFICATE-----
497/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
498/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
499/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
500/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
501/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
502/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
503/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
504/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
505/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
506/// YyRIHN8wfdVoOw==
507/// -----END CERTIFICATE-----
508///
509/// ```
510///
511/// For reasons of compatibility, an attempt is made to skip invalid sections of a certificate file but this means it's
512/// also possible for a malformed certificate to be skipped.
513///
514/// If a certificate isn't loaded, and no error is reported, check if:
515///
516/// 1. the certificate is in PEM format (see example above)
517/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
518/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
519/// 4. there is a line break after the certificate.
520///
521/// ## Errors
522///
523/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
524/// returned. Missing files or directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and treated as
525/// "no certificates available" rather than as a load failure.
526///
527/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
528pub fn load_platform_root_certificates() -> Result<(), GenericError> {
529    let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
530        .lock()
531        .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
532    if DEFAULT_ROOT_CERT_STORE.get().is_some() {
533        return Err(generic_error!(
534            "Default TLS root certificate store already initialized."
535        ));
536    }
537
538    let root_cert_store = load_platform_root_certificates_inner()?;
539
540    // The reason it should be impossible is that we intentionally only set it _here_, and we do so after acquiring the
541    // mutex, and only then do we make sure that it hasn't been set before proceeding to try to set it.
542    DEFAULT_ROOT_CERT_STORE
543        .set(Arc::new(root_cert_store))
544        .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
545
546    Ok(())
547}
548
549/// Builds a `RootCertStore` from the platform's native certificate store.
550///
551/// Behaves identically to [`load_platform_root_certificates`] with respect to which certificates are loaded, but
552/// returns the constructed store instead of writing it into the process-wide default.
553///
554/// # Errors
555///
556/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
557/// returned. Otherwise, even if some certificates failed to parse, the store is returned with whatever certificates were
558/// successfully added. Missing files or directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and do
559/// not produce an error.
560pub fn load_platform_root_certificates_inner() -> Result<RootCertStore, GenericError> {
561    let mut root_cert_store = RootCertStore::empty();
562
563    let mut result = rustls_native_certs::load_native_certs();
564
565    // Drop "not found" IO errors before evaluating success or failure: a missing `SSL_CERT_FILE` or `SSL_CERT_DIR`
566    // should look like "no certificates available" rather than a load failure, since callers may simply not have set
567    // those env vars on this host.
568    result.errors.retain(|err| {
569        !matches!(
570            &err.kind,
571            rustls_native_certs::ErrorKind::Io { inner, .. } if inner.kind() == std::io::ErrorKind::NotFound,
572        )
573    });
574
575    // For whatever certificates we _did_ get back, try and add them to the root certificate store.
576    let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
577    if failed == 0 && added > 0 {
578        debug!(
579            "Added {} certificates from environment to the default root certificate store.",
580            added
581        );
582    } else if failed > 0 && added > 0 {
583        debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
584    } else {
585        // When we don't manage to add any certificates, it either means that:
586        // - we found no certificates to add
587        // - we hit an error when loading the certificates
588        // - we hit an error when trying to add the certificates to our root certificate store
589        //
590        // We only consider this operation to have truly failed if there were errors during the initial loading of the
591        // certificates.
592        if !result.errors.is_empty() {
593            let joined_errors = result
594                .errors
595                .iter()
596                .map(|e| e.to_string())
597                .collect::<Vec<_>>()
598                .join(", ");
599
600            return Err(generic_error!(
601                "Failed to load certificates from platform's native certificate store: {}",
602                joined_errors
603            ));
604        }
605    }
606
607    Ok(root_cert_store)
608}
609
610#[cfg(test)]
611mod tests {
612    #[cfg(not(feature = "fips"))]
613    use std::fs;
614    #[cfg(all(unix, not(feature = "fips")))]
615    use std::os::unix::fs::PermissionsExt;
616    #[cfg(feature = "fips")]
617    use std::sync::{
618        atomic::{AtomicBool, Ordering},
619        Arc,
620    };
621
622    use rcgen::{generate_simple_self_signed, CertifiedKey};
623    #[cfg(feature = "fips")]
624    use rustls::KeyLog;
625    use rustls::{
626        pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer},
627        ProtocolVersion, RootCertStore, ServerConfig,
628    };
629
630    #[cfg(not(feature = "fips"))]
631    use super::{build_nss_key_log_line, open_key_log_file};
632    use super::{
633        ensure_client_config_fips_compliant, ensure_server_config_fips_compliant, ClientTLSConfigBuilder,
634        TlsMinimumVersion,
635    };
636
637    #[test]
638    fn tls12_minimum_enables_tls12_and_tls13() {
639        let versions = TlsMinimumVersion::Tls12.protocol_versions();
640
641        assert_eq!(versions.len(), 2);
642        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
643        assert_eq!(versions[1].version, ProtocolVersion::TLSv1_2);
644    }
645
646    #[test]
647    fn tls13_minimum_enables_tls13_only() {
648        let versions = TlsMinimumVersion::Tls13.protocol_versions();
649
650        assert_eq!(versions.len(), 1);
651        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
652    }
653
654    #[test]
655    fn client_config_fips_validation_accepts_builder_config() {
656        let _ = super::initialize_default_crypto_provider();
657
658        let config = ClientTLSConfigBuilder::new()
659            .with_root_cert_store(RootCertStore::empty())
660            .build()
661            .expect("client TLS config should build");
662
663        ensure_client_config_fips_compliant(&config).expect("client TLS config should pass FIPS validation");
664    }
665
666    #[test]
667    fn server_config_fips_validation_accepts_basic_server_config() {
668        let _ = super::initialize_default_crypto_provider();
669        let CertifiedKey { cert, signing_key } =
670            generate_simple_self_signed(["localhost".to_owned()]).expect("self-signed cert should be generated");
671        let cert_chain = vec![cert.der().clone()];
672        let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(signing_key.serialize_der()));
673        let mut config = ServerConfig::builder()
674            .with_no_client_auth()
675            .with_single_cert(cert_chain, key)
676            .expect("server TLS config should build");
677
678        ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
679    }
680
681    #[cfg(feature = "fips")]
682    #[test]
683    fn server_config_fips_validation_disables_secret_extraction() {
684        #[derive(Debug)]
685        struct TestKeyLog(Arc<AtomicBool>);
686
687        impl KeyLog for TestKeyLog {
688            fn log(&self, _label: &str, _client_random: &[u8], _secret: &[u8]) {
689                self.0.store(true, Ordering::Relaxed);
690            }
691        }
692
693        let _ = super::initialize_default_crypto_provider();
694        let CertifiedKey { cert, signing_key } =
695            generate_simple_self_signed(["localhost".to_owned()]).expect("self-signed cert should be generated");
696        let cert_chain = vec![cert.der().clone()];
697        let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(signing_key.serialize_der()));
698        let key_log_used = Arc::new(AtomicBool::new(false));
699        let mut config = ServerConfig::builder()
700            .with_no_client_auth()
701            .with_single_cert(cert_chain, key)
702            .expect("server TLS config should build");
703        config.key_log = Arc::new(TestKeyLog(Arc::clone(&key_log_used)));
704        config.enable_secret_extraction = true;
705
706        ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
707
708        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
709
710        assert!(!key_log_used.load(Ordering::Relaxed));
711        assert!(!config.enable_secret_extraction);
712    }
713
714    #[test]
715    #[cfg(not(feature = "fips"))]
716    fn nss_key_log_lines_are_written_in_hex_format() {
717        let output =
718            build_nss_key_log_line("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]).expect("key log line should build");
719
720        assert_eq!(output, b"CLIENT_RANDOM abcd 0123\n");
721    }
722
723    #[test]
724    #[cfg(not(feature = "fips"))]
725    fn client_config_uses_configured_key_log_file() {
726        let _ = super::initialize_default_crypto_provider();
727        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
728        let key_log_path = tempdir.path().join("sslkeylogfile");
729
730        let config = ClientTLSConfigBuilder::new()
731            .with_root_cert_store(RootCertStore::empty())
732            .with_key_log_file(&key_log_path)
733            .build()
734            .expect("client TLS config should build");
735
736        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
737
738        let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
739        assert_eq!(contents, "CLIENT_RANDOM abcd 0123\n");
740    }
741
742    #[test]
743    #[cfg(not(feature = "fips"))]
744    fn client_config_ignores_unwritable_key_log_file() {
745        let _ = super::initialize_default_crypto_provider();
746        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
747        let key_log_path = tempdir.path().join("missing").join("sslkeylogfile");
748
749        let config = ClientTLSConfigBuilder::new()
750            .with_root_cert_store(RootCertStore::empty())
751            .with_key_log_file(&key_log_path)
752            .build()
753            .expect("client TLS config should build even when the key log file cannot be opened");
754
755        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
756
757        assert!(!key_log_path.exists());
758    }
759
760    #[test]
761    #[cfg(not(feature = "fips"))]
762    fn client_configs_append_to_shared_key_log_file() {
763        let _ = super::initialize_default_crypto_provider();
764        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
765        let key_log_path = tempdir.path().join("shared-sslkeylogfile");
766
767        let first_config = ClientTLSConfigBuilder::new()
768            .with_root_cert_store(RootCertStore::empty())
769            .with_key_log_file(&key_log_path)
770            .build()
771            .expect("first client TLS config should build");
772        let second_config = ClientTLSConfigBuilder::new()
773            .with_root_cert_store(RootCertStore::empty())
774            .with_key_log_file(&key_log_path)
775            .build()
776            .expect("second client TLS config should build");
777
778        first_config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
779        second_config.key_log.log("CLIENT_RANDOM", &[0xef, 0x01], &[0x45, 0x67]);
780
781        let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
782        assert_eq!(contents, "CLIENT_RANDOM abcd 0123\nCLIENT_RANDOM ef01 4567\n");
783    }
784
785    #[cfg(all(unix, not(feature = "fips")))]
786    #[test]
787    fn key_log_file_is_created_with_owner_only_permissions() {
788        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
789        let key_log_path = tempdir.path().join("sslkeylogfile");
790
791        let file = open_key_log_file(&key_log_path).expect("key log file should open");
792        drop(file);
793
794        let mode = fs::metadata(&key_log_path)
795            .expect("key log file metadata should be readable")
796            .permissions()
797            .mode()
798            & 0o777;
799        assert_eq!(mode, 0o600);
800    }
801
802    #[cfg(windows)]
803    #[test]
804    fn windows_default_crypto_provider_builds_client_config() {
805        let _ = super::initialize_default_crypto_provider();
806
807        ClientTLSConfigBuilder::new()
808            .with_root_cert_store(RootCertStore::empty())
809            .build()
810            .expect("Windows CNG-backed TLS config should build");
811    }
812
813    #[test]
814    #[cfg(feature = "fips")]
815    fn key_log_file_ignored_in_fips_mode() {
816        let _ = super::initialize_default_crypto_provider();
817        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
818        let key_log_path = tempdir.path().join("fips-sslkeylogfile");
819
820        // FIPS builds soft-skip TLS key logging instead of failing, so a leftover key log file path does not
821        // prevent TLS client construction.
822        let config = ClientTLSConfigBuilder::new()
823            .with_root_cert_store(RootCertStore::empty())
824            .with_key_log_file(&key_log_path)
825            .build()
826            .expect("TLS config should build in FIPS mode even when a key log file is configured");
827
828        // The default no-op `KeyLog` remains in place: invoking it must not panic and must not produce a file.
829        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
830
831        assert!(!key_log_path.exists(), "FIPS builds must not create a TLS key log file");
832    }
833}