Skip to main content

libro/
verify.rs

1//! Chain verification — validate integrity of an audit chain.
2
3use tracing::warn;
4
5use crate::LibroError;
6use crate::entry::AuditEntry;
7
8/// Verify a sequence of audit entries forms a valid chain.
9pub fn verify_chain(entries: &[AuditEntry]) -> crate::Result<()> {
10    if entries.is_empty() {
11        return Ok(());
12    }
13
14    for (i, entry) in entries.iter().enumerate() {
15        let expected_hash = entry.compute_hash();
16        if entry.hash() != expected_hash {
17            warn!(
18                index = i,
19                hash = entry.hash(),
20                expected = %expected_hash,
21                "entry self-hash verification failed"
22            );
23            return Err(LibroError::IntegrityViolation {
24                index: i,
25                expected: expected_hash,
26                actual: entry.hash().to_owned(),
27            });
28        }
29        if i > 0 && entry.prev_hash() != entries[i - 1].hash() {
30            warn!(
31                index = i,
32                expected = entries[i - 1].hash(),
33                actual = entry.prev_hash(),
34                "chain linkage broken"
35            );
36            return Err(LibroError::IntegrityViolation {
37                index: i,
38                expected: entries[i - 1].hash().to_owned(),
39                actual: entry.prev_hash().to_owned(),
40            });
41        }
42    }
43
44    Ok(())
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::entry::{AuditEntry, EventSeverity};
51
52    #[test]
53    fn verify_valid_chain() {
54        let e1 = AuditEntry::new(EventSeverity::Info, "s", "a", serde_json::json!({}), "");
55        let e2 = AuditEntry::new(
56            EventSeverity::Info,
57            "s",
58            "b",
59            serde_json::json!({}),
60            e1.hash(),
61        );
62        assert!(verify_chain(&[e1, e2]).is_ok());
63    }
64
65    #[test]
66    fn verify_broken_link() {
67        let e1 = AuditEntry::new(EventSeverity::Info, "s", "a", serde_json::json!({}), "");
68        let e2 = AuditEntry::new(
69            EventSeverity::Info,
70            "s",
71            "b",
72            serde_json::json!({}),
73            "wrong",
74        );
75        assert!(verify_chain(&[e1, e2]).is_err());
76    }
77
78    #[test]
79    fn verify_empty() {
80        assert!(verify_chain(&[]).is_ok());
81    }
82
83    #[test]
84    fn verify_tampered_self_hash() {
85        let mut e1 = AuditEntry::new(EventSeverity::Info, "s", "a", serde_json::json!({}), "");
86        e1.corrupt_hash("tampered");
87        let err = verify_chain(&[e1]).unwrap_err();
88        assert!(err.to_string().contains("entry 0"));
89    }
90
91    #[test]
92    fn verify_single_valid_entry() {
93        let e1 = AuditEntry::new(EventSeverity::Info, "s", "a", serde_json::json!({}), "");
94        assert!(verify_chain(&[e1]).is_ok());
95    }
96
97    #[test]
98    fn verify_long_chain() {
99        let mut entries = Vec::new();
100        let first = AuditEntry::new(EventSeverity::Info, "s", "e0", serde_json::json!({}), "");
101        entries.push(first);
102        for i in 1..50 {
103            let prev = entries[i - 1].hash();
104            entries.push(AuditEntry::new(
105                EventSeverity::Info,
106                "s",
107                format!("e{i}"),
108                serde_json::json!({}),
109                prev,
110            ));
111        }
112        assert!(verify_chain(&entries).is_ok());
113
114        // Tamper in the middle
115        entries[25].corrupt_action("hacked");
116        assert!(verify_chain(&entries).is_err());
117    }
118}