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}