use super::event::AuditEvent;
pub struct AuditChain {
previous_hash: Option<String>,
sequence: u64,
service_name: String,
}
impl AuditChain {
pub fn new(service_name: String) -> Self {
Self {
previous_hash: None,
sequence: 0,
service_name,
}
}
pub fn resume(service_name: String, previous_hash: String, sequence: u64) -> Self {
Self {
previous_hash: Some(previous_hash),
sequence,
service_name,
}
}
pub fn seal(&mut self, mut event: AuditEvent) -> AuditEvent {
self.sequence += 1;
event.sequence = self.sequence;
event.previous_hash = self.previous_hash.clone();
event.service_name = self.service_name.clone();
let hash = self.compute_hash(&event);
event.hash = Some(hash.clone());
self.previous_hash = Some(hash);
event
}
pub fn sequence(&self) -> u64 {
self.sequence
}
pub fn previous_hash(&self) -> Option<&str> {
self.previous_hash.as_deref()
}
fn compute_hash(&self, event: &AuditEvent) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(event.sequence.to_le_bytes().as_ref());
if let Some(ref prev) = event.previous_hash {
hasher.update(prev.as_bytes());
}
hasher.update(event.id.as_bytes());
hasher.update(event.timestamp.to_rfc3339().as_bytes());
hasher.update(event.kind.to_string().as_bytes());
hasher.update(&[event.severity.as_syslog_severity()]);
hasher.update(event.service_name.as_bytes());
if let Some(ref method) = event.method {
hasher.update(method.as_bytes());
}
if let Some(ref path) = event.path {
hasher.update(path.as_bytes());
}
if let Some(code) = event.status_code {
hasher.update(code.to_le_bytes().as_ref());
}
if let Some(ref subject) = event.source.subject {
hasher.update(subject.as_bytes());
}
hasher.finalize().to_hex().to_string()
}
}
pub fn verify_chain(events: &[AuditEvent]) -> Result<(), ChainVerificationError> {
if events.is_empty() {
return Ok(());
}
let mut expected_prev: Option<String> = None;
for event in events {
if event.previous_hash != expected_prev {
return Err(ChainVerificationError {
sequence: event.sequence,
expected_previous_hash: expected_prev,
actual_previous_hash: event.previous_hash.clone(),
});
}
let recomputed = recompute_hash(event);
if event.hash.as_deref() != Some(recomputed.as_str()) {
return Err(ChainVerificationError {
sequence: event.sequence,
expected_previous_hash: expected_prev,
actual_previous_hash: event.previous_hash.clone(),
});
}
expected_prev = event.hash.clone();
}
Ok(())
}
fn recompute_hash(event: &AuditEvent) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(event.sequence.to_le_bytes().as_ref());
if let Some(ref prev) = event.previous_hash {
hasher.update(prev.as_bytes());
}
hasher.update(event.id.as_bytes());
hasher.update(event.timestamp.to_rfc3339().as_bytes());
hasher.update(event.kind.to_string().as_bytes());
hasher.update(&[event.severity.as_syslog_severity()]);
hasher.update(event.service_name.as_bytes());
if let Some(ref method) = event.method {
hasher.update(method.as_bytes());
}
if let Some(ref path) = event.path {
hasher.update(path.as_bytes());
}
if let Some(code) = event.status_code {
hasher.update(code.to_le_bytes().as_ref());
}
if let Some(ref subject) = event.source.subject {
hasher.update(subject.as_bytes());
}
hasher.finalize().to_hex().to_string()
}
#[derive(Debug)]
pub struct ChainVerificationError {
pub sequence: u64,
pub expected_previous_hash: Option<String>,
pub actual_previous_hash: Option<String>,
}
impl std::fmt::Display for ChainVerificationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Audit chain broken at sequence {}: expected previous_hash {:?}, got {:?}",
self.sequence, self.expected_previous_hash, self.actual_previous_hash
)
}
}
impl std::error::Error for ChainVerificationError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::audit::event::{AuditEventKind, AuditSeverity};
fn make_event(kind: AuditEventKind) -> AuditEvent {
AuditEvent::new(
kind,
AuditSeverity::Informational,
"test-service".to_string(),
)
}
#[test]
fn test_chain_seal_sets_fields() {
let mut chain = AuditChain::new("test-service".to_string());
let event = make_event(AuditEventKind::AuthLoginSuccess);
let sealed = chain.seal(event);
assert_eq!(sealed.sequence, 1);
assert!(sealed.hash.is_some());
assert!(sealed.previous_hash.is_none()); }
#[test]
fn test_chain_links_events() {
let mut chain = AuditChain::new("test-service".to_string());
let e1 = chain.seal(make_event(AuditEventKind::AuthLoginSuccess));
let e2 = chain.seal(make_event(AuditEventKind::HttpRequest));
assert_eq!(e1.sequence, 1);
assert_eq!(e2.sequence, 2);
assert_eq!(e2.previous_hash, e1.hash);
}
#[test]
fn test_chain_deterministic_hash() {
let mut chain1 = AuditChain::new("test-service".to_string());
let mut chain2 = AuditChain::new("test-service".to_string());
let event = make_event(AuditEventKind::AuthLoginSuccess);
let event_clone = event.clone();
let sealed1 = chain1.seal(event);
let sealed2 = chain2.seal(event_clone);
assert_eq!(sealed1.hash, sealed2.hash);
}
#[test]
fn test_chain_resume() {
let mut chain = AuditChain::new("test-service".to_string());
let e1 = chain.seal(make_event(AuditEventKind::AuthLoginSuccess));
let prev_hash = e1.hash.clone().unwrap();
let mut resumed = AuditChain::resume("test-service".to_string(), prev_hash.clone(), 1);
let e2 = resumed.seal(make_event(AuditEventKind::HttpRequest));
assert_eq!(e2.sequence, 2);
assert_eq!(e2.previous_hash, Some(prev_hash));
}
#[test]
fn test_verify_chain_valid() {
let mut chain = AuditChain::new("test-service".to_string());
let events: Vec<AuditEvent> = (0..5)
.map(|_| chain.seal(make_event(AuditEventKind::HttpRequest)))
.collect();
assert!(verify_chain(&events).is_ok());
}
#[test]
fn test_verify_chain_tampered() {
let mut chain = AuditChain::new("test-service".to_string());
let mut events: Vec<AuditEvent> = (0..5)
.map(|_| chain.seal(make_event(AuditEventKind::HttpRequest)))
.collect();
events[2].hash = Some("tampered".to_string());
let result = verify_chain(&events);
assert!(result.is_err());
}
#[test]
fn test_verify_chain_empty() {
assert!(verify_chain(&[]).is_ok());
}
#[test]
fn test_verify_chain_single_event() {
let mut chain = AuditChain::new("test-service".to_string());
let event = chain.seal(make_event(AuditEventKind::AuthLoginSuccess));
assert!(verify_chain(&[event]).is_ok());
}
#[test]
fn test_chain_sequence_monotonic() {
let mut chain = AuditChain::new("test-service".to_string());
let mut prev_seq = 0;
for _ in 0..10 {
let event = chain.seal(make_event(AuditEventKind::HttpRequest));
assert!(event.sequence > prev_seq);
prev_seq = event.sequence;
}
}
}