use std::sync::Arc;
#[derive(Clone)]
pub struct ReverseServerTlsConfig {
pub cert_pem: Vec<u8>,
pub key_pem: Vec<u8>,
pub client_ca_pem: Option<Vec<u8>>,
pub require_client_cert: bool,
}
impl std::fmt::Debug for ReverseServerTlsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReverseServerTlsConfig")
.field("has_cert", &!self.cert_pem.is_empty())
.field("has_key", &!self.key_pem.is_empty())
.field("has_client_ca", &self.client_ca_pem.is_some())
.field("require_client_cert", &self.require_client_cert)
.finish()
}
}
impl Drop for ReverseServerTlsConfig {
fn drop(&mut self) {
use zeroize::Zeroize;
self.key_pem.zeroize();
if let Some(ref mut ca) = self.client_ca_pem {
ca.zeroize();
}
}
}
impl ReverseServerTlsConfig {
pub fn validate(&self) -> Result<(), crate::ProtocolError> {
if self.cert_pem.is_empty() {
return Err(crate::ProtocolError::ConfigInvalid(
"reverse server TLS requires a certificate".to_string(),
));
}
if self.key_pem.is_empty() {
return Err(crate::ProtocolError::ConfigInvalid(
"reverse server TLS requires a private key".to_string(),
));
}
if self.require_client_cert && self.client_ca_pem.is_none() {
return Err(crate::ProtocolError::ConfigInvalid(
"reverse server mTLS requires client CA roots when require_client_cert is set"
.to_string(),
));
}
Ok(())
}
pub fn build_server_config(&self) -> Result<Arc<rustls::ServerConfig>, crate::ProtocolError> {
self.validate()?;
let mut builder = eggress_transport_tls::TlsServerConfigBuilder::new()
.with_certificate_pem(&self.cert_pem)
.map_err(|e| crate::ProtocolError::Tls(format!("invalid server certificate: {e}")))?
.with_key_pem(&self.key_pem)
.map_err(|e| crate::ProtocolError::Tls(format!("invalid server key: {e}")))?;
if let Some(ref ca_pem) = self.client_ca_pem {
builder = builder
.with_client_ca_pem(ca_pem)
.map_err(|e| crate::ProtocolError::Tls(format!("invalid client CA: {e}")))?;
}
if self.require_client_cert {
builder = builder.with_require_client_cert(true);
} else if self.client_ca_pem.is_some() {
}
builder
.build()
.map_err(|e| crate::ProtocolError::Tls(format!("invalid server TLS config: {e}")))
}
}
#[derive(Clone)]
pub struct ReverseClientTlsConfig {
pub ca_pem: Option<Vec<u8>>,
pub server_name: String,
pub client_cert_pem: Option<Vec<u8>>,
pub client_key_pem: Option<Vec<u8>>,
}
impl std::fmt::Debug for ReverseClientTlsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReverseClientTlsConfig")
.field("has_ca", &self.ca_pem.is_some())
.field("server_name", &self.server_name)
.field("has_client_cert", &self.client_cert_pem.is_some())
.field("has_client_key", &self.client_key_pem.is_some())
.finish()
}
}
impl Drop for ReverseClientTlsConfig {
fn drop(&mut self) {
use zeroize::Zeroize;
if let Some(ref mut key) = self.client_key_pem {
key.zeroize();
}
}
}
impl ReverseClientTlsConfig {
pub fn validate(&self) -> Result<(), crate::ProtocolError> {
if self.server_name.is_empty() {
return Err(crate::ProtocolError::ConfigInvalid(
"reverse client TLS requires a server_name for SNI/verification".to_string(),
));
}
let _ =
rustls::pki_types::ServerName::try_from(self.server_name.clone()).map_err(|_| {
crate::ProtocolError::ConfigInvalid(format!(
"reverse client TLS has an invalid server_name '{}'",
self.server_name
))
})?;
if self.client_cert_pem.is_some() != self.client_key_pem.is_some() {
return Err(crate::ProtocolError::ConfigInvalid(
"reverse client mTLS requires both client certificate and key".to_string(),
));
}
Ok(())
}
pub fn build_client_config(&self) -> Result<Arc<rustls::ClientConfig>, crate::ProtocolError> {
self.validate()?;
let mut builder = eggress_transport_tls::TlsClientConfigBuilder::new();
builder = match self.ca_pem.as_deref() {
Some(ca_pem) => builder
.with_custom_ca_pem(ca_pem)
.map_err(|e| crate::ProtocolError::Tls(format!("invalid client CA: {e}")))?,
None => builder.with_system_roots().map_err(|e| {
crate::ProtocolError::Tls(format!("TLS system roots unavailable: {e}"))
})?,
};
if let (Some(cert_pem), Some(key_pem)) =
(self.client_cert_pem.as_ref(), self.client_key_pem.as_ref())
{
builder = builder.with_client_cert_pem(cert_pem, key_pem);
}
builder
.build()
.map_err(|e| crate::ProtocolError::Tls(format!("invalid client TLS config: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init_crypto() {
eggress_transport_tls::install_default_crypto_provider();
}
fn cert_for(names: Vec<String>) -> (String, String) {
let params = rcgen::CertificateParams::new(names).unwrap();
let key = rcgen::KeyPair::generate().unwrap();
let cert = params.self_signed(&key).unwrap();
(cert.pem(), key.serialize_pem())
}
#[test]
fn server_tls_debug_redacts_key_material() {
let (cert, key) = cert_for(vec!["localhost".to_string()]);
let cfg = ReverseServerTlsConfig {
cert_pem: cert.into_bytes(),
key_pem: key.into_bytes(),
client_ca_pem: None,
require_client_cert: false,
};
let rendered = format!("{cfg:?}");
assert!(!rendered.contains("BEGIN PRIVATE KEY"));
assert!(!rendered.contains("BEGIN CERTIFICATE"));
}
#[test]
fn server_tls_require_without_ca_rejected() {
let (cert, key) = cert_for(vec!["localhost".to_string()]);
let cfg = ReverseServerTlsConfig {
cert_pem: cert.into_bytes(),
key_pem: key.into_bytes(),
client_ca_pem: None,
require_client_cert: true,
};
assert!(cfg.validate().is_err());
assert!(cfg.build_server_config().is_err());
}
#[test]
fn server_tls_malformed_pem_rejected() {
let cfg = ReverseServerTlsConfig {
cert_pem: b"not pem".to_vec(),
key_pem: b"not pem".to_vec(),
client_ca_pem: None,
require_client_cert: false,
};
assert!(cfg.build_server_config().is_err());
}
#[test]
fn client_tls_cert_without_key_rejected() {
let (cert, _) = cert_for(vec!["localhost".to_string()]);
let cfg = ReverseClientTlsConfig {
ca_pem: None,
server_name: "localhost".to_string(),
client_cert_pem: Some(cert.into_bytes()),
client_key_pem: None,
};
assert!(cfg.validate().is_err());
}
#[test]
fn client_tls_missing_server_name_rejected() {
let cfg = ReverseClientTlsConfig {
ca_pem: None,
server_name: String::new(),
client_cert_pem: None,
client_key_pem: None,
};
assert!(cfg.validate().is_err());
}
#[test]
fn client_tls_debug_redacts_key_material() {
init_crypto();
let (cert, key) = cert_for(vec!["localhost".to_string()]);
let cfg = ReverseClientTlsConfig {
ca_pem: Some(cert.clone().into_bytes()),
server_name: "localhost".to_string(),
client_cert_pem: Some(cert.into_bytes()),
client_key_pem: Some(key.into_bytes()),
};
let rendered = format!("{cfg:?}");
assert!(!rendered.contains("BEGIN PRIVATE KEY"));
}
}