use sha2::{Digest, Sha256};
pub const AUDIT_CHAIN_DOMAIN: &[u8] = b"VARTA-AUDIT-v2";
pub const AUDIT_CHAIN_OUT_BYTES: usize = 32;
pub fn audit_chain_hash(
prev_chain: &[u8; AUDIT_CHAIN_OUT_BYTES],
kind: &[u8],
body: &[u8],
) -> [u8; AUDIT_CHAIN_OUT_BYTES] {
let mut h = Sha256::new();
h.update(AUDIT_CHAIN_DOMAIN);
h.update([0u8]);
h.update(kind);
h.update([0u8]);
h.update(prev_chain);
h.update([0u8]);
h.update(body);
let digest = h.finalize();
let mut out = [0u8; AUDIT_CHAIN_OUT_BYTES];
out.copy_from_slice(&digest);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audit_chain_hash_fixed_vector() {
let prev = [0u8; 32];
let kind = b"boot";
let body = b"1\t1700000000000\t0\tboot\t1234\t-\tfresh";
let got = audit_chain_hash(&prev, kind, body);
let mut h = Sha256::new();
h.update(b"VARTA-AUDIT-v2");
h.update([0u8]);
h.update(b"boot");
h.update([0u8]);
h.update([0u8; 32]);
h.update([0u8]);
h.update(b"1\t1700000000000\t0\tboot\t1234\t-\tfresh");
let expected = h.finalize();
assert_eq!(&got[..], &expected[..]);
}
#[test]
fn audit_chain_hash_changes_with_each_field() {
let prev = [0u8; 32];
let h1 = audit_chain_hash(&prev, b"spawn", b"body");
let h2 = audit_chain_hash(&prev, b"complete", b"body");
let h3 = audit_chain_hash(&prev, b"spawn", b"other");
let mut prev2 = [0u8; 32];
prev2[0] = 1;
let h4 = audit_chain_hash(&prev2, b"spawn", b"body");
assert_ne!(h1, h2, "kind change must alter hash");
assert_ne!(h1, h3, "body change must alter hash");
assert_ne!(h1, h4, "prev_chain change must alter hash");
}
#[test]
fn audit_chain_hash_resists_field_boundary_confusion() {
let prev = [0u8; 32];
let a = audit_chain_hash(&prev, b"ab", b"cd");
let b = audit_chain_hash(&prev, b"abcd", b"");
assert_ne!(a, b);
}
#[test]
fn audit_chain_domain_is_versioned() {
let s = core::str::from_utf8(AUDIT_CHAIN_DOMAIN).expect("ASCII domain");
assert!(
s.ends_with("v2"),
"domain tag must encode schema version: {s}"
);
}
}