Skip to main content

argus_ledger/
lib.rs

1//! # argus-ledger
2//!
3//! Tamper-evident append-only SHA-256 provenance and audit ledger for autonomous agents.
4//!
5//! When autonomous agents execute commands, invoke external APIs, or mutate persistent
6//! memory, post-incident investigations require cryptographic certainty regarding what
7//! actions occurred and in what sequence. Standard application logs can be rewritten,
8//! reordered, or truncated.
9//!
10//! Argus provides an immutable append-only hash chain where each event records:
11//!
12//! 1. A strictly sequential sequence integer.
13//! 2. A cryptographic digest binding the payload to the previous event's hash (`prev_hash`).
14//! 3. Deterministic chain verification that validates integrity in linear time.
15//!
16//! ## Architecture Overview
17//!
18//! - [`AuditEvent`]: The individual structured log record containing event metadata,
19//!   payload, monotonic sequence number, and SHA-256 digests.
20//! - [`ChainVerificationReport`]: Audit summary produced by traversing the hash chain
21//!   from genesis to head, validating sequence ordering and hash consistency.
22//! - [`AuditLedgerWriter`]: Trait for appending new events to the audit log.
23//! - [`AuditLedgerReader`]: Trait for querying events by sequence or reading the chain head.
24//! - [`AuditVerifier`]: Trait for verifying cryptographic integrity across the chain.
25//! - [`InMemoryLedger`]: Thread-safe reference ledger implementation.
26//!
27//! ## Quick Start
28//!
29//! ```rust
30//! use argus_ledger::{AuditLedgerReader, AuditLedgerWriter, AuditVerifier, InMemoryLedger, GENESIS_PREV_HASH};
31//!
32//! // Create an in-memory audit ledger
33//! let mut ledger = InMemoryLedger::new();
34//!
35//! // 1. Record an initial initialization event
36//! let ev1 = ledger.append("agent.startup", "coordinator", "{\"status\":\"initialized\"}")
37//!     .expect("First event must succeed");
38//! assert_eq!(ev1.sequence, 1);
39//! assert_eq!(ev1.prev_hash, GENESIS_PREV_HASH);
40//!
41//! // 2. Record a tool call event cryptographically chained to ev1
42//! let ev2 = ledger.append("tool.exec", "worker_1", "{\"command\":\"build\"}")
43//!     .expect("Second event must succeed");
44//! assert_eq!(ev2.sequence, 2);
45//! assert_eq!(ev2.prev_hash, ev1.event_hash);
46//!
47//! // 3. Verify cryptographic chain integrity
48//! let report = ledger.verify_chain().expect("Verification must complete");
49//! assert!(report.valid);
50//! assert_eq!(report.total_events, 2);
51//! assert_eq!(report.head_hash, ev2.event_hash);
52//! ```
53
54use std::sync::{Arc, Mutex};
55use std::time::{SystemTime, UNIX_EPOCH};
56
57pub const VERSION: &str = env!("CARGO_PKG_VERSION");
58
59/// The fixed 64-character hexadecimal previous hash for genesis events.
60pub const GENESIS_PREV_HASH: &str =
61    "0000000000000000000000000000000000000000000000000000000000000000";
62
63/// An immutable event record committed to the audit chain.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct AuditEvent {
66    /// Monotonically increasing sequence number starting at 1.
67    pub sequence: u64,
68    /// Unique event identifier string.
69    pub event_id: String,
70    /// Event timestamp in seconds since UNIX epoch.
71    pub timestamp: String,
72    /// Dot-separated action taxonomy (for example: agent.startup, tool.exec).
73    pub event_type: String,
74    /// Process, user, or subagent identity that triggered the action.
75    pub actor: String,
76    /// Canonical JSON payload representing the event inputs and parameters.
77    pub payload: String,
78    /// The event_hash of sequence - 1, or GENESIS_PREV_HASH for sequence 1.
79    pub prev_hash: String,
80    /// SHA-256 hexadecimal hash computed across the event header and payload.
81    pub event_hash: String,
82    /// Optional detached cryptographic signature verifying author authenticity.
83    pub signature: Option<String>,
84}
85
86/// Cryptographic integrity verification report for an event chain.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ChainVerificationReport {
89    /// True if all sequence numbers, previous hashes, and event hashes match expectations.
90    pub valid: bool,
91    /// Total count of events verified in the ledger.
92    pub total_events: usize,
93    /// Event hash of the genesis event (sequence 1).
94    pub root_hash: String,
95    /// Event hash of the most recent event in the ledger.
96    pub head_hash: String,
97    /// Sequence number of the first detected discrepancy, or None if valid.
98    pub first_broken_sequence: Option<u64>,
99    /// Human-readable explanation of detected tampering or discontinuity.
100    pub error_detail: Option<String>,
101    /// Timestamp when verification was completed in seconds since UNIX epoch.
102    pub checked_at: String,
103}
104
105/// Errors returned during ledger append, retrieval, or verification operations.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum LedgerError {
108    /// Event sequence numbers are missing or out of order.
109    SequenceDiscontinuity(String),
110    /// Computed event hash does not match recorded event hash.
111    HashMismatch(String),
112    /// Mutex lock contention or thread synchronization failure.
113    LockError(String),
114}
115
116impl std::fmt::Display for LedgerError {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match self {
119            Self::SequenceDiscontinuity(msg) => write!(f, "Sequence discontinuity: {}", msg),
120            Self::HashMismatch(msg) => write!(f, "Hash mismatch: {}", msg),
121            Self::LockError(msg) => write!(f, "Lock error: {}", msg),
122        }
123    }
124}
125
126impl std::error::Error for LedgerError {}
127
128/// Trait defining append operations for audit ledgers.
129pub trait AuditLedgerWriter: Send + Sync {
130    /// Cryptographically append a new action record to the ledger.
131    fn append(
132        &mut self,
133        event_type: &str,
134        actor: &str,
135        payload: &str,
136    ) -> Result<AuditEvent, LedgerError>;
137}
138
139/// Trait defining read and query operations for audit ledgers.
140pub trait AuditLedgerReader: Send + Sync {
141    /// Fetch an individual event by its 1-indexed sequence number.
142    fn get_event(&self, sequence: u64) -> Result<Option<AuditEvent>, LedgerError>;
143    /// Retrieve the most recently committed event from the head of the chain.
144    fn head(&self) -> Result<Option<AuditEvent>, LedgerError>;
145    /// Return an in-order snapshot of all events committed to the ledger.
146    fn all_events(&self) -> Result<Vec<AuditEvent>, LedgerError>;
147}
148
149/// Trait defining full-chain cryptographic audit verification.
150pub trait AuditVerifier: Send + Sync {
151    /// Traverse the ledger from genesis to head, verifying all hashes and sequence numbers.
152    fn verify_chain(&self) -> Result<ChainVerificationReport, LedgerError>;
153}
154
155/// Standalone, pure-Rust SHA-256 implementation (FIPS 180-4 compliant, zero external dependencies).
156pub fn sha256_hex(input: &[u8]) -> String {
157    let mut h: [u32; 8] = [
158        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
159        0x5be0cd19,
160    ];
161
162    let k: [u32; 64] = [
163        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
164        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
165        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
166        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
167        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
168        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
169        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
170        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
171        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
172        0xc67178f2,
173    ];
174
175    let bit_len = (input.len() as u64) * 8;
176    let mut msg = input.to_vec();
177    msg.push(0x80);
178    while (msg.len() % 64) != 56 {
179        msg.push(0x00);
180    }
181    msg.extend_from_slice(&bit_len.to_be_bytes());
182
183    for chunk in msg.chunks_exact(64) {
184        let mut w = [0u32; 64];
185        for i in 0..16 {
186            w[i] = u32::from_be_bytes([
187                chunk[i * 4],
188                chunk[i * 4 + 1],
189                chunk[i * 4 + 2],
190                chunk[i * 4 + 3],
191            ]);
192        }
193        for i in 16..64 {
194            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
195            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
196            w[i] = w[i - 16]
197                .wrapping_add(s0)
198                .wrapping_add(w[i - 7])
199                .wrapping_add(s1);
200        }
201
202        let mut a = h[0];
203        let mut b = h[1];
204        let mut c = h[2];
205        let mut d = h[3];
206        let mut e = h[4];
207        let mut f = h[5];
208        let mut g = h[6];
209        let mut h_var = h[7];
210
211        for i in 0..64 {
212            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
213            let ch = (e & f) ^ ((!e) & g);
214            let temp1 = h_var
215                .wrapping_add(s1)
216                .wrapping_add(ch)
217                .wrapping_add(k[i])
218                .wrapping_add(w[i]);
219            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
220            let maj = (a & b) ^ (a & c) ^ (b & c);
221            let temp2 = s0.wrapping_add(maj);
222
223            h_var = g;
224            g = f;
225            f = e;
226            e = d.wrapping_add(temp1);
227            d = c;
228            c = b;
229            b = a;
230            a = temp1.wrapping_add(temp2);
231        }
232
233        h[0] = h[0].wrapping_add(a);
234        h[1] = h[1].wrapping_add(b);
235        h[2] = h[2].wrapping_add(c);
236        h[3] = h[3].wrapping_add(d);
237        h[4] = h[4].wrapping_add(e);
238        h[5] = h[5].wrapping_add(f);
239        h[6] = h[6].wrapping_add(g);
240        h[7] = h[7].wrapping_add(h_var);
241    }
242
243    let mut result = String::with_capacity(64);
244    for val in h {
245        result.push_str(&format!("{:08x}", val));
246    }
247    result
248}
249
250/// Compute the canonical SHA-256 event hash across all event metadata and payload fields.
251pub fn compute_event_hash(
252    sequence: u64,
253    event_id: &str,
254    timestamp: &str,
255    event_type: &str,
256    actor: &str,
257    prev_hash: &str,
258    payload: &str,
259) -> String {
260    let header = format!(
261        "{}:{}:{}:{}:{}:{}:{}",
262        sequence, event_id, timestamp, event_type, actor, prev_hash, payload
263    );
264    sha256_hex(header.as_bytes())
265}
266
267/// Thread-safe in-memory reference implementation of an append-only audit ledger.
268#[derive(Default, Clone)]
269pub struct InMemoryLedger {
270    events: Arc<Mutex<Vec<AuditEvent>>>,
271}
272
273impl InMemoryLedger {
274    /// Construct a new empty in-memory audit ledger.
275    pub fn new() -> Self {
276        Self::default()
277    }
278}
279
280impl AuditLedgerWriter for InMemoryLedger {
281    fn append(
282        &mut self,
283        event_type: &str,
284        actor: &str,
285        payload: &str,
286    ) -> Result<AuditEvent, LedgerError> {
287        let mut store = self
288            .events
289            .lock()
290            .map_err(|e| LedgerError::LockError(e.to_string()))?;
291        let sequence = (store.len() as u64) + 1;
292        let prev_hash = if let Some(last) = store.last() {
293            last.event_hash.clone()
294        } else {
295            GENESIS_PREV_HASH.to_string()
296        };
297
298        let now_sec = SystemTime::now()
299            .duration_since(UNIX_EPOCH)
300            .unwrap_or_default()
301            .as_secs();
302        let timestamp = format!("{}", now_sec);
303        let event_id = format!("ev_{}_{}", sequence, now_sec);
304
305        let event_hash = compute_event_hash(
306            sequence, &event_id, &timestamp, event_type, actor, &prev_hash, payload,
307        );
308
309        let event = AuditEvent {
310            sequence,
311            event_id,
312            timestamp,
313            event_type: event_type.to_string(),
314            actor: actor.to_string(),
315            payload: payload.to_string(),
316            prev_hash,
317            event_hash,
318            signature: None,
319        };
320
321        store.push(event.clone());
322        Ok(event)
323    }
324}
325
326impl AuditLedgerReader for InMemoryLedger {
327    fn get_event(&self, sequence: u64) -> Result<Option<AuditEvent>, LedgerError> {
328        let store = self
329            .events
330            .lock()
331            .map_err(|e| LedgerError::LockError(e.to_string()))?;
332        if sequence == 0 || sequence > (store.len() as u64) {
333            Ok(None)
334        } else {
335            Ok(Some(store[(sequence - 1) as usize].clone()))
336        }
337    }
338
339    fn head(&self) -> Result<Option<AuditEvent>, LedgerError> {
340        let store = self
341            .events
342            .lock()
343            .map_err(|e| LedgerError::LockError(e.to_string()))?;
344        Ok(store.last().cloned())
345    }
346
347    fn all_events(&self) -> Result<Vec<AuditEvent>, LedgerError> {
348        let store = self
349            .events
350            .lock()
351            .map_err(|e| LedgerError::LockError(e.to_string()))?;
352        Ok(store.clone())
353    }
354}
355
356impl AuditVerifier for InMemoryLedger {
357    fn verify_chain(&self) -> Result<ChainVerificationReport, LedgerError> {
358        let store = self
359            .events
360            .lock()
361            .map_err(|e| LedgerError::LockError(e.to_string()))?;
362        let now_sec = SystemTime::now()
363            .duration_since(UNIX_EPOCH)
364            .unwrap_or_default()
365            .as_secs();
366        let checked_at = format!("{}", now_sec);
367
368        if store.is_empty() {
369            return Ok(ChainVerificationReport {
370                valid: true,
371                total_events: 0,
372                root_hash: String::new(),
373                head_hash: String::new(),
374                first_broken_sequence: None,
375                error_detail: None,
376                checked_at,
377            });
378        }
379
380        let mut expected_prev = GENESIS_PREV_HASH.to_string();
381        for (idx, event) in store.iter().enumerate() {
382            let expected_seq = (idx as u64) + 1;
383            if event.sequence != expected_seq {
384                return Ok(ChainVerificationReport {
385                    valid: false,
386                    total_events: store.len(),
387                    root_hash: store[0].event_hash.clone(),
388                    head_hash: store.last().unwrap().event_hash.clone(),
389                    first_broken_sequence: Some(event.sequence),
390                    error_detail: Some(format!("Discontinuous sequence at index {}", idx)),
391                    checked_at,
392                });
393            }
394
395            if event.prev_hash != expected_prev {
396                return Ok(ChainVerificationReport {
397                    valid: false,
398                    total_events: store.len(),
399                    root_hash: store[0].event_hash.clone(),
400                    head_hash: store.last().unwrap().event_hash.clone(),
401                    first_broken_sequence: Some(event.sequence),
402                    error_detail: Some(format!(
403                        "Broken prev_hash chain at sequence {}",
404                        event.sequence
405                    )),
406                    checked_at,
407                });
408            }
409
410            let expected_hash = compute_event_hash(
411                event.sequence,
412                &event.event_id,
413                &event.timestamp,
414                &event.event_type,
415                &event.actor,
416                &event.prev_hash,
417                &event.payload,
418            );
419
420            if event.event_hash != expected_hash {
421                return Ok(ChainVerificationReport {
422                    valid: false,
423                    total_events: store.len(),
424                    root_hash: store[0].event_hash.clone(),
425                    head_hash: store.last().unwrap().event_hash.clone(),
426                    first_broken_sequence: Some(event.sequence),
427                    error_detail: Some(format!(
428                        "Tampered content hash at sequence {}",
429                        event.sequence
430                    )),
431                    checked_at,
432                });
433            }
434
435            expected_prev = event.event_hash.clone();
436        }
437
438        Ok(ChainVerificationReport {
439            valid: true,
440            total_events: store.len(),
441            root_hash: store[0].event_hash.clone(),
442            head_hash: store.last().unwrap().event_hash.clone(),
443            first_broken_sequence: None,
444            error_detail: None,
445            checked_at,
446        })
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_sha256_known_vector() {
456        assert_eq!(
457            sha256_hex(b"abc"),
458            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
459        );
460    }
461
462    #[test]
463    fn test_append_and_verify_chain() {
464        let mut ledger = InMemoryLedger::new();
465        let ev1 = ledger
466            .append("agent.init", "hermes", "{\"state\":\"ready\"}")
467            .unwrap();
468        assert_eq!(ev1.sequence, 1);
469        assert_eq!(ev1.prev_hash, GENESIS_PREV_HASH);
470
471        let ev2 = ledger
472            .append("tool.call", "hermes", "{\"tool\":\"kibisis\"}")
473            .unwrap();
474        assert_eq!(ev2.sequence, 2);
475        assert_eq!(ev2.prev_hash, ev1.event_hash);
476
477        let report = ledger.verify_chain().unwrap();
478        assert!(report.valid);
479        assert_eq!(report.total_events, 2);
480        assert_eq!(report.head_hash, ev2.event_hash);
481    }
482}