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};
11use std::{
12    path::{Path, PathBuf},
13    sync::{Arc, Mutex, OnceLock},
14};
15
16#[cfg(not(feature = "fips"))]
17use rustls::KeyLog;
18use rustls::{
19    client::{
20        danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
21        Resumption,
22    },
23    crypto::CryptoProvider,
24    pki_types::{pem::PemObject as _, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
25    server::WebPkiClientVerifier,
26    version::{TLS12, TLS13},
27    ClientConfig, DigitallySignedStruct, RootCertStore, ServerConfig, SignatureScheme, SupportedCipherSuite,
28    SupportedProtocolVersion, Tls13CipherSuite,
29};
30#[cfg(not(feature = "fips"))]
31use saluki_common::collections::FastHashMap;
32use saluki_error::{generic_error, GenericError};
33use tracing::debug;
34#[cfg(not(feature = "fips"))]
35use tracing::warn;
36
37#[cfg(any(test, feature = "test-util"))]
38pub mod test_util;
39
40/// Tracks if the default cryptography provider for `rustls` has been set.
41static DEFAULT_CRYPTO_PROVIDER_SET: OnceLock<()> = OnceLock::new();
42
43/// Default root certificate store to use for TLS when one isn't explicitly provided.
44static DEFAULT_ROOT_CERT_STORE_MUTEX: Mutex<()> = Mutex::new(());
45static DEFAULT_ROOT_CERT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
46#[cfg(not(feature = "fips"))]
47static KEY_LOG_FILES: OnceLock<Mutex<FastHashMap<PathBuf, Option<Arc<NssKeyLogFile>>>>> = OnceLock::new();
48
49// Various defaults for TLS configuration.
50const DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS: usize = 8;
51const TLS12_PLUS_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13, &TLS12];
52const TLS13_PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&TLS13];
53
54/// Minimum TLS protocol version to use for client connections.
55#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
56pub enum TlsMinimumVersion {
57    /// TLS 1.2 or newer.
58    #[default]
59    Tls12,
60
61    /// TLS 1.3 or newer.
62    Tls13,
63}
64
65impl TlsMinimumVersion {
66    const fn protocol_versions(self) -> &'static [&'static SupportedProtocolVersion] {
67        match self {
68            Self::Tls12 => TLS12_PLUS_PROTOCOL_VERSIONS,
69            Self::Tls13 => TLS13_PROTOCOL_VERSIONS,
70        }
71    }
72}
73
74/// A certificate verifier that accepts all server certificates without validation.
75///
76/// This is inherently insecure and should only be used for local/development connections where the
77/// server's identity is already established through other means (for example, connecting via Unix domain socket
78/// to a local process).
79#[derive(Debug)]
80struct AcceptAllServerCertVerifier {
81    provider: Arc<CryptoProvider>,
82}
83
84impl ServerCertVerifier for AcceptAllServerCertVerifier {
85    fn verify_server_cert(
86        &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
87        _ocsp_response: &[u8], _now: UnixTime,
88    ) -> Result<ServerCertVerified, rustls::Error> {
89        Ok(ServerCertVerified::assertion())
90    }
91
92    fn verify_tls12_signature(
93        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
94    ) -> Result<HandshakeSignatureValid, rustls::Error> {
95        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
96    }
97
98    fn verify_tls13_signature(
99        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
100    ) -> Result<HandshakeSignatureValid, rustls::Error> {
101        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
102    }
103
104    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
105        self.provider.signature_verification_algorithms.supported_schemes()
106    }
107}
108
109#[cfg(not(feature = "fips"))]
110struct NssKeyLogFile {
111    path: PathBuf,
112    file: Mutex<File>,
113}
114
115#[cfg(not(feature = "fips"))]
116impl NssKeyLogFile {
117    fn open_shared<P: Into<PathBuf>>(path: P) -> Option<Arc<Self>> {
118        let path = path.into();
119        let mut key_log_files = match KEY_LOG_FILES.get_or_init(|| Mutex::new(FastHashMap::default())).lock() {
120            Ok(key_log_files) => key_log_files,
121            Err(_) => {
122                warn!("Failed to acquire TLS key log file registry lock; TLS key logging disabled.");
123                return None;
124            }
125        };
126
127        // Open is attempted exactly once per path. Both success and failure are cached so that
128        // repeated builds for the same path do not re-open the file or re-emit warnings.
129        if let Some(cached) = key_log_files.get(&path) {
130            return cached.clone();
131        }
132
133        let key_log_file = match open_key_log_file(&path) {
134            Ok(file) => {
135                warn!(
136                    path = %path.display(),
137                    "TLS key logging enabled; TLS session secrets will be written to disk."
138                );
139                Some(Arc::new(Self {
140                    path: path.clone(),
141                    file: Mutex::new(file),
142                }))
143            }
144            Err(e) => {
145                warn!(
146                    path = %path.display(),
147                    error = %e,
148                    "Failed to open TLS key log file for appending; TLS key logging disabled."
149                );
150                None
151            }
152        };
153
154        key_log_files.insert(path, key_log_file.clone());
155        key_log_file
156    }
157}
158
159#[cfg(not(feature = "fips"))]
160impl Debug for NssKeyLogFile {
161    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
162        f.debug_struct("NssKeyLogFile").field("path", &self.path).finish()
163    }
164}
165
166#[cfg(feature = "fips")]
167static FIPS_KEY_LOG_WARNED_PATHS: OnceLock<Mutex<saluki_common::collections::FastHashSet<PathBuf>>> = OnceLock::new();
168
169#[cfg(feature = "fips")]
170fn fips_key_log_warn_once(path: PathBuf) {
171    let warned =
172        FIPS_KEY_LOG_WARNED_PATHS.get_or_init(|| Mutex::new(saluki_common::collections::FastHashSet::default()));
173    let Ok(mut warned) = warned.lock() else {
174        return;
175    };
176    if warned.insert(path.clone()) {
177        tracing::warn!(
178            path = %path.display(),
179            "FIPS build: TLS key logging is disabled because exporting TLS secrets is not FIPS-compliant."
180        );
181    }
182}
183
184#[cfg(not(feature = "fips"))]
185impl KeyLog for NssKeyLogFile {
186    fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) {
187        let line = match build_nss_key_log_line(label, client_random, secret) {
188            Ok(line) => line,
189            Err(e) => {
190                debug!(path = %self.path.display(), error = %e, "Failed to format TLS key log line.");
191                return;
192            }
193        };
194
195        match self.file.lock() {
196            Ok(mut file) => {
197                if let Err(e) = file.write_all(&line) {
198                    debug!(path = %self.path.display(), error = %e, "Failed to write TLS key log line.");
199                }
200            }
201            Err(_) => {
202                debug!(path = %self.path.display(), "TLS key log file lock poisoned; dropping TLS key log line.");
203            }
204        }
205    }
206}
207
208#[cfg(not(feature = "fips"))]
209fn open_key_log_file(path: &Path) -> io::Result<File> {
210    let mut options = OpenOptions::new();
211    options.write(true).create(true).append(true);
212
213    #[cfg(unix)]
214    options.mode(0o600);
215
216    options.open(path)
217}
218
219#[cfg(not(feature = "fips"))]
220fn build_nss_key_log_line(label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<Vec<u8>> {
221    let mut line = Vec::new();
222    write!(line, "{label} ")?;
223    write_hex(&mut line, client_random)?;
224    write!(line, " ")?;
225    write_hex(&mut line, secret)?;
226    writeln!(line)?;
227
228    Ok(line)
229}
230
231#[cfg(not(feature = "fips"))]
232fn write_hex(writer: &mut impl Write, bytes: &[u8]) -> io::Result<()> {
233    for byte in bytes {
234        write!(writer, "{byte:02x}")?;
235    }
236
237    Ok(())
238}
239
240/// A TLS client configuration builder.
241///
242/// Exposes various options for configuring a client's TLS configuration that would otherwise be cumbersome to
243/// configure, and provides sane defaults for many common options.
244///
245/// # Missing
246///
247/// - ability to configure client authentication
248pub struct ClientTLSConfigBuilder {
249    key_log_file_path: Option<PathBuf>,
250    max_tls12_resumption_sessions: Option<usize>,
251    min_tls_version: TlsMinimumVersion,
252    root_cert_store: Option<RootCertStore>,
253    danger_accept_invalid_certs: bool,
254}
255
256impl ClientTLSConfigBuilder {
257    pub fn new() -> Self {
258        Self {
259            key_log_file_path: None,
260            max_tls12_resumption_sessions: None,
261            min_tls_version: TlsMinimumVersion::default(),
262            root_cert_store: None,
263            danger_accept_invalid_certs: false,
264        }
265    }
266
267    /// Enables logging of TLS key material to the given file path.
268    ///
269    /// TLS key material will be logged to the given file path in the [NSS Key Log][nss_key_log]
270    /// format, which can be used for debugging TLS issues, as well as decrypting captured
271    /// TLS traffic in tools such as Wireshark.
272    ///
273    /// Newly created files are created with owner read/write permissions on Unix.
274    /// Existing file permissions are preserved.
275    ///
276    /// [nss_key_log]: https://nss-crypto.org/reference/security/nss/legacy/key_log_format/index.html
277    pub fn with_key_log_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
278        self.key_log_file_path = Some(path.into());
279        self
280    }
281
282    /// Sets the maximum number of TLS 1.2 sessions to cache.
283    ///
284    /// Defaults to 8.
285    pub fn with_max_tls12_resumption_sessions(mut self, max: usize) -> Self {
286        self.max_tls12_resumption_sessions = Some(max);
287        self
288    }
289
290    /// Sets the root certificate store to use for the client.
291    ///
292    /// Defaults to the "default" root certificate store initialized from the platform. (See [`load_platform_root_certificates`].)
293    pub fn with_root_cert_store(mut self, store: RootCertStore) -> Self {
294        self.root_cert_store = Some(store);
295        self
296    }
297
298    /// Sets the minimum TLS protocol version to allow for client connections.
299    ///
300    /// Defaults to TLS 1.2.
301    pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
302        self.min_tls_version = version;
303        self
304    }
305
306    /// Disables server certificate verification entirely.
307    ///
308    /// This is inherently insecure and should only be used for local/development connections where
309    /// the server's identity is already established through other means (for example, connecting via Unix
310    /// domain socket to a local process).
311    pub fn danger_accept_invalid_certs(mut self) -> Self {
312        self.danger_accept_invalid_certs = true;
313        self
314    }
315
316    /// Builds the client TLS configuration.
317    ///
318    /// # Errors
319    ///
320    /// If the default root cert store (see [`load_platform_root_certificates`]) hasn't been initialized, and a root
321    /// cert store hasn't been provided, or if the resulting configuration isn't FIPS compliant, an error will be
322    /// returned.
323    pub fn build(self) -> Result<ClientConfig, GenericError> {
324        let max_tls12_resumption_sessions = self
325            .max_tls12_resumption_sessions
326            .unwrap_or(DEFAULT_MAX_TLS12_RESUMPTION_SESSIONS);
327        let protocol_versions = self.min_tls_version.protocol_versions();
328
329        let mut config = if self.danger_accept_invalid_certs {
330            let crypto_provider = CryptoProvider::get_default()
331                .map(Arc::clone)
332                .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
333            let verifier = Arc::new(AcceptAllServerCertVerifier {
334                provider: crypto_provider,
335            });
336
337            ClientConfig::builder_with_protocol_versions(protocol_versions)
338                .dangerous()
339                .with_custom_certificate_verifier(verifier)
340                .with_no_client_auth()
341        } else {
342            let root_cert_store = self.root_cert_store.map(Arc::new).map(Ok).unwrap_or_else(|| {
343                DEFAULT_ROOT_CERT_STORE
344                    .get()
345                    .map(Arc::clone)
346                    .ok_or(generic_error!("Default TLS root certificate store not initialized."))
347            })?;
348
349            ClientConfig::builder_with_protocol_versions(protocol_versions)
350                .with_root_certificates(root_cert_store)
351                .with_no_client_auth()
352        };
353
354        if let Some(path) = self.key_log_file_path {
355            #[cfg(feature = "fips")]
356            fips_key_log_warn_once(path);
357
358            #[cfg(not(feature = "fips"))]
359            if let Some(key_log) = NssKeyLogFile::open_shared(path) {
360                config.key_log = key_log;
361            }
362        }
363
364        // One unfortunate thing is that by creating `config` above, it assigns the default value for `Resumption` before
365        // we reset it down here... which means the big, beefy default one gets allocated and then immediately thrown
366        // away.
367        config.resumption = Resumption::in_memory_sessions(max_tls12_resumption_sessions);
368
369        ensure_client_config_fips_compliant(&config)?;
370
371        Ok(config)
372    }
373}
374
375/// A TLS server configuration builder.
376///
377/// Exposes options for configuring a server's TLS configuration by loading certificates and keys from PEM files
378/// on disk, and provides sane defaults for many common options.
379///
380/// # Missing
381///
382/// - ability to configure TLS key logging
383/// - ability to configure minimum/maximum TLS protocol versions
384/// - ability to configure cipher suites and curve preferences
385pub struct ServerTLSConfigBuilder {
386    cert_file: Option<PathBuf>,
387    key_file: Option<PathBuf>,
388    ca_file: Option<PathBuf>,
389}
390
391impl ServerTLSConfigBuilder {
392    /// Creates a new server TLS configuration builder with no certificates or keys configured.
393    pub fn new() -> Self {
394        Self {
395            cert_file: None,
396            key_file: None,
397            ca_file: None,
398        }
399    }
400
401    /// Sets the path to the PEM-encoded certificate chain file.
402    ///
403    /// The file may contain a single certificate (leaf) or a full certificate chain (leaf followed by intermediates).
404    /// All certificates in the file are presented to clients during the TLS handshake.
405    pub fn with_cert_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
406        self.cert_file = Some(path.into());
407        self
408    }
409
410    /// Sets the path to the PEM-encoded private key file.
411    ///
412    /// The private key must correspond to the leaf certificate in the certificate chain.
413    pub fn with_key_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
414        self.key_file = Some(path.into());
415        self
416    }
417
418    /// Sets the path to the PEM-encoded CA certificate file used to verify client certificates.
419    ///
420    /// When set, the server requests client certificates and verifies them against the CA certificates in this file,
421    /// but does not require a client certificate. If the client presents a certificate, it must be valid; if the
422    /// client presents no certificate, the connection is still accepted.
423    ///
424    /// The file may contain multiple CA certificates in PEM format.
425    pub fn with_ca_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
426        self.ca_file = Some(path.into());
427        self
428    }
429
430    /// Builds the server TLS configuration.
431    ///
432    /// # Errors
433    ///
434    /// If the certificate or key files cannot be read or parsed, or if the resulting configuration isn't FIPS
435    /// compliant, an error will be returned.
436    pub fn build(self) -> Result<ServerConfig, GenericError> {
437        let cert_file = self
438            .cert_file
439            .ok_or_else(|| generic_error!("No certificate file configured for server TLS."))?;
440        let key_file = self
441            .key_file
442            .ok_or_else(|| generic_error!("No private key file configured for server TLS."))?;
443
444        // Load the certificate chain.
445        let cert_bytes = std::fs::read(&cert_file)
446            .map_err(|e| generic_error!("Failed to read certificate file '{}': {}", cert_file.display(), e))?;
447        let cert_chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_bytes)
448            .collect::<Result<Vec<_>, _>>()
449            .map_err(|e| generic_error!("Failed to parse certificate file '{}': {}", cert_file.display(), e))?;
450
451        if cert_chain.is_empty() {
452            return Err(generic_error!(
453                "No PEM-encoded certificates found in certificate file '{}'.",
454                cert_file.display()
455            ));
456        }
457
458        // Load the private key.
459        let key_bytes = std::fs::read(&key_file)
460            .map_err(|e| generic_error!("Failed to read private key file '{}': {}", key_file.display(), e))?;
461        let private_key = PrivateKeyDer::from_pem_slice(&key_bytes)
462            .map_err(|e| generic_error!("Failed to parse private key file '{}': {}", key_file.display(), e))?;
463
464        let mut config = if let Some(ca_file) = self.ca_file {
465            build_server_config_with_client_verifier(&ca_file, cert_chain, private_key)?
466        } else {
467            ServerConfig::builder()
468                .with_no_client_auth()
469                .with_single_cert(cert_chain, private_key)
470                .map_err(|e| generic_error!("Failed to build server TLS configuration: {}", e))?
471        };
472
473        ensure_server_config_fips_compliant(&mut config)?;
474
475        Ok(config)
476    }
477}
478
479/// Builds a `ServerConfig` with a client certificate verifier loaded from a CA file.
480///
481/// The server requests client certificates and verifies them if presented, but does not require them (optional
482/// verification). This matches the OpenTelemetry Collector's `ca_file` semantics for a server.
483fn build_server_config_with_client_verifier(
484    ca_file: &Path, cert_chain: Vec<CertificateDer<'static>>, private_key: PrivateKeyDer<'static>,
485) -> Result<ServerConfig, GenericError> {
486    let ca_bytes = std::fs::read(ca_file)
487        .map_err(|e| generic_error!("Failed to read CA certificate file '{}': {}", ca_file.display(), e))?;
488    let mut root_cert_store = RootCertStore::empty();
489    let ca_certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&ca_bytes)
490        .collect::<Result<Vec<_>, _>>()
491        .map_err(|e| generic_error!("Failed to parse CA certificate file '{}': {}", ca_file.display(), e))?;
492
493    if ca_certs.is_empty() {
494        return Err(generic_error!(
495            "No PEM-encoded certificates found in CA file '{}'.",
496            ca_file.display()
497        ));
498    }
499
500    for ca_cert in ca_certs {
501        root_cert_store
502            .add(ca_cert)
503            .map_err(|e| generic_error!("Failed to add CA certificate to root store: {}", e))?;
504    }
505
506    let client_verifier = WebPkiClientVerifier::builder(Arc::new(root_cert_store))
507        .allow_unauthenticated()
508        .build()
509        .map_err(|e| generic_error!("Failed to build client certificate verifier: {}", e))?;
510
511    ServerConfig::builder()
512        .with_client_cert_verifier(client_verifier)
513        .with_single_cert(cert_chain, private_key)
514        .map_err(|e| generic_error!("Failed to build server TLS configuration: {}", e))
515}
516
517/// Ensures that a client TLS configuration is FIPS compliant.
518///
519/// In FIPS builds, this checks the Rustls FIPS marker on the configuration. In non-FIPS builds, this is a no-op.
520///
521/// # Errors
522///
523/// If FIPS support is enabled and the configuration is not FIPS compliant, an error is returned.
524pub fn ensure_client_config_fips_compliant(config: &ClientConfig) -> Result<(), GenericError> {
525    #[cfg(feature = "fips")]
526    if !config.fips() {
527        return Err(generic_error!("Client TLS configuration is not FIPS compliant."));
528    }
529
530    #[cfg(not(feature = "fips"))]
531    let _ = config;
532
533    Ok(())
534}
535
536/// Ensures that a server TLS configuration is FIPS compliant.
537///
538/// In FIPS builds, this disables operational secret extraction settings and checks the Rustls FIPS marker on the
539/// configuration. In non-FIPS builds, this is a no-op.
540///
541/// # Errors
542///
543/// If FIPS support is enabled and the configuration is not FIPS compliant, an error is returned.
544pub fn ensure_server_config_fips_compliant(config: &mut ServerConfig) -> Result<(), GenericError> {
545    #[cfg(feature = "fips")]
546    {
547        config.key_log = Arc::new(rustls::NoKeyLog);
548        config.enable_secret_extraction = false;
549
550        if !config.fips() {
551            return Err(generic_error!("Server TLS configuration is not FIPS compliant."));
552        }
553    }
554
555    #[cfg(not(feature = "fips"))]
556    let _ = config;
557
558    Ok(())
559}
560
561/// Initializes the default TLS cryptography provider used by `rustls`.
562///
563/// This explicitly sets the platform default provider for all future TLS configurations: CNG on Windows and AWS-LC on
564/// other platforms. FIPS builds configure the selected platform provider for FIPS mode.
565///
566/// # Errors
567///
568/// If the default cryptography provider has already been set, an error will be returned.
569pub fn initialize_default_crypto_provider() -> Result<(), GenericError> {
570    if DEFAULT_CRYPTO_PROVIDER_SET.get().is_some() {
571        return Err(generic_error!("Default TLS cryptography provider already initialized."));
572    }
573
574    default_crypto_provider().install_default().map_err(|_| {
575        generic_error!(
576            "Failed to install the default TLS cryptography provider. This is likely due to a conflicting provider already being installed."
577        )
578    })?;
579
580    // With the process-wide default having been set, mark it as having been set.
581    DEFAULT_CRYPTO_PROVIDER_SET
582        .set(())
583        .expect("should be impossible for DEFAULT_CRYPTO_PROVIDER_SET to be initialized twice");
584
585    Ok(())
586}
587
588#[cfg(not(windows))]
589fn default_crypto_provider() -> CryptoProvider {
590    let provider = rustls::crypto::aws_lc_rs::default_provider();
591
592    #[cfg(feature = "fips")]
593    {
594        let mut provider = provider;
595        provider.cipher_suites.retain(|suite| suite.fips());
596        provider.kx_groups.retain(|group| group.fips());
597        provider
598    }
599
600    #[cfg(not(feature = "fips"))]
601    provider
602}
603
604#[cfg(windows)]
605fn default_crypto_provider() -> CryptoProvider {
606    rustls_cng_crypto::default_provider()
607}
608
609/// Computes the SHA-256 digest of `data` with the platform's TLS crypto provider.
610///
611/// This is AWS-LC on non-Windows platforms (its FIPS module when the `fips` feature is enabled) and CNG on Windows, so
612/// the digest comes from the same module as TLS.
613pub fn sha256(data: &[u8]) -> [u8; 32] {
614    SHA256_SUITE
615        .common
616        .hash_provider
617        .hash(data)
618        .as_ref()
619        .try_into()
620        .expect("SHA-256 digest should be 32 bytes")
621}
622
623// Neither provider exports its hash directly, so borrow it from a TLS 1.3 suite. This is evaluated at compile time.
624#[cfg(not(windows))]
625static SHA256_SUITE: &Tls13CipherSuite = tls13_suite(rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256);
626#[cfg(windows)]
627static SHA256_SUITE: &Tls13CipherSuite = tls13_suite(rustls_cng_crypto::cipher_suite::TLS13_AES_128_GCM_SHA256);
628
629const fn tls13_suite(suite: SupportedCipherSuite) -> &'static Tls13CipherSuite {
630    match suite.tls13() {
631        Some(suite) => suite,
632        None => panic!("SHA-256 source should be a TLS 1.3 cipher suite"),
633    }
634}
635
636/// Initializes the default root certificate store from the platform's native certificate store.
637///
638/// ## Environment Variables
639///
640/// | Environment Variable | Description                                                                           |
641/// |----------------------|---------------------------------------------------------------------------------------|
642/// | SSL_CERT_FILE        | File containing an arbitrary number of certificates in PEM format.                    |
643/// | SSL_CERT_DIR         | Directory utilizing the hierarchy and naming convention used by OpenSSL's `c_rehash`. |
644///
645/// If **either** (or **both**) are set, certificates are only loaded from the locations specified via environment
646/// variables and not the platform- native certificate store.
647///
648/// ## Certificate Validity
649///
650/// All certificates are expected to be in PEM format. A file may contain multiple certificates.
651///
652/// Example:
653///
654/// ```text
655/// -----BEGIN CERTIFICATE-----
656/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
657/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
658/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
659/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
660/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
661/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
662/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
663/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
664/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
665/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
666/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
667/// /q4AaOeMSQ+2b1tbFfLn
668/// -----END CERTIFICATE-----
669/// -----BEGIN CERTIFICATE-----
670/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
671/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
672/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
673/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
674/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
675/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
676/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
677/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
678/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
679/// YyRIHN8wfdVoOw==
680/// -----END CERTIFICATE-----
681///
682/// ```
683///
684/// For reasons of compatibility, an attempt is made to skip invalid sections of a certificate file but this means it's
685/// also possible for a malformed certificate to be skipped.
686///
687/// If a certificate isn't loaded, and no error is reported, check if:
688///
689/// 1. the certificate is in PEM format (see example above)
690/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
691/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
692/// 4. there is a line break after the certificate.
693///
694/// ## Errors
695///
696/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
697/// returned. Missing or unreadable files and directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are tolerated and
698/// treated as "no certificates available" rather than as a load failure; only certificate-content errors (such as
699/// malformed PEM) or other IO failures can fail the load.
700///
701/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
702pub fn load_platform_root_certificates() -> Result<(), GenericError> {
703    let _guard = DEFAULT_ROOT_CERT_STORE_MUTEX
704        .lock()
705        .map_err(|_| generic_error!("Default TLS root certificate store update lock poisoned."))?;
706    if DEFAULT_ROOT_CERT_STORE.get().is_some() {
707        return Err(generic_error!(
708            "Default TLS root certificate store already initialized."
709        ));
710    }
711
712    let root_cert_store = load_platform_root_certificates_inner()?;
713
714    // The reason it should be impossible is that we intentionally only set it _here_, and we do so after acquiring the
715    // mutex, and only then do we make sure that it hasn't been set before proceeding to try to set it.
716    DEFAULT_ROOT_CERT_STORE
717        .set(Arc::new(root_cert_store))
718        .expect("should be impossible for DEFAULT_ROOT_CERT_STORE to be initialized twice");
719
720    Ok(())
721}
722
723/// Builds a `RootCertStore` from the platform's native certificate store.
724///
725/// Behaves identically to [`load_platform_root_certificates`] with respect to which certificates are loaded, but
726/// returns the constructed store instead of writing it into the process-wide default.
727///
728/// # Errors
729///
730/// If errors occur during certificate loading and no certificates were ultimately added to the store, an error is
731/// returned. Otherwise, even if some certificates failed to parse, the store is returned with whatever certificates were
732/// successfully added. Missing or unreadable files and directories referenced by `SSL_CERT_FILE`/`SSL_CERT_DIR` are
733/// tolerated and do not produce an error.
734pub fn load_platform_root_certificates_inner() -> Result<RootCertStore, GenericError> {
735    let mut root_cert_store = RootCertStore::empty();
736
737    let mut result = rustls_native_certs::load_native_certs();
738
739    // Drop tolerable filesystem-access IO errors before evaluating success or failure. A missing (`NotFound`) or
740    // unreadable (`PermissionDenied`) `SSL_CERT_FILE`/`SSL_CERT_DIR` entry should look like "no certificates available"
741    // rather than a load failure: callers may simply not have set those env vars on this host, and a host's cert store
742    // may reference files this process can't read. This mirrors Go's TLS stack (and thus the Datadog Agent), which
743    // skips cert files it can't read. Other IO errors and all certificate-content errors (PEM parse / OS store) are
744    // retained and can still fail the load below when nothing was added.
745    result.errors.retain(|err| match &err.kind {
746        rustls_native_certs::ErrorKind::Io { inner, path }
747            if matches!(
748                inner.kind(),
749                std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied
750            ) =>
751        {
752            debug!(
753                error = %inner,
754                path = %path.display(),
755                "Skipping missing or unreadable certificate source while loading platform root certificates."
756            );
757            false
758        }
759        _ => true,
760    });
761
762    // For whatever certificates we _did_ get back, try and add them to the root certificate store.
763    let (added, failed) = root_cert_store.add_parsable_certificates(result.certs);
764    if failed == 0 && added > 0 {
765        debug!(
766            "Added {} certificates from environment to the default root certificate store.",
767            added
768        );
769    } else if failed > 0 && added > 0 {
770        debug!("Added {} certificates from environment to the default root certificate store, but failed to add {} certificates.", added, failed);
771    } else {
772        // When we don't manage to add any certificates, it either means that:
773        // - we found no certificates to add
774        // - we hit an error when loading the certificates
775        // - we hit an error when trying to add the certificates to our root certificate store
776        //
777        // We only consider this operation to have truly failed if there were errors during the initial loading of the
778        // certificates.
779        if !result.errors.is_empty() {
780            let joined_errors = result
781                .errors
782                .iter()
783                .map(|e| e.to_string())
784                .collect::<Vec<_>>()
785                .join(", ");
786
787            return Err(generic_error!(
788                "Failed to load certificates from platform's native certificate store: {}",
789                joined_errors
790            ));
791        }
792    }
793
794    Ok(root_cert_store)
795}
796
797#[cfg(test)]
798mod tests {
799    use std::fs;
800    #[cfg(all(unix, not(feature = "fips")))]
801    use std::os::unix::fs::PermissionsExt;
802    use std::path::Path;
803    #[cfg(feature = "fips")]
804    use std::sync::{
805        atomic::{AtomicBool, Ordering},
806        Arc,
807    };
808
809    #[cfg(feature = "fips")]
810    use rustls::KeyLog;
811    use rustls::{
812        client::danger::ServerCertVerifier,
813        crypto::CryptoProvider,
814        pki_types::{ServerName, UnixTime},
815        ClientConfig, ProtocolVersion, RootCertStore, ServerConfig,
816    };
817
818    #[cfg(not(feature = "fips"))]
819    use super::build_nss_key_log_line;
820    #[cfg(all(unix, not(feature = "fips")))]
821    use super::open_key_log_file;
822    use super::test_util::SelfSignedCert;
823    use super::{
824        ensure_client_config_fips_compliant, ensure_server_config_fips_compliant, AcceptAllServerCertVerifier,
825        ClientTLSConfigBuilder, ServerTLSConfigBuilder, TlsMinimumVersion,
826    };
827
828    /// Builds a client TLS configuration that logs key material to `path`, initializing the default crypto provider
829    /// first so the configuration can be built. Every key-log test shares this setup.
830    fn client_config_with_key_log(path: &Path) -> ClientConfig {
831        let _ = super::initialize_default_crypto_provider();
832
833        ClientTLSConfigBuilder::new()
834            .with_root_cert_store(RootCertStore::empty())
835            .with_key_log_file(path)
836            .build()
837            .expect("client TLS config should build")
838    }
839
840    /// Creates a temporary directory and returns it (to keep it alive) alongside a path to `file_name` within it.
841    fn temp_file_path(file_name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
842        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
843        let path = tempdir.path().join(file_name);
844        (tempdir, path)
845    }
846
847    #[test]
848    fn sha256_matches_known_answer() {
849        // FIPS 180-2, appendix B.1.
850        let expected = [
851            0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, 0xb0, 0x03,
852            0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad,
853        ];
854        assert_eq!(super::sha256(b"abc"), expected);
855    }
856
857    #[test]
858    fn tls12_minimum_enables_tls12_and_tls13() {
859        let versions = TlsMinimumVersion::Tls12.protocol_versions();
860
861        assert_eq!(versions.len(), 2);
862        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
863        assert_eq!(versions[1].version, ProtocolVersion::TLSv1_2);
864    }
865
866    #[test]
867    fn tls13_minimum_enables_tls13_only() {
868        let versions = TlsMinimumVersion::Tls13.protocol_versions();
869
870        assert_eq!(versions.len(), 1);
871        assert_eq!(versions[0].version, ProtocolVersion::TLSv1_3);
872    }
873
874    #[test]
875    fn client_config_fips_validation_accepts_builder_config() {
876        let _ = super::initialize_default_crypto_provider();
877
878        let config = ClientTLSConfigBuilder::new()
879            .with_root_cert_store(RootCertStore::empty())
880            .build()
881            .expect("client TLS config should build");
882
883        ensure_client_config_fips_compliant(&config).expect("client TLS config should pass FIPS validation");
884    }
885
886    #[test]
887    fn accept_all_verifier_accepts_mismatched_server_certificate() {
888        // The accept-all verifier performs no validation by design, so it must accept even a self-signed certificate
889        // presented for a completely different server name — a case any real verifier would reject. This is the
890        // behavior that backs `ClientTLSConfigBuilder::danger_accept_invalid_certs`.
891        let _ = super::initialize_default_crypto_provider();
892        let provider = CryptoProvider::get_default()
893            .cloned()
894            .expect("default crypto provider should be installed");
895        let verifier = AcceptAllServerCertVerifier { provider };
896
897        let cert = SelfSignedCert::new(["localhost"]);
898        let cert_chain = cert.cert_chain();
899        let server_name = ServerName::try_from("totally.different.example").expect("server name should parse");
900
901        let result = verifier.verify_server_cert(&cert_chain[0], &[], &server_name, &[], UnixTime::now());
902
903        assert!(
904            result.is_ok(),
905            "accept-all verifier must accept a certificate presented for a mismatched server name"
906        );
907    }
908
909    #[test]
910    fn danger_accept_invalid_certs_builds_without_root_cert_store() {
911        let _ = super::initialize_default_crypto_provider();
912
913        // Without an explicit root cert store (and with no process-wide default initialized in this test), a normal
914        // build fails: there is nothing to verify server certificates against.
915        let missing_store_error = ClientTLSConfigBuilder::new()
916            .build()
917            .expect_err("client TLS config should fail to build without any root cert store");
918        assert!(
919            missing_store_error
920                .to_string()
921                .contains("root certificate store not initialized"),
922            "unexpected error: {missing_store_error}"
923        );
924
925        // Enabling the dangerous accept-all verifier removes the need for a root cert store entirely, so the same
926        // builder now succeeds.
927        ClientTLSConfigBuilder::new()
928            .danger_accept_invalid_certs()
929            .build()
930            .expect("client TLS config should build with an accept-all verifier and no root cert store");
931    }
932
933    #[test]
934    fn server_config_fips_validation_accepts_basic_server_config() {
935        let _ = super::initialize_default_crypto_provider();
936        let cert = SelfSignedCert::localhost();
937        let mut config = ServerConfig::builder()
938            .with_no_client_auth()
939            .with_single_cert(cert.cert_chain(), cert.private_key())
940            .expect("server TLS config should build");
941
942        ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
943    }
944
945    #[cfg(feature = "fips")]
946    #[test]
947    fn server_config_fips_validation_disables_secret_extraction() {
948        #[derive(Debug)]
949        struct TestKeyLog(Arc<AtomicBool>);
950
951        impl KeyLog for TestKeyLog {
952            fn log(&self, _label: &str, _client_random: &[u8], _secret: &[u8]) {
953                self.0.store(true, Ordering::Relaxed);
954            }
955        }
956
957        let _ = super::initialize_default_crypto_provider();
958        let cert = SelfSignedCert::localhost();
959        let key_log_used = Arc::new(AtomicBool::new(false));
960        let mut config = ServerConfig::builder()
961            .with_no_client_auth()
962            .with_single_cert(cert.cert_chain(), cert.private_key())
963            .expect("server TLS config should build");
964        config.key_log = Arc::new(TestKeyLog(Arc::clone(&key_log_used)));
965        config.enable_secret_extraction = true;
966
967        ensure_server_config_fips_compliant(&mut config).expect("server TLS config should pass FIPS validation");
968
969        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
970
971        assert!(!key_log_used.load(Ordering::Relaxed));
972        assert!(!config.enable_secret_extraction);
973    }
974
975    // The FIPS-compliance checks return an error when a configuration is not FIPS compliant. To exercise that
976    // documented error path in a FIPS build, the two tests below build configurations using the *unfiltered*
977    // aws-lc-rs provider, whose default cipher suites include non-FIPS-approved algorithms (e.g. ChaCha20), so
978    // `fips()` reports false even though the crate is compiled with FIPS support.
979    #[cfg(all(feature = "fips", not(windows)))]
980    #[test]
981    fn client_config_fips_validation_rejects_non_fips_config() {
982        let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
983        let config = ClientConfig::builder_with_provider(provider)
984            .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
985            .expect("client config builder should accept protocol versions")
986            .with_root_certificates(RootCertStore::empty())
987            .with_no_client_auth();
988
989        let error =
990            ensure_client_config_fips_compliant(&config).expect_err("a non-FIPS client configuration must be rejected");
991        assert!(
992            error.to_string().contains("not FIPS compliant"),
993            "unexpected error: {error}"
994        );
995    }
996
997    #[cfg(all(feature = "fips", not(windows)))]
998    #[test]
999    fn server_config_fips_validation_rejects_non_fips_config() {
1000        let cert = SelfSignedCert::localhost();
1001        let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1002        let mut config = ServerConfig::builder_with_provider(provider)
1003            .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
1004            .expect("server config builder should accept protocol versions")
1005            .with_no_client_auth()
1006            .with_single_cert(cert.cert_chain(), cert.private_key())
1007            .expect("server TLS config should build");
1008
1009        let error = ensure_server_config_fips_compliant(&mut config)
1010            .expect_err("a non-FIPS server configuration must be rejected");
1011        assert!(
1012            error.to_string().contains("not FIPS compliant"),
1013            "unexpected error: {error}"
1014        );
1015    }
1016
1017    #[test]
1018    #[cfg(not(feature = "fips"))]
1019    fn nss_key_log_lines_are_written_in_hex_format() {
1020        let output =
1021            build_nss_key_log_line("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]).expect("key log line should build");
1022
1023        assert_eq!(output, b"CLIENT_RANDOM abcd 0123\n");
1024    }
1025
1026    #[test]
1027    #[cfg(not(feature = "fips"))]
1028    fn client_config_uses_configured_key_log_file() {
1029        let (_tempdir, key_log_path) = temp_file_path("sslkeylogfile");
1030
1031        let config = client_config_with_key_log(&key_log_path);
1032        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1033
1034        let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
1035        assert_eq!(contents, "CLIENT_RANDOM abcd 0123\n");
1036    }
1037
1038    #[test]
1039    #[cfg(not(feature = "fips"))]
1040    fn client_config_ignores_unwritable_key_log_file() {
1041        // The key log path points into a nonexistent subdirectory, so the file can never be opened. Building the
1042        // configuration must still succeed, and no file should be created.
1043        let (_tempdir, key_log_path) = temp_file_path("missing/sslkeylogfile");
1044
1045        let config = client_config_with_key_log(&key_log_path);
1046        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1047
1048        assert!(!key_log_path.exists());
1049    }
1050
1051    #[test]
1052    #[cfg(not(feature = "fips"))]
1053    fn client_configs_append_to_shared_key_log_file() {
1054        let (_tempdir, key_log_path) = temp_file_path("shared-sslkeylogfile");
1055
1056        let first_config = client_config_with_key_log(&key_log_path);
1057        let second_config = client_config_with_key_log(&key_log_path);
1058
1059        first_config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1060        second_config.key_log.log("CLIENT_RANDOM", &[0xef, 0x01], &[0x45, 0x67]);
1061
1062        let contents = fs::read_to_string(&key_log_path).expect("key log file should be readable");
1063        assert_eq!(contents, "CLIENT_RANDOM abcd 0123\nCLIENT_RANDOM ef01 4567\n");
1064    }
1065
1066    #[cfg(all(unix, not(feature = "fips")))]
1067    #[test]
1068    fn key_log_file_is_created_with_owner_only_permissions() {
1069        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1070        let key_log_path = tempdir.path().join("sslkeylogfile");
1071
1072        let file = open_key_log_file(&key_log_path).expect("key log file should open");
1073        drop(file);
1074
1075        let mode = fs::metadata(&key_log_path)
1076            .expect("key log file metadata should be readable")
1077            .permissions()
1078            .mode()
1079            & 0o777;
1080        assert_eq!(mode, 0o600);
1081    }
1082
1083    #[cfg(windows)]
1084    #[test]
1085    fn windows_default_crypto_provider_builds_client_config() {
1086        let _ = super::initialize_default_crypto_provider();
1087
1088        ClientTLSConfigBuilder::new()
1089            .with_root_cert_store(RootCertStore::empty())
1090            .build()
1091            .expect("Windows CNG-backed TLS config should build");
1092    }
1093
1094    #[test]
1095    #[cfg(feature = "fips")]
1096    fn key_log_file_ignored_in_fips_mode() {
1097        // FIPS builds soft-skip TLS key logging instead of failing, so a leftover key log file path does not
1098        // prevent TLS client construction.
1099        let (_tempdir, key_log_path) = temp_file_path("fips-sslkeylogfile");
1100
1101        let config = client_config_with_key_log(&key_log_path);
1102
1103        // The default no-op `KeyLog` remains in place: invoking it must not panic and must not produce a file.
1104        config.key_log.log("CLIENT_RANDOM", &[0xab, 0xcd], &[0x01, 0x23]);
1105
1106        assert!(!key_log_path.exists(), "FIPS builds must not create a TLS key log file");
1107    }
1108
1109    #[test]
1110    fn server_tls_config_builder_loads_cert_and_key_files() {
1111        let _ = super::initialize_default_crypto_provider();
1112        let cert = SelfSignedCert::localhost();
1113        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1114        let cert_path = tempdir.path().join("cert.pem");
1115        let key_path = tempdir.path().join("key.pem");
1116        cert.write_cert_pem(&cert_path);
1117        cert.write_key_pem(&key_path);
1118
1119        ServerTLSConfigBuilder::new()
1120            .with_cert_file(&cert_path)
1121            .with_key_file(&key_path)
1122            .build()
1123            .expect("server TLS config should build from cert and key files");
1124    }
1125
1126    #[test]
1127    fn server_tls_config_builder_loads_ca_file_for_client_auth() {
1128        let _ = super::initialize_default_crypto_provider();
1129        let server_cert = SelfSignedCert::localhost();
1130        let ca_cert = SelfSignedCert::new(["test-ca"]);
1131        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1132        let cert_path = tempdir.path().join("server-cert.pem");
1133        let key_path = tempdir.path().join("server-key.pem");
1134        let ca_path = tempdir.path().join("ca.pem");
1135        server_cert.write_cert_pem(&cert_path);
1136        server_cert.write_key_pem(&key_path);
1137        ca_cert.write_cert_pem(&ca_path);
1138
1139        ServerTLSConfigBuilder::new()
1140            .with_cert_file(&cert_path)
1141            .with_key_file(&key_path)
1142            .with_ca_file(&ca_path)
1143            .build()
1144            .expect("server TLS config with CA should build");
1145    }
1146
1147    #[test]
1148    fn server_tls_config_builder_errors_without_cert_file() {
1149        let _ = super::initialize_default_crypto_provider();
1150
1151        let error = ServerTLSConfigBuilder::new()
1152            .with_key_file("/tmp/key.pem")
1153            .build()
1154            .expect_err("server TLS config should fail without a cert file");
1155
1156        assert!(
1157            error.to_string().contains("No certificate file"),
1158            "unexpected error: {error}"
1159        );
1160    }
1161
1162    #[test]
1163    fn server_tls_config_builder_errors_without_key_file() {
1164        let _ = super::initialize_default_crypto_provider();
1165
1166        let error = ServerTLSConfigBuilder::new()
1167            .with_cert_file("/tmp/cert.pem")
1168            .build()
1169            .expect_err("server TLS config should fail without a key file");
1170
1171        assert!(
1172            error.to_string().contains("No private key file"),
1173            "unexpected error: {error}"
1174        );
1175    }
1176
1177    #[test]
1178    fn server_tls_config_builder_errors_on_unreadable_cert_file() {
1179        let _ = super::initialize_default_crypto_provider();
1180        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1181        let key_path = tempdir.path().join("key.pem");
1182        let cert_path = tempdir.path().join("nonexistent.pem");
1183        let cert = SelfSignedCert::localhost();
1184        cert.write_key_pem(&key_path);
1185
1186        let error = ServerTLSConfigBuilder::new()
1187            .with_cert_file(&cert_path)
1188            .with_key_file(&key_path)
1189            .build()
1190            .expect_err("server TLS config should fail with unreadable cert file");
1191
1192        assert!(
1193            error.to_string().contains("Failed to read certificate file"),
1194            "unexpected error: {error}"
1195        );
1196    }
1197
1198    #[test]
1199    fn server_tls_config_builder_errors_on_non_pem_cert_file() {
1200        let _ = super::initialize_default_crypto_provider();
1201        let tempdir = tempfile::tempdir().expect("temporary directory should be created");
1202        let cert_path = tempdir.path().join("cert.pem");
1203        let key_path = tempdir.path().join("key.pem");
1204        // Write garbage that is not valid PEM.
1205        fs::write(&cert_path, "this is not a certificate").expect("should write garbage file");
1206        let cert = SelfSignedCert::localhost();
1207        cert.write_key_pem(&key_path);
1208
1209        let error = ServerTLSConfigBuilder::new()
1210            .with_cert_file(&cert_path)
1211            .with_key_file(&key_path)
1212            .build()
1213            .expect_err("server TLS config should fail with non-PEM cert file");
1214
1215        assert!(
1216            error.to_string().contains("No PEM-encoded certificates found"),
1217            "unexpected error: {error}"
1218        );
1219    }
1220}