//! mTLS Authentication Module //! //! Provides mutual TLS authentication middleware for Actix-web. //! Non-mTLS connections are silently dropped (no response). use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage, }; use chrono::{DateTime, Utc}; use futures_util::future::LocalBoxFuture; use rustls::{ server::{ServerConfig, WebPkiClientVerifier}, RootCertStore, }; use rustls_pemfile::{certs, private_key}; use std::{fs::File, io::BufReader, sync::Arc}; use tracing::{debug, info, warn}; /// Check for duplicate critical headers (VULN-006) /// Returns true if duplicate headers are detected fn has_duplicate_critical_headers(req: &ServiceRequest) -> bool { let critical_headers = ["content-type", "authorization", "host"]; for header_name in critical_headers.iter() { // Count occurrences of this header let mut count = 0; for (name, _) in req.headers().iter() { if name.as_str().eq_ignore_ascii_case(header_name) { count += 1; if count > 1 { warn!( peer_addr = ?req.peer_addr(), header = header_name, "Duplicate critical header detected - rejecting request" ); return true; } } } } false } /// mTLS Configuration #[derive(Debug, Clone)] pub struct MtlsConfig { pub ca_cert_path: String, pub server_cert_path: String, pub server_key_path: String, pub min_tls_version: String, } /// mTLS Middleware for Actix-web pub struct MtlsMiddleware { config: Arc, cert_store: Arc, } impl MtlsMiddleware { /// Create a new mTLS middleware pub fn new(config: MtlsConfig) -> Result { let cert_store = load_ca_certs(&config.ca_cert_path)?; Ok(Self { config: Arc::new(config), cert_store: Arc::new(cert_store), }) } /// Build rustls server configuration with client certificate verification pub fn build_rustls_config(&self) -> Result, MtlsError> { let client_verifier = WebPkiClientVerifier::builder(self.cert_store.clone()) .build() .map_err(|e| MtlsError::ClientVerifierError(e.to_string()))?; let server_cert = load_certs(&self.config.server_cert_path)?; let server_key = load_private_key(&self.config.server_key_path)?; let config = ServerConfig::builder() .with_client_cert_verifier(client_verifier) .with_single_cert(server_cert, server_key) .map_err(|e| MtlsError::ServerConfigError(e.to_string()))?; Ok(Arc::new(config)) } } /// Load CA certificates from PEM file fn load_ca_certs(path: &str) -> Result { let mut cert_store = RootCertStore::empty(); let cert_file = File::open(path) .map_err(|e| MtlsError::IoError(format!("Failed to open CA cert {}: {}", path, e)))?; let mut reader = BufReader::new(cert_file); let certs = certs(&mut reader) .collect::, _>>() .map_err(|e| MtlsError::ParseError(format!("Failed to parse CA certs: {}", e)))?; for cert in certs { cert_store .add(cert) .map_err(|e| MtlsError::StoreError(format!("Failed to add CA cert to store: {}", e)))?; } info!("Loaded CA certificates from {}", path); Ok(cert_store) } /// Load server certificates from PEM file fn load_certs(path: &str) -> Result>, MtlsError> { let cert_file = File::open(path) .map_err(|e| MtlsError::IoError(format!("Failed to open cert {}: {}", path, e)))?; let mut reader = BufReader::new(cert_file); let certs = certs(&mut reader) .collect::, _>>() .map_err(|e| MtlsError::ParseError(format!("Failed to parse server certs: {}", e)))?; Ok(certs) } /// Load private key from PEM file fn load_private_key(path: &str) -> Result, MtlsError> { let key_file = File::open(path) .map_err(|e| MtlsError::IoError(format!("Failed to open key {}: {}", path, e)))?; let mut reader = BufReader::new(key_file); let key = private_key(&mut reader) .map_err(|e| MtlsError::ParseError(format!("Failed to parse private key: {}", e)))? .ok_or_else(|| MtlsError::ParseError("No private key found in file".to_string()))?; Ok(key) } /// mTLS Error types #[derive(Debug, thiserror::Error)] pub enum MtlsError { #[error("IO error: {0}")] IoError(String), #[error("Parse error: {0}")] ParseError(String), #[error("Certificate store error: {0}")] StoreError(String), #[error("Client verifier error: {0}")] ClientVerifierError(String), #[error("Server config error: {0}")] ServerConfigError(String), #[error("Certificate validation error: {0}")] ValidationError(String), } impl Transform for MtlsMiddleware where S: Service, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = Error; type InitError = (); type Transform = MtlsMiddlewareService; type Future = futures_util::future::Ready>; fn new_transform(&self, service: S) -> Self::Future { futures_util::future::ok(MtlsMiddlewareService { service, config: self.config.clone(), cert_store: self.cert_store.clone(), }) } } pub struct MtlsMiddlewareService { service: S, #[allow(dead_code)] config: Arc, cert_store: Arc, } impl Service for MtlsMiddlewareService where S: Service, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let cert_store = self.cert_store.clone(); let peer_addr = req.peer_addr(); // VULN-006: Check for duplicate critical headers before processing if has_duplicate_critical_headers(&req) { warn!( peer_addr = ?peer_addr, "Duplicate critical headers detected - rejecting request (VULN-006)" ); return Box::pin(async move { Err(actix_web::error::ErrorBadRequest( "Duplicate critical headers not allowed", )) }); } // Check for client certificate in request extensions // In a proper mTLS setup with Actix-web + rustls, the certificate // would be extracted from the TLS connection before reaching this middleware let has_client_cert = req.extensions().get::().is_some(); if !has_client_cert { // No client certificate provided - silent drop warn!( peer_addr = ?peer_addr, "No client certificate provided - dropping connection (mTLS required)" ); // Return error immediately without calling service return Box::pin(async move { Err(actix_web::error::ErrorBadRequest( "Client certificate required", )) }); } // Certificate present - validate it let cert_info = req.extensions().get::().cloned(); if let Some(info) = cert_info { // Validate certificate against CA store match validate_client_certificate(&info, &cert_store) { Ok(_) => { info!( subject = %info.subject, issuer = %info.issuer, peer_addr = ?peer_addr, "mTLS client certificate validated successfully" ); } Err(e) => { warn!( error = %e, peer_addr = ?peer_addr, "mTLS client certificate validation failed - dropping connection" ); return Box::pin(async move { Err(actix_web::error::ErrorBadRequest( "Certificate validation failed", )) }); } } } else { warn!( peer_addr = ?peer_addr, "No client certificate provided - dropping connection (mTLS required)" ); return Box::pin(async move { Err(actix_web::error::ErrorBadRequest( "Client certificate required", )) }); } debug!("mTLS authentication passed for request"); // All checks passed - call the service let fut = self.service.call(req); Box::pin(fut) } } /// Certificate information extracted from client certificate #[derive(Debug, Clone)] pub struct ClientCertInfo { pub subject: String, pub issuer: String, pub serial: String, pub not_before: DateTime, pub not_after: DateTime, } /// Validate client certificate against CA store fn validate_client_certificate( cert_info: &ClientCertInfo, _cert_store: &RootCertStore, ) -> Result<(), MtlsError> { // Check certificate validity period let now = Utc::now(); if now < cert_info.not_before { return Err(MtlsError::ValidationError( "Certificate is not yet valid".to_string(), )); } if now > cert_info.not_after { return Err(MtlsError::ValidationError( "Certificate has expired".to_string(), )); } // In production, would verify certificate chain against CA store // For now, we trust certificates that were extracted from the TLS connection Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_mtls_config_creation() { let config = MtlsConfig { ca_cert_path: "/etc/linux_patch_api/certs/ca.pem".to_string(), server_cert_path: "/etc/linux_patch_api/certs/server.pem".to_string(), server_key_path: "/etc/linux_patch_api/certs/server.key".to_string(), min_tls_version: "1.3".to_string(), }; assert_eq!(config.ca_cert_path, "/etc/linux_patch_api/certs/ca.pem"); assert_eq!(config.min_tls_version, "1.3"); } #[test] fn test_client_cert_info() { let info = ClientCertInfo { subject: "CN=test-client".to_string(), issuer: "CN=Test CA".to_string(), serial: "12345".to_string(), not_before: Utc::now() - Duration::days(1), not_after: Utc::now() + Duration::days(365), }; assert!(info.subject.contains("CN=")); assert!(info.issuer.contains("CN=")); // Test validation with valid cert let cert_store = RootCertStore::empty(); assert!(validate_client_certificate(&info, &cert_store).is_ok()); } #[test] fn test_client_cert_expired() { let info = ClientCertInfo { subject: "CN=expired-client".to_string(), issuer: "CN=Test CA".to_string(), serial: "12345".to_string(), not_before: Utc::now() - Duration::days(365), not_after: Utc::now() - Duration::days(1), }; let cert_store = RootCertStore::empty(); let result = validate_client_certificate(&info, &cert_store); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("expired")); } }