feat(crypto,pki): implement Wave 1 — hybrid KEM + PKI

aura-crypto: X25519 + ML-KEM-768 (FIPS 203) hybrid KEM, HKDF-SHA256 session
key derivation, ChaCha20-Poly1305 AeadSession with counter nonces; genuine
NIST ACVP ML-KEM-768 KAT (decapsulation vector). 16 tests green, clippy clean.

aura-pki: self-signed CA, server/client cert issuance (rcgen 0.14), mutual
X.509 chain verification via rustls-webpki, CRL revocation. 8 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-25 17:55:06 +03:00
parent f78633e04f
commit b8ce58ddf0
18 changed files with 1712 additions and 5 deletions
+186
View File
@@ -0,0 +1,186 @@
//! 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<Self, PkiError> {
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<Item = String>) {
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<String, PkiError> {
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<EndEntityCert<'a>, 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<String, PkiError> {
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<Vec<u8>, 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)
}