use crate::{
config::TlsConfig,
error::{FusekiError, FusekiResult},
};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, info, warn};
#[cfg(feature = "tls")]
use rustls::{
pki_types::{CertificateDer, PrivateKeyDer},
ServerConfig,
};
#[cfg(feature = "tls")]
use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
pub struct TlsManager {
config: TlsConfig,
}
impl TlsManager {
pub fn new(config: TlsConfig) -> Self {
TlsManager { config }
}
#[cfg(feature = "tls")]
pub fn build_server_config(&self) -> FusekiResult<Arc<ServerConfig>> {
info!("Loading TLS certificates...");
let certs = self.load_certificates(&self.config.cert_path)?;
info!("Loaded {} certificate(s)", certs.len());
let private_key = self.load_private_key(&self.config.key_path)?;
info!("Loaded private key");
let mut config = if self.config.require_client_cert {
info!("Client certificate authentication enabled");
self.build_config_with_client_auth(certs, private_key)?
} else {
ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, private_key)
.map_err(|e| {
FusekiError::configuration(format!("Failed to build TLS config: {}", e))
})?
};
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(Arc::new(config))
}
#[cfg(not(feature = "tls"))]
pub fn build_server_config(&self) -> FusekiResult<Arc<()>> {
Err(FusekiError::configuration(
"TLS support not enabled. Rebuild with --features tls".to_string(),
))
}
#[cfg(feature = "tls")]
fn load_certificates(&self, path: &Path) -> FusekiResult<Vec<CertificateDer<'static>>> {
let cert_file = File::open(path).map_err(|e| {
FusekiError::configuration(format!(
"Failed to open certificate file {}: {}",
path.display(),
e
))
})?;
let mut reader = BufReader::new(cert_file);
let certs: Vec<CertificateDer<'static>> = certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
FusekiError::configuration(format!("Failed to parse certificates: {}", e))
})?;
if certs.is_empty() {
return Err(FusekiError::configuration(format!(
"No certificates found in {}",
path.display()
)));
}
Ok(certs)
}
#[cfg(feature = "tls")]
fn load_private_key(&self, path: &Path) -> FusekiResult<PrivateKeyDer<'static>> {
let key_file = File::open(path).map_err(|e| {
FusekiError::configuration(format!(
"Failed to open private key file {}: {}",
path.display(),
e
))
})?;
let mut reader = BufReader::new(key_file);
let mut keys: Vec<_> = pkcs8_private_keys(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
FusekiError::configuration(format!("Failed to parse PKCS8 private key: {}", e))
})?;
if let Some(key) = keys.drain(..).next() {
debug!("Loaded PKCS8 private key");
return Ok(PrivateKeyDer::Pkcs8(key));
}
let key_file = File::open(path).map_err(|e| {
FusekiError::configuration(format!(
"Failed to reopen private key file {}: {}",
path.display(),
e
))
})?;
let mut reader = BufReader::new(key_file);
let mut keys: Vec<_> = rsa_private_keys(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
FusekiError::configuration(format!("Failed to parse RSA private key: {}", e))
})?;
if let Some(key) = keys.drain(..).next() {
debug!("Loaded RSA private key");
return Ok(PrivateKeyDer::Pkcs1(key));
}
Err(FusekiError::configuration(format!(
"No valid private key found in {}",
path.display()
)))
}
#[cfg(feature = "tls")]
fn build_config_with_client_auth(
&self,
certs: Vec<CertificateDer<'static>>,
private_key: PrivateKeyDer<'static>,
) -> FusekiResult<ServerConfig> {
if let Some(ca_cert_path) = &self.config.ca_cert_path {
info!("Loading CA certificate for client authentication");
let ca_certs = self.load_certificates(ca_cert_path)?;
let mut root_store = rustls::RootCertStore::empty();
for cert in ca_certs {
root_store.add(cert).map_err(|e| {
FusekiError::configuration(format!("Failed to add CA certificate: {}", e))
})?;
}
let client_verifier =
rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| {
FusekiError::configuration(format!(
"Failed to build client certificate verifier: {}",
e
))
})?;
let config = ServerConfig::builder()
.with_client_cert_verifier(client_verifier)
.with_single_cert(certs, private_key)
.map_err(|e| {
FusekiError::configuration(format!(
"Failed to build TLS config with client auth: {}",
e
))
})?;
Ok(config)
} else {
warn!("Client certificate authentication required but no CA certificate path provided");
Err(FusekiError::configuration(
"Client certificate authentication requires ca_cert_path".to_string(),
))
}
}
pub fn validate(&self) -> FusekiResult<()> {
if !self.config.cert_path.exists() {
return Err(FusekiError::configuration(format!(
"Certificate file not found: {}",
self.config.cert_path.display()
)));
}
if !self.config.key_path.exists() {
return Err(FusekiError::configuration(format!(
"Private key file not found: {}",
self.config.key_path.display()
)));
}
if self.config.require_client_cert {
if let Some(ca_path) = &self.config.ca_cert_path {
if !ca_path.exists() {
return Err(FusekiError::configuration(format!(
"CA certificate file not found: {}",
ca_path.display()
)));
}
} else {
return Err(FusekiError::configuration(
"Client certificate authentication requires ca_cert_path".to_string(),
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_tls_config_validation() {
let config = TlsConfig {
cert_path: PathBuf::from("/nonexistent/cert.pem"),
key_path: PathBuf::from("/nonexistent/key.pem"),
require_client_cert: false,
ca_cert_path: None,
};
let manager = TlsManager::new(config);
assert!(manager.validate().is_err());
}
#[test]
fn test_mtls_config_validation() {
let temp_dir = std::env::temp_dir();
let cert_path = temp_dir.join("test_cert.pem");
let key_path = temp_dir.join("test_key.pem");
std::fs::write(&cert_path, "").ok();
std::fs::write(&key_path, "").ok();
let config = TlsConfig {
cert_path,
key_path,
require_client_cert: true,
ca_cert_path: None, };
let manager = TlsManager::new(config);
let result = manager.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("requires ca_cert_path"));
}
}