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:
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user