Files
AuraVPN/crates/aura-pki/src/store.rs
T
xah30 b8ce58ddf0 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>
2026-05-25 17:55:06 +03:00

89 lines
2.6 KiB
Rust

//! 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);
}
}