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 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-21 19:19:35 +01:00
parent 2fa459cf08
commit 4c23f8c50c
2 changed files with 109 additions and 17 deletions

View File

@@ -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

View File

@@ -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(())
}