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
50use crate::error::{RepError, Result};
51
52// ─── TlsIdentity ─────────────────────────────────────────────────────────────
53
54/// Certificate and private key material that identifies this replication node.
55///
56/// ## Backend compatibility
57///
58/// | Variant | `tls-rustls` | `tls-native` |
59/// |---------|:------------:|:------------:|
60/// | `SelfSigned` | ✓ | ✗ |
61/// | `PemFiles` | ✓ | ✗ |
62/// | `PemBytes` | ✓ | ✗ |
63/// | `Pkcs12` | ✗ | ✓ |
64///
65/// For `tls-native`, create a PKCS #12 archive with:
66/// ```sh
67/// openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem
68/// ```
69#[derive(Clone)]
70#[non_exhaustive]
71pub enum TlsIdentity {
72    /// Generate a fresh self-signed certificate at runtime.
73    ///
74    /// Supported by the `tls-rustls` backend only.  Suitable for internal,
75    /// trusted replication networks where setting up a certificate authority
76    /// is undesirable.
77    SelfSigned {
78        /// Subject Alternative Names for the certificate (e.g. DNS hostnames
79        /// or IP addresses for this node).
80        subject_alt_names: Vec<String>,
81    },
82
83    /// Load certificate chain and private key from PEM files on disk.
84    ///
85    /// Supported by the `tls-rustls` backend only.
86    PemFiles {
87        /// Path to a PEM-encoded certificate chain.
88        cert: std::path::PathBuf,
89        /// Path to a PEM-encoded private key (PKCS #8 or PKCS #1 RSA).
90        key: std::path::PathBuf,
91    },
92
93    /// Certificate chain and private key as in-memory PEM bytes.
94    ///
95    /// Supported by the `tls-rustls` backend only.
96    PemBytes {
97        /// PEM-encoded certificate chain bytes.
98        cert: Vec<u8>,
99        /// PEM-encoded private key bytes.
100        key: Vec<u8>,
101    },
102
103    /// PKCS #12 archive (certificate + key bundled) as DER bytes.
104    ///
105    /// Supported by the `tls-native` backend only (OpenSSL / LibreSSL).
106    /// Load with:
107    /// ```ignore
108    /// let der = std::fs::read("/etc/noxu/identity.p12")?;
109    /// let identity = TlsIdentity::Pkcs12 { der, password: "secret".into() };
110    /// ```
111    Pkcs12 {
112        /// DER-encoded PKCS #12 archive.
113        der: Vec<u8>,
114        /// Password used to decrypt the archive.
115        password: String,
116    },
117}
118
119impl std::fmt::Debug for TlsIdentity {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            Self::SelfSigned { subject_alt_names } => f
123                .debug_struct("SelfSigned")
124                .field("subject_alt_names", subject_alt_names)
125                .finish(),
126            Self::PemFiles { cert, key } => f
127                .debug_struct("PemFiles")
128                .field("cert", cert)
129                .field("key", key)
130                .finish(),
131            Self::PemBytes { cert, .. } => f
132                .debug_struct("PemBytes")
133                .field("cert_len", &cert.len())
134                .field("key", &"<redacted>")
135                .finish(),
136            Self::Pkcs12 { .. } => f
137                .debug_struct("Pkcs12")
138                .field("der", &"<redacted>")
139                .field("password", &"<redacted>")
140                .finish(),
141        }
142    }
143}
144
145// ─── TrustedCerts ────────────────────────────────────────────────────────────
146
147/// Policy for verifying the remote peer's certificate.
148#[derive(Clone)]
149#[non_exhaustive]
150pub enum TrustedCerts {
151    /// Accept any certificate without verification.
152    ///
153    /// **Insecure.** Use only on private, trusted networks where all nodes
154    /// are implicitly trusted (authenticated at the Paxos / VLSN layer).
155    SkipVerification,
156
157    /// Trust CA certificates loaded from PEM files on disk.
158    CaFiles(Vec<std::path::PathBuf>),
159
160    /// Trust in-memory PEM-encoded CA certificates.
161    CaBytes(Vec<Vec<u8>>),
162}
163
164impl std::fmt::Debug for TrustedCerts {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self {
167            Self::SkipVerification => write!(f, "SkipVerification"),
168            Self::CaFiles(paths) => {
169                f.debug_tuple("CaFiles").field(paths).finish()
170            }
171            Self::CaBytes(pems) => {
172                let sizes: Vec<usize> = pems.iter().map(|p| p.len()).collect();
173                f.debug_struct("CaBytes").field("blob_sizes", &sizes).finish()
174            }
175        }
176    }
177}
178
179// ─── TlsConfig ───────────────────────────────────────────────────────────────
180
181/// TLS configuration for Noxu DB replication channels.
182///
183/// A `TlsConfig` bundles this node's identity (certificate + key) with the
184/// policy for verifying remote peers.  Pass it to:
185///
186/// - `TlsTcpChannelListener::bind_with_tls` — encrypted TCP server
187/// - `TlsTcpChannel::connect_with_tls` — encrypted TCP client
188/// - `TlsConfig::to_quinn_server_config` — QUIC server with real certs
189/// - `TlsConfig::to_quinn_client_config` — QUIC client with real certs
190#[derive(Clone)]
191pub struct TlsConfig {
192    /// This node's certificate and private key.
193    pub identity: TlsIdentity,
194    /// How to verify the remote peer's certificate.
195    pub trusted_certs: TrustedCerts,
196    /// TLS SNI server name used by the client during the handshake.
197    ///
198    /// Must match the certificate's `Common Name` or a `Subject Alternative
199    /// Name`.  Use `"localhost"` when connecting to a `SelfSigned` cert with
200    /// `subject_alt_names = ["localhost"]`.
201    pub server_name: String,
202}
203
204impl std::fmt::Debug for TlsConfig {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        f.debug_struct("TlsConfig")
207            .field("server_name", &self.server_name)
208            .field("identity", &self.identity)
209            .field("trusted_certs", &self.trusted_certs)
210            .finish()
211    }
212}
213
214impl TlsConfig {
215    /// Create an insecure TLS configuration for trusted private networks.
216    ///
217    /// Generates a self-signed certificate at first use and skips peer
218    /// certificate verification entirely.  Equivalent to the current default
219    /// QUIC channel behaviour.
220    ///
221    /// Requires the `tls-rustls` feature.
222    pub fn insecure(server_name: impl Into<String>) -> Self {
223        TlsConfig {
224            identity: TlsIdentity::SelfSigned {
225                subject_alt_names: vec!["localhost".into()],
226            },
227            trusted_certs: TrustedCerts::SkipVerification,
228            server_name: server_name.into(),
229        }
230    }
231
232    /// Create a TLS configuration using PEM cert/key files and a CA file.
233    ///
234    /// Verifies the remote peer's certificate against `ca`.
235    /// Requires the `tls-rustls` feature.
236    pub fn from_pem_files(
237        cert: impl Into<std::path::PathBuf>,
238        key: impl Into<std::path::PathBuf>,
239        ca: impl Into<std::path::PathBuf>,
240        server_name: impl Into<String>,
241    ) -> Self {
242        TlsConfig {
243            identity: TlsIdentity::PemFiles {
244                cert: cert.into(),
245                key: key.into(),
246            },
247            trusted_certs: TrustedCerts::CaFiles(vec![ca.into()]),
248            server_name: server_name.into(),
249        }
250    }
251
252    /// Create a TLS configuration from a PKCS #12 archive.
253    ///
254    /// Verifies the remote peer against `ca_pem` bytes.
255    /// Requires the `tls-native` feature.
256    pub fn from_pkcs12(
257        der: Vec<u8>,
258        password: impl Into<String>,
259        ca_pem: Vec<u8>,
260        server_name: impl Into<String>,
261    ) -> Self {
262        TlsConfig {
263            identity: TlsIdentity::Pkcs12 { der, password: password.into() },
264            trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
265            server_name: server_name.into(),
266        }
267    }
268
269    /// Create a TLS configuration intended for **replication**:
270    /// requires both a non-self-signed identity and a non-empty
271    /// CA list. Returns `Err` for any input that would
272    /// produce a configuration where the peer cannot be
273    /// authenticated.
274    ///
275    /// This is the documented path for production replication
276    /// per `docs/src/internal/auth-mtls-design-2026-05.md`. It
277    /// is stricter than the `from_pem_files` and `from_pkcs12`
278    /// constructors:
279    ///
280    ///   - Rejects `TlsIdentity::SelfSigned` (a runtime-generated
281    ///     cert has no consistent subject across restarts —
282    ///     incompatible with subject-based authorisation).
283    ///   - Rejects `TrustedCerts::SkipVerification` (skip-verify
284    ///     is a development-only path).
285    ///   - Rejects empty `CaFiles` / `CaBytes` lists.
286    ///
287    /// `[`TlsConfig::insecure`]` remains available for tests and
288    /// trusted-network deployments where the operator
289    /// explicitly accepts an unauthenticated transport.
290    pub fn for_replication(
291        identity: TlsIdentity,
292        trusted_certs: TrustedCerts,
293        server_name: impl Into<String>,
294    ) -> Result<Self> {
295        // Validate identity.
296        if matches!(identity, TlsIdentity::SelfSigned { .. }) {
297            return Err(RepError::ConfigError(
298                "TlsConfig::for_replication rejects TlsIdentity::SelfSigned: \
299                 runtime-generated certs have no stable subject and cannot \
300                 be matched against a peer allowlist. Use PemFiles, \
301                 PemBytes, or Pkcs12 with a real CA-issued cert."
302                    .into(),
303            ));
304        }
305        // Validate trust.
306        match &trusted_certs {
307            TrustedCerts::SkipVerification => {
308                return Err(RepError::ConfigError(
309                    "TlsConfig::for_replication rejects \
310                     TrustedCerts::SkipVerification: replication peer \
311                     authentication requires CA-rooted chain validation. \
312                     Use TrustedCerts::CaFiles or CaBytes with the \
313                     replication CA's certificate."
314                        .into(),
315                ));
316            }
317            TrustedCerts::CaFiles(v) if v.is_empty() => {
318                return Err(RepError::ConfigError(
319                    "TlsConfig::for_replication rejects empty CaFiles: \
320                     at least one CA must be provided for peer cert \
321                     validation."
322                        .into(),
323                ));
324            }
325            TrustedCerts::CaBytes(v) if v.is_empty() => {
326                return Err(RepError::ConfigError(
327                    "TlsConfig::for_replication rejects empty CaBytes: \
328                     at least one CA must be provided for peer cert \
329                     validation."
330                        .into(),
331                ));
332            }
333            _ => {}
334        }
335        Ok(TlsConfig {
336            identity,
337            trusted_certs,
338            server_name: server_name.into(),
339        })
340    }
341}
342
343// ─── rustls helpers ──────────────────────────────────────────────────────────
344
345#[cfg(feature = "tls-rustls")]
346impl TlsConfig {
347    /// Build a `rustls::ServerConfig` from this configuration.
348    ///
349    /// The server does **not** request a client certificate (no mTLS).
350    /// Use [`to_rustls_server_config_with_allowlist`] when
351    /// `RepConfig::peer_allowlist` is configured.
352    ///
353    /// Used by [`TlsTcpChannelListener`] and the QUIC server path.
354    ///
355    /// [`to_rustls_server_config_with_allowlist`]: Self::to_rustls_server_config_with_allowlist
356    pub(crate) fn to_rustls_server_config(
357        &self,
358    ) -> Result<std::sync::Arc<rustls::ServerConfig>> {
359        let (certs, key) = self.rustls_cert_and_key()?;
360
361        let cfg = rustls::ServerConfig::builder()
362            .with_no_client_auth()
363            .with_single_cert(certs, key)
364            .map_err(|e| {
365                RepError::NetworkError(format!("TLS server config: {e}"))
366            })?;
367        Ok(std::sync::Arc::new(cfg))
368    }
369
370    /// Build a `rustls::ServerConfig` that enforces `peer_allowlist`.
371    ///
372    /// This is the **mTLS enforcement path** introduced in Phase 2 (v3.1.0).
373    /// Compared to [`to_rustls_server_config`]:
374    ///
375    /// - The server requests a client certificate (`client_auth_mandatory = true`).
376    /// - The client certificate chain is validated against the CA roots in
377    ///   this `TlsConfig`.
378    /// - The peer's Subject CN and DNS SANs are extracted and checked against
379    ///   `allowlist`; the TLS handshake is aborted if no name matches.
380    ///
381    /// # Errors
382    ///
383    /// - `RepError::ConfigError` if `trusted_certs` is
384    ///   [`TrustedCerts::SkipVerification`] (no CA to validate client certs).
385    /// - `RepError::ConfigError` if `allowlist` is empty (fail-closed per
386    ///   design doc: an empty allowlist admits no peer).
387    /// - `RepError::NetworkError` if the cert/key material fails to parse.
388    ///
389    /// [`to_rustls_server_config`]: Self::to_rustls_server_config
390    pub(crate) fn to_rustls_server_config_with_allowlist(
391        &self,
392        allowlist: crate::auth::PeerAllowlist,
393    ) -> Result<std::sync::Arc<rustls::ServerConfig>> {
394        if matches!(&self.trusted_certs, TrustedCerts::SkipVerification) {
395            return Err(RepError::ConfigError(
396                "to_rustls_server_config_with_allowlist requires a                  CA-rooted TrustedCerts configuration (CaFiles or CaBytes);                  SkipVerification cannot be used for mTLS enforcement because                  there is no CA to validate peer certificates against."
397                    .into(),
398            ));
399        }
400        let (certs, key) = self.rustls_cert_and_key()?;
401        let root_store = self.rustls_root_store()?;
402        let verifier = crate::auth::PeerAllowlistVerifier::new(
403            std::sync::Arc::new(root_store),
404            allowlist,
405        )?;
406        let cfg = rustls::ServerConfig::builder()
407            .with_client_cert_verifier(std::sync::Arc::new(verifier))
408            .with_single_cert(certs, key)
409            .map_err(|e| {
410                RepError::NetworkError(format!("TLS server config (mTLS): {e}"))
411            })?;
412        Ok(std::sync::Arc::new(cfg))
413    }
414
415    /// Build a `rustls::ClientConfig` from this configuration.
416    ///
417    /// **mTLS client-auth behaviour (Phase 2, v3.1.0)**:  When the identity
418    /// is [`TlsIdentity::PemFiles`] or [`TlsIdentity::PemBytes`] and
419    /// `trusted_certs` is not [`TrustedCerts::SkipVerification`], the client
420    /// presents its own certificate during the TLS handshake so that a server
421    /// running [`crate::auth::PeerAllowlistVerifier`] can verify it.
422    ///
423    /// [`TlsIdentity::SelfSigned`] always uses `with_no_client_auth` — a
424    /// runtime-generated self-signed cert would fail CA chain validation on
425    /// the server side regardless.
426    ///
427    /// Used by [`TlsTcpChannel`] and the QUIC client path.
428    pub(crate) fn to_rustls_client_config(
429        &self,
430    ) -> Result<std::sync::Arc<rustls::ClientConfig>> {
431        // Insecure (dev) path: skip server-cert verification, no client cert.
432        if matches!(&self.trusted_certs, TrustedCerts::SkipVerification) {
433            let cfg = rustls::ClientConfig::builder()
434                .dangerous()
435                .with_custom_certificate_verifier(std::sync::Arc::new(
436                    SkipCertVerification::new(),
437                ))
438                .with_no_client_auth();
439            return Ok(std::sync::Arc::new(cfg));
440        }
441
442        let root_store = self.rustls_root_store()?;
443
444        // Production mTLS path: present client cert for PemFiles / PemBytes.
445        // SelfSigned identity stays with_no_client_auth — a runtime-generated
446        // self-signed cert would fail CA chain validation on the server side
447        // anyway; keeping the dev-convenience path functional without change.
448        match &self.identity {
449            TlsIdentity::SelfSigned { .. } => {
450                let cfg = rustls::ClientConfig::builder()
451                    .with_root_certificates(root_store)
452                    .with_no_client_auth();
453                Ok(std::sync::Arc::new(cfg))
454            }
455            TlsIdentity::PemFiles { .. } | TlsIdentity::PemBytes { .. } => {
456                let (certs, key) = self.rustls_cert_and_key()?;
457                let cfg = rustls::ClientConfig::builder()
458                    .with_root_certificates(root_store)
459                    .with_client_auth_cert(certs, key)
460                    .map_err(|e| {
461                        RepError::NetworkError(format!(
462                            "TLS client auth cert: {e}"
463                        ))
464                    })?;
465                Ok(std::sync::Arc::new(cfg))
466            }
467            TlsIdentity::Pkcs12 { .. } => Err(RepError::NetworkError(
468                "Pkcs12 identity is not supported by the tls-rustls                  backend; use PemFiles or PemBytes instead"
469                    .into(),
470            )),
471        }
472    }
473
474    /// Build a `quinn::ServerConfig` backed by this `TlsConfig`.
475    ///
476    /// Replaces the default self-signed / skip-verify server config for
477    /// production deployments that bring their own certificates.
478    ///
479    /// # Example
480    /// ```ignore
481    /// let tls = TlsConfig::from_pem_files("cert.pem", "key.pem", "ca.pem", "node1");
482    /// let server_cfg = tls.to_quinn_server_config()?;
483    /// let listener = QuicMultiplexedChannelListener::with_server_config(addr, server_cfg)?;
484    /// ```
485    #[cfg(feature = "quic")]
486    pub fn to_quinn_server_config(&self) -> Result<quinn::ServerConfig> {
487        let rustls_cfg = self.to_rustls_server_config()?;
488        let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(
489            rustls::ServerConfig::clone(&rustls_cfg),
490        )
491        .map_err(|e| {
492            RepError::NetworkError(format!("QUIC server config: {e}"))
493        })?;
494        let mut cfg =
495            quinn::ServerConfig::with_crypto(std::sync::Arc::new(quic_cfg));
496        let mut transport = quinn::TransportConfig::default();
497        transport.mtu_discovery_config(None);
498        transport.datagram_receive_buffer_size(Some(64 * 1024));
499        cfg.transport_config(std::sync::Arc::new(transport));
500        Ok(cfg)
501    }
502
503    /// Build a `quinn::ServerConfig` that enforces `peer_allowlist` via mTLS.
504    ///
505    /// This is the **QUIC mTLS enforcement path** introduced in Phase 3.
506    /// Compared to [`to_quinn_server_config`]:
507    ///
508    /// - The server requests a client certificate (`client_auth_mandatory = true`).
509    /// - The client certificate chain is validated against the CA roots in this
510    ///   `TlsConfig`.
511    /// - The peer's Subject CN and DNS SANs are checked against `allowlist`;
512    ///   the QUIC (TLS 1.3) handshake is aborted if no name matches.
513    ///
514    /// The empty-allowlist fail-closed policy applies: constructing with an
515    /// empty `PeerAllowlist` returns `Err(RepError::ConfigError)`.
516    ///
517    /// # Errors
518    ///
519    /// Same as [`to_rustls_server_config_with_allowlist`] plus QUIC config
520    /// conversion errors.
521    ///
522    /// [`to_rustls_server_config_with_allowlist`]: Self::to_rustls_server_config_with_allowlist
523    /// [`to_quinn_server_config`]: Self::to_quinn_server_config
524    #[cfg(feature = "quic")]
525    pub fn to_quinn_server_config_with_allowlist(
526        &self,
527        allowlist: crate::auth::PeerAllowlist,
528    ) -> Result<quinn::ServerConfig> {
529        let rustls_cfg =
530            self.to_rustls_server_config_with_allowlist(allowlist)?;
531        let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(
532            rustls::ServerConfig::clone(&rustls_cfg),
533        )
534        .map_err(|e| {
535            RepError::NetworkError(format!("QUIC server config (mTLS): {e}"))
536        })?;
537        let mut cfg =
538            quinn::ServerConfig::with_crypto(std::sync::Arc::new(quic_cfg));
539        let mut transport = quinn::TransportConfig::default();
540        transport.mtu_discovery_config(None);
541        transport.datagram_receive_buffer_size(Some(64 * 1024));
542        cfg.transport_config(std::sync::Arc::new(transport));
543        Ok(cfg)
544    }
545
546    /// Build a `quinn::ClientConfig` backed by this `TlsConfig`.
547    ///
548    /// Replaces the default skip-verify client config for production
549    /// deployments that verify server certificates against a CA.
550    #[cfg(feature = "quic")]
551    pub fn to_quinn_client_config(&self) -> Result<quinn::ClientConfig> {
552        let rustls_cfg = self.to_rustls_client_config()?;
553        let quic_cfg = quinn::crypto::rustls::QuicClientConfig::try_from(
554            rustls::ClientConfig::clone(&rustls_cfg),
555        )
556        .map_err(|e| {
557            RepError::NetworkError(format!("QUIC client config: {e}"))
558        })?;
559        let mut cfg = quinn::ClientConfig::new(std::sync::Arc::new(quic_cfg));
560        let mut transport = quinn::TransportConfig::default();
561        transport.mtu_discovery_config(None);
562        transport.datagram_receive_buffer_size(Some(64 * 1024));
563        cfg.transport_config(std::sync::Arc::new(transport));
564        Ok(cfg)
565    }
566
567    // ── Private helpers ──────────────────────────────────────────────────
568
569    fn rustls_cert_and_key(
570        &self,
571    ) -> Result<(
572        Vec<rustls::pki_types::CertificateDer<'static>>,
573        rustls::pki_types::PrivateKeyDer<'static>,
574    )> {
575        use rustls::pki_types::{
576            CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer,
577        };
578
579        match &self.identity {
580            TlsIdentity::SelfSigned { subject_alt_names } => {
581                let ck = rcgen::generate_simple_self_signed(
582                    subject_alt_names.clone(),
583                )
584                .map_err(|e| RepError::NetworkError(format!("rcgen: {e}")))?;
585                let cert = CertificateDer::from(ck.cert.der().to_vec());
586                let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
587                    ck.key_pair.serialize_der(),
588                ));
589                Ok((vec![cert], key))
590            }
591            TlsIdentity::PemFiles { cert, key } => {
592                let cert_bytes = std::fs::read(cert).map_err(|e| {
593                    RepError::NetworkError(format!("cert file: {e}"))
594                })?;
595                let key_bytes = std::fs::read(key).map_err(|e| {
596                    RepError::NetworkError(format!("key file: {e}"))
597                })?;
598                Self::parse_pem_cert_and_key(&cert_bytes, &key_bytes)
599            }
600            TlsIdentity::PemBytes { cert, key } => {
601                Self::parse_pem_cert_and_key(cert, key)
602            }
603            TlsIdentity::Pkcs12 { .. } => Err(RepError::NetworkError(
604                "Pkcs12 identity is not supported by the tls-rustls backend; \
605                 use PemFiles or PemBytes instead"
606                    .into(),
607            )),
608        }
609    }
610
611    fn parse_pem_cert_and_key(
612        cert_pem: &[u8],
613        key_pem: &[u8],
614    ) -> Result<(
615        Vec<rustls::pki_types::CertificateDer<'static>>,
616        rustls::pki_types::PrivateKeyDer<'static>,
617    )> {
618        use rustls_pemfile::{certs, private_key};
619        use std::io::BufReader;
620
621        let cert_chain: Vec<_> = certs(&mut BufReader::new(cert_pem))
622            .collect::<std::result::Result<_, _>>()
623            .map_err(|e| RepError::NetworkError(format!("cert parse: {e}")))?;
624        if cert_chain.is_empty() {
625            return Err(RepError::NetworkError(
626                "no certificates found in PEM".into(),
627            ));
628        }
629
630        let key = private_key(&mut BufReader::new(key_pem))
631            .map_err(|e| RepError::NetworkError(format!("key parse: {e}")))?
632            .ok_or_else(|| {
633                RepError::NetworkError("no private key found in PEM".into())
634            })?;
635
636        Ok((cert_chain, key))
637    }
638
639    fn rustls_root_store(&self) -> Result<rustls::RootCertStore> {
640        use rustls_pemfile::certs;
641        use std::io::BufReader;
642
643        let mut store = rustls::RootCertStore::empty();
644
645        match &self.trusted_certs {
646            TrustedCerts::SkipVerification => {
647                // For skip-verification the root store is unused; the caller
648                // must install a custom verifier (as quic_channel.rs does).
649                // For TCP TLS we return an empty store here and the TlsTcpChannel
650                // will install SkipCertVerification when this variant is set.
651            }
652            TrustedCerts::CaFiles(paths) => {
653                // TLS-2: an empty CaFiles list is a misconfiguration. It
654                // would silently produce an empty trust store, which validates
655                // nothing without the explicit `SkipVerification` opt-out.
656                if paths.is_empty() {
657                    return Err(RepError::ConfigError(
658                        "TrustedCerts::CaFiles configured with no paths; \
659                         this is a misconfiguration. Use \
660                         TrustedCerts::SkipVerification to explicitly opt \
661                         out of CA verification."
662                            .into(),
663                    ));
664                }
665                for path in paths {
666                    let pem = std::fs::read(path).map_err(|e| {
667                        RepError::NetworkError(format!("CA file: {e}"))
668                    })?;
669                    let parsed: Vec<_> =
670                        certs(&mut BufReader::new(pem.as_slice()))
671                            .collect::<std::result::Result<Vec<_>, _>>()
672                            .map_err(|e| {
673                                RepError::NetworkError(format!("CA parse: {e}"))
674                            })?;
675                    // TLS-3: rustls_pemfile silently skips non-cert PEM blocks
676                    // (and any non-PEM bytes). If the file had bytes but
677                    // produced zero certificates, treat that as a parse error
678                    // rather than silently building an empty trust store.
679                    if !pem.is_empty() && parsed.is_empty() {
680                        return Err(RepError::ConfigError(format!(
681                            "CA file {} parsed but contained 0 certificates",
682                            path.display()
683                        )));
684                    }
685                    for cert in parsed {
686                        store.add(cert).map_err(|e| {
687                            RepError::NetworkError(format!("CA add: {e}"))
688                        })?;
689                    }
690                }
691            }
692            TrustedCerts::CaBytes(pems) => {
693                // TLS-2: empty CaBytes list — same misconfiguration as
694                // empty CaFiles. Reject explicitly.
695                if pems.is_empty() {
696                    return Err(RepError::ConfigError(
697                        "TrustedCerts::CaBytes configured with no PEM blobs; \
698                         this is a misconfiguration. Use \
699                         TrustedCerts::SkipVerification to explicitly opt \
700                         out of CA verification."
701                            .into(),
702                    ));
703                }
704                for (idx, pem) in pems.iter().enumerate() {
705                    let parsed: Vec<_> =
706                        certs(&mut BufReader::new(pem.as_slice()))
707                            .collect::<std::result::Result<Vec<_>, _>>()
708                            .map_err(|e| {
709                                RepError::NetworkError(format!("CA parse: {e}"))
710                            })?;
711                    // TLS-3: bytes provided but no certs decoded.
712                    if !pem.is_empty() && parsed.is_empty() {
713                        return Err(RepError::ConfigError(format!(
714                            "CA bytes (index {idx}) parsed but contained 0 \
715                             certificates"
716                        )));
717                    }
718                    for cert in parsed {
719                        store.add(cert).map_err(|e| {
720                            RepError::NetworkError(format!("CA add: {e}"))
721                        })?;
722                    }
723                }
724            }
725        }
726
727        Ok(store)
728    }
729}
730
731// ─── SkipCertVerification ────────────────────────────────────────────────────
732
733/// A `rustls` `ServerCertVerifier` that accepts any certificate without chain
734/// validation.
735///
736/// Suitable for internal, trusted replication networks where nodes are
737/// implicitly trusted (authenticated at the Paxos / VLSN layer).
738#[cfg(feature = "tls-rustls")]
739#[derive(Debug)]
740pub(crate) struct SkipCertVerification(
741    std::sync::Arc<rustls::crypto::CryptoProvider>,
742);
743
744#[cfg(feature = "tls-rustls")]
745impl SkipCertVerification {
746    pub(crate) fn new() -> Self {
747        Self(std::sync::Arc::new(rustls::crypto::ring::default_provider()))
748    }
749}
750
751#[cfg(feature = "tls-rustls")]
752impl rustls::client::danger::ServerCertVerifier for SkipCertVerification {
753    fn verify_server_cert(
754        &self,
755        _end_entity: &rustls::pki_types::CertificateDer<'_>,
756        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
757        _server_name: &rustls::pki_types::ServerName<'_>,
758        _ocsp_response: &[u8],
759        _now: rustls::pki_types::UnixTime,
760    ) -> std::result::Result<
761        rustls::client::danger::ServerCertVerified,
762        rustls::Error,
763    > {
764        Ok(rustls::client::danger::ServerCertVerified::assertion())
765    }
766
767    fn verify_tls12_signature(
768        &self,
769        message: &[u8],
770        cert: &rustls::pki_types::CertificateDer<'_>,
771        dss: &rustls::DigitallySignedStruct,
772    ) -> std::result::Result<
773        rustls::client::danger::HandshakeSignatureValid,
774        rustls::Error,
775    > {
776        rustls::crypto::verify_tls12_signature(
777            message,
778            cert,
779            dss,
780            &self.0.signature_verification_algorithms,
781        )
782    }
783
784    fn verify_tls13_signature(
785        &self,
786        message: &[u8],
787        cert: &rustls::pki_types::CertificateDer<'_>,
788        dss: &rustls::DigitallySignedStruct,
789    ) -> std::result::Result<
790        rustls::client::danger::HandshakeSignatureValid,
791        rustls::Error,
792    > {
793        rustls::crypto::verify_tls13_signature(
794            message,
795            cert,
796            dss,
797            &self.0.signature_verification_algorithms,
798        )
799    }
800
801    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
802        self.0.signature_verification_algorithms.supported_schemes()
803    }
804}
805
806// ─── native-tls helpers ──────────────────────────────────────────────────────
807
808#[cfg(feature = "tls-native")]
809impl TlsConfig {
810    /// Build a `native_tls::TlsAcceptor` for TCP server use.
811    ///
812    /// Requires `TlsIdentity::Pkcs12`.  Create a PKCS #12 archive with:
813    /// ```sh
814    /// openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem
815    /// ```
816    pub(crate) fn to_native_acceptor(&self) -> Result<native_tls::TlsAcceptor> {
817        // TLS-4: `native_tls::TlsAcceptorBuilder` exposes no CA-root or
818        // accept-invalid-client-cert knobs, so any non-empty CA list on
819        // this server transport is an mTLS configuration the runtime
820        // cannot honour. A `log::warn!` here is not a security boundary;
821        // proceeding silently would build an acceptor with NO client-cert
822        // verification despite the operator's expressed intent. Refuse
823        // up front (before identity parsing) so the misconfiguration is
824        // loud and surfaces independently of any identity-format errors.
825        let mtls_intent = match &self.trusted_certs {
826            TrustedCerts::CaFiles(v) => !v.is_empty(),
827            TrustedCerts::CaBytes(v) => !v.is_empty(),
828            TrustedCerts::SkipVerification => false,
829        };
830        if mtls_intent {
831            return Err(RepError::ConfigError(
832                "mTLS is configured (TrustedCerts has CA roots) but the \
833                 tls-native server transport does not support it: \
834                 native_tls::TlsAcceptorBuilder exposes no client-cert \
835                 verification knobs. Use the tls-rustls feature for mTLS, \
836                 or set TrustedCerts::SkipVerification on this transport."
837                    .into(),
838            ));
839        }
840        let identity = self.native_identity()?;
841        let builder = native_tls::TlsAcceptor::builder(identity);
842        builder
843            .build()
844            .map_err(|e| RepError::NetworkError(format!("TLS acceptor: {e}")))
845    }
846
847    /// Build a `native_tls::TlsConnector` for TCP client use.
848    pub(crate) fn to_native_connector(
849        &self,
850    ) -> Result<native_tls::TlsConnector> {
851        let mut builder = native_tls::TlsConnector::builder();
852
853        // Install identity if present (optional for client-only auth).
854        if !matches!(&self.identity, TlsIdentity::SelfSigned { .. }) {
855            let id = self.native_identity()?;
856            builder.identity(id);
857        }
858
859        self.apply_native_trust(&mut builder)?;
860        builder
861            .build()
862            .map_err(|e| RepError::NetworkError(format!("TLS connector: {e}")))
863    }
864
865    fn native_identity(&self) -> Result<native_tls::Identity> {
866        match &self.identity {
867            TlsIdentity::Pkcs12 { der, password } => native_tls::Identity::from_pkcs12(der, password)
868                .map_err(|e| RepError::NetworkError(format!("PKCS12 identity: {e}"))),
869            TlsIdentity::SelfSigned { .. } => Err(RepError::NetworkError(
870                "SelfSigned identity is not supported by the tls-native backend; \
871                 use the tls-rustls feature instead, or supply a Pkcs12 identity"
872                    .into(),
873            )),
874            TlsIdentity::PemFiles { .. } | TlsIdentity::PemBytes { .. } => {
875                Err(RepError::NetworkError(
876                    "PEM identities are not supported by the tls-native backend; \
877                     convert to PKCS12 with: openssl pkcs12 -export -out id.p12 \
878                     -inkey key.pem -in cert.pem"
879                        .into(),
880                ))
881            }
882        }
883    }
884
885    fn apply_native_trust(
886        &self,
887        builder: &mut native_tls::TlsConnectorBuilder,
888    ) -> Result<()> {
889        match &self.trusted_certs {
890            TrustedCerts::SkipVerification => {
891                builder.danger_accept_invalid_certs(true);
892            }
893            TrustedCerts::CaFiles(paths) => {
894                for path in paths {
895                    let pem = std::fs::read(path).map_err(|e| {
896                        RepError::NetworkError(format!("CA file: {e}"))
897                    })?;
898                    let cert = native_tls::Certificate::from_pem(&pem)
899                        .map_err(|e| {
900                            RepError::NetworkError(format!("CA parse: {e}"))
901                        })?;
902                    builder.add_root_certificate(cert);
903                }
904            }
905            TrustedCerts::CaBytes(pems) => {
906                for pem in pems {
907                    let cert = native_tls::Certificate::from_pem(pem).map_err(
908                        |e| RepError::NetworkError(format!("CA parse: {e}")),
909                    )?;
910                    builder.add_root_certificate(cert);
911                }
912            }
913        }
914        Ok(())
915    }
916}
917
918// `apply_native_trust` is a `TlsConnectorBuilder`-only helper. The
919// previous shared trait `NativeTlsBuilderExt` was removed because the
920// `native_tls::TlsAcceptorBuilder` does not expose `add_root_certificate`
921// or `danger_accept_invalid_certs`, and the trait impls for it were
922// unconditionally recursive (a real bug — they would have stack-
923// overflowed on first call).
924
925// ─── tests ───────────────────────────────────────────────────────────────────
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    // ── Constructors (no feature gate; pure data shape) ──────────────
932
933    #[test]
934    fn insecure_constructor_uses_self_signed_localhost() {
935        let cfg = TlsConfig::insecure("node-a");
936        assert_eq!(cfg.server_name, "node-a");
937        match cfg.identity {
938            TlsIdentity::SelfSigned { subject_alt_names } => {
939                assert_eq!(subject_alt_names, vec!["localhost".to_string()]);
940            }
941            _ => panic!("insecure should produce SelfSigned identity"),
942        }
943        assert!(matches!(cfg.trusted_certs, TrustedCerts::SkipVerification));
944    }
945
946    #[test]
947    fn from_pem_files_constructor_records_paths() {
948        let cfg = TlsConfig::from_pem_files(
949            "/tmp/cert.pem",
950            "/tmp/key.pem",
951            "/tmp/ca.pem",
952            "node-b",
953        );
954        assert_eq!(cfg.server_name, "node-b");
955        match cfg.identity {
956            TlsIdentity::PemFiles { cert, key } => {
957                assert_eq!(cert, std::path::PathBuf::from("/tmp/cert.pem"));
958                assert_eq!(key, std::path::PathBuf::from("/tmp/key.pem"));
959            }
960            _ => panic!("from_pem_files should produce PemFiles identity"),
961        }
962        match cfg.trusted_certs {
963            TrustedCerts::CaFiles(paths) => {
964                assert_eq!(
965                    paths,
966                    vec![std::path::PathBuf::from("/tmp/ca.pem")]
967                );
968            }
969            _ => panic!("from_pem_files should produce CaFiles trust"),
970        }
971    }
972
973    #[test]
974    fn from_pkcs12_constructor_holds_bytes_and_password() {
975        let der = vec![0x30, 0x82, 0x00, 0x10]; // dummy DER prefix
976        let ca_pem = b"-----BEGIN CERTIFICATE-----\n".to_vec();
977        let cfg = TlsConfig::from_pkcs12(
978            der.clone(),
979            "secret".to_string(),
980            ca_pem.clone(),
981            "node-c",
982        );
983        assert_eq!(cfg.server_name, "node-c");
984        match cfg.identity {
985            TlsIdentity::Pkcs12 { der: d, password } => {
986                assert_eq!(d, der);
987                assert_eq!(password, "secret");
988            }
989            _ => panic!("from_pkcs12 should produce Pkcs12 identity"),
990        }
991        match cfg.trusted_certs {
992            TrustedCerts::CaBytes(pems) => {
993                assert_eq!(pems, vec![ca_pem]);
994            }
995            _ => panic!("from_pkcs12 should produce CaBytes trust"),
996        }
997    }
998
999    // ── rustls path (requires tls-rustls feature) ────────────────────
1000
1001    #[cfg(feature = "tls-rustls")]
1002    #[test]
1003    fn rustls_server_config_from_self_signed_succeeds() {
1004        // SelfSigned + SkipVerification is the "insecure" config; the
1005        // rustls server side generates a fresh self-signed cert at
1006        // build time. Should produce a valid ServerConfig.
1007        let cfg = TlsConfig::insecure("node-self");
1008        let sc = cfg.to_rustls_server_config();
1009        assert!(
1010            sc.is_ok(),
1011            "to_rustls_server_config from insecure() should succeed: {:?}",
1012            sc.err()
1013        );
1014    }
1015
1016    #[cfg(feature = "tls-rustls")]
1017    #[test]
1018    fn rustls_client_config_skip_verification_succeeds() {
1019        // SkipVerification is the trust mode for development clusters;
1020        // the client config should be built using the
1021        // SkipCertVerification verifier.
1022        let cfg = TlsConfig::insecure("any-name");
1023        let cc = cfg.to_rustls_client_config();
1024        assert!(
1025            cc.is_ok(),
1026            "to_rustls_client_config with SkipVerification should succeed: \
1027             {:?}",
1028            cc.err()
1029        );
1030    }
1031
1032    #[cfg(feature = "tls-rustls")]
1033    #[test]
1034    fn rustls_client_config_with_empty_ca_bytes_errors() {
1035        // TLS-2: empty CaBytes is a misconfiguration. Without an explicit
1036        // SkipVerification opt-out, an empty trust store would validate
1037        // nothing — refuse at config-build time.
1038        let cfg = TlsConfig {
1039            identity: TlsIdentity::SelfSigned {
1040                subject_alt_names: vec!["localhost".into()],
1041            },
1042            trusted_certs: TrustedCerts::CaBytes(vec![]),
1043            server_name: "x".into(),
1044        };
1045        let cc = cfg.to_rustls_client_config();
1046        assert!(
1047            cc.is_err(),
1048            "empty CaBytes must be a misconfiguration error, got Ok"
1049        );
1050        let msg = format!("{}", cc.err().unwrap());
1051        assert!(
1052            msg.contains("CaBytes") && msg.contains("misconfiguration"),
1053            "error should mention CaBytes/misconfiguration, got: {msg}"
1054        );
1055    }
1056
1057    #[cfg(feature = "tls-rustls")]
1058    #[test]
1059    fn rustls_client_config_with_malformed_ca_bytes_errors() {
1060        // TLS-3: bytes were provided but rustls_pemfile produced 0 certs.
1061        // Refuse rather than silently build an empty trust store.
1062        let cfg = TlsConfig {
1063            identity: TlsIdentity::SelfSigned {
1064                subject_alt_names: vec!["localhost".into()],
1065            },
1066            trusted_certs: TrustedCerts::CaBytes(vec![b"not-a-pem".to_vec()]),
1067            server_name: "x".into(),
1068        };
1069        let cc = cfg.to_rustls_client_config();
1070        assert!(
1071            cc.is_err(),
1072            "malformed CaBytes must error rather than build an empty store, \
1073             got Ok"
1074        );
1075        let msg = format!("{}", cc.err().unwrap());
1076        assert!(
1077            msg.contains("0 certificates"),
1078            "error should mention 0 certificates, got: {msg}"
1079        );
1080    }
1081
1082    #[cfg(feature = "tls-rustls")]
1083    #[test]
1084    fn skip_cert_verification_returns_ok_for_any_cert() {
1085        use rustls::client::danger::ServerCertVerifier;
1086        let v = SkipCertVerification::new();
1087        let cert = rustls::pki_types::CertificateDer::from(vec![0u8; 8]);
1088        let server_name =
1089            rustls::pki_types::ServerName::try_from("localhost").unwrap();
1090        let now = rustls::pki_types::UnixTime::now();
1091        let r = v.verify_server_cert(&cert, &[], &server_name, &[], now);
1092        assert!(r.is_ok(), "SkipCertVerification must return Ok for any cert");
1093    }
1094
1095    #[cfg(feature = "tls-rustls")]
1096    #[test]
1097    fn skip_cert_verification_supports_some_schemes() {
1098        use rustls::client::danger::ServerCertVerifier;
1099        let v = SkipCertVerification::new();
1100        let schemes = v.supported_verify_schemes();
1101        assert!(
1102            !schemes.is_empty(),
1103            "SkipCertVerification must report at least one signature scheme"
1104        );
1105    }
1106
1107    // ── native-tls path (requires tls-native feature) ────────────────
1108
1109    #[cfg(feature = "tls-native")]
1110    #[test]
1111    fn native_acceptor_requires_pkcs12_identity() {
1112        // SelfSigned identity is rejected because native_tls cannot
1113        // generate certs at runtime (only Pkcs12 is supported).
1114        let cfg = TlsConfig {
1115            identity: TlsIdentity::SelfSigned {
1116                subject_alt_names: vec!["localhost".into()],
1117            },
1118            trusted_certs: TrustedCerts::SkipVerification,
1119            server_name: "x".into(),
1120        };
1121        let r = cfg.to_native_acceptor();
1122        assert!(
1123            r.is_err(),
1124            "SelfSigned identity with native-tls must error, got Ok"
1125        );
1126    }
1127
1128    #[cfg(feature = "tls-native")]
1129    #[test]
1130    fn native_connector_skip_verification_succeeds() {
1131        let cfg = TlsConfig {
1132            identity: TlsIdentity::SelfSigned {
1133                subject_alt_names: vec!["localhost".into()],
1134            },
1135            trusted_certs: TrustedCerts::SkipVerification,
1136            server_name: "any".into(),
1137        };
1138        // The client side does not need to load the local identity
1139        // (clients without a cert is normal).
1140        let r = cfg.to_native_connector();
1141        assert!(
1142            r.is_ok(),
1143            "native_tls client with SkipVerification should succeed: {:?}",
1144            r.err()
1145        );
1146    }
1147
1148    // ── End-to-end with real X.509 (uses rcgen, only available
1149    //    under tls-rustls because that's where rcgen is gated). ──
1150
1151    #[cfg(feature = "tls-rustls")]
1152    fn make_self_signed_pem(san: &[&str]) -> (Vec<u8>, Vec<u8>) {
1153        // Returns (cert_pem_bytes, key_pem_bytes).
1154        let sans: Vec<String> = san.iter().map(|s| s.to_string()).collect();
1155        let ck = rcgen::generate_simple_self_signed(sans).unwrap();
1156        let cert_pem = ck.cert.pem().into_bytes();
1157        let key_pem = ck.key_pair.serialize_pem().into_bytes();
1158        (cert_pem, key_pem)
1159    }
1160
1161    #[cfg(feature = "tls-rustls")]
1162    #[test]
1163    fn rustls_server_config_from_pem_bytes() {
1164        // Generate a real self-signed pair and feed it to the
1165        // server-config builder via PemBytes.
1166        let (cert_pem, key_pem) = make_self_signed_pem(&["localhost"]);
1167        let cfg = TlsConfig {
1168            identity: TlsIdentity::PemBytes { cert: cert_pem, key: key_pem },
1169            trusted_certs: TrustedCerts::SkipVerification,
1170            server_name: "localhost".into(),
1171        };
1172        let sc = cfg.to_rustls_server_config();
1173        assert!(sc.is_ok(), "PemBytes server config: {:?}", sc.err());
1174    }
1175
1176    #[cfg(feature = "tls-rustls")]
1177    #[test]
1178    fn rustls_server_config_from_pem_files_on_disk() {
1179        // Write the generated cert/key to tempfiles, then use the
1180        // PemFiles identity. This exercises the file-IO path in
1181        // rustls_cert_and_key.
1182        let (cert_pem, key_pem) = make_self_signed_pem(&["localhost"]);
1183        let dir = tempfile::tempdir().unwrap();
1184        let cert_path = dir.path().join("cert.pem");
1185        let key_path = dir.path().join("key.pem");
1186        std::fs::write(&cert_path, &cert_pem).unwrap();
1187        std::fs::write(&key_path, &key_pem).unwrap();
1188
1189        let cfg = TlsConfig {
1190            identity: TlsIdentity::PemFiles { cert: cert_path, key: key_path },
1191            trusted_certs: TrustedCerts::SkipVerification,
1192            server_name: "localhost".into(),
1193        };
1194        let sc = cfg.to_rustls_server_config();
1195        assert!(sc.is_ok(), "PemFiles server config: {:?}", sc.err());
1196    }
1197
1198    #[cfg(feature = "tls-rustls")]
1199    #[test]
1200    fn rustls_client_config_with_real_ca_bytes() {
1201        // Use a generated cert as a "CA" — rustls accepts it as a
1202        // root cert; that's enough to exercise the full
1203        // CaBytes -> RootCertStore::add path.
1204        let (ca_pem, _ca_key) = make_self_signed_pem(&["test-ca"]);
1205        let cfg = TlsConfig {
1206            identity: TlsIdentity::SelfSigned {
1207                subject_alt_names: vec!["localhost".into()],
1208            },
1209            trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
1210            server_name: "localhost".into(),
1211        };
1212        let cc = cfg.to_rustls_client_config();
1213        assert!(cc.is_ok(), "real CA bytes: {:?}", cc.err());
1214    }
1215
1216    #[cfg(feature = "tls-rustls")]
1217    #[test]
1218    fn rustls_client_config_with_real_ca_file() {
1219        let (ca_pem, _ca_key) = make_self_signed_pem(&["test-ca"]);
1220        let dir = tempfile::tempdir().unwrap();
1221        let ca_path = dir.path().join("ca.pem");
1222        std::fs::write(&ca_path, &ca_pem).unwrap();
1223
1224        let cfg = TlsConfig {
1225            identity: TlsIdentity::SelfSigned {
1226                subject_alt_names: vec!["localhost".into()],
1227            },
1228            trusted_certs: TrustedCerts::CaFiles(vec![ca_path]),
1229            server_name: "localhost".into(),
1230        };
1231        let cc = cfg.to_rustls_client_config();
1232        assert!(cc.is_ok(), "real CA file: {:?}", cc.err());
1233    }
1234
1235    #[cfg(feature = "tls-rustls")]
1236    #[test]
1237    fn rustls_server_config_with_pem_files_missing_cert_errors() {
1238        let dir = tempfile::tempdir().unwrap();
1239        let nonexistent = dir.path().join("does-not-exist.pem");
1240        let key_path = dir.path().join("key.pem");
1241        let (_, key_pem) = make_self_signed_pem(&["localhost"]);
1242        std::fs::write(&key_path, &key_pem).unwrap();
1243
1244        let cfg = TlsConfig {
1245            identity: TlsIdentity::PemFiles {
1246                cert: nonexistent,
1247                key: key_path,
1248            },
1249            trusted_certs: TrustedCerts::SkipVerification,
1250            server_name: "localhost".into(),
1251        };
1252        let sc = cfg.to_rustls_server_config();
1253        assert!(sc.is_err(), "missing cert file should error, got Ok");
1254    }
1255
1256    #[cfg(feature = "tls-rustls")]
1257    #[test]
1258    fn rustls_server_config_with_pem_files_missing_key_errors() {
1259        let dir = tempfile::tempdir().unwrap();
1260        let cert_path = dir.path().join("cert.pem");
1261        let nonexistent = dir.path().join("nonexistent-key.pem");
1262        let (cert_pem, _) = make_self_signed_pem(&["localhost"]);
1263        std::fs::write(&cert_path, &cert_pem).unwrap();
1264
1265        let cfg = TlsConfig {
1266            identity: TlsIdentity::PemFiles {
1267                cert: cert_path,
1268                key: nonexistent,
1269            },
1270            trusted_certs: TrustedCerts::SkipVerification,
1271            server_name: "localhost".into(),
1272        };
1273        let sc = cfg.to_rustls_server_config();
1274        assert!(sc.is_err(), "missing key file should error, got Ok");
1275    }
1276
1277    #[cfg(feature = "tls-rustls")]
1278    #[test]
1279    fn rustls_root_store_with_malformed_ca_file_errors() {
1280        // TLS-3: the file has bytes but rustls_pemfile decodes 0
1281        // certificates. Surface this as a structured error rather
1282        // than silently producing an empty trust store.
1283        let dir = tempfile::tempdir().unwrap();
1284        let bad_ca = dir.path().join("bad.pem");
1285        std::fs::write(&bad_ca, b"this is not a PEM file\n").unwrap();
1286
1287        let cfg = TlsConfig {
1288            identity: TlsIdentity::SelfSigned {
1289                subject_alt_names: vec!["localhost".into()],
1290            },
1291            trusted_certs: TrustedCerts::CaFiles(vec![bad_ca]),
1292            server_name: "x".into(),
1293        };
1294        let cc = cfg.to_rustls_client_config();
1295        assert!(
1296            cc.is_err(),
1297            "garbage CA file must error rather than yield empty trust"
1298        );
1299        let msg = format!("{}", cc.err().unwrap());
1300        assert!(
1301            msg.contains("0 certificates"),
1302            "error should mention 0 certificates, got: {msg}"
1303        );
1304    }
1305
1306    #[cfg(feature = "tls-rustls")]
1307    #[test]
1308    fn rustls_client_config_with_missing_ca_file_errors() {
1309        let cfg = TlsConfig {
1310            identity: TlsIdentity::SelfSigned {
1311                subject_alt_names: vec!["localhost".into()],
1312            },
1313            trusted_certs: TrustedCerts::CaFiles(vec![
1314                std::path::PathBuf::from("/nonexistent/ca.pem"),
1315            ]),
1316            server_name: "x".into(),
1317        };
1318        let cc = cfg.to_rustls_client_config();
1319        assert!(cc.is_err(), "missing CA file should error");
1320    }
1321
1322    #[cfg(feature = "tls-rustls")]
1323    #[test]
1324    fn rustls_server_config_self_signed_runtime() {
1325        // SelfSigned identity goes through rcgen at build time inside
1326        // rustls_cert_and_key. Verify the generated cert is parseable
1327        // and the ServerConfig builds.
1328        let cfg = TlsConfig {
1329            identity: TlsIdentity::SelfSigned {
1330                subject_alt_names: vec!["host-a".into(), "host-b".into()],
1331            },
1332            trusted_certs: TrustedCerts::SkipVerification,
1333            server_name: "host-a".into(),
1334        };
1335        let sc = cfg.to_rustls_server_config();
1336        assert!(sc.is_ok(), "SelfSigned runtime cert: {:?}", sc.err());
1337    }
1338
1339    // ── verify_tls12_signature / verify_tls13_signature exercise ──
1340    //
1341    // Skipped at the unit-test level because constructing a
1342    // `rustls::DigitallySignedStruct` requires a private API; a
1343    // future integration test that performs a real TLS handshake
1344    // (TlsTcpChannel + TlsTcpChannelListener with a generated cert
1345    // pair) will exercise these arms naturally.
1346
1347    #[cfg(feature = "tls-rustls")]
1348    #[test]
1349    fn rustls_pkcs12_identity_is_rejected() {
1350        // The tls-rustls backend explicitly rejects Pkcs12 (it's the
1351        // tls-native identity); covers the dedicated error arm.
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_rustls_server_config();
1361        assert!(r.is_err(), "Pkcs12 with rustls must error");
1362        let msg = format!("{}", r.err().unwrap());
1363        assert!(
1364            msg.contains("Pkcs12") || msg.contains("not supported"),
1365            "error should mention Pkcs12 or not-supported, got: {msg}"
1366        );
1367    }
1368
1369    #[cfg(feature = "tls-rustls")]
1370    #[test]
1371    fn rustls_pem_bytes_no_certificates_errors() {
1372        let cfg = TlsConfig {
1373            identity: TlsIdentity::PemBytes {
1374                cert: b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n"
1375                    .to_vec(),
1376                key: b"-----BEGIN PRIVATE KEY-----\nMC4CAQA=\n-----END PRIVATE KEY-----\n"
1377                    .to_vec(),
1378            },
1379            trusted_certs: TrustedCerts::SkipVerification,
1380            server_name: "x".into(),
1381        };
1382        let r = cfg.to_rustls_server_config();
1383        assert!(r.is_err(), "PEM with no certificates must error, got Ok");
1384    }
1385
1386    #[cfg(feature = "tls-rustls")]
1387    #[test]
1388    fn rustls_pem_bytes_no_private_key_errors() {
1389        let (cert_pem, _key_pem) = make_self_signed_pem(&["localhost"]);
1390        let cfg = TlsConfig {
1391            identity: TlsIdentity::PemBytes {
1392                cert: cert_pem,
1393                key: b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n"
1394                    .to_vec(),
1395            },
1396            trusted_certs: TrustedCerts::SkipVerification,
1397            server_name: "x".into(),
1398        };
1399        let r = cfg.to_rustls_server_config();
1400        assert!(r.is_err(), "PEM with no private key must error, got Ok");
1401    }
1402
1403    #[cfg(feature = "tls-rustls")]
1404    #[test]
1405    fn rustls_skip_verification_client_config_succeeds() {
1406        // SkipVerification is the explicit "skip-verify" opt-out; it must
1407        // continue to build a client config without requiring CA roots.
1408        // (TLS-2 enforces the empty-CaBytes/CaFiles path errors instead.)
1409        let skip_cfg = TlsConfig::insecure("localhost");
1410        let cc = skip_cfg.to_rustls_client_config();
1411        assert!(cc.is_ok());
1412    }
1413
1414    // ── Security-review hardening: TLS-2, TLS-3, TLS-4 ──
1415    //
1416    // Each test below targets a specific finding from
1417    // the 2026 review. They fail before the
1418    // hardening change and pass after it.
1419
1420    // TLS-2: empty `CaFiles(vec![])` is a misconfiguration, not a silent
1421    // empty trust store.
1422    #[cfg(feature = "tls-rustls")]
1423    #[test]
1424    fn tls2_empty_ca_files_errors() {
1425        let cfg = TlsConfig {
1426            identity: TlsIdentity::SelfSigned {
1427                subject_alt_names: vec!["localhost".into()],
1428            },
1429            trusted_certs: TrustedCerts::CaFiles(vec![]),
1430            server_name: "x".into(),
1431        };
1432        let cc = cfg.to_rustls_client_config();
1433        assert!(
1434            cc.is_err(),
1435            "empty CaFiles must be a misconfiguration error, got Ok"
1436        );
1437        let msg = format!("{}", cc.err().unwrap());
1438        assert!(
1439            msg.contains("CaFiles") && msg.contains("misconfiguration"),
1440            "error should mention CaFiles/misconfiguration, got: {msg}"
1441        );
1442    }
1443
1444    // TLS-2: empty `CaBytes(vec![])` is a misconfiguration.
1445    #[cfg(feature = "tls-rustls")]
1446    #[test]
1447    fn tls2_empty_ca_bytes_errors() {
1448        let cfg = TlsConfig {
1449            identity: TlsIdentity::SelfSigned {
1450                subject_alt_names: vec!["localhost".into()],
1451            },
1452            trusted_certs: TrustedCerts::CaBytes(vec![]),
1453            server_name: "x".into(),
1454        };
1455        let cc = cfg.to_rustls_client_config();
1456        assert!(
1457            cc.is_err(),
1458            "empty CaBytes must be a misconfiguration error, got Ok"
1459        );
1460        let msg = format!("{}", cc.err().unwrap());
1461        assert!(
1462            msg.contains("CaBytes") && msg.contains("misconfiguration"),
1463            "error should mention CaBytes/misconfiguration, got: {msg}"
1464        );
1465    }
1466
1467    // TLS-2: `SkipVerification` is preserved as the explicit opt-out.
1468    #[cfg(feature = "tls-rustls")]
1469    #[test]
1470    fn tls2_skip_verification_still_works() {
1471        let cfg = TlsConfig::insecure("localhost");
1472        let cc = cfg.to_rustls_client_config();
1473        assert!(
1474            cc.is_ok(),
1475            "SkipVerification must remain the supported opt-out, got Err: \
1476             {:?}",
1477            cc.err()
1478        );
1479    }
1480
1481    // TLS-3: a non-empty PEM byte-blob that decodes to zero certificates
1482    // is a parse error, not a silent empty trust store.
1483    #[cfg(feature = "tls-rustls")]
1484    #[test]
1485    fn tls3_ca_bytes_with_zero_decoded_certs_errors() {
1486        let cfg = TlsConfig {
1487            identity: TlsIdentity::SelfSigned {
1488                subject_alt_names: vec!["localhost".into()],
1489            },
1490            trusted_certs: TrustedCerts::CaBytes(vec![
1491                b"this looks like text but is not a PEM certificate\n".to_vec(),
1492            ]),
1493            server_name: "x".into(),
1494        };
1495        let cc = cfg.to_rustls_client_config();
1496        assert!(
1497            cc.is_err(),
1498            "non-empty PEM with zero certs must error, got Ok"
1499        );
1500        let msg = format!("{}", cc.err().unwrap());
1501        assert!(
1502            msg.contains("0 certificates"),
1503            "error should mention 0 certificates, got: {msg}"
1504        );
1505    }
1506
1507    // TLS-3: a non-empty CA file that decodes to zero certificates errors.
1508    #[cfg(feature = "tls-rustls")]
1509    #[test]
1510    fn tls3_ca_file_with_zero_decoded_certs_errors() {
1511        let dir = tempfile::tempdir().unwrap();
1512        let bad_ca = dir.path().join("bad.pem");
1513        // PEM-shaped wrapper around a non-cert label — rustls_pemfile
1514        // accepts no certificates.
1515        std::fs::write(
1516            &bad_ca,
1517            b"-----BEGIN GARBAGE-----\nAAAA\n-----END GARBAGE-----\n",
1518        )
1519        .unwrap();
1520
1521        let cfg = TlsConfig {
1522            identity: TlsIdentity::SelfSigned {
1523                subject_alt_names: vec!["localhost".into()],
1524            },
1525            trusted_certs: TrustedCerts::CaFiles(vec![bad_ca.clone()]),
1526            server_name: "x".into(),
1527        };
1528        let cc = cfg.to_rustls_client_config();
1529        assert!(cc.is_err(), "CA file with 0 certificates must error");
1530        let msg = format!("{}", cc.err().unwrap());
1531        assert!(
1532            msg.contains("0 certificates")
1533                && msg.contains(&bad_ca.display().to_string()),
1534            "error should mention 0 certificates and the file path, got: \
1535             {msg}"
1536        );
1537    }
1538
1539    // TLS-4: a `tls-native` server with mTLS intent (non-empty CA roots)
1540    // must error with an mTLS-specific message, since
1541    // native_tls::TlsAcceptorBuilder cannot enforce client-cert
1542    // verification. A warning is not a security boundary. The check
1543    // runs before identity parsing so that the misconfiguration
1544    // surfaces independently of any identity-format issues.
1545    #[cfg(feature = "tls-native")]
1546    #[test]
1547    fn tls4_native_acceptor_with_ca_files_intent_errors() {
1548        let cfg = TlsConfig {
1549            identity: TlsIdentity::Pkcs12 {
1550                der: vec![0x30, 0x82, 0x00, 0x10],
1551                password: "x".into(),
1552            },
1553            trusted_certs: TrustedCerts::CaFiles(vec![
1554                std::path::PathBuf::from("/etc/ssl/certs/ca.pem"),
1555            ]),
1556            server_name: "x".into(),
1557        };
1558        let r = cfg.to_native_acceptor();
1559        assert!(
1560            r.is_err(),
1561            "mTLS intent on tls-native server must error rather than warn"
1562        );
1563        let msg = format!("{}", r.err().unwrap());
1564        assert!(
1565            msg.contains("mTLS")
1566                && msg.contains("tls-native")
1567                && msg.contains("tls-rustls"),
1568            "error must point at mTLS / tls-native / tls-rustls remediation, \
1569             got: {msg}"
1570        );
1571    }
1572
1573    #[cfg(feature = "tls-native")]
1574    #[test]
1575    fn tls4_native_acceptor_with_ca_bytes_intent_errors() {
1576        let cfg = TlsConfig {
1577            identity: TlsIdentity::Pkcs12 {
1578                der: vec![0x30, 0x82, 0x00, 0x10],
1579                password: "x".into(),
1580            },
1581            trusted_certs: TrustedCerts::CaBytes(vec![
1582                b"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"
1583                    .to_vec(),
1584            ]),
1585            server_name: "x".into(),
1586        };
1587        let r = cfg.to_native_acceptor();
1588        assert!(
1589            r.is_err(),
1590            "non-empty CaBytes on tls-native server must error"
1591        );
1592        let msg = format!("{}", r.err().unwrap());
1593        assert!(msg.contains("mTLS"), "error must mention mTLS, got: {msg}");
1594    }
1595
1596    // TLS-4: SkipVerification (no mTLS intent) must remain functional on
1597    // the tls-native server path — the Refusal is conditional on intent.
1598    #[cfg(feature = "tls-native")]
1599    #[test]
1600    fn tls4_native_acceptor_skip_verification_unaffected() {
1601        // SkipVerification = no mTLS intent, so the new TLS-4 check
1602        // must not fire. The dummy DER will fail at native_identity,
1603        // but the failure must NOT come from the mTLS misconfiguration
1604        // check (verified by message).
1605        let cfg = TlsConfig {
1606            identity: TlsIdentity::Pkcs12 {
1607                der: vec![0x30, 0x82, 0x00, 0x10],
1608                password: "x".into(),
1609            },
1610            trusted_certs: TrustedCerts::SkipVerification,
1611            server_name: "x".into(),
1612        };
1613        let r = cfg.to_native_acceptor();
1614        if let Err(e) = r {
1615            let msg = format!("{e}");
1616            assert!(
1617                !msg.contains("mTLS"),
1618                "SkipVerification must not trigger mTLS check, got: {msg}"
1619            );
1620        }
1621    }
1622
1623    // ── QUIC config builders (under quic feature) ──
1624
1625    #[cfg(all(feature = "tls-rustls", feature = "quic"))]
1626    #[test]
1627    fn quinn_server_config_builds_from_self_signed() {
1628        let cfg = TlsConfig::insecure("localhost");
1629        let qc = cfg.to_quinn_server_config();
1630        assert!(qc.is_ok(), "quinn server config: {:?}", qc.err());
1631    }
1632
1633    #[cfg(all(feature = "tls-rustls", feature = "quic"))]
1634    #[test]
1635    fn quinn_client_config_builds_from_skip_verification() {
1636        let cfg = TlsConfig::insecure("localhost");
1637        let qc = cfg.to_quinn_client_config();
1638        assert!(qc.is_ok(), "quinn client config: {:?}", qc.err());
1639    }
1640
1641    #[cfg(all(feature = "tls-rustls", feature = "quic"))]
1642    #[test]
1643    fn quinn_client_config_with_real_ca_bytes() {
1644        let (ca_pem, _) = make_self_signed_pem(&["test-ca"]);
1645        let cfg = TlsConfig {
1646            identity: TlsIdentity::SelfSigned {
1647                subject_alt_names: vec!["localhost".into()],
1648            },
1649            trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
1650            server_name: "localhost".into(),
1651        };
1652        let qc = cfg.to_quinn_client_config();
1653        assert!(qc.is_ok(), "quinn client config with CA: {:?}", qc.err());
1654    }
1655
1656    // ── for_replication: stricter constructor for production
1657    //    replication. Rejects every shape that would produce
1658    //    an unauthenticated transport.
1659
1660    #[test]
1661    fn for_replication_rejects_self_signed_identity() {
1662        let r = TlsConfig::for_replication(
1663            TlsIdentity::SelfSigned { subject_alt_names: vec!["x".into()] },
1664            TrustedCerts::CaBytes(vec![
1665                b"-----BEGIN CERTIFICATE-----".to_vec(),
1666            ]),
1667            "x",
1668        );
1669        assert!(r.is_err(), "self-signed identity must be rejected");
1670        let msg = format!("{}", r.err().unwrap());
1671        assert!(
1672            msg.contains("SelfSigned") || msg.contains("self-signed"),
1673            "error must mention SelfSigned, got: {msg}"
1674        );
1675    }
1676
1677    #[test]
1678    fn for_replication_rejects_skip_verification() {
1679        let r = TlsConfig::for_replication(
1680            TlsIdentity::PemBytes { cert: vec![], key: vec![] },
1681            TrustedCerts::SkipVerification,
1682            "x",
1683        );
1684        assert!(r.is_err());
1685        let msg = format!("{}", r.err().unwrap());
1686        assert!(
1687            msg.contains("SkipVerification") || msg.contains("skip"),
1688            "error must mention SkipVerification, got: {msg}"
1689        );
1690    }
1691
1692    #[test]
1693    fn for_replication_rejects_empty_ca_files() {
1694        let r = TlsConfig::for_replication(
1695            TlsIdentity::PemFiles {
1696                cert: "/tmp/cert.pem".into(),
1697                key: "/tmp/key.pem".into(),
1698            },
1699            TrustedCerts::CaFiles(vec![]),
1700            "x",
1701        );
1702        assert!(r.is_err());
1703        let msg = format!("{}", r.err().unwrap());
1704        assert!(msg.contains("empty CaFiles"));
1705    }
1706
1707    #[test]
1708    fn for_replication_rejects_empty_ca_bytes() {
1709        let r = TlsConfig::for_replication(
1710            TlsIdentity::PemFiles {
1711                cert: "/tmp/cert.pem".into(),
1712                key: "/tmp/key.pem".into(),
1713            },
1714            TrustedCerts::CaBytes(vec![]),
1715            "x",
1716        );
1717        assert!(r.is_err());
1718        let msg = format!("{}", r.err().unwrap());
1719        assert!(msg.contains("empty CaBytes"));
1720    }
1721
1722    #[test]
1723    fn for_replication_accepts_pem_files_with_real_ca() {
1724        let r = TlsConfig::for_replication(
1725            TlsIdentity::PemFiles {
1726                cert: "/etc/noxu/cert.pem".into(),
1727                key: "/etc/noxu/key.pem".into(),
1728            },
1729            TrustedCerts::CaFiles(vec!["/etc/noxu/ca.pem".into()]),
1730            "node-1.cluster.example",
1731        );
1732        assert!(r.is_ok());
1733        let cfg = r.unwrap();
1734        assert_eq!(cfg.server_name, "node-1.cluster.example");
1735    }
1736
1737    #[test]
1738    fn for_replication_accepts_pkcs12_with_ca() {
1739        let r = TlsConfig::for_replication(
1740            TlsIdentity::Pkcs12 { der: vec![0; 128], password: "p".into() },
1741            TrustedCerts::CaBytes(vec![
1742                b"-----BEGIN CERTIFICATE-----".to_vec(),
1743            ]),
1744            "node-2",
1745        );
1746        assert!(r.is_ok());
1747    }
1748}