Skip to main content

audit_trail/
verify.rs

1//! Chain verification: replay records and prove the chain is untampered.
2//!
3//! A [`Verifier`] feeds records back through the same canonical hashing
4//! protocol used by [`crate::Chain::append`] and checks three invariants
5//! per record:
6//!
7//! 1. **Id linkage.** The record's id is the expected next id in the chain.
8//! 2. **Prev-hash linkage.** The record's `prev_hash` equals the previous
9//!    record's `hash` (or [`Digest::ZERO`] for the genesis record).
10//! 3. **Hash integrity.** The record's stored `hash` equals the digest
11//!    recomputed from `(id, timestamp, actor, action, target, outcome,
12//!    prev_hash)` using the same field separator the chain uses.
13//!
14//! Optionally, a [`Verifier`] also enforces strict timestamp monotonicity
15//! (each record's timestamp must be strictly greater than the previous).
16//!
17//! The verifier is **stateful and sequential**: callers feed records in
18//! chain order. A failure leaves the verifier's state at the last record
19//! it accepted, so the caller can inspect [`Verifier::next_id`] /
20//! [`Verifier::last_hash`] to learn how far verification got.
21
22use crate::canonical;
23use crate::clock::Timestamp;
24use crate::error::{Error, Result};
25use crate::hash::{Digest, Hasher};
26use crate::record::{Record, RecordId};
27
28/// Replays a chain of records and proves their hash linkage is intact.
29///
30/// The verifier is generic over its [`Hasher`]: callers must supply an
31/// implementation that produces the same digests the original [`Chain`]
32/// produced. Two different hash algorithms will not interoperate.
33///
34/// [`Chain`]: crate::Chain
35///
36/// # Example
37///
38/// ```
39/// use audit_trail::{
40///     Action, Actor, Chain, Clock, Digest, Hasher, Outcome, Record, RecordId, Sink,
41///     SinkError, Target, Timestamp, Verifier, HASH_LEN,
42/// };
43///
44/// // XOR-fold hasher (insecure — for demonstration only).
45/// struct XorHasher([u8; HASH_LEN], usize);
46/// impl Hasher for XorHasher {
47///     fn reset(&mut self) { self.0 = [0u8; HASH_LEN]; self.1 = 0; }
48///     fn update(&mut self, bytes: &[u8]) {
49///         for b in bytes { self.0[self.1 % HASH_LEN] ^= *b; self.1 = self.1.wrapping_add(1); }
50///     }
51///     fn finalize(&mut self, out: &mut Digest) { *out = Digest::from_bytes(self.0); }
52/// }
53///
54/// // Capture every record the chain emits.
55/// #[derive(Default)]
56/// struct CaptureSink { records: Vec<(RecordId, Timestamp, Digest, Digest, Outcome)> }
57/// impl Sink for CaptureSink {
58///     fn write(&mut self, r: &Record<'_>) -> Result<(), SinkError> {
59///         self.records.push((r.id(), r.timestamp(), r.prev_hash(), r.hash(), r.outcome()));
60///         Ok(())
61///     }
62/// }
63///
64/// struct Tick(core::cell::Cell<u64>);
65/// impl Clock for Tick {
66///     fn now(&self) -> Timestamp {
67///         let v = self.0.get(); self.0.set(v + 1); Timestamp::from_nanos(v)
68///     }
69/// }
70///
71/// // Build a 3-record chain.
72/// let mut chain = Chain::new(
73///     XorHasher([0u8; HASH_LEN], 0),
74///     CaptureSink::default(),
75///     Tick(core::cell::Cell::new(1)),
76/// );
77/// chain.append(Actor::new("a"), Action::new("x"), Target::new("t"), Outcome::Success).unwrap();
78/// chain.append(Actor::new("a"), Action::new("y"), Target::new("u"), Outcome::Success).unwrap();
79/// chain.append(Actor::new("a"), Action::new("z"), Target::new("v"), Outcome::Failure).unwrap();
80/// let (_h, sink, _c) = chain.into_parts();
81///
82/// // Replay every captured record through the verifier.
83/// let actors = ["a", "a", "a"];
84/// let actions = ["x", "y", "z"];
85/// let targets = ["t", "u", "v"];
86/// let mut verifier = Verifier::new(XorHasher([0u8; HASH_LEN], 0));
87/// for (i, (id, ts, prev, hash, outcome)) in sink.records.iter().enumerate() {
88///     let record = Record::new(
89///         *id, *ts,
90///         Actor::new(actors[i]), Action::new(actions[i]), Target::new(targets[i]),
91///         *outcome, *prev, *hash,
92///     );
93///     verifier.verify(&record).unwrap();
94/// }
95/// assert_eq!(verifier.next_id(), RecordId::from_u64(3));
96/// ```
97#[derive(Debug)]
98pub struct Verifier<H>
99where
100    H: Hasher,
101{
102    hasher: H,
103    next_id: u64,
104    last_hash: Digest,
105    last_timestamp: Timestamp,
106    strict_timestamps: bool,
107    started: bool,
108}
109
110impl<H> Verifier<H>
111where
112    H: Hasher,
113{
114    /// Construct a verifier that expects to start from the genesis record.
115    ///
116    /// Timestamp monotonicity is enforced by default. Disable it with
117    /// [`Verifier::with_strict_timestamps`].
118    #[inline]
119    pub fn new(hasher: H) -> Self {
120        Self {
121            hasher,
122            next_id: 0,
123            last_hash: Digest::ZERO,
124            last_timestamp: Timestamp::EPOCH,
125            strict_timestamps: true,
126            started: false,
127        }
128    }
129
130    /// Construct a verifier that resumes mid-chain.
131    ///
132    /// `next_id`, `last_hash`, and `last_timestamp` must match the state of
133    /// the chain immediately after the most recently verified record.
134    #[inline]
135    pub fn resume(
136        hasher: H,
137        next_id: RecordId,
138        last_hash: Digest,
139        last_timestamp: Timestamp,
140    ) -> Self {
141        Self {
142            hasher,
143            next_id: next_id.as_u64(),
144            last_hash,
145            last_timestamp,
146            strict_timestamps: true,
147            started: true,
148        }
149    }
150
151    /// Toggle strict timestamp monotonicity. Default is `true`.
152    ///
153    /// When `false`, timestamps may be equal or out of order without
154    /// triggering [`Error::NonMonotonicClock`]. This is occasionally useful
155    /// when verifying chains imported from systems with coarser clocks.
156    #[inline]
157    pub const fn with_strict_timestamps(mut self, strict: bool) -> Self {
158        self.strict_timestamps = strict;
159        self
160    }
161
162    /// Id the next verified record must carry.
163    #[inline]
164    pub const fn next_id(&self) -> RecordId {
165        RecordId::from_u64(self.next_id)
166    }
167
168    /// Hash of the last record this verifier accepted, or [`Digest::ZERO`]
169    /// if none yet.
170    #[inline]
171    pub const fn last_hash(&self) -> Digest {
172        self.last_hash
173    }
174
175    /// Timestamp of the last record this verifier accepted, or
176    /// [`Timestamp::EPOCH`] if none yet.
177    #[inline]
178    pub const fn last_timestamp(&self) -> Timestamp {
179        self.last_timestamp
180    }
181
182    /// Verify a single record against the running chain state.
183    ///
184    /// On success the verifier's internal cursor advances. On failure the
185    /// verifier's cursor is left unchanged so callers can inspect
186    /// [`Verifier::next_id`] to learn which record failed.
187    ///
188    /// # Errors
189    ///
190    /// * [`Error::IdMismatch`] — record id is not the expected next id.
191    /// * [`Error::LinkMismatch`] — record's `prev_hash` does not equal the
192    ///   previous record's `hash`.
193    /// * [`Error::HashMismatch`] — record's stored `hash` does not match
194    ///   the digest recomputed from its fields.
195    /// * [`Error::NonMonotonicClock`] — record's timestamp is not strictly
196    ///   greater than the previous record's (only when strict timestamps
197    ///   are enabled).
198    pub fn verify(&mut self, record: &Record<'_>) -> Result<()> {
199        if record.id().as_u64() != self.next_id {
200            return Err(Error::IdMismatch(record.id()));
201        }
202
203        if record.prev_hash() != self.last_hash {
204            return Err(Error::LinkMismatch(record.id()));
205        }
206
207        if self.strict_timestamps && self.started && record.timestamp() <= self.last_timestamp {
208            return Err(Error::NonMonotonicClock);
209        }
210
211        let recomputed = canonical::compute(&mut self.hasher, record);
212        if recomputed != record.hash() {
213            return Err(Error::HashMismatch(record.id()));
214        }
215
216        self.next_id = self
217            .next_id
218            .checked_add(1)
219            .ok_or_else(|| Error::IdMismatch(record.id()))?;
220        self.last_hash = record.hash();
221        self.last_timestamp = record.timestamp();
222        self.started = true;
223        Ok(())
224    }
225
226    /// Consume the verifier and return its hasher.
227    #[inline]
228    pub fn into_hasher(self) -> H {
229        self.hasher
230    }
231}