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:
@@ -0,0 +1,88 @@
|
||||
//! Trust material storage: a simple v1 CRL (set of revoked identifiers).
|
||||
//!
|
||||
//! The Aura v1 revocation list is deliberately minimal: a set of opaque
|
||||
//! identifier strings. An identifier is either a certificate serial number
|
||||
//! (lowercase hex, no separators) or a client id / Common Name. A certificate
|
||||
//! is rejected if any of those identifiers is present in the set.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
/// A set of revoked certificate identifiers (serials and/or client ids).
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct CrlStore {
|
||||
revoked: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl CrlStore {
|
||||
/// Create an empty CRL.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add a single revoked identifier (serial hex or client id).
|
||||
pub fn revoke(&mut self, id: impl Into<String>) {
|
||||
self.revoked.insert(id.into());
|
||||
}
|
||||
|
||||
/// True if `id` is in the revocation set.
|
||||
pub fn contains(&self, id: &str) -> bool {
|
||||
self.revoked.contains(id)
|
||||
}
|
||||
|
||||
/// True if no certificates are revoked.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.revoked.is_empty()
|
||||
}
|
||||
|
||||
/// Number of revoked identifiers.
|
||||
pub fn len(&self) -> usize {
|
||||
self.revoked.len()
|
||||
}
|
||||
|
||||
/// Iterate over the revoked identifiers (sorted).
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> {
|
||||
self.revoked.iter().map(String::as_str)
|
||||
}
|
||||
|
||||
/// Persist the CRL, one identifier per line.
|
||||
pub fn save(&self, path: &Path) -> anyhow::Result<()> {
|
||||
let mut body = String::new();
|
||||
for id in &self.revoked {
|
||||
body.push_str(id);
|
||||
body.push('\n');
|
||||
}
|
||||
fs::write(path, body).with_context(|| format!("writing CRL to {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a CRL written by [`CrlStore::save`] (one identifier per line; blank
|
||||
/// lines and `#` comments are ignored).
|
||||
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||
let text = fs::read_to_string(path)
|
||||
.with_context(|| format!("reading CRL from {}", path.display()))?;
|
||||
Ok(Self::from_iter(
|
||||
text.lines()
|
||||
.map(str::trim)
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||
.map(str::to_string),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<String> for CrlStore {
|
||||
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
|
||||
Self {
|
||||
revoked: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<String> for CrlStore {
|
||||
fn extend<T: IntoIterator<Item = String>>(&mut self, iter: T) {
|
||||
self.revoked.extend(iter);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user