//! mTLS (Mutual TLS) authentication //! //! Provides certificate-based authentication for service-to-service communication. use std::collections::HashMap; use iam_types::{Error, IamError, PrincipalKind, PrincipalRef, Result}; /// Certificate information extracted from mTLS connection #[derive(Debug, Clone)] pub struct CertificateInfo { /// Common Name (CN) from the certificate subject pub common_name: String, /// Organization (O) from the certificate subject pub organization: Option, /// Organizational Unit (OU) from the certificate subject pub organizational_unit: Option, /// Serial number of the certificate pub serial_number: String, /// Certificate fingerprint (SHA-256) pub fingerprint: String, /// Not before timestamp (Unix seconds) pub not_before: u64, /// Not after timestamp (Unix seconds) pub not_after: u64, /// Subject Alternative Names (SANs) pub sans: Vec, } /// Subject Alternative Name types #[derive(Debug, Clone)] pub enum SubjectAltName { /// DNS name Dns(String), /// URI Uri(String), /// IP address Ip(String), /// Email Email(String), } /// Configuration for mTLS verifier #[derive(Debug, Clone)] pub struct MtlsVerifierConfig { /// Mapping from CN patterns to principal references /// Pattern can use * as wildcard pub cn_mappings: HashMap, /// Required organization pub required_org: Option, /// Required organizational unit pub required_ou: Option, /// Whether to validate certificate expiration pub validate_expiration: bool, } /// Mapping configuration for a CN pattern #[derive(Debug, Clone)] pub struct PrincipalMapping { /// Principal kind to assign pub kind: PrincipalKind, /// ID template (can use {cn}, {ou}, {o} placeholders) pub id_template: String, /// Optional node ID extraction from CN pub node_id_from_cn: bool, } impl Default for MtlsVerifierConfig { fn default() -> Self { Self { cn_mappings: HashMap::new(), required_org: None, required_ou: None, validate_expiration: true, } } } impl MtlsVerifierConfig { /// Create a new config pub fn new() -> Self { Self::default() } /// Add a CN to principal mapping pub fn add_mapping(mut self, cn_pattern: impl Into, mapping: PrincipalMapping) -> Self { self.cn_mappings.insert(cn_pattern.into(), mapping); self } /// Set required organization pub fn with_required_org(mut self, org: impl Into) -> Self { self.required_org = Some(org.into()); self } /// Set required OU pub fn with_required_ou(mut self, ou: impl Into) -> Self { self.required_ou = Some(ou.into()); self } } /// mTLS certificate verifier pub struct MtlsVerifier { config: MtlsVerifierConfig, } impl MtlsVerifier { /// Create a new mTLS verifier pub fn new(config: MtlsVerifierConfig) -> Self { Self { config } } /// Create with default service account mapping pub fn with_default_sa_mapping() -> Self { let config = MtlsVerifierConfig::new() .add_mapping( "*.service.internal", PrincipalMapping { kind: PrincipalKind::ServiceAccount, id_template: "{cn}".into(), node_id_from_cn: false, }, ) .add_mapping( "node-*.compute.internal", PrincipalMapping { kind: PrincipalKind::ServiceAccount, id_template: "compute-agent".into(), node_id_from_cn: true, }, ); Self::new(config) } /// Verify a certificate and return the authenticated principal pub fn verify(&self, cert_info: &CertificateInfo) -> Result { // 1. Validate expiration if self.config.validate_expiration { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); if now < cert_info.not_before { return Err(Error::Iam(IamError::InvalidToken( "Certificate not yet valid".into(), ))); } if now > cert_info.not_after { return Err(Error::Iam(IamError::InvalidToken( "Certificate expired".into(), ))); } } // 2. Validate organization if required if let Some(required_org) = &self.config.required_org { match &cert_info.organization { Some(org) if org == required_org => {} Some(org) => { return Err(Error::Iam(IamError::AuthnFailed(format!( "Invalid organization: expected {}, got {}", required_org, org )))); } None => { return Err(Error::Iam(IamError::AuthnFailed( "Certificate missing organization".into(), ))); } } } // 3. Validate OU if required if let Some(required_ou) = &self.config.required_ou { match &cert_info.organizational_unit { Some(ou) if ou == required_ou => {} Some(ou) => { return Err(Error::Iam(IamError::AuthnFailed(format!( "Invalid organizational unit: expected {}, got {}", required_ou, ou )))); } None => { return Err(Error::Iam(IamError::AuthnFailed( "Certificate missing organizational unit".into(), ))); } } } // 4. Find matching CN pattern let (_pattern, mapping) = self .find_matching_pattern(&cert_info.common_name) .ok_or_else(|| { Error::Iam(IamError::AuthnFailed(format!( "No mapping for CN: {}", cert_info.common_name ))) })?; // 5. Build principal reference let principal_id = self.expand_template(&mapping.id_template, cert_info); let principal_ref = PrincipalRef::new(mapping.kind.clone(), principal_id); // 6. Extract node ID if configured let node_id = if mapping.node_id_from_cn { Some(self.extract_node_id(&cert_info.common_name)) } else { None }; Ok(MtlsAuthResult { principal_ref, node_id, certificate_fingerprint: cert_info.fingerprint.clone(), }) } fn find_matching_pattern(&self, cn: &str) -> Option<(&String, &PrincipalMapping)> { for (pattern, mapping) in &self.config.cn_mappings { if self.matches_pattern(pattern, cn) { return Some((pattern, mapping)); } } None } fn matches_pattern(&self, pattern: &str, value: &str) -> bool { if pattern == "*" { return true; } if !pattern.contains('*') { return pattern == value; } // Simple glob matching let parts: Vec<&str> = pattern.split('*').collect(); if parts.len() == 2 { let (prefix, suffix) = (parts[0], parts[1]); return value.starts_with(prefix) && value.ends_with(suffix); } // For more complex patterns, do exact match pattern == value } fn expand_template(&self, template: &str, cert_info: &CertificateInfo) -> String { let mut result = template.to_string(); result = result.replace("{cn}", &cert_info.common_name); if let Some(o) = &cert_info.organization { result = result.replace("{o}", o); } if let Some(ou) = &cert_info.organizational_unit { result = result.replace("{ou}", ou); } result } fn extract_node_id(&self, cn: &str) -> String { // Extract node ID from CN like "node-abc123.compute.internal" cn.split('.').next().unwrap_or(cn).to_string() } } /// Result of mTLS authentication #[derive(Debug, Clone)] pub struct MtlsAuthResult { /// Authenticated principal reference pub principal_ref: PrincipalRef, /// Node ID (if applicable) pub node_id: Option, /// Certificate fingerprint pub certificate_fingerprint: String, } #[cfg(test)] mod tests { use super::*; fn test_cert_info() -> CertificateInfo { CertificateInfo { common_name: "compute-agent.service.internal".into(), organization: Some("cloud-platform".into()), organizational_unit: Some("compute".into()), serial_number: "123456".into(), fingerprint: "sha256:abc123".into(), not_before: 0, not_after: u64::MAX, sans: vec![], } } #[test] fn test_mtls_verification() { let config = MtlsVerifierConfig::new() .add_mapping( "*.service.internal", PrincipalMapping { kind: PrincipalKind::ServiceAccount, id_template: "{cn}".into(), node_id_from_cn: false, }, ) .with_required_org("cloud-platform"); let verifier = MtlsVerifier::new(config); let cert = test_cert_info(); let result = verifier.verify(&cert).unwrap(); assert_eq!(result.principal_ref.kind, PrincipalKind::ServiceAccount); assert_eq!(result.principal_ref.id, "compute-agent.service.internal"); } #[test] fn test_node_id_extraction() { let config = MtlsVerifierConfig::new().add_mapping( "node-*.compute.internal", PrincipalMapping { kind: PrincipalKind::ServiceAccount, id_template: "compute-agent".into(), node_id_from_cn: true, }, ); let verifier = MtlsVerifier::new(config); let mut cert = test_cert_info(); cert.common_name = "node-abc123.compute.internal".into(); cert.organization = None; let result = verifier.verify(&cert).unwrap(); assert_eq!(result.node_id, Some("node-abc123".into())); } #[test] fn test_pattern_matching() { let verifier = MtlsVerifier::new(MtlsVerifierConfig::default()); assert!(verifier.matches_pattern("*.example.com", "foo.example.com")); assert!(verifier.matches_pattern("*.example.com", "bar.example.com")); assert!(!verifier.matches_pattern("*.example.com", "foo.other.com")); assert!(verifier.matches_pattern("exact-match", "exact-match")); assert!(!verifier.matches_pattern("exact-match", "other")); assert!(verifier.matches_pattern("*", "anything")); } }