#[cfg(any(feature = "tls-rustls", feature = "tls-native"))]
use crate::error::{RepError, Result};
#[derive(Clone)]
#[non_exhaustive]
pub enum TlsIdentity {
SelfSigned {
subject_alt_names: Vec<String>,
},
PemFiles {
cert: std::path::PathBuf,
key: std::path::PathBuf,
},
PemBytes {
cert: Vec<u8>,
key: Vec<u8>,
},
Pkcs12 {
der: Vec<u8>,
password: String,
},
}
#[derive(Clone)]
#[non_exhaustive]
pub enum TrustedCerts {
SkipVerification,
CaFiles(Vec<std::path::PathBuf>),
CaBytes(Vec<Vec<u8>>),
}
#[derive(Clone)]
pub struct TlsConfig {
pub identity: TlsIdentity,
pub trusted_certs: TrustedCerts,
pub server_name: String,
}
impl TlsConfig {
pub fn insecure(server_name: impl Into<String>) -> Self {
TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: server_name.into(),
}
}
pub fn from_pem_files(
cert: impl Into<std::path::PathBuf>,
key: impl Into<std::path::PathBuf>,
ca: impl Into<std::path::PathBuf>,
server_name: impl Into<String>,
) -> Self {
TlsConfig {
identity: TlsIdentity::PemFiles {
cert: cert.into(),
key: key.into(),
},
trusted_certs: TrustedCerts::CaFiles(vec![ca.into()]),
server_name: server_name.into(),
}
}
pub fn from_pkcs12(
der: Vec<u8>,
password: impl Into<String>,
ca_pem: Vec<u8>,
server_name: impl Into<String>,
) -> Self {
TlsConfig {
identity: TlsIdentity::Pkcs12 { der, password: password.into() },
trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
server_name: server_name.into(),
}
}
}
#[cfg(feature = "tls-rustls")]
impl TlsConfig {
pub(crate) fn to_rustls_server_config(
&self,
) -> Result<std::sync::Arc<rustls::ServerConfig>> {
let (certs, key) = self.rustls_cert_and_key()?;
let cfg = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| {
RepError::NetworkError(format!("TLS server config: {e}"))
})?;
Ok(std::sync::Arc::new(cfg))
}
pub(crate) fn to_rustls_client_config(
&self,
) -> Result<std::sync::Arc<rustls::ClientConfig>> {
if matches!(&self.trusted_certs, TrustedCerts::SkipVerification) {
let cfg = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(
SkipCertVerification::new(),
))
.with_no_client_auth();
return Ok(std::sync::Arc::new(cfg));
}
let root_store = self.rustls_root_store()?;
let cfg = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(std::sync::Arc::new(cfg))
}
#[cfg(feature = "quic")]
pub fn to_quinn_server_config(&self) -> Result<quinn::ServerConfig> {
let rustls_cfg = self.to_rustls_server_config()?;
let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(
rustls::ServerConfig::clone(&rustls_cfg),
)
.map_err(|e| {
RepError::NetworkError(format!("QUIC server config: {e}"))
})?;
let mut cfg =
quinn::ServerConfig::with_crypto(std::sync::Arc::new(quic_cfg));
let mut transport = quinn::TransportConfig::default();
transport.mtu_discovery_config(None);
transport.datagram_receive_buffer_size(Some(64 * 1024));
cfg.transport_config(std::sync::Arc::new(transport));
Ok(cfg)
}
#[cfg(feature = "quic")]
pub fn to_quinn_client_config(&self) -> Result<quinn::ClientConfig> {
let rustls_cfg = self.to_rustls_client_config()?;
let quic_cfg = quinn::crypto::rustls::QuicClientConfig::try_from(
rustls::ClientConfig::clone(&rustls_cfg),
)
.map_err(|e| {
RepError::NetworkError(format!("QUIC client config: {e}"))
})?;
let mut cfg = quinn::ClientConfig::new(std::sync::Arc::new(quic_cfg));
let mut transport = quinn::TransportConfig::default();
transport.mtu_discovery_config(None);
transport.datagram_receive_buffer_size(Some(64 * 1024));
cfg.transport_config(std::sync::Arc::new(transport));
Ok(cfg)
}
fn rustls_cert_and_key(
&self,
) -> Result<(
Vec<rustls::pki_types::CertificateDer<'static>>,
rustls::pki_types::PrivateKeyDer<'static>,
)> {
use rustls::pki_types::{
CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer,
};
match &self.identity {
TlsIdentity::SelfSigned { subject_alt_names } => {
let ck = rcgen::generate_simple_self_signed(
subject_alt_names.clone(),
)
.map_err(|e| RepError::NetworkError(format!("rcgen: {e}")))?;
let cert = CertificateDer::from(ck.cert.der().to_vec());
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
ck.key_pair.serialize_der(),
));
Ok((vec![cert], key))
}
TlsIdentity::PemFiles { cert, key } => {
let cert_bytes = std::fs::read(cert).map_err(|e| {
RepError::NetworkError(format!("cert file: {e}"))
})?;
let key_bytes = std::fs::read(key).map_err(|e| {
RepError::NetworkError(format!("key file: {e}"))
})?;
Self::parse_pem_cert_and_key(&cert_bytes, &key_bytes)
}
TlsIdentity::PemBytes { cert, key } => {
Self::parse_pem_cert_and_key(cert, key)
}
TlsIdentity::Pkcs12 { .. } => Err(RepError::NetworkError(
"Pkcs12 identity is not supported by the tls-rustls backend; \
use PemFiles or PemBytes instead"
.into(),
)),
}
}
fn parse_pem_cert_and_key(
cert_pem: &[u8],
key_pem: &[u8],
) -> Result<(
Vec<rustls::pki_types::CertificateDer<'static>>,
rustls::pki_types::PrivateKeyDer<'static>,
)> {
use rustls_pemfile::{certs, private_key};
use std::io::BufReader;
let cert_chain: Vec<_> = certs(&mut BufReader::new(cert_pem))
.collect::<std::result::Result<_, _>>()
.map_err(|e| RepError::NetworkError(format!("cert parse: {e}")))?;
if cert_chain.is_empty() {
return Err(RepError::NetworkError(
"no certificates found in PEM".into(),
));
}
let key = private_key(&mut BufReader::new(key_pem))
.map_err(|e| RepError::NetworkError(format!("key parse: {e}")))?
.ok_or_else(|| {
RepError::NetworkError("no private key found in PEM".into())
})?;
Ok((cert_chain, key))
}
fn rustls_root_store(&self) -> Result<rustls::RootCertStore> {
use rustls_pemfile::certs;
use std::io::BufReader;
let mut store = rustls::RootCertStore::empty();
match &self.trusted_certs {
TrustedCerts::SkipVerification => {
}
TrustedCerts::CaFiles(paths) => {
if paths.is_empty() {
return Err(RepError::ConfigError(
"TrustedCerts::CaFiles configured with no paths; \
this is a misconfiguration. Use \
TrustedCerts::SkipVerification to explicitly opt \
out of CA verification."
.into(),
));
}
for path in paths {
let pem = std::fs::read(path).map_err(|e| {
RepError::NetworkError(format!("CA file: {e}"))
})?;
let parsed: Vec<_> =
certs(&mut BufReader::new(pem.as_slice()))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| {
RepError::NetworkError(format!("CA parse: {e}"))
})?;
if !pem.is_empty() && parsed.is_empty() {
return Err(RepError::ConfigError(format!(
"CA file {} parsed but contained 0 certificates",
path.display()
)));
}
for cert in parsed {
store.add(cert).map_err(|e| {
RepError::NetworkError(format!("CA add: {e}"))
})?;
}
}
}
TrustedCerts::CaBytes(pems) => {
if pems.is_empty() {
return Err(RepError::ConfigError(
"TrustedCerts::CaBytes configured with no PEM blobs; \
this is a misconfiguration. Use \
TrustedCerts::SkipVerification to explicitly opt \
out of CA verification."
.into(),
));
}
for (idx, pem) in pems.iter().enumerate() {
let parsed: Vec<_> =
certs(&mut BufReader::new(pem.as_slice()))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| {
RepError::NetworkError(format!("CA parse: {e}"))
})?;
if !pem.is_empty() && parsed.is_empty() {
return Err(RepError::ConfigError(format!(
"CA bytes (index {idx}) parsed but contained 0 \
certificates"
)));
}
for cert in parsed {
store.add(cert).map_err(|e| {
RepError::NetworkError(format!("CA add: {e}"))
})?;
}
}
}
}
Ok(store)
}
}
#[cfg(feature = "tls-rustls")]
#[derive(Debug)]
pub(crate) struct SkipCertVerification(
std::sync::Arc<rustls::crypto::CryptoProvider>,
);
#[cfg(feature = "tls-rustls")]
impl SkipCertVerification {
pub(crate) fn new() -> Self {
Self(std::sync::Arc::new(rustls::crypto::ring::default_provider()))
}
}
#[cfg(feature = "tls-rustls")]
impl rustls::client::danger::ServerCertVerifier for SkipCertVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> std::result::Result<
rustls::client::danger::ServerCertVerified,
rustls::Error,
> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<
rustls::client::danger::HandshakeSignatureValid,
rustls::Error,
> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&self.0.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<
rustls::client::danger::HandshakeSignatureValid,
rustls::Error,
> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&self.0.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.0.signature_verification_algorithms.supported_schemes()
}
}
#[cfg(feature = "tls-native")]
impl TlsConfig {
pub(crate) fn to_native_acceptor(&self) -> Result<native_tls::TlsAcceptor> {
let mtls_intent = match &self.trusted_certs {
TrustedCerts::CaFiles(v) => !v.is_empty(),
TrustedCerts::CaBytes(v) => !v.is_empty(),
TrustedCerts::SkipVerification => false,
};
if mtls_intent {
return Err(RepError::ConfigError(
"mTLS is configured (TrustedCerts has CA roots) but the \
tls-native server transport does not support it: \
native_tls::TlsAcceptorBuilder exposes no client-cert \
verification knobs. Use the tls-rustls feature for mTLS, \
or set TrustedCerts::SkipVerification on this transport."
.into(),
));
}
let identity = self.native_identity()?;
let builder = native_tls::TlsAcceptor::builder(identity);
builder
.build()
.map_err(|e| RepError::NetworkError(format!("TLS acceptor: {e}")))
}
pub(crate) fn to_native_connector(
&self,
) -> Result<native_tls::TlsConnector> {
let mut builder = native_tls::TlsConnector::builder();
if !matches!(&self.identity, TlsIdentity::SelfSigned { .. }) {
let id = self.native_identity()?;
builder.identity(id);
}
self.apply_native_trust(&mut builder)?;
builder
.build()
.map_err(|e| RepError::NetworkError(format!("TLS connector: {e}")))
}
fn native_identity(&self) -> Result<native_tls::Identity> {
match &self.identity {
TlsIdentity::Pkcs12 { der, password } => native_tls::Identity::from_pkcs12(der, password)
.map_err(|e| RepError::NetworkError(format!("PKCS12 identity: {e}"))),
TlsIdentity::SelfSigned { .. } => Err(RepError::NetworkError(
"SelfSigned identity is not supported by the tls-native backend; \
use the tls-rustls feature instead, or supply a Pkcs12 identity"
.into(),
)),
TlsIdentity::PemFiles { .. } | TlsIdentity::PemBytes { .. } => {
Err(RepError::NetworkError(
"PEM identities are not supported by the tls-native backend; \
convert to PKCS12 with: openssl pkcs12 -export -out id.p12 \
-inkey key.pem -in cert.pem"
.into(),
))
}
}
}
fn apply_native_trust(
&self,
builder: &mut native_tls::TlsConnectorBuilder,
) -> Result<()> {
match &self.trusted_certs {
TrustedCerts::SkipVerification => {
builder.danger_accept_invalid_certs(true);
}
TrustedCerts::CaFiles(paths) => {
for path in paths {
let pem = std::fs::read(path).map_err(|e| {
RepError::NetworkError(format!("CA file: {e}"))
})?;
let cert = native_tls::Certificate::from_pem(&pem)
.map_err(|e| {
RepError::NetworkError(format!("CA parse: {e}"))
})?;
builder.add_root_certificate(cert);
}
}
TrustedCerts::CaBytes(pems) => {
for pem in pems {
let cert = native_tls::Certificate::from_pem(pem).map_err(
|e| RepError::NetworkError(format!("CA parse: {e}")),
)?;
builder.add_root_certificate(cert);
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insecure_constructor_uses_self_signed_localhost() {
let cfg = TlsConfig::insecure("node-a");
assert_eq!(cfg.server_name, "node-a");
match cfg.identity {
TlsIdentity::SelfSigned { subject_alt_names } => {
assert_eq!(subject_alt_names, vec!["localhost".to_string()]);
}
_ => panic!("insecure should produce SelfSigned identity"),
}
assert!(matches!(cfg.trusted_certs, TrustedCerts::SkipVerification));
}
#[test]
fn from_pem_files_constructor_records_paths() {
let cfg = TlsConfig::from_pem_files(
"/tmp/cert.pem",
"/tmp/key.pem",
"/tmp/ca.pem",
"node-b",
);
assert_eq!(cfg.server_name, "node-b");
match cfg.identity {
TlsIdentity::PemFiles { cert, key } => {
assert_eq!(cert, std::path::PathBuf::from("/tmp/cert.pem"));
assert_eq!(key, std::path::PathBuf::from("/tmp/key.pem"));
}
_ => panic!("from_pem_files should produce PemFiles identity"),
}
match cfg.trusted_certs {
TrustedCerts::CaFiles(paths) => {
assert_eq!(
paths,
vec![std::path::PathBuf::from("/tmp/ca.pem")]
);
}
_ => panic!("from_pem_files should produce CaFiles trust"),
}
}
#[test]
fn from_pkcs12_constructor_holds_bytes_and_password() {
let der = vec![0x30, 0x82, 0x00, 0x10]; let ca_pem = b"-----BEGIN CERTIFICATE-----\n".to_vec();
let cfg = TlsConfig::from_pkcs12(
der.clone(),
"secret".to_string(),
ca_pem.clone(),
"node-c",
);
assert_eq!(cfg.server_name, "node-c");
match cfg.identity {
TlsIdentity::Pkcs12 { der: d, password } => {
assert_eq!(d, der);
assert_eq!(password, "secret");
}
_ => panic!("from_pkcs12 should produce Pkcs12 identity"),
}
match cfg.trusted_certs {
TrustedCerts::CaBytes(pems) => {
assert_eq!(pems, vec![ca_pem]);
}
_ => panic!("from_pkcs12 should produce CaBytes trust"),
}
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_server_config_from_self_signed_succeeds() {
let cfg = TlsConfig::insecure("node-self");
let sc = cfg.to_rustls_server_config();
assert!(
sc.is_ok(),
"to_rustls_server_config from insecure() should succeed: {:?}",
sc.err()
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_client_config_skip_verification_succeeds() {
let cfg = TlsConfig::insecure("any-name");
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_ok(),
"to_rustls_client_config with SkipVerification should succeed: \
{:?}",
cc.err()
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_client_config_with_empty_ca_bytes_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaBytes(vec![]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_err(),
"empty CaBytes must be a misconfiguration error, got Ok"
);
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("CaBytes") && msg.contains("misconfiguration"),
"error should mention CaBytes/misconfiguration, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_client_config_with_malformed_ca_bytes_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaBytes(vec![b"not-a-pem".to_vec()]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_err(),
"malformed CaBytes must error rather than build an empty store, \
got Ok"
);
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("0 certificates"),
"error should mention 0 certificates, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn skip_cert_verification_returns_ok_for_any_cert() {
use rustls::client::danger::ServerCertVerifier;
let v = SkipCertVerification::new();
let cert = rustls::pki_types::CertificateDer::from(vec![0u8; 8]);
let server_name =
rustls::pki_types::ServerName::try_from("localhost").unwrap();
let now = rustls::pki_types::UnixTime::now();
let r = v.verify_server_cert(&cert, &[], &server_name, &[], now);
assert!(r.is_ok(), "SkipCertVerification must return Ok for any cert");
}
#[cfg(feature = "tls-rustls")]
#[test]
fn skip_cert_verification_supports_some_schemes() {
use rustls::client::danger::ServerCertVerifier;
let v = SkipCertVerification::new();
let schemes = v.supported_verify_schemes();
assert!(
!schemes.is_empty(),
"SkipCertVerification must report at least one signature scheme"
);
}
#[cfg(feature = "tls-native")]
#[test]
fn native_acceptor_requires_pkcs12_identity() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "x".into(),
};
let r = cfg.to_native_acceptor();
assert!(
r.is_err(),
"SelfSigned identity with native-tls must error, got Ok"
);
}
#[cfg(feature = "tls-native")]
#[test]
fn native_connector_skip_verification_succeeds() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "any".into(),
};
let r = cfg.to_native_connector();
assert!(
r.is_ok(),
"native_tls client with SkipVerification should succeed: {:?}",
r.err()
);
}
#[cfg(feature = "tls-rustls")]
fn make_self_signed_pem(san: &[&str]) -> (Vec<u8>, Vec<u8>) {
let sans: Vec<String> = san.iter().map(|s| s.to_string()).collect();
let ck = rcgen::generate_simple_self_signed(sans).unwrap();
let cert_pem = ck.cert.pem().into_bytes();
let key_pem = ck.key_pair.serialize_pem().into_bytes();
(cert_pem, key_pem)
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_server_config_from_pem_bytes() {
let (cert_pem, key_pem) = make_self_signed_pem(&["localhost"]);
let cfg = TlsConfig {
identity: TlsIdentity::PemBytes { cert: cert_pem, key: key_pem },
trusted_certs: TrustedCerts::SkipVerification,
server_name: "localhost".into(),
};
let sc = cfg.to_rustls_server_config();
assert!(sc.is_ok(), "PemBytes server config: {:?}", sc.err());
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_server_config_from_pem_files_on_disk() {
let (cert_pem, key_pem) = make_self_signed_pem(&["localhost"]);
let dir = tempfile::tempdir().unwrap();
let cert_path = dir.path().join("cert.pem");
let key_path = dir.path().join("key.pem");
std::fs::write(&cert_path, &cert_pem).unwrap();
std::fs::write(&key_path, &key_pem).unwrap();
let cfg = TlsConfig {
identity: TlsIdentity::PemFiles { cert: cert_path, key: key_path },
trusted_certs: TrustedCerts::SkipVerification,
server_name: "localhost".into(),
};
let sc = cfg.to_rustls_server_config();
assert!(sc.is_ok(), "PemFiles server config: {:?}", sc.err());
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_client_config_with_real_ca_bytes() {
let (ca_pem, _ca_key) = make_self_signed_pem(&["test-ca"]);
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
server_name: "localhost".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(cc.is_ok(), "real CA bytes: {:?}", cc.err());
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_client_config_with_real_ca_file() {
let (ca_pem, _ca_key) = make_self_signed_pem(&["test-ca"]);
let dir = tempfile::tempdir().unwrap();
let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, &ca_pem).unwrap();
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaFiles(vec![ca_path]),
server_name: "localhost".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(cc.is_ok(), "real CA file: {:?}", cc.err());
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_server_config_with_pem_files_missing_cert_errors() {
let dir = tempfile::tempdir().unwrap();
let nonexistent = dir.path().join("does-not-exist.pem");
let key_path = dir.path().join("key.pem");
let (_, key_pem) = make_self_signed_pem(&["localhost"]);
std::fs::write(&key_path, &key_pem).unwrap();
let cfg = TlsConfig {
identity: TlsIdentity::PemFiles {
cert: nonexistent,
key: key_path,
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "localhost".into(),
};
let sc = cfg.to_rustls_server_config();
assert!(sc.is_err(), "missing cert file should error, got Ok");
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_server_config_with_pem_files_missing_key_errors() {
let dir = tempfile::tempdir().unwrap();
let cert_path = dir.path().join("cert.pem");
let nonexistent = dir.path().join("nonexistent-key.pem");
let (cert_pem, _) = make_self_signed_pem(&["localhost"]);
std::fs::write(&cert_path, &cert_pem).unwrap();
let cfg = TlsConfig {
identity: TlsIdentity::PemFiles {
cert: cert_path,
key: nonexistent,
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "localhost".into(),
};
let sc = cfg.to_rustls_server_config();
assert!(sc.is_err(), "missing key file should error, got Ok");
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_root_store_with_malformed_ca_file_errors() {
let dir = tempfile::tempdir().unwrap();
let bad_ca = dir.path().join("bad.pem");
std::fs::write(&bad_ca, b"this is not a PEM file\n").unwrap();
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaFiles(vec![bad_ca]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_err(),
"garbage CA file must error rather than yield empty trust"
);
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("0 certificates"),
"error should mention 0 certificates, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_client_config_with_missing_ca_file_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaFiles(vec![
std::path::PathBuf::from("/nonexistent/ca.pem"),
]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(cc.is_err(), "missing CA file should error");
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_server_config_self_signed_runtime() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["host-a".into(), "host-b".into()],
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "host-a".into(),
};
let sc = cfg.to_rustls_server_config();
assert!(sc.is_ok(), "SelfSigned runtime cert: {:?}", sc.err());
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_pkcs12_identity_is_rejected() {
let cfg = TlsConfig {
identity: TlsIdentity::Pkcs12 {
der: vec![0x30, 0x82, 0x00, 0x10],
password: "x".into(),
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "x".into(),
};
let r = cfg.to_rustls_server_config();
assert!(r.is_err(), "Pkcs12 with rustls must error");
let msg = format!("{}", r.err().unwrap());
assert!(
msg.contains("Pkcs12") || msg.contains("not supported"),
"error should mention Pkcs12 or not-supported, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_pem_bytes_no_certificates_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::PemBytes {
cert: b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n"
.to_vec(),
key: b"-----BEGIN PRIVATE KEY-----\nMC4CAQA=\n-----END PRIVATE KEY-----\n"
.to_vec(),
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "x".into(),
};
let r = cfg.to_rustls_server_config();
assert!(r.is_err(), "PEM with no certificates must error, got Ok");
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_pem_bytes_no_private_key_errors() {
let (cert_pem, _key_pem) = make_self_signed_pem(&["localhost"]);
let cfg = TlsConfig {
identity: TlsIdentity::PemBytes {
cert: cert_pem,
key: b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n"
.to_vec(),
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "x".into(),
};
let r = cfg.to_rustls_server_config();
assert!(r.is_err(), "PEM with no private key must error, got Ok");
}
#[cfg(feature = "tls-rustls")]
#[test]
fn rustls_skip_verification_client_config_succeeds() {
let skip_cfg = TlsConfig::insecure("localhost");
let cc = skip_cfg.to_rustls_client_config();
assert!(cc.is_ok());
}
#[cfg(feature = "tls-rustls")]
#[test]
fn tls2_empty_ca_files_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaFiles(vec![]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_err(),
"empty CaFiles must be a misconfiguration error, got Ok"
);
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("CaFiles") && msg.contains("misconfiguration"),
"error should mention CaFiles/misconfiguration, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn tls2_empty_ca_bytes_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaBytes(vec![]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_err(),
"empty CaBytes must be a misconfiguration error, got Ok"
);
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("CaBytes") && msg.contains("misconfiguration"),
"error should mention CaBytes/misconfiguration, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn tls2_skip_verification_still_works() {
let cfg = TlsConfig::insecure("localhost");
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_ok(),
"SkipVerification must remain the supported opt-out, got Err: \
{:?}",
cc.err()
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn tls3_ca_bytes_with_zero_decoded_certs_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaBytes(vec![
b"this looks like text but is not a PEM certificate\n".to_vec(),
]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(
cc.is_err(),
"non-empty PEM with zero certs must error, got Ok"
);
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("0 certificates"),
"error should mention 0 certificates, got: {msg}"
);
}
#[cfg(feature = "tls-rustls")]
#[test]
fn tls3_ca_file_with_zero_decoded_certs_errors() {
let dir = tempfile::tempdir().unwrap();
let bad_ca = dir.path().join("bad.pem");
std::fs::write(
&bad_ca,
b"-----BEGIN GARBAGE-----\nAAAA\n-----END GARBAGE-----\n",
)
.unwrap();
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaFiles(vec![bad_ca.clone()]),
server_name: "x".into(),
};
let cc = cfg.to_rustls_client_config();
assert!(cc.is_err(), "CA file with 0 certificates must error");
let msg = format!("{}", cc.err().unwrap());
assert!(
msg.contains("0 certificates")
&& msg.contains(&bad_ca.display().to_string()),
"error should mention 0 certificates and the file path, got: \
{msg}"
);
}
#[cfg(feature = "tls-native")]
#[test]
fn tls4_native_acceptor_with_ca_files_intent_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::Pkcs12 {
der: vec![0x30, 0x82, 0x00, 0x10],
password: "x".into(),
},
trusted_certs: TrustedCerts::CaFiles(vec![
std::path::PathBuf::from("/etc/ssl/certs/ca.pem"),
]),
server_name: "x".into(),
};
let r = cfg.to_native_acceptor();
assert!(
r.is_err(),
"mTLS intent on tls-native server must error rather than warn"
);
let msg = format!("{}", r.err().unwrap());
assert!(
msg.contains("mTLS")
&& msg.contains("tls-native")
&& msg.contains("tls-rustls"),
"error must point at mTLS / tls-native / tls-rustls remediation, \
got: {msg}"
);
}
#[cfg(feature = "tls-native")]
#[test]
fn tls4_native_acceptor_with_ca_bytes_intent_errors() {
let cfg = TlsConfig {
identity: TlsIdentity::Pkcs12 {
der: vec![0x30, 0x82, 0x00, 0x10],
password: "x".into(),
},
trusted_certs: TrustedCerts::CaBytes(vec![
b"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"
.to_vec(),
]),
server_name: "x".into(),
};
let r = cfg.to_native_acceptor();
assert!(
r.is_err(),
"non-empty CaBytes on tls-native server must error"
);
let msg = format!("{}", r.err().unwrap());
assert!(msg.contains("mTLS"), "error must mention mTLS, got: {msg}");
}
#[cfg(feature = "tls-native")]
#[test]
fn tls4_native_acceptor_skip_verification_unaffected() {
let cfg = TlsConfig {
identity: TlsIdentity::Pkcs12 {
der: vec![0x30, 0x82, 0x00, 0x10],
password: "x".into(),
},
trusted_certs: TrustedCerts::SkipVerification,
server_name: "x".into(),
};
let r = cfg.to_native_acceptor();
if let Err(e) = r {
let msg = format!("{e}");
assert!(
!msg.contains("mTLS"),
"SkipVerification must not trigger mTLS check, got: {msg}"
);
}
}
#[cfg(all(feature = "tls-rustls", feature = "quic"))]
#[test]
fn quinn_server_config_builds_from_self_signed() {
let cfg = TlsConfig::insecure("localhost");
let qc = cfg.to_quinn_server_config();
assert!(qc.is_ok(), "quinn server config: {:?}", qc.err());
}
#[cfg(all(feature = "tls-rustls", feature = "quic"))]
#[test]
fn quinn_client_config_builds_from_skip_verification() {
let cfg = TlsConfig::insecure("localhost");
let qc = cfg.to_quinn_client_config();
assert!(qc.is_ok(), "quinn client config: {:?}", qc.err());
}
#[cfg(all(feature = "tls-rustls", feature = "quic"))]
#[test]
fn quinn_client_config_with_real_ca_bytes() {
let (ca_pem, _) = make_self_signed_pem(&["test-ca"]);
let cfg = TlsConfig {
identity: TlsIdentity::SelfSigned {
subject_alt_names: vec!["localhost".into()],
},
trusted_certs: TrustedCerts::CaBytes(vec![ca_pem]),
server_name: "localhost".into(),
};
let qc = cfg.to_quinn_client_config();
assert!(qc.is_ok(), "quinn client config with CA: {:?}", qc.err());
}
}