est-ca 0.1.0

RFC 7030 Enrollment over Secure Transport (EST) — client, server, and an internal X.509 CA in pure Rust.
//! Serial-number management.
//!
//! X.509 serials must be unique within a given issuer. We generate 64
//! random bits of entropy at issuance time (~2^32 issuances before a
//! collision becomes plausible — vastly more than this CA is sized for)
//! and persist a monotonic counter as a defence-in-depth uniqueness
//! check. A [`SerialStore`] abstracts the counter persistence so tests
//! can stay in-memory.

use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};

use rand::RngCore;

use crate::error::{Error, Result};

/// Persistence for a strictly-monotonic serial counter.
pub trait SerialStore: Send + Sync + 'static {
    /// Atomically increment and return the counter's next value. Must
    /// survive process restarts — callers rely on monotonicity.
    fn next(&self) -> Result<u64>;
}

/// File-backed serial counter. Writes the value as decimal ASCII so
/// operators can inspect / bump it with `cat` / `echo`.
pub struct FileSerialStore {
    path: PathBuf,
    current: parking_lot::Mutex<u64>,
}

impl FileSerialStore {
    /// Open (or create) the counter file at `path`.
    pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
        let path = path.into();
        let start = match std::fs::read_to_string(&path) {
            Ok(s) => s.trim().parse::<u64>().unwrap_or(0),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => 0,
            Err(e) => return Err(Error::Io(e)),
        };
        Ok(Self { path, current: parking_lot::Mutex::new(start) })
    }
}

impl SerialStore for FileSerialStore {
    fn next(&self) -> Result<u64> {
        let mut guard = self.current.lock();
        *guard = guard.saturating_add(1);
        let next = *guard;
        std::fs::write(&self.path, next.to_string())?;
        Ok(next)
    }
}

/// In-memory `SerialStore` — **do not use in production** (resets on
/// restart, which lets the CA re-issue a serial a revoked cert once held).
pub struct InMemorySerialStore(AtomicU64);

impl InMemorySerialStore {
    /// Start from zero.
    pub fn new() -> Self {
        Self(AtomicU64::new(0))
    }
}

impl Default for InMemorySerialStore {
    fn default() -> Self {
        Self::new()
    }
}

impl SerialStore for InMemorySerialStore {
    fn next(&self) -> Result<u64> {
        Ok(self.0.fetch_add(1, Ordering::Relaxed).saturating_add(1))
    }
}

/// Build a 20-byte random serial (RFC 5280 §4.1.2.2 upper bound) with
/// the high-order bit cleared to keep the ASN.1 integer positive.
pub fn random_serial_bytes() -> [u8; 20] {
    let mut buf = [0u8; 20];
    rand::rngs::OsRng.fill_bytes(&mut buf);
    buf[0] &= 0x7F;
    buf
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn file_serial_survives_reopen() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("serial");
        let a = FileSerialStore::open(&path).unwrap();
        assert_eq!(a.next().unwrap(), 1);
        assert_eq!(a.next().unwrap(), 2);
        drop(a);
        let b = FileSerialStore::open(&path).unwrap();
        assert_eq!(b.next().unwrap(), 3);
    }
}