use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
use tracing::debug;
use crate::config::TlsConfig;
use crate::error::{Error, Result};
pub fn build_tls_config(config: &TlsConfig) -> Result<ClientConfig> {
let root_store = build_root_store(config.ca_cert.as_deref())?;
let tls_config = match (&config.client_cert, &config.client_key) {
(Some(cert_path), Some(key_path)) => {
debug!(
"Configuring mTLS with cert={}, key={}",
cert_path.display(),
key_path.display()
);
let certs = load_certificates(cert_path)?;
let key = load_private_key(key_path)?;
ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(certs, key)
.map_err(|e| {
Error::Authentication(format!(
"Failed to configure client authentication: {}",
e
))
})?
}
(Some(cert_path), None) => {
return Err(Error::Config(format!(
"client_cert ({}) provided without client_key. Both required for mTLS.",
cert_path.display()
)));
}
(None, Some(key_path)) => {
return Err(Error::Config(format!(
"client_key ({}) provided without client_cert. Both required for mTLS.",
key_path.display()
)));
}
(None, None) => {
debug!("Configuring TLS without client authentication");
ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth()
}
};
Ok(tls_config)
}
fn build_root_store(ca_path: Option<&Path>) -> Result<RootCertStore> {
match ca_path {
Some(path) => {
let certs = load_certificates(path)?;
let mut root_store = RootCertStore::empty();
for cert in certs {
root_store.add(cert).map_err(|e| {
Error::Authentication(format!("Failed to add CA certificate: {}", e))
})?;
}
if root_store.is_empty() {
return Err(Error::Authentication(format!(
"No valid certificates found in {}",
path.display()
)));
}
debug!(
"Loaded {} CA certificate(s) from {}",
root_store.len(),
path.display()
);
Ok(root_store)
}
None => {
debug!("Using webpki-roots for TLS verification");
Ok(RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
})
}
}
}
fn load_certificates(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
let file = File::open(path)
.map_err(|e| Error::Authentication(format!("Failed to open {}: {}", path.display(), e)))?;
let mut reader = BufReader::new(file);
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| {
Error::Authentication(format!(
"Failed to parse certificates from {}: {}",
path.display(),
e
))
})?;
if certs.is_empty() {
return Err(Error::Authentication(format!(
"No certificates found in {}",
path.display()
)));
}
debug!(
"Loaded {} certificate(s) from {}",
certs.len(),
path.display()
);
Ok(certs)
}
fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>> {
let file = File::open(path)
.map_err(|e| Error::Authentication(format!("Failed to open {}: {}", path.display(), e)))?;
let mut reader = BufReader::new(file);
rustls_pemfile::private_key(&mut reader)
.map_err(|e| {
Error::Authentication(format!(
"Failed to parse private key from {}: {}",
path.display(),
e
))
})?
.ok_or_else(|| Error::Authentication(format!("No private key found in {}", path.display())))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_root_store_webpki_fallback() {
let store = build_root_store(None).unwrap();
assert!(!store.is_empty());
}
#[test]
fn test_build_root_store_missing_file() {
let result = build_root_store(Some(Path::new("/nonexistent/ca.pem")));
assert!(result.is_err());
}
#[test]
fn test_mtls_requires_both_cert_and_key() {
let config = TlsConfig {
enabled: true,
ca_cert: None,
client_cert: Some("/path/to/cert.pem".into()),
client_key: None,
};
let result = build_tls_config(&config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("client_key"));
}
}