use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::ServerConfig;
use rustls_pemfile::{certs, pkcs8_private_keys};
use std::fs::File;
use std::io::{self, BufReader};
use std::path::Path;
use std::sync::Arc;
#[derive(Debug)]
pub enum TlsError {
CertificateNotFound(String),
KeyNotFound(String),
InvalidCertificate(String),
InvalidPrivateKey(String),
PemParseError(String),
IoError(io::Error),
}
impl std::fmt::Display for TlsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TlsError::CertificateNotFound(path) => write!(f, "Certificate not found: {}", path),
TlsError::KeyNotFound(path) => write!(f, "Private key not found: {}", path),
TlsError::InvalidCertificate(msg) => write!(f, "Invalid certificate: {}", msg),
TlsError::InvalidPrivateKey(msg) => write!(f, "Invalid private key: {}", msg),
TlsError::PemParseError(msg) => write!(f, "PEM parse error: {}", msg),
TlsError::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for TlsError {}
impl From<io::Error> for TlsError {
fn from(err: io::Error) -> Self {
TlsError::IoError(err)
}
}
pub struct TlsConfiguration {
pub config: Arc<ServerConfig>,
}
impl TlsConfiguration {
pub fn from_files<P: AsRef<Path>>(cert_path: P, key_path: P) -> Result<Self, TlsError> {
let cert_path = cert_path.as_ref();
let key_path = key_path.as_ref();
if !cert_path.exists() {
return Err(TlsError::CertificateNotFound(
cert_path.to_string_lossy().to_string(),
));
}
if !key_path.exists() {
return Err(TlsError::KeyNotFound(
key_path.to_string_lossy().to_string(),
));
}
let cert_file = File::open(cert_path)?;
let mut cert_reader = BufReader::new(cert_file);
let certificates: Vec<CertificateDer<'static>> = certs(&mut cert_reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| {
TlsError::InvalidCertificate("Failed to parse PEM certificates".to_string())
})?;
if certificates.is_empty() {
return Err(TlsError::InvalidCertificate(
"No certificates found in file".to_string(),
));
}
let key_file = File::open(key_path)?;
let mut key_reader = BufReader::new(key_file);
let mut keys: Vec<PrivateKeyDer<'static>> = pkcs8_private_keys(&mut key_reader)
.map(|r| r.map(PrivateKeyDer::from))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| {
TlsError::InvalidPrivateKey("Failed to parse PEM private keys".to_string())
})?;
if keys.is_empty() {
return Err(TlsError::InvalidPrivateKey(
"No private keys found in file".to_string(),
));
}
let private_key = keys.remove(0);
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certificates, private_key)
.map_err(|e| {
TlsError::InvalidCertificate(format!("Failed to build config: {:?}", e))
})?;
Ok(TlsConfiguration {
config: Arc::new(config),
})
}
#[allow(dead_code)]
pub fn for_development() -> Result<Self, TlsError> {
Err(TlsError::PemParseError(
"Development certificates not available. Use from_files() with proper certificates."
.to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_missing_certificate_error() {
let result = TlsConfiguration::from_files("/nonexistent/cert.pem", "/nonexistent/key.pem");
assert!(result.is_err());
match result {
Err(TlsError::CertificateNotFound(path)) => {
assert!(path.contains("cert.pem"));
}
_ => panic!("Expected CertificateNotFound error"),
}
}
#[test]
fn test_tls_error_display() {
let err = TlsError::CertificateNotFound("test.pem".to_string());
assert!(err.to_string().contains("Certificate not found"));
let err = TlsError::InvalidCertificate("bad format".to_string());
assert!(err.to_string().contains("Invalid certificate"));
let err = TlsError::InvalidPrivateKey("bad format".to_string());
assert!(err.to_string().contains("Invalid private key"));
}
#[test]
fn test_tls_error_display_remaining_variants() {
let err = TlsError::KeyNotFound("mykey.pem".to_string());
let s = err.to_string();
assert!(s.contains("Private key not found"), "got: {}", s);
assert!(s.contains("mykey.pem"), "got: {}", s);
let err = TlsError::PemParseError("bad header".to_string());
let s = err.to_string();
assert!(s.contains("PEM parse error"), "got: {}", s);
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
let err = TlsError::IoError(io_err);
let s = err.to_string();
assert!(s.contains("IO error"), "got: {}", s);
}
#[test]
fn test_tls_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let tls_err: TlsError = TlsError::from(io_err);
assert!(matches!(tls_err, TlsError::IoError(_)));
}
#[test]
fn test_for_development_returns_pem_parse_error() {
let result = TlsConfiguration::for_development();
assert!(result.is_err());
match result {
Err(TlsError::PemParseError(msg)) => {
assert!(msg.contains("Development certificates not available"), "got: {}", msg);
}
_ => panic!("Expected PemParseError from for_development()"),
}
}
}