Skip to main content

noxu_rep/
tls.rs

1//! TLS configuration for Noxu DB replication channels.
2//!
3//! Noxu DB replication traffic can be encrypted using one of two TLS backends:
4//!
5//! | Feature | Backend | Dependencies |
6//! |---------|---------|--------------|
7//! | `tls-rustls` (default) | [rustls](https://github.com/rustls/rustls) | Pure Rust, no C |
8//! | `tls-native` | [native-tls](https://github.com/sfackler/rust-native-tls) | System OpenSSL or LibreSSL |
9//!
10//! ## Why not quiche?
11//!
12//! [quiche](https://github.com/cloudflare/quiche) is Cloudflare's QUIC
13//! implementation, written in C with Rust FFI bindings. It requires BoringSSL
14//! and introduces `unsafe` FFI into the dependency tree.
15//!
16//! Noxu DB targets zero `unsafe` in its core and prefers pure-Rust
17//! dependencies. [quinn](https://github.com/quinn-rs/quinn) provides the same
18//! RFC 9000 QUIC semantics (including 0-RTT, unreliable datagrams, per-stream
19//! flow control) using only safe Rust and `rustls` for TLS — exactly what
20//! Noxu DB needs.
21//!
22//! ## Encryption status
23//!
24//! - **QUIC channels**: Always encrypted. QUIC mandates TLS 1.3 (RFC 9001).
25//!   The default configuration uses a runtime-generated self-signed certificate
26//!   suitable for trusted private networks. For production deployments supply
27//!   a [`TlsConfig`] via the `connect_with_config` / `with_server_config`
28//!   constructors on the QUIC channel types.
29//!
30//! - **TCP channels**: Unencrypted by default (`TcpChannel`). Use
31//!   `TlsTcpChannel` (in `crate::net::channel`) for encrypted TCP
32//!   connections. Enable at least one TLS feature (`tls-rustls` or
33//!   `tls-native`) to make those types available.
34//!
35//! ## Quick start
36//!
37//! ```ignore
38//! // Internal cluster with self-signed certs (no external CA required):
39//! let tls = TlsConfig::self_signed("my-node.internal");
40//!
41//! // Production with PEM files (tls-rustls backend):
42//! let tls = TlsConfig::from_pem_files(
43//!     "/etc/noxu/cert.pem",
44//!     "/etc/noxu/key.pem",
45//!     "/etc/noxu/ca.pem",
46//!     "my-node.internal",
47//! );
48//! ```
49
50#[cfg(any(feature = "tls-rustls", feature = "tls-native"))]
51use crate::error::{RepError, Result};
52
53// ─── TlsIdentity ─────────────────────────────────────────────────────────────
54
55/// Certificate and private key material that identifies this replication node.
56///
57/// ## Backend compatibility
58///
59/// | Variant | `tls-rustls` | `tls-native` |
60/// |---------|:------------:|:------------:|
61/// | `SelfSigned` | ✓ | ✗ |
62/// | `PemFiles` | ✓ | ✗ |
63/// | `PemBytes` | ✓ | ✗ |
64/// | `Pkcs12` | ✗ | ✓ |
65///
66/// For `tls-native`, create a PKCS #12 archive with:
67/// ```sh
68/// openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem
69/// ```
70#[derive(Clone)]
71#[non_exhaustive]
72pub enum TlsIdentity {
73    /// Generate a fresh self-signed certificate at runtime.
74    ///
75    /// Supported by the `tls-rustls` backend only.  Suitable for internal,
76    /// trusted replication networks where setting up a certificate authority
77    /// is undesirable.
78    SelfSigned {
79        /// Subject Alternative Names for the certificate (e.g. DNS hostnames
80        /// or IP addresses for this node).
81        subject_alt_names: Vec<String>,
82    },
83
84    /// Load certificate chain and private key from PEM files on disk.
85    ///
86    /// Supported by the `tls-rustls` backend only.
87    PemFiles {
88        /// Path to a PEM-encoded certificate chain.
89        cert: std::path::PathBuf,
90        /// Path to a PEM-encoded private key (PKCS #8 or PKCS #1 RSA).
91        key: std::path::PathBuf,
92    },
93
94    /// Certificate chain and private key as in-memory PEM bytes.
95    ///
96    /// Supported by the `tls-rustls` backend only.
97    PemBytes {
98        /// PEM-encoded certificate chain bytes.
99        cert: Vec<u8>,
100        /// PEM-encoded private key bytes.
101        key: Vec<u8>,
102    },
103
104    /// PKCS #12 archive (certificate + key bundled) as DER bytes.
105    ///
106    /// Supported by the `tls-native` backend only (OpenSSL / LibreSSL).
107    /// Load with:
108    /// ```ignore
109    /// let der = std::fs::read("/etc/noxu/identity.p12")?;
110    /// let identity = TlsIdentity::Pkcs12 { der, password: "secret".into() };
111    /// ```
112    Pkcs12 {
113        /// DER-encoded PKCS #12 archive.
114        der: Vec<u8>,
115        /// Password used to decrypt the archive.
116        password: String,
117    },
118}
119
120// ─── TrustedCerts ────────────────────────────────────────────────────────────
121
122/// Policy for verifying the remote peer's certificate.
123#[derive(Clone)]
124#[non_exhaustive]
125pub enum TrustedCerts {
126    /// Accept any certificate without verification.
127    ///
128    /// **Insecure.** Use only on private, trusted networks where all nodes
129    /// are implicitly trusted (authenticated at the Paxos / VLSN layer).
130    SkipVerification,
131
132    /// Trust CA certificates loaded from PEM files on disk.
133    CaFiles(Vec<std::path::PathBuf>),
134
135    /// Trust in-memory PEM-encoded CA certificates.
136    CaBytes(Vec<Vec<u8>>),
137}
138
139// ─── TlsConfig ───────────────────────────────────────────────────────────────
140
141/// TLS configuration for Noxu DB replication channels.
142///
143/// A `TlsConfig` bundles this node's identity (certificate + key) with the
144/// policy for verifying remote peers.  Pass it to:
145///
146/// - `TlsTcpChannelListener::bind_with_tls` — encrypted TCP server
147/// - `TlsTcpChannel::connect_with_tls` — encrypted TCP client
148/// - `TlsConfig::to_quinn_server_config` — QUIC server with real certs
149/// - `TlsConfig::to_quinn_client_config` — QUIC client with real certs
150#[derive(Clone)]
151pub struct TlsConfig {
152    /// This node's certificate and private key.
153    pub identity: TlsIdentity,
154    /// How to verify the remote peer's certificate.
155    pub trusted_certs: TrustedCerts,
156    /// TLS SNI server name used by the client during the handshake.
157    ///
158    /// Must match the certificate's `Common Name` or a `Subject Alternative
159    /// Name`.  Use `"localhost"` when connecting to a `SelfSigned` cert with
160    /// `subject_alt_names = ["localhost"]`.
161    pub server_name: String,
162}
163
164impl TlsConfig {
165    /// Create an insecure TLS configuration for trusted private networks.
166    ///
167    /// Generates a self-signed certificate at first use and skips peer
168    /// certificate verification entirely.  Equivalent to the current default
169    /// QUIC channel behaviour.
170    ///
171    /// Requires the `tls-rustls` feature.
172    pub fn insecure(server_name: impl Into<String>) -> Self {
173        TlsConfig {
174            identity: TlsIdentity::SelfSigned {
175                subject_alt_names: vec!["localhost".into()],
176            },
177            trusted_certs: TrustedCerts::SkipVerification,
178            server_name: server_name.into(),
179        }
180    }
181
182    /// Create a TLS configuration using PEM cert/key files and a CA file.
183    ///
184    /// Verifies the remote peer's certificate against `ca`.
185    /// Requires the `tls-rustls` feature.
186    pub fn from_pem_files(
187        cert: impl Into<std::path::PathBuf>,
188        key: impl Into<std::path::PathBuf>,
189        ca: impl Into<std::path::PathBuf>,
190        server_name: impl Into<String>,
191    ) -> Self {
192        TlsConfig {
193            identity: TlsIdentity::PemFiles {
194                cert: cert.into(),
195                key: key.into(),
196            },
197            trusted_certs: TrustedCerts::CaFiles(vec![ca.into()]),
198            server_name: server_name.into(),
199        }
200    }
201
202    /// Create a TLS configuration from a PKCS #12 archive.
203    ///
204    /// Verifies the remote peer against `ca_pem` bytes.
205    /// Requires the `tls-native` feature.
206    pub fn from_pkcs12(
207        der: Vec<u8>,
208        password: impl Into<String>,
209        ca_pem: Vec<u8>,
210        server_name: impl Into<String>,
211    ) -> Self {
212        TlsConfig {
213            identity: TlsIdentity::Pkcs12 { der, password: password.into() },
214            trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
215            server_name: server_name.into(),
216        }
217    }
218}
219
220// ─── rustls helpers ──────────────────────────────────────────────────────────
221
222#[cfg(feature = "tls-rustls")]
223impl TlsConfig {
224    /// Build a `rustls::ServerConfig` from this configuration.
225    ///
226    /// Used by [`TlsTcpChannelListener`] and the QUIC server path.
227    pub(crate) fn to_rustls_server_config(
228        &self,
229    ) -> Result<std::sync::Arc<rustls::ServerConfig>> {
230        let (certs, key) = self.rustls_cert_and_key()?;
231
232        let cfg = rustls::ServerConfig::builder()
233            .with_no_client_auth()
234            .with_single_cert(certs, key)
235            .map_err(|e| {
236                RepError::NetworkError(format!("TLS server config: {e}"))
237            })?;
238        Ok(std::sync::Arc::new(cfg))
239    }
240
241    /// Build a `rustls::ClientConfig` from this configuration.
242    ///
243    /// Used by [`TlsTcpChannel`] and the QUIC client path.
244    pub(crate) fn to_rustls_client_config(
245        &self,
246    ) -> Result<std::sync::Arc<rustls::ClientConfig>> {
247        if matches!(&self.trusted_certs, TrustedCerts::SkipVerification) {
248            let cfg = rustls::ClientConfig::builder()
249                .dangerous()
250                .with_custom_certificate_verifier(std::sync::Arc::new(
251                    SkipCertVerification::new(),
252                ))
253                .with_no_client_auth();
254            return Ok(std::sync::Arc::new(cfg));
255        }
256
257        let root_store = self.rustls_root_store()?;
258        let cfg = rustls::ClientConfig::builder()
259            .with_root_certificates(root_store)
260            .with_no_client_auth();
261        Ok(std::sync::Arc::new(cfg))
262    }
263
264    /// Build a `quinn::ServerConfig` backed by this `TlsConfig`.
265    ///
266    /// Replaces the default self-signed / skip-verify server config for
267    /// production deployments that bring their own certificates.
268    ///
269    /// # Example
270    /// ```ignore
271    /// let tls = TlsConfig::from_pem_files("cert.pem", "key.pem", "ca.pem", "node1");
272    /// let server_cfg = tls.to_quinn_server_config()?;
273    /// let listener = QuicMultiplexedChannelListener::with_server_config(addr, server_cfg)?;
274    /// ```
275    #[cfg(feature = "quic")]
276    pub fn to_quinn_server_config(&self) -> Result<quinn::ServerConfig> {
277        let rustls_cfg = self.to_rustls_server_config()?;
278        let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(
279            rustls::ServerConfig::clone(&rustls_cfg),
280        )
281        .map_err(|e| {
282            RepError::NetworkError(format!("QUIC server config: {e}"))
283        })?;
284        let mut cfg =
285            quinn::ServerConfig::with_crypto(std::sync::Arc::new(quic_cfg));
286        let mut transport = quinn::TransportConfig::default();
287        transport.mtu_discovery_config(None);
288        transport.datagram_receive_buffer_size(Some(64 * 1024));
289        cfg.transport_config(std::sync::Arc::new(transport));
290        Ok(cfg)
291    }
292
293    /// Build a `quinn::ClientConfig` backed by this `TlsConfig`.
294    ///
295    /// Replaces the default skip-verify client config for production
296    /// deployments that verify server certificates against a CA.
297    #[cfg(feature = "quic")]
298    pub fn to_quinn_client_config(&self) -> Result<quinn::ClientConfig> {
299        let rustls_cfg = self.to_rustls_client_config()?;
300        let quic_cfg = quinn::crypto::rustls::QuicClientConfig::try_from(
301            rustls::ClientConfig::clone(&rustls_cfg),
302        )
303        .map_err(|e| {
304            RepError::NetworkError(format!("QUIC client config: {e}"))
305        })?;
306        let mut cfg = quinn::ClientConfig::new(std::sync::Arc::new(quic_cfg));
307        let mut transport = quinn::TransportConfig::default();
308        transport.mtu_discovery_config(None);
309        transport.datagram_receive_buffer_size(Some(64 * 1024));
310        cfg.transport_config(std::sync::Arc::new(transport));
311        Ok(cfg)
312    }
313
314    // ── Private helpers ──────────────────────────────────────────────────
315
316    fn rustls_cert_and_key(
317        &self,
318    ) -> Result<(
319        Vec<rustls::pki_types::CertificateDer<'static>>,
320        rustls::pki_types::PrivateKeyDer<'static>,
321    )> {
322        use rustls::pki_types::{
323            CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer,
324        };
325
326        match &self.identity {
327            TlsIdentity::SelfSigned { subject_alt_names } => {
328                let ck = rcgen::generate_simple_self_signed(
329                    subject_alt_names.clone(),
330                )
331                .map_err(|e| RepError::NetworkError(format!("rcgen: {e}")))?;
332                let cert = CertificateDer::from(ck.cert.der().to_vec());
333                let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
334                    ck.key_pair.serialize_der(),
335                ));
336                Ok((vec![cert], key))
337            }
338            TlsIdentity::PemFiles { cert, key } => {
339                let cert_bytes = std::fs::read(cert).map_err(|e| {
340                    RepError::NetworkError(format!("cert file: {e}"))
341                })?;
342                let key_bytes = std::fs::read(key).map_err(|e| {
343                    RepError::NetworkError(format!("key file: {e}"))
344                })?;
345                Self::parse_pem_cert_and_key(&cert_bytes, &key_bytes)
346            }
347            TlsIdentity::PemBytes { cert, key } => {
348                Self::parse_pem_cert_and_key(cert, key)
349            }
350            TlsIdentity::Pkcs12 { .. } => Err(RepError::NetworkError(
351                "Pkcs12 identity is not supported by the tls-rustls backend; \
352                 use PemFiles or PemBytes instead"
353                    .into(),
354            )),
355        }
356    }
357
358    fn parse_pem_cert_and_key(
359        cert_pem: &[u8],
360        key_pem: &[u8],
361    ) -> Result<(
362        Vec<rustls::pki_types::CertificateDer<'static>>,
363        rustls::pki_types::PrivateKeyDer<'static>,
364    )> {
365        use rustls_pemfile::{certs, private_key};
366        use std::io::BufReader;
367
368        let cert_chain: Vec<_> = certs(&mut BufReader::new(cert_pem))
369            .collect::<std::result::Result<_, _>>()
370            .map_err(|e| RepError::NetworkError(format!("cert parse: {e}")))?;
371        if cert_chain.is_empty() {
372            return Err(RepError::NetworkError(
373                "no certificates found in PEM".into(),
374            ));
375        }
376
377        let key = private_key(&mut BufReader::new(key_pem))
378            .map_err(|e| RepError::NetworkError(format!("key parse: {e}")))?
379            .ok_or_else(|| {
380                RepError::NetworkError("no private key found in PEM".into())
381            })?;
382
383        Ok((cert_chain, key))
384    }
385
386    fn rustls_root_store(&self) -> Result<rustls::RootCertStore> {
387        use rustls_pemfile::certs;
388        use std::io::BufReader;
389
390        let mut store = rustls::RootCertStore::empty();
391
392        match &self.trusted_certs {
393            TrustedCerts::SkipVerification => {
394                // For skip-verification the root store is unused; the caller
395                // must install a custom verifier (as quic_channel.rs does).
396                // For TCP TLS we return an empty store here and the TlsTcpChannel
397                // will install SkipCertVerification when this variant is set.
398            }
399            TrustedCerts::CaFiles(paths) => {
400                // TLS-2: an empty CaFiles list is a misconfiguration. It
401                // would silently produce an empty trust store, which validates
402                // nothing without the explicit `SkipVerification` opt-out.
403                if paths.is_empty() {
404                    return Err(RepError::ConfigError(
405                        "TrustedCerts::CaFiles configured with no paths; \
406                         this is a misconfiguration. Use \
407                         TrustedCerts::SkipVerification to explicitly opt \
408                         out of CA verification."
409                            .into(),
410                    ));
411                }
412                for path in paths {
413                    let pem = std::fs::read(path).map_err(|e| {
414                        RepError::NetworkError(format!("CA file: {e}"))
415                    })?;
416                    let parsed: Vec<_> =
417                        certs(&mut BufReader::new(pem.as_slice()))
418                            .collect::<std::result::Result<Vec<_>, _>>()
419                            .map_err(|e| {
420                                RepError::NetworkError(format!("CA parse: {e}"))
421                            })?;
422                    // TLS-3: rustls_pemfile silently skips non-cert PEM blocks
423                    // (and any non-PEM bytes). If the file had bytes but
424                    // produced zero certificates, treat that as a parse error
425                    // rather than silently building an empty trust store.
426                    if !pem.is_empty() && parsed.is_empty() {
427                        return Err(RepError::ConfigError(format!(
428                            "CA file {} parsed but contained 0 certificates",
429                            path.display()
430                        )));
431                    }
432                    for cert in parsed {
433                        store.add(cert).map_err(|e| {
434                            RepError::NetworkError(format!("CA add: {e}"))
435                        })?;
436                    }
437                }
438            }
439            TrustedCerts::CaBytes(pems) => {
440                // TLS-2: empty CaBytes list — same misconfiguration as
441                // empty CaFiles. Reject explicitly.
442                if pems.is_empty() {
443                    return Err(RepError::ConfigError(
444                        "TrustedCerts::CaBytes configured with no PEM blobs; \
445                         this is a misconfiguration. Use \
446                         TrustedCerts::SkipVerification to explicitly opt \
447                         out of CA verification."
448                            .into(),
449                    ));
450                }
451                for (idx, pem) in pems.iter().enumerate() {
452                    let parsed: Vec<_> =
453                        certs(&mut BufReader::new(pem.as_slice()))
454                            .collect::<std::result::Result<Vec<_>, _>>()
455                            .map_err(|e| {
456                                RepError::NetworkError(format!("CA parse: {e}"))
457                            })?;
458                    // TLS-3: bytes provided but no certs decoded.
459                    if !pem.is_empty() && parsed.is_empty() {
460                        return Err(RepError::ConfigError(format!(
461                            "CA bytes (index {idx}) parsed but contained 0 \
462                             certificates"
463                        )));
464                    }
465                    for cert in parsed {
466                        store.add(cert).map_err(|e| {
467                            RepError::NetworkError(format!("CA add: {e}"))
468                        })?;
469                    }
470                }
471            }
472        }
473
474        Ok(store)
475    }
476}
477
478// ─── SkipCertVerification ────────────────────────────────────────────────────
479
480/// A `rustls` `ServerCertVerifier` that accepts any certificate without chain
481/// validation.
482///
483/// Suitable for internal, trusted replication networks where nodes are
484/// implicitly trusted (authenticated at the Paxos / VLSN layer).
485#[cfg(feature = "tls-rustls")]
486#[derive(Debug)]
487pub(crate) struct SkipCertVerification(
488    std::sync::Arc<rustls::crypto::CryptoProvider>,
489);
490
491#[cfg(feature = "tls-rustls")]
492impl SkipCertVerification {
493    pub(crate) fn new() -> Self {
494        Self(std::sync::Arc::new(rustls::crypto::ring::default_provider()))
495    }
496}
497
498#[cfg(feature = "tls-rustls")]
499impl rustls::client::danger::ServerCertVerifier for SkipCertVerification {
500    fn verify_server_cert(
501        &self,
502        _end_entity: &rustls::pki_types::CertificateDer<'_>,
503        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
504        _server_name: &rustls::pki_types::ServerName<'_>,
505        _ocsp_response: &[u8],
506        _now: rustls::pki_types::UnixTime,
507    ) -> std::result::Result<
508        rustls::client::danger::ServerCertVerified,
509        rustls::Error,
510    > {
511        Ok(rustls::client::danger::ServerCertVerified::assertion())
512    }
513
514    fn verify_tls12_signature(
515        &self,
516        message: &[u8],
517        cert: &rustls::pki_types::CertificateDer<'_>,
518        dss: &rustls::DigitallySignedStruct,
519    ) -> std::result::Result<
520        rustls::client::danger::HandshakeSignatureValid,
521        rustls::Error,
522    > {
523        rustls::crypto::verify_tls12_signature(
524            message,
525            cert,
526            dss,
527            &self.0.signature_verification_algorithms,
528        )
529    }
530
531    fn verify_tls13_signature(
532        &self,
533        message: &[u8],
534        cert: &rustls::pki_types::CertificateDer<'_>,
535        dss: &rustls::DigitallySignedStruct,
536    ) -> std::result::Result<
537        rustls::client::danger::HandshakeSignatureValid,
538        rustls::Error,
539    > {
540        rustls::crypto::verify_tls13_signature(
541            message,
542            cert,
543            dss,
544            &self.0.signature_verification_algorithms,
545        )
546    }
547
548    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
549        self.0.signature_verification_algorithms.supported_schemes()
550    }
551}
552
553// ─── native-tls helpers ──────────────────────────────────────────────────────
554
555#[cfg(feature = "tls-native")]
556impl TlsConfig {
557    /// Build a `native_tls::TlsAcceptor` for TCP server use.
558    ///
559    /// Requires `TlsIdentity::Pkcs12`.  Create a PKCS #12 archive with:
560    /// ```sh
561    /// openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem
562    /// ```
563    pub(crate) fn to_native_acceptor(&self) -> Result<native_tls::TlsAcceptor> {
564        // TLS-4: `native_tls::TlsAcceptorBuilder` exposes no CA-root or
565        // accept-invalid-client-cert knobs, so any non-empty CA list on
566        // this server transport is an mTLS configuration the runtime
567        // cannot honour. A `log::warn!` here is not a security boundary;
568        // proceeding silently would build an acceptor with NO client-cert
569        // verification despite the operator's expressed intent. Refuse
570        // up front (before identity parsing) so the misconfiguration is
571        // loud and surfaces independently of any identity-format errors.
572        let mtls_intent = match &self.trusted_certs {
573            TrustedCerts::CaFiles(v) => !v.is_empty(),
574            TrustedCerts::CaBytes(v) => !v.is_empty(),
575            TrustedCerts::SkipVerification => false,
576        };
577        if mtls_intent {
578            return Err(RepError::ConfigError(
579                "mTLS is configured (TrustedCerts has CA roots) but the \
580                 tls-native server transport does not support it: \
581                 native_tls::TlsAcceptorBuilder exposes no client-cert \
582                 verification knobs. Use the tls-rustls feature for mTLS, \
583                 or set TrustedCerts::SkipVerification on this transport."
584                    .into(),
585            ));
586        }
587        let identity = self.native_identity()?;
588        let builder = native_tls::TlsAcceptor::builder(identity);
589        builder
590            .build()
591            .map_err(|e| RepError::NetworkError(format!("TLS acceptor: {e}")))
592    }
593
594    /// Build a `native_tls::TlsConnector` for TCP client use.
595    pub(crate) fn to_native_connector(
596        &self,
597    ) -> Result<native_tls::TlsConnector> {
598        let mut builder = native_tls::TlsConnector::builder();
599
600        // Install identity if present (optional for client-only auth).
601        if !matches!(&self.identity, TlsIdentity::SelfSigned { .. }) {
602            let id = self.native_identity()?;
603            builder.identity(id);
604        }
605
606        self.apply_native_trust(&mut builder)?;
607        builder
608            .build()
609            .map_err(|e| RepError::NetworkError(format!("TLS connector: {e}")))
610    }
611
612    fn native_identity(&self) -> Result<native_tls::Identity> {
613        match &self.identity {
614            TlsIdentity::Pkcs12 { der, password } => native_tls::Identity::from_pkcs12(der, password)
615                .map_err(|e| RepError::NetworkError(format!("PKCS12 identity: {e}"))),
616            TlsIdentity::SelfSigned { .. } => Err(RepError::NetworkError(
617                "SelfSigned identity is not supported by the tls-native backend; \
618                 use the tls-rustls feature instead, or supply a Pkcs12 identity"
619                    .into(),
620            )),
621            TlsIdentity::PemFiles { .. } | TlsIdentity::PemBytes { .. } => {
622                Err(RepError::NetworkError(
623                    "PEM identities are not supported by the tls-native backend; \
624                     convert to PKCS12 with: openssl pkcs12 -export -out id.p12 \
625                     -inkey key.pem -in cert.pem"
626                        .into(),
627                ))
628            }
629        }
630    }
631
632    fn apply_native_trust(
633        &self,
634        builder: &mut native_tls::TlsConnectorBuilder,
635    ) -> Result<()> {
636        match &self.trusted_certs {
637            TrustedCerts::SkipVerification => {
638                builder.danger_accept_invalid_certs(true);
639            }
640            TrustedCerts::CaFiles(paths) => {
641                for path in paths {
642                    let pem = std::fs::read(path).map_err(|e| {
643                        RepError::NetworkError(format!("CA file: {e}"))
644                    })?;
645                    let cert = native_tls::Certificate::from_pem(&pem)
646                        .map_err(|e| {
647                            RepError::NetworkError(format!("CA parse: {e}"))
648                        })?;
649                    builder.add_root_certificate(cert);
650                }
651            }
652            TrustedCerts::CaBytes(pems) => {
653                for pem in pems {
654                    let cert = native_tls::Certificate::from_pem(pem).map_err(
655                        |e| RepError::NetworkError(format!("CA parse: {e}")),
656                    )?;
657                    builder.add_root_certificate(cert);
658                }
659            }
660        }
661        Ok(())
662    }
663}
664
665// `apply_native_trust` is a `TlsConnectorBuilder`-only helper. The
666// previous shared trait `NativeTlsBuilderExt` was removed because the
667// `native_tls::TlsAcceptorBuilder` does not expose `add_root_certificate`
668// or `danger_accept_invalid_certs`, and the trait impls for it were
669// unconditionally recursive (a real bug — they would have stack-
670// overflowed on first call).
671
672// ─── tests ───────────────────────────────────────────────────────────────────
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    // ── Constructors (no feature gate; pure data shape) ──────────────
679
680    #[test]
681    fn insecure_constructor_uses_self_signed_localhost() {
682        let cfg = TlsConfig::insecure("node-a");
683        assert_eq!(cfg.server_name, "node-a");
684        match cfg.identity {
685            TlsIdentity::SelfSigned { subject_alt_names } => {
686                assert_eq!(subject_alt_names, vec!["localhost".to_string()]);
687            }
688            _ => panic!("insecure should produce SelfSigned identity"),
689        }
690        assert!(matches!(cfg.trusted_certs, TrustedCerts::SkipVerification));
691    }
692
693    #[test]
694    fn from_pem_files_constructor_records_paths() {
695        let cfg = TlsConfig::from_pem_files(
696            "/tmp/cert.pem",
697            "/tmp/key.pem",
698            "/tmp/ca.pem",
699            "node-b",
700        );
701        assert_eq!(cfg.server_name, "node-b");
702        match cfg.identity {
703            TlsIdentity::PemFiles { cert, key } => {
704                assert_eq!(cert, std::path::PathBuf::from("/tmp/cert.pem"));
705                assert_eq!(key, std::path::PathBuf::from("/tmp/key.pem"));
706            }
707            _ => panic!("from_pem_files should produce PemFiles identity"),
708        }
709        match cfg.trusted_certs {
710            TrustedCerts::CaFiles(paths) => {
711                assert_eq!(
712                    paths,
713                    vec![std::path::PathBuf::from("/tmp/ca.pem")]
714                );
715            }
716            _ => panic!("from_pem_files should produce CaFiles trust"),
717        }
718    }
719
720    #[test]
721    fn from_pkcs12_constructor_holds_bytes_and_password() {
722        let der = vec![0x30, 0x82, 0x00, 0x10]; // dummy DER prefix
723        let ca_pem = b"-----BEGIN CERTIFICATE-----\n".to_vec();
724        let cfg = TlsConfig::from_pkcs12(
725            der.clone(),
726            "secret".to_string(),
727            ca_pem.clone(),
728            "node-c",
729        );
730        assert_eq!(cfg.server_name, "node-c");
731        match cfg.identity {
732            TlsIdentity::Pkcs12 { der: d, password } => {
733                assert_eq!(d, der);
734                assert_eq!(password, "secret");
735            }
736            _ => panic!("from_pkcs12 should produce Pkcs12 identity"),
737        }
738        match cfg.trusted_certs {
739            TrustedCerts::CaBytes(pems) => {
740                assert_eq!(pems, vec![ca_pem]);
741            }
742            _ => panic!("from_pkcs12 should produce CaBytes trust"),
743        }
744    }
745
746    // ── rustls path (requires tls-rustls feature) ────────────────────
747
748    #[cfg(feature = "tls-rustls")]
749    #[test]
750    fn rustls_server_config_from_self_signed_succeeds() {
751        // SelfSigned + SkipVerification is the "insecure" config; the
752        // rustls server side generates a fresh self-signed cert at
753        // build time. Should produce a valid ServerConfig.
754        let cfg = TlsConfig::insecure("node-self");
755        let sc = cfg.to_rustls_server_config();
756        assert!(
757            sc.is_ok(),
758            "to_rustls_server_config from insecure() should succeed: {:?}",
759            sc.err()
760        );
761    }
762
763    #[cfg(feature = "tls-rustls")]
764    #[test]
765    fn rustls_client_config_skip_verification_succeeds() {
766        // SkipVerification is the trust mode for development clusters;
767        // the client config should be built using the
768        // SkipCertVerification verifier.
769        let cfg = TlsConfig::insecure("any-name");
770        let cc = cfg.to_rustls_client_config();
771        assert!(
772            cc.is_ok(),
773            "to_rustls_client_config with SkipVerification should succeed: \
774             {:?}",
775            cc.err()
776        );
777    }
778
779    #[cfg(feature = "tls-rustls")]
780    #[test]
781    fn rustls_client_config_with_empty_ca_bytes_errors() {
782        // TLS-2: empty CaBytes is a misconfiguration. Without an explicit
783        // SkipVerification opt-out, an empty trust store would validate
784        // nothing — refuse at config-build time.
785        let cfg = TlsConfig {
786            identity: TlsIdentity::SelfSigned {
787                subject_alt_names: vec!["localhost".into()],
788            },
789            trusted_certs: TrustedCerts::CaBytes(vec![]),
790            server_name: "x".into(),
791        };
792        let cc = cfg.to_rustls_client_config();
793        assert!(
794            cc.is_err(),
795            "empty CaBytes must be a misconfiguration error, got Ok"
796        );
797        let msg = format!("{}", cc.err().unwrap());
798        assert!(
799            msg.contains("CaBytes") && msg.contains("misconfiguration"),
800            "error should mention CaBytes/misconfiguration, got: {msg}"
801        );
802    }
803
804    #[cfg(feature = "tls-rustls")]
805    #[test]
806    fn rustls_client_config_with_malformed_ca_bytes_errors() {
807        // TLS-3: bytes were provided but rustls_pemfile produced 0 certs.
808        // Refuse rather than silently build an empty trust store.
809        let cfg = TlsConfig {
810            identity: TlsIdentity::SelfSigned {
811                subject_alt_names: vec!["localhost".into()],
812            },
813            trusted_certs: TrustedCerts::CaBytes(vec![b"not-a-pem".to_vec()]),
814            server_name: "x".into(),
815        };
816        let cc = cfg.to_rustls_client_config();
817        assert!(
818            cc.is_err(),
819            "malformed CaBytes must error rather than build an empty store, \
820             got Ok"
821        );
822        let msg = format!("{}", cc.err().unwrap());
823        assert!(
824            msg.contains("0 certificates"),
825            "error should mention 0 certificates, got: {msg}"
826        );
827    }
828
829    #[cfg(feature = "tls-rustls")]
830    #[test]
831    fn skip_cert_verification_returns_ok_for_any_cert() {
832        use rustls::client::danger::ServerCertVerifier;
833        let v = SkipCertVerification::new();
834        let cert = rustls::pki_types::CertificateDer::from(vec![0u8; 8]);
835        let server_name =
836            rustls::pki_types::ServerName::try_from("localhost").unwrap();
837        let now = rustls::pki_types::UnixTime::now();
838        let r = v.verify_server_cert(&cert, &[], &server_name, &[], now);
839        assert!(r.is_ok(), "SkipCertVerification must return Ok for any cert");
840    }
841
842    #[cfg(feature = "tls-rustls")]
843    #[test]
844    fn skip_cert_verification_supports_some_schemes() {
845        use rustls::client::danger::ServerCertVerifier;
846        let v = SkipCertVerification::new();
847        let schemes = v.supported_verify_schemes();
848        assert!(
849            !schemes.is_empty(),
850            "SkipCertVerification must report at least one signature scheme"
851        );
852    }
853
854    // ── native-tls path (requires tls-native feature) ────────────────
855
856    #[cfg(feature = "tls-native")]
857    #[test]
858    fn native_acceptor_requires_pkcs12_identity() {
859        // SelfSigned identity is rejected because native_tls cannot
860        // generate certs at runtime (only Pkcs12 is supported).
861        let cfg = TlsConfig {
862            identity: TlsIdentity::SelfSigned {
863                subject_alt_names: vec!["localhost".into()],
864            },
865            trusted_certs: TrustedCerts::SkipVerification,
866            server_name: "x".into(),
867        };
868        let r = cfg.to_native_acceptor();
869        assert!(
870            r.is_err(),
871            "SelfSigned identity with native-tls must error, got Ok"
872        );
873    }
874
875    #[cfg(feature = "tls-native")]
876    #[test]
877    fn native_connector_skip_verification_succeeds() {
878        let cfg = TlsConfig {
879            identity: TlsIdentity::SelfSigned {
880                subject_alt_names: vec!["localhost".into()],
881            },
882            trusted_certs: TrustedCerts::SkipVerification,
883            server_name: "any".into(),
884        };
885        // The client side does not need to load the local identity
886        // (clients without a cert is normal).
887        let r = cfg.to_native_connector();
888        assert!(
889            r.is_ok(),
890            "native_tls client with SkipVerification should succeed: {:?}",
891            r.err()
892        );
893    }
894
895    // ── End-to-end with real X.509 (uses rcgen, only available
896    //    under tls-rustls because that's where rcgen is gated). ──
897
898    #[cfg(feature = "tls-rustls")]
899    fn make_self_signed_pem(san: &[&str]) -> (Vec<u8>, Vec<u8>) {
900        // Returns (cert_pem_bytes, key_pem_bytes).
901        let sans: Vec<String> = san.iter().map(|s| s.to_string()).collect();
902        let ck = rcgen::generate_simple_self_signed(sans).unwrap();
903        let cert_pem = ck.cert.pem().into_bytes();
904        let key_pem = ck.key_pair.serialize_pem().into_bytes();
905        (cert_pem, key_pem)
906    }
907
908    #[cfg(feature = "tls-rustls")]
909    #[test]
910    fn rustls_server_config_from_pem_bytes() {
911        // Generate a real self-signed pair and feed it to the
912        // server-config builder via PemBytes.
913        let (cert_pem, key_pem) = make_self_signed_pem(&["localhost"]);
914        let cfg = TlsConfig {
915            identity: TlsIdentity::PemBytes { cert: cert_pem, key: key_pem },
916            trusted_certs: TrustedCerts::SkipVerification,
917            server_name: "localhost".into(),
918        };
919        let sc = cfg.to_rustls_server_config();
920        assert!(sc.is_ok(), "PemBytes server config: {:?}", sc.err());
921    }
922
923    #[cfg(feature = "tls-rustls")]
924    #[test]
925    fn rustls_server_config_from_pem_files_on_disk() {
926        // Write the generated cert/key to tempfiles, then use the
927        // PemFiles identity. This exercises the file-IO path in
928        // rustls_cert_and_key.
929        let (cert_pem, key_pem) = make_self_signed_pem(&["localhost"]);
930        let dir = tempfile::tempdir().unwrap();
931        let cert_path = dir.path().join("cert.pem");
932        let key_path = dir.path().join("key.pem");
933        std::fs::write(&cert_path, &cert_pem).unwrap();
934        std::fs::write(&key_path, &key_pem).unwrap();
935
936        let cfg = TlsConfig {
937            identity: TlsIdentity::PemFiles { cert: cert_path, key: key_path },
938            trusted_certs: TrustedCerts::SkipVerification,
939            server_name: "localhost".into(),
940        };
941        let sc = cfg.to_rustls_server_config();
942        assert!(sc.is_ok(), "PemFiles server config: {:?}", sc.err());
943    }
944
945    #[cfg(feature = "tls-rustls")]
946    #[test]
947    fn rustls_client_config_with_real_ca_bytes() {
948        // Use a generated cert as a "CA" — rustls accepts it as a
949        // root cert; that's enough to exercise the full
950        // CaBytes -> RootCertStore::add path.
951        let (ca_pem, _ca_key) = make_self_signed_pem(&["test-ca"]);
952        let cfg = TlsConfig {
953            identity: TlsIdentity::SelfSigned {
954                subject_alt_names: vec!["localhost".into()],
955            },
956            trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
957            server_name: "localhost".into(),
958        };
959        let cc = cfg.to_rustls_client_config();
960        assert!(cc.is_ok(), "real CA bytes: {:?}", cc.err());
961    }
962
963    #[cfg(feature = "tls-rustls")]
964    #[test]
965    fn rustls_client_config_with_real_ca_file() {
966        let (ca_pem, _ca_key) = make_self_signed_pem(&["test-ca"]);
967        let dir = tempfile::tempdir().unwrap();
968        let ca_path = dir.path().join("ca.pem");
969        std::fs::write(&ca_path, &ca_pem).unwrap();
970
971        let cfg = TlsConfig {
972            identity: TlsIdentity::SelfSigned {
973                subject_alt_names: vec!["localhost".into()],
974            },
975            trusted_certs: TrustedCerts::CaFiles(vec![ca_path]),
976            server_name: "localhost".into(),
977        };
978        let cc = cfg.to_rustls_client_config();
979        assert!(cc.is_ok(), "real CA file: {:?}", cc.err());
980    }
981
982    #[cfg(feature = "tls-rustls")]
983    #[test]
984    fn rustls_server_config_with_pem_files_missing_cert_errors() {
985        let dir = tempfile::tempdir().unwrap();
986        let nonexistent = dir.path().join("does-not-exist.pem");
987        let key_path = dir.path().join("key.pem");
988        let (_, key_pem) = make_self_signed_pem(&["localhost"]);
989        std::fs::write(&key_path, &key_pem).unwrap();
990
991        let cfg = TlsConfig {
992            identity: TlsIdentity::PemFiles {
993                cert: nonexistent,
994                key: key_path,
995            },
996            trusted_certs: TrustedCerts::SkipVerification,
997            server_name: "localhost".into(),
998        };
999        let sc = cfg.to_rustls_server_config();
1000        assert!(sc.is_err(), "missing cert file should error, got Ok");
1001    }
1002
1003    #[cfg(feature = "tls-rustls")]
1004    #[test]
1005    fn rustls_server_config_with_pem_files_missing_key_errors() {
1006        let dir = tempfile::tempdir().unwrap();
1007        let cert_path = dir.path().join("cert.pem");
1008        let nonexistent = dir.path().join("nonexistent-key.pem");
1009        let (cert_pem, _) = make_self_signed_pem(&["localhost"]);
1010        std::fs::write(&cert_path, &cert_pem).unwrap();
1011
1012        let cfg = TlsConfig {
1013            identity: TlsIdentity::PemFiles {
1014                cert: cert_path,
1015                key: nonexistent,
1016            },
1017            trusted_certs: TrustedCerts::SkipVerification,
1018            server_name: "localhost".into(),
1019        };
1020        let sc = cfg.to_rustls_server_config();
1021        assert!(sc.is_err(), "missing key file should error, got Ok");
1022    }
1023
1024    #[cfg(feature = "tls-rustls")]
1025    #[test]
1026    fn rustls_root_store_with_malformed_ca_file_errors() {
1027        // TLS-3: the file has bytes but rustls_pemfile decodes 0
1028        // certificates. Surface this as a structured error rather
1029        // than silently producing an empty trust store.
1030        let dir = tempfile::tempdir().unwrap();
1031        let bad_ca = dir.path().join("bad.pem");
1032        std::fs::write(&bad_ca, b"this is not a PEM file\n").unwrap();
1033
1034        let cfg = TlsConfig {
1035            identity: TlsIdentity::SelfSigned {
1036                subject_alt_names: vec!["localhost".into()],
1037            },
1038            trusted_certs: TrustedCerts::CaFiles(vec![bad_ca]),
1039            server_name: "x".into(),
1040        };
1041        let cc = cfg.to_rustls_client_config();
1042        assert!(
1043            cc.is_err(),
1044            "garbage CA file must error rather than yield empty trust"
1045        );
1046        let msg = format!("{}", cc.err().unwrap());
1047        assert!(
1048            msg.contains("0 certificates"),
1049            "error should mention 0 certificates, got: {msg}"
1050        );
1051    }
1052
1053    #[cfg(feature = "tls-rustls")]
1054    #[test]
1055    fn rustls_client_config_with_missing_ca_file_errors() {
1056        let cfg = TlsConfig {
1057            identity: TlsIdentity::SelfSigned {
1058                subject_alt_names: vec!["localhost".into()],
1059            },
1060            trusted_certs: TrustedCerts::CaFiles(vec![
1061                std::path::PathBuf::from("/nonexistent/ca.pem"),
1062            ]),
1063            server_name: "x".into(),
1064        };
1065        let cc = cfg.to_rustls_client_config();
1066        assert!(cc.is_err(), "missing CA file should error");
1067    }
1068
1069    #[cfg(feature = "tls-rustls")]
1070    #[test]
1071    fn rustls_server_config_self_signed_runtime() {
1072        // SelfSigned identity goes through rcgen at build time inside
1073        // rustls_cert_and_key. Verify the generated cert is parseable
1074        // and the ServerConfig builds.
1075        let cfg = TlsConfig {
1076            identity: TlsIdentity::SelfSigned {
1077                subject_alt_names: vec!["host-a".into(), "host-b".into()],
1078            },
1079            trusted_certs: TrustedCerts::SkipVerification,
1080            server_name: "host-a".into(),
1081        };
1082        let sc = cfg.to_rustls_server_config();
1083        assert!(sc.is_ok(), "SelfSigned runtime cert: {:?}", sc.err());
1084    }
1085
1086    // ── verify_tls12_signature / verify_tls13_signature exercise ──
1087    //
1088    // Skipped at the unit-test level because constructing a
1089    // `rustls::DigitallySignedStruct` requires a private API; a
1090    // future integration test that performs a real TLS handshake
1091    // (TlsTcpChannel + TlsTcpChannelListener with a generated cert
1092    // pair) will exercise these arms naturally.
1093
1094    #[cfg(feature = "tls-rustls")]
1095    #[test]
1096    fn rustls_pkcs12_identity_is_rejected() {
1097        // The tls-rustls backend explicitly rejects Pkcs12 (it's the
1098        // tls-native identity); covers the dedicated error arm.
1099        let cfg = TlsConfig {
1100            identity: TlsIdentity::Pkcs12 {
1101                der: vec![0x30, 0x82, 0x00, 0x10],
1102                password: "x".into(),
1103            },
1104            trusted_certs: TrustedCerts::SkipVerification,
1105            server_name: "x".into(),
1106        };
1107        let r = cfg.to_rustls_server_config();
1108        assert!(r.is_err(), "Pkcs12 with rustls must error");
1109        let msg = format!("{}", r.err().unwrap());
1110        assert!(
1111            msg.contains("Pkcs12") || msg.contains("not supported"),
1112            "error should mention Pkcs12 or not-supported, got: {msg}"
1113        );
1114    }
1115
1116    #[cfg(feature = "tls-rustls")]
1117    #[test]
1118    fn rustls_pem_bytes_no_certificates_errors() {
1119        let cfg = TlsConfig {
1120            identity: TlsIdentity::PemBytes {
1121                cert: b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n"
1122                    .to_vec(),
1123                key: b"-----BEGIN PRIVATE KEY-----\nMC4CAQA=\n-----END PRIVATE KEY-----\n"
1124                    .to_vec(),
1125            },
1126            trusted_certs: TrustedCerts::SkipVerification,
1127            server_name: "x".into(),
1128        };
1129        let r = cfg.to_rustls_server_config();
1130        assert!(r.is_err(), "PEM with no certificates must error, got Ok");
1131    }
1132
1133    #[cfg(feature = "tls-rustls")]
1134    #[test]
1135    fn rustls_pem_bytes_no_private_key_errors() {
1136        let (cert_pem, _key_pem) = make_self_signed_pem(&["localhost"]);
1137        let cfg = TlsConfig {
1138            identity: TlsIdentity::PemBytes {
1139                cert: cert_pem,
1140                key: b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n"
1141                    .to_vec(),
1142            },
1143            trusted_certs: TrustedCerts::SkipVerification,
1144            server_name: "x".into(),
1145        };
1146        let r = cfg.to_rustls_server_config();
1147        assert!(r.is_err(), "PEM with no private key must error, got Ok");
1148    }
1149
1150    #[cfg(feature = "tls-rustls")]
1151    #[test]
1152    fn rustls_skip_verification_client_config_succeeds() {
1153        // SkipVerification is the explicit "skip-verify" opt-out; it must
1154        // continue to build a client config without requiring CA roots.
1155        // (TLS-2 enforces the empty-CaBytes/CaFiles path errors instead.)
1156        let skip_cfg = TlsConfig::insecure("localhost");
1157        let cc = skip_cfg.to_rustls_client_config();
1158        assert!(cc.is_ok());
1159    }
1160
1161    // ── Security-review hardening: TLS-2, TLS-3, TLS-4 ──
1162    //
1163    // Each test below targets a specific finding from
1164    // docs/src/internal/security-review-2026-05.md. They fail before the
1165    // hardening change and pass after it.
1166
1167    // TLS-2: empty `CaFiles(vec![])` is a misconfiguration, not a silent
1168    // empty trust store.
1169    #[cfg(feature = "tls-rustls")]
1170    #[test]
1171    fn tls2_empty_ca_files_errors() {
1172        let cfg = TlsConfig {
1173            identity: TlsIdentity::SelfSigned {
1174                subject_alt_names: vec!["localhost".into()],
1175            },
1176            trusted_certs: TrustedCerts::CaFiles(vec![]),
1177            server_name: "x".into(),
1178        };
1179        let cc = cfg.to_rustls_client_config();
1180        assert!(
1181            cc.is_err(),
1182            "empty CaFiles must be a misconfiguration error, got Ok"
1183        );
1184        let msg = format!("{}", cc.err().unwrap());
1185        assert!(
1186            msg.contains("CaFiles") && msg.contains("misconfiguration"),
1187            "error should mention CaFiles/misconfiguration, got: {msg}"
1188        );
1189    }
1190
1191    // TLS-2: empty `CaBytes(vec![])` is a misconfiguration.
1192    #[cfg(feature = "tls-rustls")]
1193    #[test]
1194    fn tls2_empty_ca_bytes_errors() {
1195        let cfg = TlsConfig {
1196            identity: TlsIdentity::SelfSigned {
1197                subject_alt_names: vec!["localhost".into()],
1198            },
1199            trusted_certs: TrustedCerts::CaBytes(vec![]),
1200            server_name: "x".into(),
1201        };
1202        let cc = cfg.to_rustls_client_config();
1203        assert!(
1204            cc.is_err(),
1205            "empty CaBytes must be a misconfiguration error, got Ok"
1206        );
1207        let msg = format!("{}", cc.err().unwrap());
1208        assert!(
1209            msg.contains("CaBytes") && msg.contains("misconfiguration"),
1210            "error should mention CaBytes/misconfiguration, got: {msg}"
1211        );
1212    }
1213
1214    // TLS-2: `SkipVerification` is preserved as the explicit opt-out.
1215    #[cfg(feature = "tls-rustls")]
1216    #[test]
1217    fn tls2_skip_verification_still_works() {
1218        let cfg = TlsConfig::insecure("localhost");
1219        let cc = cfg.to_rustls_client_config();
1220        assert!(
1221            cc.is_ok(),
1222            "SkipVerification must remain the supported opt-out, got Err: \
1223             {:?}",
1224            cc.err()
1225        );
1226    }
1227
1228    // TLS-3: a non-empty PEM byte-blob that decodes to zero certificates
1229    // is a parse error, not a silent empty trust store.
1230    #[cfg(feature = "tls-rustls")]
1231    #[test]
1232    fn tls3_ca_bytes_with_zero_decoded_certs_errors() {
1233        let cfg = TlsConfig {
1234            identity: TlsIdentity::SelfSigned {
1235                subject_alt_names: vec!["localhost".into()],
1236            },
1237            trusted_certs: TrustedCerts::CaBytes(vec![
1238                b"this looks like text but is not a PEM certificate\n".to_vec(),
1239            ]),
1240            server_name: "x".into(),
1241        };
1242        let cc = cfg.to_rustls_client_config();
1243        assert!(
1244            cc.is_err(),
1245            "non-empty PEM with zero certs must error, got Ok"
1246        );
1247        let msg = format!("{}", cc.err().unwrap());
1248        assert!(
1249            msg.contains("0 certificates"),
1250            "error should mention 0 certificates, got: {msg}"
1251        );
1252    }
1253
1254    // TLS-3: a non-empty CA file that decodes to zero certificates errors.
1255    #[cfg(feature = "tls-rustls")]
1256    #[test]
1257    fn tls3_ca_file_with_zero_decoded_certs_errors() {
1258        let dir = tempfile::tempdir().unwrap();
1259        let bad_ca = dir.path().join("bad.pem");
1260        // PEM-shaped wrapper around a non-cert label — rustls_pemfile
1261        // accepts no certificates.
1262        std::fs::write(
1263            &bad_ca,
1264            b"-----BEGIN GARBAGE-----\nAAAA\n-----END GARBAGE-----\n",
1265        )
1266        .unwrap();
1267
1268        let cfg = TlsConfig {
1269            identity: TlsIdentity::SelfSigned {
1270                subject_alt_names: vec!["localhost".into()],
1271            },
1272            trusted_certs: TrustedCerts::CaFiles(vec![bad_ca.clone()]),
1273            server_name: "x".into(),
1274        };
1275        let cc = cfg.to_rustls_client_config();
1276        assert!(cc.is_err(), "CA file with 0 certificates must error");
1277        let msg = format!("{}", cc.err().unwrap());
1278        assert!(
1279            msg.contains("0 certificates")
1280                && msg.contains(&bad_ca.display().to_string()),
1281            "error should mention 0 certificates and the file path, got: \
1282             {msg}"
1283        );
1284    }
1285
1286    // TLS-4: a `tls-native` server with mTLS intent (non-empty CA roots)
1287    // must error with an mTLS-specific message, since
1288    // native_tls::TlsAcceptorBuilder cannot enforce client-cert
1289    // verification. A warning is not a security boundary. The check
1290    // runs before identity parsing so that the misconfiguration
1291    // surfaces independently of any identity-format issues.
1292    #[cfg(feature = "tls-native")]
1293    #[test]
1294    fn tls4_native_acceptor_with_ca_files_intent_errors() {
1295        let cfg = TlsConfig {
1296            identity: TlsIdentity::Pkcs12 {
1297                der: vec![0x30, 0x82, 0x00, 0x10],
1298                password: "x".into(),
1299            },
1300            trusted_certs: TrustedCerts::CaFiles(vec![
1301                std::path::PathBuf::from("/etc/ssl/certs/ca.pem"),
1302            ]),
1303            server_name: "x".into(),
1304        };
1305        let r = cfg.to_native_acceptor();
1306        assert!(
1307            r.is_err(),
1308            "mTLS intent on tls-native server must error rather than warn"
1309        );
1310        let msg = format!("{}", r.err().unwrap());
1311        assert!(
1312            msg.contains("mTLS")
1313                && msg.contains("tls-native")
1314                && msg.contains("tls-rustls"),
1315            "error must point at mTLS / tls-native / tls-rustls remediation, \
1316             got: {msg}"
1317        );
1318    }
1319
1320    #[cfg(feature = "tls-native")]
1321    #[test]
1322    fn tls4_native_acceptor_with_ca_bytes_intent_errors() {
1323        let cfg = TlsConfig {
1324            identity: TlsIdentity::Pkcs12 {
1325                der: vec![0x30, 0x82, 0x00, 0x10],
1326                password: "x".into(),
1327            },
1328            trusted_certs: TrustedCerts::CaBytes(vec![
1329                b"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"
1330                    .to_vec(),
1331            ]),
1332            server_name: "x".into(),
1333        };
1334        let r = cfg.to_native_acceptor();
1335        assert!(
1336            r.is_err(),
1337            "non-empty CaBytes on tls-native server must error"
1338        );
1339        let msg = format!("{}", r.err().unwrap());
1340        assert!(msg.contains("mTLS"), "error must mention mTLS, got: {msg}");
1341    }
1342
1343    // TLS-4: SkipVerification (no mTLS intent) must remain functional on
1344    // the tls-native server path — the Refusal is conditional on intent.
1345    #[cfg(feature = "tls-native")]
1346    #[test]
1347    fn tls4_native_acceptor_skip_verification_unaffected() {
1348        // SkipVerification = no mTLS intent, so the new TLS-4 check
1349        // must not fire. The dummy DER will fail at native_identity,
1350        // but the failure must NOT come from the mTLS misconfiguration
1351        // check (verified by message).
1352        let cfg = TlsConfig {
1353            identity: TlsIdentity::Pkcs12 {
1354                der: vec![0x30, 0x82, 0x00, 0x10],
1355                password: "x".into(),
1356            },
1357            trusted_certs: TrustedCerts::SkipVerification,
1358            server_name: "x".into(),
1359        };
1360        let r = cfg.to_native_acceptor();
1361        if let Err(e) = r {
1362            let msg = format!("{e}");
1363            assert!(
1364                !msg.contains("mTLS"),
1365                "SkipVerification must not trigger mTLS check, got: {msg}"
1366            );
1367        }
1368    }
1369
1370    // ── QUIC config builders (under quic feature) ──
1371
1372    #[cfg(all(feature = "tls-rustls", feature = "quic"))]
1373    #[test]
1374    fn quinn_server_config_builds_from_self_signed() {
1375        let cfg = TlsConfig::insecure("localhost");
1376        let qc = cfg.to_quinn_server_config();
1377        assert!(qc.is_ok(), "quinn server config: {:?}", qc.err());
1378    }
1379
1380    #[cfg(all(feature = "tls-rustls", feature = "quic"))]
1381    #[test]
1382    fn quinn_client_config_builds_from_skip_verification() {
1383        let cfg = TlsConfig::insecure("localhost");
1384        let qc = cfg.to_quinn_client_config();
1385        assert!(qc.is_ok(), "quinn client config: {:?}", qc.err());
1386    }
1387
1388    #[cfg(all(feature = "tls-rustls", feature = "quic"))]
1389    #[test]
1390    fn quinn_client_config_with_real_ca_bytes() {
1391        let (ca_pem, _) = make_self_signed_pem(&["test-ca"]);
1392        let cfg = TlsConfig {
1393            identity: TlsIdentity::SelfSigned {
1394                subject_alt_names: vec!["localhost".into()],
1395            },
1396            trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
1397            server_name: "localhost".into(),
1398        };
1399        let qc = cfg.to_quinn_client_config();
1400        assert!(qc.is_ok(), "quinn client config with CA: {:?}", qc.err());
1401    }
1402}