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