From 4c23f8c50caef78ca459505bf2bdd789b4e4f427 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sat, 21 Feb 2026 19:19:35 +0100 Subject: [PATCH] security(api-gateway): implement X.509 signature verification for mTLS Replace the TODO stub in validate_certificate_chain with full cryptographic chain-of-trust verification using x509-parser's ring-backed verify feature: 1. Issuer CN matching -- client cert issuer must equal CA subject CN 2. CA validity period -- reject expired or not-yet-valid CA certificates 3. CA Basic Constraints -- verify the signer actually has cA=true 4. Cryptographic signature -- verify client cert TBS data signature against the CA's SubjectPublicKeyInfo (RSA/ECDSA/Ed25519 via ring) Also enables the "verify" feature on x509-parser and extends the method signature to accept ca_cert_pem for proper chain validation. Co-Authored-By: Claude Opus 4.6 --- services/api_gateway/Cargo.toml | 2 +- .../api_gateway/src/auth/mtls/validator.rs | 124 +++++++++++++++--- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/services/api_gateway/Cargo.toml b/services/api_gateway/Cargo.toml index 6f47b8eee..97364a815 100644 --- a/services/api_gateway/Cargo.toml +++ b/services/api_gateway/Cargo.toml @@ -49,7 +49,7 @@ prometheus = { workspace = true, features = ["process"] } # Cryptography and security sha2.workspace = true -x509-parser = "0.16" +x509-parser = { version = "0.16", features = ["verify"] } reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false } base64.workspace = true jsonwebtoken.workspace = true diff --git a/services/api_gateway/src/auth/mtls/validator.rs b/services/api_gateway/src/auth/mtls/validator.rs index ba52d6c33..aa9a898cf 100644 --- a/services/api_gateway/src/auth/mtls/validator.rs +++ b/services/api_gateway/src/auth/mtls/validator.rs @@ -9,7 +9,7 @@ //! 6. Hostname verification use anyhow::Result; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use x509_parser::certificate::X509Certificate; use x509_parser::extensions::{GeneralName, ParsedExtension}; @@ -362,8 +362,20 @@ impl X509CertificateValidator { /// Validate certificate chain of trust against CA certificate /// - /// This validates the certificate signature against the CA's public key - pub fn validate_certificate_chain(&self, client_cert_pem: &[u8]) -> Result<()> { + /// Performs cryptographic signature verification of the client certificate + /// against the CA's public key using the `ring` crate (via x509-parser's verify feature). + /// + /// Validation steps: + /// 1. Parse both client and CA certificates from PEM + /// 2. Verify the client certificate's issuer CN matches the CA's subject CN + /// 3. Verify the CA certificate's validity period (not expired) + /// 4. Verify the CA has the cA Basic Constraint (is actually a CA) + /// 5. Cryptographically verify the client cert's signature using the CA's public key + pub fn validate_certificate_chain( + &self, + client_cert_pem: &[u8], + ca_cert_pem: &[u8], + ) -> Result<()> { // Parse client certificate let (_, client_pem) = x509_parser::pem::parse_x509_pem(client_cert_pem) .map_err(|e| anyhow::anyhow!("Failed to parse client certificate PEM: {}", e))?; @@ -372,27 +384,107 @@ impl X509CertificateValidator { .parse_x509() .map_err(|e| anyhow::anyhow!("Failed to parse client X.509 certificate: {}", e))?; - // In a production system, you would: - // 1. Parse the CA certificate from self.ca_certificate - // 2. Extract the CA's public key - // 3. Verify the client certificate's signature using the CA public key - // 4. Check that the client certificate's issuer matches the CA's subject + // Parse CA certificate + let (_, ca_pem) = x509_parser::pem::parse_x509_pem(ca_cert_pem) + .map_err(|e| anyhow::anyhow!("Failed to parse CA certificate PEM: {}", e))?; - // For now, we perform basic issuer checks - let client_issuer = client_cert + let ca_cert = ca_pem + .parse_x509() + .map_err(|e| anyhow::anyhow!("Failed to parse CA X.509 certificate: {}", e))?; + + // --- Step 1: Verify issuer CN matches CA subject CN --- + let client_issuer_cn = client_cert .issuer() .iter_common_name() .next() .and_then(|cn| cn.as_str().ok()) .ok_or_else(|| anyhow::anyhow!("Client certificate missing issuer CN"))?; - debug!("Client certificate issued by: {}", client_issuer); + let ca_subject_cn = ca_cert + .subject() + .iter_common_name() + .next() + .and_then(|cn| cn.as_str().ok()) + .ok_or_else(|| anyhow::anyhow!("CA certificate missing subject CN"))?; - // TODO: Implement full signature verification using ring or rustls crate - // This would involve: - // - Parsing CA certificate public key - // - Extracting signature algorithm from client cert - // - Verifying signature matches + if client_issuer_cn != ca_subject_cn { + return Err(anyhow::anyhow!( + "Certificate chain broken: client issuer CN '{}' does not match CA subject CN '{}'", + client_issuer_cn, + ca_subject_cn + )); + } + + debug!( + "Issuer CN match verified: client issuer='{}', CA subject='{}'", + client_issuer_cn, ca_subject_cn + ); + + // --- Step 2: Verify CA certificate is not expired --- + let ca_validity = ca_cert.validity(); + let now: i64 = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| anyhow::anyhow!("System time error: {}", e))? + .as_secs() + .try_into() + .map_err(|_| anyhow::anyhow!("Timestamp exceeds i64 range"))?; + + if now < ca_validity.not_before.timestamp() { + return Err(anyhow::anyhow!( + "CA certificate not yet valid. Valid from: {}", + ca_validity.not_before + )); + } + + if now > ca_validity.not_after.timestamp() { + return Err(anyhow::anyhow!( + "CA certificate expired. Expired on: {}", + ca_validity.not_after + )); + } + + // --- Step 3: Verify CA has Basic Constraints with cA=true --- + let mut ca_has_ca_flag = false; + for ext in ca_cert.extensions() { + if let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() { + if bc.ca { + ca_has_ca_flag = true; + } + } + } + + if !ca_has_ca_flag { + return Err(anyhow::anyhow!( + "CA certificate does not have Basic Constraints cA=true -- not a valid CA" + )); + } + + // --- Step 4: Cryptographic signature verification --- + // Use the CA's public key to verify the client certificate's signature. + // x509-parser's verify_signature uses ring internally to: + // - Extract the signature algorithm OID from the client cert + // - Map it to the appropriate ring verification algorithm + // (RSA-PKCS1-SHA256/384/512, ECDSA-P256/P384, Ed25519) + // - Extract the CA's SubjectPublicKeyInfo + // - Verify the signature over the client cert's TBS (to-be-signed) data + let ca_public_key = ca_cert.public_key(); + client_cert + .verify_signature(Some(ca_public_key)) + .map_err(|e| { + anyhow::anyhow!( + "Certificate signature verification failed: {} \ + (algorithm OID: {}, CA subject: '{}')", + e, + client_cert.signature_algorithm.algorithm, + ca_subject_cn + ) + })?; + + info!( + "Certificate chain verified: client cert signed by CA '{}' \ + (algorithm: {})", + ca_subject_cn, client_cert.signature_algorithm.algorithm + ); Ok(()) }