use crate::AuditEvent;
use sha2::{Digest, Sha256};
use thiserror::Error;
pub const HASH_PREFIX: &str = "sha256:";
#[derive(Debug, Error)]
pub enum ChainError {
#[error("empty chain")]
EmptyChain,
#[error("genesis event must have prev_event_hash=None (event_id={event_id})")]
InvalidGenesis {
event_id: String,
},
#[error("prev_event_hash mismatch at event_id={event_id}: expected {expected:?}, got {got:?}")]
PrevHashMismatch {
event_id: String,
expected: Option<String>,
got: Option<String>,
},
#[error("this_event_hash does not match recomputed (event_id={event_id})")]
HashMismatch {
event_id: String,
},
#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
}
pub fn canonicalize(value: &serde_json::Value) -> Result<Vec<u8>, serde_json::Error> {
let mut out = Vec::new();
write_canonical(value, &mut out)?;
Ok(out)
}
fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) -> Result<(), serde_json::Error> {
match value {
serde_json::Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
out.push(b'{');
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push(b',');
}
out.extend(serde_jcs::to_vec(key)?);
out.push(b':');
write_canonical(&map[*key], out)?;
}
out.push(b'}');
}
serde_json::Value::Array(items) => {
out.push(b'[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_canonical(item, out)?;
}
out.push(b']');
}
primitive => out.extend(serde_jcs::to_vec(primitive)?),
}
Ok(())
}
#[must_use]
pub fn event_value_for_hashing(event: &serde_json::Value) -> serde_json::Value {
let mut v = event.clone();
if let serde_json::Value::Object(map) = &mut v {
map.remove("this_event_hash");
if matches!(map.get("signature"), Some(serde_json::Value::Null)) {
map.remove("signature");
}
}
v
}
pub fn compute_event_hash(
event_without_hash: &serde_json::Value,
prev_event_hash: Option<&str>,
) -> Result<String, ChainError> {
let canonical = canonicalize(event_without_hash)?;
let mut hasher = Sha256::new();
hasher.update(&canonical);
hasher.update(prev_event_hash.unwrap_or("").as_bytes());
let digest = hasher.finalize();
Ok(format!("{HASH_PREFIX}{digest:x}"))
}
pub fn append_event(
mut event: AuditEvent,
prev_event_hash: Option<String>,
) -> Result<AuditEvent, ChainError> {
event.prev_event_hash = prev_event_hash;
let value = event_value_for_hashing(&serde_json::to_value(&event)?);
let new_hash = compute_event_hash(&value, event.prev_event_hash.as_deref())?;
event.this_event_hash = new_hash;
Ok(event)
}
pub fn verify_event(event: &AuditEvent) -> Result<bool, ChainError> {
let value = event_value_for_hashing(&serde_json::to_value(event)?);
let expected = compute_event_hash(&value, event.prev_event_hash.as_deref())?;
Ok(expected == event.this_event_hash)
}
pub fn append_event_value(
event: &serde_json::Value,
prev_event_hash: Option<&str>,
) -> Result<serde_json::Value, ChainError> {
let sealed = seal_prev(event, prev_event_hash)?;
finish_append(sealed, prev_event_hash)
}
#[cfg(feature = "signing")]
pub fn append_event_value_signed(
event: &serde_json::Value,
prev_event_hash: Option<&str>,
signer: &crate::signing::Ed25519EventSigner,
) -> Result<serde_json::Value, ChainError> {
let mut sealed = seal_prev(event, prev_event_hash)?;
let data = crate::signing::attestation_bytes(&sealed, prev_event_hash)?;
let signature = serde_json::json!({
"alg": crate::signing::SIGNATURE_ALG,
"key_id": signer.key_id(),
"value": signer.sign(&data),
});
if let Some(obj) = sealed.as_object_mut() {
obj.insert("signature".to_string(), signature);
}
finish_append(sealed, prev_event_hash)
}
fn seal_prev(
event: &serde_json::Value,
prev_event_hash: Option<&str>,
) -> Result<serde_json::Value, ChainError> {
let mut sealed = event.clone();
let obj = sealed
.as_object_mut()
.ok_or_else(|| ChainError::Serde(serde::de::Error::custom("event is not a JSON object")))?;
obj.insert(
"prev_event_hash".to_string(),
match prev_event_hash {
Some(h) => serde_json::Value::String(h.to_string()),
None => serde_json::Value::Null,
},
);
Ok(sealed)
}
fn finish_append(
mut sealed: serde_json::Value,
prev_event_hash: Option<&str>,
) -> Result<serde_json::Value, ChainError> {
let view = event_value_for_hashing(&sealed);
let hash = compute_event_hash(&view, prev_event_hash)?;
if let Some(obj) = sealed.as_object_mut() {
obj.insert(
"this_event_hash".to_string(),
serde_json::Value::String(hash),
);
}
Ok(sealed)
}
pub fn verify_event_value(event: &serde_json::Value) -> Result<bool, ChainError> {
let obj = event
.as_object()
.ok_or_else(|| ChainError::Serde(serde::de::Error::custom("event is not a JSON object")))?;
let this_hash = obj
.get("this_event_hash")
.and_then(|v| v.as_str())
.ok_or_else(|| ChainError::Serde(serde::de::Error::custom("missing this_event_hash")))?;
let prev_hash = obj.get("prev_event_hash").and_then(|v| v.as_str());
let value = event_value_for_hashing(event);
let expected = compute_event_hash(&value, prev_hash)?;
Ok(expected == this_hash)
}
pub fn verify_chain_values(events: &[serde_json::Value]) -> Result<String, ChainError> {
let get_id = |e: &serde_json::Value| -> String {
e.get("event_id")
.and_then(|v| v.as_str())
.unwrap_or("<missing event_id>")
.to_string()
};
let first = events.first().ok_or(ChainError::EmptyChain)?;
if first.get("prev_event_hash").is_some_and(|v| !v.is_null()) {
return Err(ChainError::InvalidGenesis {
event_id: get_id(first),
});
}
let mut prev: Option<String> = None;
for ev in events {
let ev_prev = ev
.get("prev_event_hash")
.and_then(|v| v.as_str())
.map(str::to_string);
if ev_prev != prev {
return Err(ChainError::PrevHashMismatch {
event_id: get_id(ev),
expected: prev.clone(),
got: ev_prev,
});
}
if !verify_event_value(ev)? {
return Err(ChainError::HashMismatch {
event_id: get_id(ev),
});
}
prev = ev
.get("this_event_hash")
.and_then(|v| v.as_str())
.map(str::to_string);
}
prev.ok_or(ChainError::EmptyChain)
}
pub fn verify_chain(events: &[AuditEvent]) -> Result<String, ChainError> {
let first = events.first().ok_or(ChainError::EmptyChain)?;
if first.prev_event_hash.is_some() {
return Err(ChainError::InvalidGenesis {
event_id: first.event_id.clone(),
});
}
let mut prev: Option<String> = None;
for ev in events {
if ev.prev_event_hash != prev {
return Err(ChainError::PrevHashMismatch {
event_id: ev.event_id.clone(),
expected: prev.clone(),
got: ev.prev_event_hash.clone(),
});
}
if !verify_event(ev)? {
return Err(ChainError::HashMismatch {
event_id: ev.event_id.clone(),
});
}
prev = Some(ev.this_event_hash.clone());
}
prev.ok_or(ChainError::EmptyChain)
}