Skip to main content

aae/
hashchain.rs

1//! Hash chain helpers: canonicalization, chain construction, chain verification.
2
3use crate::AuditEvent;
4use sha2::{Digest, Sha256};
5use thiserror::Error;
6
7/// Hash prefix for SHA-256 digests in this protocol.
8pub const HASH_PREFIX: &str = "sha256:";
9
10/// Errors raised by chain verification.
11#[derive(Debug, Error)]
12pub enum ChainError {
13    /// Empty chain provided to verifier.
14    #[error("empty chain")]
15    EmptyChain,
16    /// Genesis event has non-null `prev_event_hash`.
17    #[error("genesis event must have prev_event_hash=None (event_id={event_id})")]
18    InvalidGenesis {
19        /// Event id of the offending event.
20        event_id: String,
21    },
22    /// `prev_event_hash` on event N does not match `this_event_hash` on event N-1.
23    #[error("prev_event_hash mismatch at event_id={event_id}: expected {expected:?}, got {got:?}")]
24    PrevHashMismatch {
25        /// Event id of the offending event.
26        event_id: String,
27        /// Expected `prev_event_hash` (i.e., previous event's `this_event_hash`).
28        expected: Option<String>,
29        /// Actual `prev_event_hash` on this event.
30        got: Option<String>,
31    },
32    /// `this_event_hash` does not match recomputed value.
33    #[error("this_event_hash does not match recomputed (event_id={event_id})")]
34    HashMismatch {
35        /// Event id of the offending event.
36        event_id: String,
37    },
38    /// Serialization failure during canonicalization.
39    #[error("serialization error: {0}")]
40    Serde(#[from] serde_json::Error),
41}
42
43/// Canonicalize a JSON value to RFC 8785 (JCS) bytes for hashing.
44///
45/// Uses `serde_jcs`, a full RFC 8785 implementation: lexicographic key ordering
46/// over UTF-16 code units, ECMAScript number serialization, specified string
47/// escaping. Byte-for-byte parity with the Python (`rfc8785`) and TypeScript
48/// (`json-canonicalize`) SDKs is enforced by the shared vectors in
49/// `reference-tests/test-vectors/canonicalization.json`.
50///
51/// # Errors
52///
53/// Returns `serde_json::Error` if the value cannot be serialized (e.g.,
54/// non-finite floats, which JSON cannot represent).
55pub fn canonicalize(value: &serde_json::Value) -> Result<Vec<u8>, serde_json::Error> {
56    let mut out = Vec::new();
57    write_canonical(value, &mut out)?;
58    Ok(out)
59}
60
61/// String escaping and number formatting delegate to `serde_jcs`; object-key
62/// ordering is implemented here because RFC 8785 requires lexicographic
63/// order over **UTF-16 code units**, and `serde_jcs` (as of 0.1.x) sorts by
64/// Unicode code point โ€” the two disagree for astral-plane keys (surrogate
65/// units 0xD800..0xDFFF sort before U+E000+ in UTF-16, after them in
66/// code-point order). Caught by the `unicode_key_sort_utf16` shared vector.
67fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) -> Result<(), serde_json::Error> {
68    match value {
69        serde_json::Value::Object(map) => {
70            let mut keys: Vec<&String> = map.keys().collect();
71            keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
72            out.push(b'{');
73            for (i, key) in keys.iter().enumerate() {
74                if i > 0 {
75                    out.push(b',');
76                }
77                out.extend(serde_jcs::to_vec(key)?);
78                out.push(b':');
79                write_canonical(&map[*key], out)?;
80            }
81            out.push(b'}');
82        }
83        serde_json::Value::Array(items) => {
84            out.push(b'[');
85            for (i, item) in items.iter().enumerate() {
86                if i > 0 {
87                    out.push(b',');
88                }
89                write_canonical(item, out)?;
90            }
91            out.push(b']');
92        }
93        primitive => out.extend(serde_jcs::to_vec(primitive)?),
94    }
95    Ok(())
96}
97
98/// Produce the hashing view of an event JSON value (normative; see the Python
99/// SDK's `_event_to_canonical_dict`, the single point of truth):
100///   - `this_event_hash` is excluded.
101///   - `signature` is excluded when null (None-valued optional fields don't
102///     pollute the hash).
103///   - `prev_event_hash` is INCLUDED even when null (genesis marker is
104///     structurally significant).
105#[must_use]
106pub fn event_value_for_hashing(event: &serde_json::Value) -> serde_json::Value {
107    let mut v = event.clone();
108    if let serde_json::Value::Object(map) = &mut v {
109        map.remove("this_event_hash");
110        if matches!(map.get("signature"), Some(serde_json::Value::Null)) {
111            map.remove("signature");
112        }
113    }
114    v
115}
116
117/// Compute `this_event_hash` for an event represented as a JSON value with
118/// `this_event_hash` absent.
119///
120/// # Errors
121///
122/// Returns [`ChainError::Serde`] if the event value cannot be canonicalized.
123pub fn compute_event_hash(
124    event_without_hash: &serde_json::Value,
125    prev_event_hash: Option<&str>,
126) -> Result<String, ChainError> {
127    let canonical = canonicalize(event_without_hash)?;
128    let mut hasher = Sha256::new();
129    hasher.update(&canonical);
130    hasher.update(prev_event_hash.unwrap_or("").as_bytes());
131    let digest = hasher.finalize();
132    Ok(format!("{HASH_PREFIX}{digest:x}"))
133}
134
135/// Compute and assign `this_event_hash` for an event, given the previous chain tip.
136///
137/// Returns the event with `this_event_hash` and `prev_event_hash` set.
138///
139/// # Errors
140///
141/// Returns [`ChainError::Serde`] if the event cannot be serialized for hashing.
142pub fn append_event(
143    mut event: AuditEvent,
144    prev_event_hash: Option<String>,
145) -> Result<AuditEvent, ChainError> {
146    event.prev_event_hash = prev_event_hash;
147    let value = event_value_for_hashing(&serde_json::to_value(&event)?);
148    let new_hash = compute_event_hash(&value, event.prev_event_hash.as_deref())?;
149    event.this_event_hash = new_hash;
150    Ok(event)
151}
152
153/// Verify a single event's `this_event_hash` matches its recomputed hash.
154///
155/// # Errors
156///
157/// Returns [`ChainError::Serde`] if the event cannot be serialized for hashing.
158pub fn verify_event(event: &AuditEvent) -> Result<bool, ChainError> {
159    let value = event_value_for_hashing(&serde_json::to_value(event)?);
160    let expected = compute_event_hash(&value, event.prev_event_hash.as_deref())?;
161    Ok(expected == event.this_event_hash)
162}
163
164/// Value-based chain production: seal a raw event JSON value onto the chain.
165///
166/// Sets `prev_event_hash` and `this_event_hash` on a copy of `event` and
167/// returns it. Producers using this path control their serialization
168/// byte-for-byte (C-3: no typed round-trip can alter timestamp precision).
169///
170/// # Errors
171///
172/// Returns [`ChainError::Serde`] if the value is not an object or cannot be
173/// canonicalized.
174pub fn append_event_value(
175    event: &serde_json::Value,
176    prev_event_hash: Option<&str>,
177) -> Result<serde_json::Value, ChainError> {
178    let sealed = seal_prev(event, prev_event_hash)?;
179    finish_append(sealed, prev_event_hash)
180}
181
182/// Value-based chain production with an event SIGNATURE (v0.7 ยง3.5).
183///
184/// Signs the attestation bytes, attaches the signature, THEN hashes the view
185/// including it โ€” so the chain commits to the signature and stripping it is
186/// hash-detectable without keys.
187///
188/// # Errors
189///
190/// Returns [`ChainError::Serde`] if the value is not an object or cannot be
191/// canonicalized.
192#[cfg(feature = "signing")]
193pub fn append_event_value_signed(
194    event: &serde_json::Value,
195    prev_event_hash: Option<&str>,
196    signer: &crate::signing::Ed25519EventSigner,
197) -> Result<serde_json::Value, ChainError> {
198    let mut sealed = seal_prev(event, prev_event_hash)?;
199    let data = crate::signing::attestation_bytes(&sealed, prev_event_hash)?;
200    let signature = serde_json::json!({
201        "alg": crate::signing::SIGNATURE_ALG,
202        "key_id": signer.key_id(),
203        "value": signer.sign(&data),
204    });
205    if let Some(obj) = sealed.as_object_mut() {
206        obj.insert("signature".to_string(), signature);
207    }
208    finish_append(sealed, prev_event_hash)
209}
210
211fn seal_prev(
212    event: &serde_json::Value,
213    prev_event_hash: Option<&str>,
214) -> Result<serde_json::Value, ChainError> {
215    let mut sealed = event.clone();
216    let obj = sealed
217        .as_object_mut()
218        .ok_or_else(|| ChainError::Serde(serde::de::Error::custom("event is not a JSON object")))?;
219    obj.insert(
220        "prev_event_hash".to_string(),
221        match prev_event_hash {
222            Some(h) => serde_json::Value::String(h.to_string()),
223            None => serde_json::Value::Null,
224        },
225    );
226    Ok(sealed)
227}
228
229fn finish_append(
230    mut sealed: serde_json::Value,
231    prev_event_hash: Option<&str>,
232) -> Result<serde_json::Value, ChainError> {
233    let view = event_value_for_hashing(&sealed);
234    let hash = compute_event_hash(&view, prev_event_hash)?;
235    if let Some(obj) = sealed.as_object_mut() {
236        obj.insert(
237            "this_event_hash".to_string(),
238            serde_json::Value::String(hash),
239        );
240    }
241    Ok(sealed)
242}
243
244/// Verify a single event supplied as a raw JSON value.
245///
246/// Prefer this over [`verify_event`] when verifying chains produced by other
247/// implementations: C-3 requires fractional-second precision in timestamps to
248/// be preserved, and a typed round-trip (serde -> chrono -> serde) may
249/// re-serialize `ts` with different precision than the producer used. Hashing
250/// the received bytes' JSON value avoids that entirely.
251///
252/// # Errors
253///
254/// Returns [`ChainError::Serde`] if the value is not an object, lacks
255/// `this_event_hash`, or cannot be canonicalized.
256pub fn verify_event_value(event: &serde_json::Value) -> Result<bool, ChainError> {
257    let obj = event
258        .as_object()
259        .ok_or_else(|| ChainError::Serde(serde::de::Error::custom("event is not a JSON object")))?;
260    let this_hash = obj
261        .get("this_event_hash")
262        .and_then(|v| v.as_str())
263        .ok_or_else(|| ChainError::Serde(serde::de::Error::custom("missing this_event_hash")))?;
264    let prev_hash = obj.get("prev_event_hash").and_then(|v| v.as_str());
265    let value = event_value_for_hashing(event);
266    let expected = compute_event_hash(&value, prev_hash)?;
267    Ok(expected == this_hash)
268}
269
270/// Verify a chain of events supplied as raw JSON values. Returns the tip hash.
271///
272/// Same semantics as [`verify_chain`]; see [`verify_event_value`] for why the
273/// value-based form is the right tool for cross-implementation verification.
274///
275/// # Errors
276///
277/// Returns the corresponding [`ChainError`] variant on an empty chain, a
278/// non-null genesis `prev_event_hash`, a broken link, or a hash mismatch.
279pub fn verify_chain_values(events: &[serde_json::Value]) -> Result<String, ChainError> {
280    let get_id = |e: &serde_json::Value| -> String {
281        e.get("event_id")
282            .and_then(|v| v.as_str())
283            .unwrap_or("<missing event_id>")
284            .to_string()
285    };
286
287    let first = events.first().ok_or(ChainError::EmptyChain)?;
288    if first.get("prev_event_hash").is_some_and(|v| !v.is_null()) {
289        return Err(ChainError::InvalidGenesis {
290            event_id: get_id(first),
291        });
292    }
293
294    let mut prev: Option<String> = None;
295    for ev in events {
296        let ev_prev = ev
297            .get("prev_event_hash")
298            .and_then(|v| v.as_str())
299            .map(str::to_string);
300        if ev_prev != prev {
301            return Err(ChainError::PrevHashMismatch {
302                event_id: get_id(ev),
303                expected: prev.clone(),
304                got: ev_prev,
305            });
306        }
307        if !verify_event_value(ev)? {
308            return Err(ChainError::HashMismatch {
309                event_id: get_id(ev),
310            });
311        }
312        prev = ev
313            .get("this_event_hash")
314            .and_then(|v| v.as_str())
315            .map(str::to_string);
316    }
317
318    prev.ok_or(ChainError::EmptyChain)
319}
320
321/// Verify a sequence of events forms a valid hash chain.
322///
323/// Returns the tip hash on success.
324///
325/// # Errors
326///
327/// Returns the corresponding [`ChainError`] variant on an empty chain, a
328/// non-null genesis `prev_event_hash`, a broken link, or a hash mismatch.
329pub fn verify_chain(events: &[AuditEvent]) -> Result<String, ChainError> {
330    let first = events.first().ok_or(ChainError::EmptyChain)?;
331    if first.prev_event_hash.is_some() {
332        return Err(ChainError::InvalidGenesis {
333            event_id: first.event_id.clone(),
334        });
335    }
336
337    let mut prev: Option<String> = None;
338    for ev in events {
339        if ev.prev_event_hash != prev {
340            return Err(ChainError::PrevHashMismatch {
341                event_id: ev.event_id.clone(),
342                expected: prev.clone(),
343                got: ev.prev_event_hash.clone(),
344            });
345        }
346        if !verify_event(ev)? {
347            return Err(ChainError::HashMismatch {
348                event_id: ev.event_id.clone(),
349            });
350        }
351        prev = Some(ev.this_event_hash.clone());
352    }
353
354    prev.ok_or(ChainError::EmptyChain)
355}