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