//! Certificate verification: chain validation against the Aura CA plus identity //! extraction and CRL (revocation) checks. use rustls_pki_types::{CertificateDer, ServerName, UnixTime}; use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage}; use x509_parser::certificate::X509Certificate; use x509_parser::prelude::FromDer; use crate::store::CrlStore; use crate::PkiError; /// Signature algorithms accepted during chain verification. /// /// rcgen's [`rcgen::KeyPair::generate`] defaults to ECDSA P-256 / SHA-256, so /// that algorithm must be present. The others are included so the verifier keeps /// working if a deployment switches the CA/leaf key type later. static SUPPORTED_ALGS: &[&dyn rustls_pki_types::SignatureVerificationAlgorithm] = &[ webpki::ring::ECDSA_P256_SHA256, webpki::ring::ECDSA_P384_SHA384, webpki::ring::ED25519, ]; /// Verifies certificate chains against a trusted Aura CA, with an optional CRL. pub struct AuraCertVerifier { /// DER of the trusted CA certificate (the trust anchor is borrowed from it /// per-verification, so we keep the owning bytes here). ca_der: CertificateDer<'static>, /// Revoked certificates, keyed by serial number (lowercase hex, no /// separators) and/or by Common Name / client id. crl: CrlStore, } impl AuraCertVerifier { /// Build a verifier trusting the given CA certificate (PEM). pub fn new(ca_cert_pem: &str) -> Result { let der = pem_to_der(ca_cert_pem)?; let ca_der = CertificateDer::from(der); // Validate up front that the CA is usable as a trust anchor. anchor_from_trusted_cert(&ca_der).map_err(|e| PkiError::TrustAnchor(e.to_string()))?; Ok(Self { ca_der, crl: CrlStore::new(), }) } /// Load a CRL: a set of revoked serial numbers (hex) and/or client ids /// (Common Names). Replaces any previously configured set. pub fn set_revoked(&mut self, revoked: impl IntoIterator) { self.crl = CrlStore::from_iter(revoked); } /// Access the underlying revocation store (e.g. to persist it). pub fn crl(&self) -> &CrlStore { &self.crl } /// Verify a client certificate chain against the CA. /// /// On success returns the client id (Common Name from the leaf subject). pub fn verify_client_cert(&self, cert_chain: &[CertificateDer]) -> Result { let leaf = self.verify_chain(cert_chain, KeyUsage::client_auth())?; let client_id = common_name(&leaf)?; self.check_not_revoked(&leaf, Some(&client_id))?; Ok(client_id) } /// Verify a server certificate chain against the CA and that the leaf is /// valid for `server_name` (DNS SAN match). pub fn verify_server_cert( &self, cert_chain: &[CertificateDer], server_name: &str, ) -> Result<(), PkiError> { let leaf = self.verify_chain(cert_chain, KeyUsage::server_auth())?; let name = ServerName::try_from(server_name) .map_err(|_| PkiError::NameMismatch(server_name.to_string()))?; leaf.verify_is_valid_for_subject_name(&name) .map_err(|_| PkiError::NameMismatch(server_name.to_string()))?; self.check_not_revoked(&leaf, None)?; Ok(()) } /// Run webpki chain verification and return the parsed end-entity cert. fn verify_chain<'a>( &self, cert_chain: &'a [CertificateDer], usage: KeyUsage, ) -> Result, PkiError> { let (leaf_der, intermediates) = cert_chain.split_first().ok_or(PkiError::EmptyChain)?; let leaf = EndEntityCert::try_from(leaf_der).map_err(|e| PkiError::CertParse(e.to_string()))?; let anchor = anchor_from_trusted_cert(&self.ca_der) .map_err(|e| PkiError::TrustAnchor(e.to_string()))?; let anchors = [anchor]; leaf.verify_for_usage( SUPPORTED_ALGS, &anchors, intermediates, UnixTime::now(), usage, None, // revocation handled separately via the simple CRL store None, ) .map_err(|e| PkiError::Verification(e.to_string()))?; Ok(leaf) } /// Reject the leaf if its serial or Common Name is in the CRL. fn check_not_revoked( &self, leaf: &EndEntityCert<'_>, known_cn: Option<&str>, ) -> Result<(), PkiError> { if self.crl.is_empty() { return Ok(()); } // Extract owned identifiers, then drop the parsed cert before checking. let (serial, cn) = { let der = leaf.der(); let (_, parsed) = X509Certificate::from_der(der.as_ref()) .map_err(|e| PkiError::CertParse(e.to_string()))?; let serial = serial_hex(&parsed); let cn = match known_cn { Some(cn) => Some(cn.to_string()), None => parsed .subject() .iter_common_name() .next() .and_then(|a| a.as_str().ok()) .map(str::to_string), }; (serial, cn) }; if self.crl.contains(&serial) { return Err(PkiError::Revoked(format!("serial {serial}"))); } if let Some(cn) = cn { if self.crl.contains(&cn) { return Err(PkiError::Revoked(format!("client id {cn}"))); } } Ok(()) } } /// Extract the first Common Name from the leaf's subject. fn common_name(leaf: &EndEntityCert<'_>) -> Result { let der = leaf.der(); let (_, parsed) = X509Certificate::from_der(der.as_ref()).map_err(|e| PkiError::CertParse(e.to_string()))?; let cn = parsed .subject() .iter_common_name() .next() .and_then(|attr| attr.as_str().ok()) .map(str::to_string); cn.ok_or_else(|| PkiError::MissingIdentity("no Common Name in subject".into())) } /// Lowercase, separator-free hex of the certificate serial number. fn serial_hex(cert: &X509Certificate<'_>) -> String { cert.tbs_certificate .raw_serial() .iter() .map(|b| format!("{b:02x}")) .collect() } /// Decode the first certificate from a PEM string into DER bytes. fn pem_to_der(pem: &str) -> Result, PkiError> { let (_, item) = x509_parser::pem::parse_x509_pem(pem.as_bytes()) .map_err(|e| PkiError::CertParse(format!("invalid PEM: {e}")))?; if item.label != "CERTIFICATE" { return Err(PkiError::CertParse(format!( "expected CERTIFICATE PEM block, found '{}'", item.label ))); } Ok(item.contents) }