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#[cfg(any(test, feature = "test-util"))]
37pub mod test_util;
38
39/// Tracks if the default cryptography provider for `rustls` has been set.
40static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
41
42/// Default root certificate store to use for TLS when one isn't explicitly provided.
43static 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
48// Various defaults for TLS configuration.
49const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
50const TLS12_PLUS_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
51const TLS13_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13];
52
53/// Minimum TLS protocol version to use for client connections.
54#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55pub enum TlsMinimumVersion {
56    /// TLS 1.2 or newer.
57    #[default]
58    Tls12,
59
60    /// TLS 1.3 or newer.
61    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/// A certificate verifier that accepts all server certificates without validation.
74///
75/// This is inherently insecure and should only be used for local/development connections where the
76/// server's identity is already established through other means (for example, connecting via Unix domain socket
77/// to a local process).
78#[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        // Open is attempted exactly once per path. Both success and failure are cached so that
127        // repeated builds for the same path do not re-open the file or re-emit warnings.
128        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
239/// A TLS client configuration builder.
240///
241/// Exposes various options for configuring a client's TLS configuration that would otherwise be cumbersome to
242/// configure, and provides sane defaults for many common options.
243///
244/// # Missing
245///
246/// - ability to configure client authentication
247pub 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    /// Enables logging of TLS key material to the given file path.
267    ///
268    /// TLS key material will be logged to the given file path in the [NSS Key Log][nss_key_log]
269    /// format, which can be used for debugging TLS issues, as well as decrypting captured
270    /// TLS traffic in tools such as Wireshark.
271    ///
272    /// Newly created files are created with owner read/write permissions on Unix.
273    /// Existing file permissions are preserved.
274    ///
275    /// [nss_key_log]: https://nss-crypto.org/reference/security/nss/legacy/key_log_format/index.html
276    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    /// Sets the maximum number of TLS 1.2 sessions to cache.
282    ///
283    /// Defaults to 8.
284    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    /// Sets the root certificate store to use for the client.
290    ///
291    /// Defaults to the "default" root certificate store initialized from the platform. (See [`load_platform_root_certificates`].)
292    pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
293        self.root_cert_store = Some(store);
294        self
295    }
296
297    /// Sets the minimum TLS protocol version to allow for client connections.
298    ///
299    /// Defaults to TLS 1.2.
300    pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
301        self.min_tls_version = version;
302        self
303    }
304
305    /// Disables server certificate verification entirely.
306    ///
307    /// This is inherently insecure and should only be used for local/development connections where
308    /// the server's identity is already established through other means (for example, connecting via Unix
309    /// domain socket to a local process).
310    pub fn danger_accept_invalid_certs(mut self) -> Self {
311        self.danger_accept_invalid_certs = true;
312        self
313    }
314
315    /// Builds the client TLS configuration.
316    ///
317    /// # Errors
318    ///
319    /// If the default root cert store (see [`load_platform_root_certificates`]) hasn't been initialized, and a root
320    /// cert store hasn't been provided, or if the resulting configuration isn't FIPS compliant, an error will be
321    /// returned.
322    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        // One unfortunate thing is that by creating `config` above, it assigns the default value for `Resumption` before
364        // we reset it down here... which means the big, beefy default one gets allocated and then immediately thrown
365        // away.
366        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
374/// Ensures that a client TLS configuration is FIPS compliant.
375///
376/// In FIPS builds, this checks the Rustls FIPS marker on the configuration. In non-FIPS builds, this is a no-op.
377///
378/// # Errors
379///
380/// If FIPS support is enabled and the configuration is not FIPS compliant, an error is returned.
381pub fn ensure_client_config_fips_compliant(config: &ClientConfig) -> Result<(), GenericError> {
382    #[cfg(feature = "fips")]
383    if !config.fips() {
384        return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
385    }
386
387    #[cfg(not(feature = "fips"))]
388    let _ = config;
389
390    Ok(())
391}
392
393/// Ensures that a server TLS configuration is FIPS compliant.
394///
395/// In FIPS builds, this disables operational secret extraction settings and checks the Rustls FIPS marker on the
396/// configuration. In non-FIPS builds, this is a no-op.
397///
398/// # Errors
399///
400/// If FIPS support is enabled and the configuration is not FIPS compliant, an error is returned.
401pub fn ensure_server_config_fips_compliant(config: &mut ServerConfig) -> Result<(), GenericError> {
402    #[cfg(feature = "fips")]
403    {
404        config.key_log = Arc::new(rustls::NoKeyLog);
405        config.enable_secret_extraction = false;
406
407        if !config.fips() {
408            return Err(generic_error!("Server TLS configuration is not FIPS compliant."));
409        }
410    }
411
412    #[cfg(not(feature = "fips"))]
413    let _ = config;
414
415    Ok(())
416}
417
418/// Initializes the default TLS cryptography provider used by `rustls`.
419///
420/// This explicitly sets the platform default provider for all future TLS configurations: CNG on Windows and AWS-LC on
421/// other platforms. FIPS builds configure the selected platform provider for FIPS mode.
422///
423/// # Errors
424///
425/// If the default cryptography provider has already been set, an error will be returned.
426pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
427    if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
428        return Err(generic_error!("Default TLS cryptography provider already initialized."));
429    }
430
431    default_crypto_provider().install_default().map_err(|_| {
432        generic_error!(
433            "Failed to install the default TLS cryptography provider. This is likely due to a conflicting provider already being installed."
434        )
435    })?;
436
437    // With the process-wide default having been set, mark it as having been set.
438    DEFAULT_CRYPTO_PROVIDER_SET
439        .set(())
440        .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
441
442    Ok(())
443}
444
445#[cfg(not(windows))]
446fn default_crypto_provider() -> CryptoProvider {
447    let provider = rustls::crypto::aws_lc_rs::default_provider();
448
449    #[cfg(feature = "fips")]
450    {
451        let mut provider = provider;
452        provider.cipher_suites.retain(|suite| suite.fips());
453        provider.kx_groups.retain(|group| group.fips());
454        provider
455    }
456
457    #[cfg(not(feature = "fips"))]
458    provider
459}
460
461#[cfg(windows)]
462fn default_crypto_provider() -> CryptoProvider {
463    rustls_cng_crypto::default_provider()
464}
465
466/// Initializes the default root certificate store from the platform's native certificate store.
467///
468/// ## Environment Variables
469///
470/// | Environment Variable | Description                                                                           |
471/// |----------------------|---------------------------------------------------------------------------------------|
472/// | SSL_CERT_FILE        | File containing an arbitrary number of certificates in PEM format.                    |
473/// | SSL_CERT_DIR         | Directory utilizing the hierarchy and naming convention used by OpenSSL's `c_rehash`. |
474///
475/// If **either** (or **both**) are set, certificates are only loaded from the locations specified via environment
476/// variables and not the platform- native certificate store.
477///
478/// ## Certificate Validity
479///
480/// All certificates are expected to be in PEM format. A file may contain multiple certificates.
481///
482/// Example:
483///
484/// ```text
485/// -----BEGIN CERTIFICATE-----
486/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
487/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
488/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
489/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
490/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
491/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
492/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
493/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
494/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
495/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
496/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
497/// /q4AaOeMSQ+2b1tbFfLn
498/// -----END CERTIFICATE-----
499/// -----BEGIN CERTIFICATE-----
500/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
501/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
502/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
503/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
504/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
505/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
506/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
507/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
508/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
509/// YyRIHN8wfdVoOw==
510/// -----END CERTIFICATE-----
511///
512/// ```
513///
514/// For reasons of compatibility, an attempt is made to skip invalid sections of a certificate file but this means it's
515/// also possible for a malformed certificate to be skipped.
516///
517/// If a certificate isn't loaded, and no error is reported, check if:
518///
519/// 1. the certificate is in PEM format (see example above)
520/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
521/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
522/// 4. there is a line break after the certificate.
523///
524/// ## Errors
525///
526/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
527/// returned. Missing or unreadable files and directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and
528/// treated as "no certificates available" rather than as a load failure; only certificate-content errors (such as
529/// malformed PEM) or other IO failures can fail the load.
530///
531/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
532pub fn load_platform_root_certificates() -> Result<(), GenericError> {
533    let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
534        .lock()
535        .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
536    if DEFAULT_ROOT_CERT_STORE.get().is_some() {
537        return Err(generic_error!(
538            "Default TLS root certificate store already initialized."
539        ));
540    }
541
542    let root_cert_store = load_platform_root_certificates_inner()?;
543
544    // The reason it should be impossible is that we intentionally only set it _here_, and we do so after acquiring the
545    // mutex, and only then do we make sure that it hasn't been set before proceeding to try to set it.
546    DEFAULT_ROOT_CERT_STORE
547        .set(Arc::new(root_cert_store))
548        .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
549
550    Ok(())
551}
552
553/// Builds a `RootCertStore` from the platform's native certificate store.
554///
555/// Behaves identically to [`load_platform_root_certificates`] with respect to which certificates are loaded, but
556/// returns the constructed store instead of writing it into the process-wide default.
557///
558/// # Errors
559///
560/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
561/// returned. Otherwise, even if some certificates failed to parse, the store is returned with whatever certificates were
562/// successfully added. Missing or unreadable files and directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are
563/// tolerated and do not produce an error.
564pub fn load_platform_root_certificates_inner() -> Result<RootCertStore, GenericError> {
565    let mut root_cert_store = RootCertStore::empty();
566
567    let mut result = rustls_native_certs::load_native_certs();
568
569    // Drop tolerable filesystem-access IO errors before evaluating success or failure. A missing (`NotFound`) or
570    // unreadable (`PermissionDenied`) `SSL_CERT_FILE`/`SSL_CERT_DIR` entry should look like "no certificates available"
571    // rather than a load failure: callers may simply not have set those env vars on this host, and a host's cert store
572    // may reference files this process can't read. This mirrors Go's TLS stack (and thus the Datadog Agent), which
573    // skips cert files it can't read. Other IO errors and all certificate-content errors (PEM parse / OS store) are
574    // retained and can still fail the load below when nothing was added.
575    result.errors.retain(|err| match &err.kind {
576        rustls_native_certs::ErrorKind::Io { inner, path }
577            if matches!(
578                inner.kind(),
579                std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied
580            ) =>
581        {
582            debug!(
583                error = %inner,
584                path = %path.display(),
585                "Skipping missing or unreadable certificate source while loading platform root certificates."
586            );
587            false
588        }
589        _ => true,
590    });
591
592    // For whatever certificates we _did_ get back, try and add them to the root certificate store.
593    let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
594    if failed == 0 && added > 0 {
595        debug!(
596            "Added {} certificates from environment to the default root certificate store.",
597            added
598        );
599    } else if failed > 0 && added > 0 {
600        debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
601    } else {
602        // When we don't manage to add any certificates, it either means that:
603        // - we found no certificates to add
604        // - we hit an error when loading the certificates
605        // - we hit an error when trying to add the certificates to our root certificate store
606        //
607        // We only consider this operation to have truly failed if there were errors during the initial loading of the
608        // certificates.
609        if !result.errors.is_empty() {
610            let joined_errors = result
611                .errors
612                .iter()
613                .map(|e| e.to_string())
614                .collect::<Vec<_>>()
615                .join(", ");
616
617            return Err(generic_error!(
618                "Failed to load certificates from platform's native certificate store: {}",
619                joined_errors
620            ));
621        }
622    }
623
624    Ok(root_cert_store)
625}
626
627#[cfg(test)]
628mod tests {
629    #[cfg(not(feature = "fips"))]
630    use std::fs;
631    #[cfg(all(unix, not(feature = "fips")))]
632    use std::os::unix::fs::PermissionsExt;
633    use std::path::Path;
634    #[cfg(feature = "fips")]
635    use std::sync::{
636        atomic::{AtomicBool, Ordering},
637        Arc,
638    };
639
640    #[cfg(feature = "fips")]
641    use rustls::KeyLog;
642    use rustls::{
643        client::danger::ServerCertVerifier,
644        crypto::CryptoProvider,
645        pki_types::{ServerName, UnixTime},
646        ClientConfig, ProtocolVersion, RootCertStore, ServerConfig,
647    };
648
649    use super::test_util::SelfSignedCert;
650    #[cfg(not(feature = "fips"))]
651    use super::{build_nss_key_log_line, open_key_log_file};
652    use super::{
653        ensure_client_config_fips_compliant, ensure_server_config_fips_compliant, AcceptAllServerCertVerifier,
654        ClientTLSConfigBuilder, TlsMinimumVersion,
655    };
656
657    /// Builds a client TLS configuration that logs key material to `path`, initializing the default crypto provider
658    /// first so the configuration can be built. Every key-log test shares this setup.
659    fn client_config_with_key_log(path: &Path) -> ClientConfig {
660        let _ = super::initialize_default_crypto_provider();
661
662        ClientTLSConfigBuilder::new()
663            .with_root_cert_store(RootCertStore::empty())
664            .with_key_log_file(path)
665            .build()
666            .expect("client TLS config should build")
667    }
668
669    /// Creates a temporary directory and returns it (to keep it alive) alongside a path to `file_name` within it.
670    fn temp_file_path(file_name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
671        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
672        let path = tempdir.path().join(file_name);
673        (tempdir, path)
674    }
675
676    #[test]
677    fn tls12_minimum_enables_tls12_and_tls13() {
678        let versions = TlsMinimumVersion::Tls12.protocol_versions();
679
680        assert_eq!(versions.len(), 2);
681        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
682        assert_eq!(versions[1].version, ProtocolVersion::TLSv1_2);
683    }
684
685    #[test]
686    fn tls13_minimum_enables_tls13_only() {
687        let versions = TlsMinimumVersion::Tls13.protocol_versions();
688
689        assert_eq!(versions.len(), 1);
690        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
691    }
692
693    #[test]
694    fn client_config_fips_validation_accepts_builder_config() {
695        let _ = super::initialize_default_crypto_provider();
696
697        let config = ClientTLSConfigBuilder::new()
698            .with_root_cert_store(RootCertStore::empty())
699            .build()
700            .expect("client TLS config should build");
701
702        ensure_client_config_fips_compliant(&config).expect("client TLS config should pass FIPS validation");
703    }
704
705    #[test]
706    fn accept_all_verifier_accepts_mismatched_server_certificate() {
707        // The accept-all verifier performs no validation by design, so it must accept even a self-signed certificate
708        // presented for a completely different server name — a case any real verifier would reject. This is the
709        // behavior that backs `ClientTLSConfigBuilder::danger_accept_invalid_certs`.
710        let _ = super::initialize_default_crypto_provider();
711        let provider = CryptoProvider::get_default()
712            .cloned()
713            .expect("default crypto provider should be installed");
714        let verifier = AcceptAllServerCertVerifier { provider };
715
716        let cert = SelfSignedCert::new(["localhost"]);
717        let cert_chain = cert.cert_chain();
718        let server_name = ServerName::try_from("totally.different.example").expect("server name should parse");
719
720        let result = verifier.verify_server_cert(&cert_chain[0], &[], &server_name, &[], UnixTime::now());
721
722        assert!(
723            result.is_ok(),
724            "accept-all verifier must accept a certificate presented for a mismatched server name"
725        );
726    }
727
728    #[test]
729    fn danger_accept_invalid_certs_builds_without_root_cert_store() {
730        let _ = super::initialize_default_crypto_provider();
731
732        // Without an explicit root cert store (and with no process-wide default initialized in this test), a normal
733        // build fails: there is nothing to verify server certificates against.
734        let missing_store_error = ClientTLSConfigBuilder::new()
735            .build()
736            .expect_err("client TLS config should fail to build without any root cert store");
737        assert!(
738            missing_store_error
739                .to_string()
740                .contains("root certificate store not initialized"),
741            "unexpected error: {missing_store_error}"
742        );
743
744        // Enabling the dangerous accept-all verifier removes the need for a root cert store entirely, so the same
745        // builder now succeeds.
746        ClientTLSConfigBuilder::new()
747            .danger_accept_invalid_certs()
748            .build()
749            .expect("client TLS config should build with an accept-all verifier and no root cert store");
750    }
751
752    #[test]
753    fn server_config_fips_validation_accepts_basic_server_config() {
754        let _ = super::initialize_default_crypto_provider();
755        let cert = SelfSignedCert::localhost();
756        let mut config = ServerConfig::builder()
757            .with_no_client_auth()
758            .with_single_cert(cert.cert_chain(), cert.private_key())
759            .expect("server TLS config should build");
760
761        ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
762    }
763
764    #[cfg(feature = "fips")]
765    #[test]
766    fn server_config_fips_validation_disables_secret_extraction() {
767        #[derive(Debug)]
768        struct TestKeyLog(Arc<AtomicBool>);
769
770        impl KeyLog for TestKeyLog {
771            fn log(&self, _label: &str, _client_random: &[u8], _secret: &[u8]) {
772                self.0.store(true, Ordering::Relaxed);
773            }
774        }
775
776        let _ = super::initialize_default_crypto_provider();
777        let cert = SelfSignedCert::localhost();
778        let key_log_used = Arc::new(AtomicBool::new(false));
779        let mut config = ServerConfig::builder()
780            .with_no_client_auth()
781            .with_single_cert(cert.cert_chain(), cert.private_key())
782            .expect("server TLS config should build");
783        config.key_log = Arc::new(TestKeyLog(Arc::clone(&key_log_used)));
784        config.enable_secret_extraction = true;
785
786        ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
787
788        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
789
790        assert!(!key_log_used.load(Ordering::Relaxed));
791        assert!(!config.enable_secret_extraction);
792    }
793
794    // The FIPS-compliance checks return an error when a configuration is not FIPS compliant. To exercise that
795    // documented error path in a FIPS build, the two tests below build configurations using the *unfiltered*
796    // aws-lc-rs provider, whose default cipher suites include non-FIPS-approved algorithms (e.g. ChaCha20), so
797    // `fips()` reports false even though the crate is compiled with FIPS support.
798    #[cfg(all(feature = "fips", not(windows)))]
799    #[test]
800    fn client_config_fips_validation_rejects_non_fips_config() {
801        let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
802        let config = ClientConfig::builder_with_provider(provider)
803            .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
804            .expect("client config builder should accept protocol versions")
805            .with_root_certificates(RootCertStore::empty())
806            .with_no_client_auth();
807
808        let error =
809            ensure_client_config_fips_compliant(&config).expect_err("a non-FIPS client configuration must be rejected");
810        assert!(
811            error.to_string().contains("not FIPS compliant"),
812            "unexpected error: {error}"
813        );
814    }
815
816    #[cfg(all(feature = "fips", not(windows)))]
817    #[test]
818    fn server_config_fips_validation_rejects_non_fips_config() {
819        let cert = SelfSignedCert::localhost();
820        let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
821        let mut config = ServerConfig::builder_with_provider(provider)
822            .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
823            .expect("server config builder should accept protocol versions")
824            .with_no_client_auth()
825            .with_single_cert(cert.cert_chain(), cert.private_key())
826            .expect("server TLS config should build");
827
828        let error = ensure_server_config_fips_compliant(&mut config)
829            .expect_err("a non-FIPS server configuration must be rejected");
830        assert!(
831            error.to_string().contains("not FIPS compliant"),
832            "unexpected error: {error}"
833        );
834    }
835
836    #[test]
837    #[cfg(not(feature = "fips"))]
838    fn nss_key_log_lines_are_written_in_hex_format() {
839        let output =
840            build_nss_key_log_line("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]).expect("key log line should build");
841
842        assert_eq!(output, b"CLIENT_RANDOM abcd 0123\n");
843    }
844
845    #[test]
846    #[cfg(not(feature = "fips"))]
847    fn client_config_uses_configured_key_log_file() {
848        let (_tempdir, key_log_path) = temp_file_path("sslkeylogfile");
849
850        let config = client_config_with_key_log(&key_log_path);
851        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
852
853        let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
854        assert_eq!(contents, "CLIENT_RANDOM abcd 0123\n");
855    }
856
857    #[test]
858    #[cfg(not(feature = "fips"))]
859    fn client_config_ignores_unwritable_key_log_file() {
860        // The key log path points into a nonexistent subdirectory, so the file can never be opened. Building the
861        // configuration must still succeed, and no file should be created.
862        let (_tempdir, key_log_path) = temp_file_path("missing/sslkeylogfile");
863
864        let config = client_config_with_key_log(&key_log_path);
865        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
866
867        assert!(!key_log_path.exists());
868    }
869
870    #[test]
871    #[cfg(not(feature = "fips"))]
872    fn client_configs_append_to_shared_key_log_file() {
873        let (_tempdir, key_log_path) = temp_file_path("shared-sslkeylogfile");
874
875        let first_config = client_config_with_key_log(&key_log_path);
876        let second_config = client_config_with_key_log(&key_log_path);
877
878        first_config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
879        second_config.key_log.log("CLIENT_RANDOM", &[0xef, 0x01], &[0x45, 0x67]);
880
881        let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
882        assert_eq!(contents, "CLIENT_RANDOM abcd 0123\nCLIENT_RANDOM ef01 4567\n");
883    }
884
885    #[cfg(all(unix, not(feature = "fips")))]
886    #[test]
887    fn key_log_file_is_created_with_owner_only_permissions() {
888        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
889        let key_log_path = tempdir.path().join("sslkeylogfile");
890
891        let file = open_key_log_file(&key_log_path).expect("key log file should open");
892        drop(file);
893
894        let mode = fs::metadata(&key_log_path)
895            .expect("key log file metadata should be readable")
896            .permissions()
897            .mode()
898            & 0o777;
899        assert_eq!(mode, 0o600);
900    }
901
902    #[cfg(windows)]
903    #[test]
904    fn windows_default_crypto_provider_builds_client_config() {
905        let _ = super::initialize_default_crypto_provider();
906
907        ClientTLSConfigBuilder::new()
908            .with_root_cert_store(RootCertStore::empty())
909            .build()
910            .expect("Windows CNG-backed TLS config should build");
911    }
912
913    #[test]
914    #[cfg(feature = "fips")]
915    fn key_log_file_ignored_in_fips_mode() {
916        // FIPS builds soft-skip TLS key logging instead of failing, so a leftover key log file path does not
917        // prevent TLS client construction.
918        let (_tempdir, key_log_path) = temp_file_path("fips-sslkeylogfile");
919
920        let config = client_config_with_key_log(&key_log_path);
921
922        // The default no-op `KeyLog` remains in place: invoking it must not panic and must not produce a file.
923        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
924
925        assert!(!key_log_path.exists(), "FIPS builds must not create a TLS key log file");
926    }
927}