1use crate::error::{RepError, Result};
51
52#[derive(Clone)]
70#[non_exhaustive]
71pub enum TlsIdentity {
72 SelfSigned {
78 subject_alt_names: Vec<String>,
81 },
82
83 PemFiles {
87 cert: std::path::PathBuf,
89 key: std::path::PathBuf,
91 },
92
93 PemBytes {
97 cert: Vec<u8>,
99 key: Vec<u8>,
101 },
102
103 Pkcs12 {
112 der: Vec<u8>,
114 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#[derive(Clone)]
149#[non_exhaustive]
150pub enum TrustedCerts {
151 SkipVerification,
156
157 CaFiles(Vec<std::path::PathBuf>),
159
160 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#[derive(Clone)]
191pub struct TlsConfig {
192 pub identity: TlsIdentity,
194 pub trusted_certs: TrustedCerts,
196 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 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 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 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 pub fn for_replication(
291 identity: TlsIdentity,
292 trusted_certs: TrustedCerts,
293 server_name: impl Into<String>,
294 ) -> Result<Self> {
295 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 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#[cfg(feature = "tls-rustls")]
346impl TlsConfig {
347 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 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 pub(crate) fn to_rustls_client_config(
429 &self,
430 ) -> Result<std::sync::Arc<rustls::ClientConfig>> {
431 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 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 #[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 #[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 #[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 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 }
652 TrustedCerts::CaFiles(paths) => {
653 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 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 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 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#[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#[cfg(feature = "tls-native")]
809impl TlsConfig {
810 pub(crate) fn to_native_acceptor(&self) -> Result<native_tls::TlsAcceptor> {
817 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 pub(crate) fn to_native_connector(
849 &self,
850 ) -> Result<native_tls::TlsConnector> {
851 let mut builder = native_tls::TlsConnector::builder();
852
853 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#[cfg(test)]
928mod tests {
929 use super::*;
930
931 #[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]; 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 #[cfg(feature = "tls-rustls")]
1002 #[test]
1003 fn rustls_server_config_from_self_signed_succeeds() {
1004 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 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 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 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 #[cfg(feature = "tls-native")]
1110 #[test]
1111 fn native_acceptor_requires_pkcs12_identity() {
1112 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 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 #[cfg(feature = "tls-rustls")]
1152 fn make_self_signed_pem(san: &[&str]) -> (Vec<u8>, Vec<u8>) {
1153 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 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 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 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 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 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 #[cfg(feature = "tls-rustls")]
1348 #[test]
1349 fn rustls_pkcs12_identity_is_rejected() {
1350 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 let skip_cfg = TlsConfig::insecure("localhost");
1410 let cc = skip_cfg.to_rustls_client_config();
1411 assert!(cc.is_ok());
1412 }
1413
1414 #[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 #[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 #[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 #[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 #[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 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 #[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 #[cfg(feature = "tls-native")]
1599 #[test]
1600 fn tls4_native_acceptor_skip_verification_unaffected() {
1601 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 #[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 #[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}