//! mTLS Authentication Module //! //! Provides mutual TLS authentication middleware for Actix-web. //! Non-mTLS connections are silently dropped (no response). //! Supports CRL-aware client certificate verification when CRL is available. use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage, }; #[allow(unused_imports)] use chrono::{DateTime, Duration, Utc}; use futures_util::future::LocalBoxFuture; use rustls::{ client::danger::HandshakeSignatureValid, crypto::aws_lc_rs, pki_types::{CertificateDer, UnixTime}, server::{ danger::{ClientCertVerified, ClientCertVerifier}, ServerConfig, WebPkiClientVerifier, }, version::TLS13, DigitallySignedStruct, DistinguishedName, Error as RustlsError, RootCertStore, SignatureScheme, }; use rustls_pemfile::{certs, private_key}; use std::{fs::File, io::BufReader, sync::Arc}; use tracing::{debug, error, info, warn}; use super::crl::{cert_serial_hex, SharedCrlState}; /// 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 } /// CRL-aware client certificate verifier. /// /// Wraps WebPkiClientVerifier for chain validation, then checks the /// end-entity certificate serial against the in-memory CRL index. /// If CRL is unavailable (Missing/Degraded), falls back to WebPKI-only. #[derive(Debug)] struct CrlAwareVerifier { inner: Arc, crl_state: SharedCrlState, } impl CrlAwareVerifier { fn new(inner: Arc, crl_state: SharedCrlState) -> Self { Self { inner, crl_state } } } impl ClientCertVerifier for CrlAwareVerifier { fn offer_client_auth(&self) -> bool { self.inner.offer_client_auth() } fn client_auth_mandatory(&self) -> bool { self.inner.client_auth_mandatory() } fn root_hint_subjects(&self) -> &[DistinguishedName] { self.inner.root_hint_subjects() } fn verify_client_cert( &self, end_entity: &CertificateDer<'_>, intermediates: &[CertificateDer<'_>], now: UnixTime, ) -> Result { // 1. Delegate chain validation to WebPKI self.inner .verify_client_cert(end_entity, intermediates, now)?; // 2. Check CRL revocation status let crl = self.crl_state.load(); match crl.status { super::crl::CrlStatus::Valid | super::crl::CrlStatus::Expired => { // CRL is available -- check serial if let Some(serial_hex) = cert_serial_hex(end_entity.as_ref()) { if crl.is_revoked(&serial_hex) { warn!( serial = %serial_hex, "Client certificate is revoked per CRL -- rejecting connection" ); return Err(RustlsError::InvalidCertificate( rustls::CertificateError::Revoked, )); } } Ok(ClientCertVerified::assertion()) } super::crl::CrlStatus::Missing | super::crl::CrlStatus::Degraded => { // No CRL available -- fall back to WebPKI-only (already passed above) warn!( status = %crl.status, "CRL not available -- allowing connection with WebPKI-only verification" ); Ok(ClientCertVerified::assertion()) } super::crl::CrlStatus::Invalid => { // Invalid CRL signature -- fail-closed error!( "CRL signature is invalid -- refusing all client certificates (fail-closed)" ); Err(RustlsError::InvalidCertificate( rustls::CertificateError::Revoked, )) } } } fn verify_tls12_signature( &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { self.inner.verify_tls12_signature(message, cert, dss) } fn verify_tls13_signature( &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { self.inner.verify_tls13_signature(message, cert, dss) } fn supported_verify_schemes(&self) -> Vec { self.inner.supported_verify_schemes() } } /// 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. /// /// When `crl_state` is provided and the CRL is available, wraps the /// WebPkiClientVerifier with CrlAwareVerifier for revocation checking. /// When CRL is missing/degraded, falls back to WebPKI-only verification. pub fn build_rustls_config( &self, crl_state: Option, ) -> Result, MtlsError> { let webpki_verifier = WebPkiClientVerifier::builder(self.cert_store.clone()) .build() .map_err(|e| MtlsError::ClientVerifierError(e.to_string()))?; let client_verifier: Arc = match crl_state { Some(state) => { info!("CRL-aware client verification enabled"); Arc::new(CrlAwareVerifier::new(webpki_verifier, state)) } None => { info!("No CRL state provided -- using WebPKI-only client verification"); webpki_verifier } }; 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_provider(Arc::new(aws_lc_rs::default_provider())) .with_protocol_versions(&[&TLS13]) .map_err(|e| { MtlsError::ServerConfigError(format!("Failed to set TLS 1.3 only: {}", e)) })? .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")); } // ----------------------------------------------------------------------- // CrlAwareVerifier unit tests // ----------------------------------------------------------------------- /// Test that CrlAwareVerifier can be constructed with a WebPKI verifier /// and a SharedCrlState. This verifies the wiring is correct. #[test] fn crl_aware_verifier_construction() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); use super::super::crl::{new_shared_state, CrlState, CrlStatus}; use std::collections::HashSet; // Build a simple CA cert + key for the root store. let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); let mut ca_params = rcgen::CertificateParams::default(); ca_params.not_before = time::OffsetDateTime::now_utc(); ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365); ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign]; let mut dn = rcgen::DistinguishedName::new(); dn.push(rcgen::DnType::CommonName, "Test CA for Verifier"); ca_params.distinguished_name = dn; let ca_cert = ca_params.self_signed(&ca_key).unwrap(); // Build root cert store with the CA. let mut root_store = RootCertStore::empty(); root_store.add(ca_cert.der().to_owned()).unwrap(); // Build WebPKI verifier — build() returns Arc // which coerces to Arc. let webpki_verifier: Arc = WebPkiClientVerifier::builder(root_store.into()) .build() .unwrap(); // Build CRL state in Valid status. let crl_state = new_shared_state(); let valid_state = CrlState { status: CrlStatus::Valid, revoked_serials: HashSet::new(), crl_mtime: None, loaded_at: std::time::SystemTime::now(), }; crl_state.store(Arc::new(valid_state)); // Construct CrlAwareVerifier — should succeed. let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state); // If we reach here without panic, construction succeeded. } /// Test that CrlAwareVerifier with Missing CRL state can be constructed. /// Missing CRL means the verifier falls back to WebPKI-only. #[test] fn crl_aware_verifier_with_missing_crl() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); use super::super::crl::new_shared_state; let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); let mut ca_params = rcgen::CertificateParams::default(); ca_params.not_before = time::OffsetDateTime::now_utc(); ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365); ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign]; let mut dn = rcgen::DistinguishedName::new(); dn.push(rcgen::DnType::CommonName, "Test CA for Verifier"); ca_params.distinguished_name = dn; let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let mut root_store = RootCertStore::empty(); root_store.add(ca_cert.der().to_owned()).unwrap(); let webpki_verifier: Arc = WebPkiClientVerifier::builder(root_store.into()) .build() .unwrap(); // Default state is Missing. let crl_state = new_shared_state(); let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state); } /// Test that CrlAwareVerifier with Invalid CRL state can be constructed. /// Invalid CRL means the verifier should reject ALL client certificates. #[test] fn crl_aware_verifier_with_invalid_crl() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); use super::super::crl::{new_shared_state, CrlState, CrlStatus}; use std::collections::HashSet; let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); let mut ca_params = rcgen::CertificateParams::default(); ca_params.not_before = time::OffsetDateTime::now_utc(); ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365); ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign]; let mut dn = rcgen::DistinguishedName::new(); dn.push(rcgen::DnType::CommonName, "Test CA for Verifier"); ca_params.distinguished_name = dn; let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let mut root_store = RootCertStore::empty(); root_store.add(ca_cert.der().to_owned()).unwrap(); let webpki_verifier: Arc = WebPkiClientVerifier::builder(root_store.into()) .build() .unwrap(); let crl_state = new_shared_state(); let invalid_state = CrlState { status: CrlStatus::Invalid, revoked_serials: HashSet::new(), crl_mtime: None, loaded_at: std::time::SystemTime::now(), }; crl_state.store(Arc::new(invalid_state)); let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state); } /// Test that CrlAwareVerifier with a revoked serial in Valid CRL state /// can be constructed. The actual verification logic is tested through /// integration tests since it requires a full TLS handshake. #[test] fn crl_aware_verifier_with_revoked_serial() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); use super::super::crl::{new_shared_state, CrlState, CrlStatus}; use std::collections::HashSet; let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); let mut ca_params = rcgen::CertificateParams::default(); ca_params.not_before = time::OffsetDateTime::now_utc(); ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365); ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign]; let mut dn = rcgen::DistinguishedName::new(); dn.push(rcgen::DnType::CommonName, "Test CA for Verifier"); ca_params.distinguished_name = dn; let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let mut root_store = RootCertStore::empty(); root_store.add(ca_cert.der().to_owned()).unwrap(); let webpki_verifier: Arc = WebPkiClientVerifier::builder(root_store.into()) .build() .unwrap(); let crl_state = new_shared_state(); let mut revoked = HashSet::new(); revoked.insert("deadbeef".to_string()); let valid_with_revoked = CrlState { status: CrlStatus::Valid, revoked_serials: revoked, crl_mtime: None, loaded_at: std::time::SystemTime::now(), }; crl_state.store(Arc::new(valid_with_revoked)); let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state); // Construction succeeded — the verifier is ready to reject revoked certs. } }