Files
wifi-densepose/vendor/ruvector/crates/rvf/rvf-crypto/src/witness.rs

190 lines
6.0 KiB
Rust

//! WITNESS_SEG support for cryptographic audit trails.
//!
//! Each witness entry chains to the previous via hashes, forming a
//! tamper-evident log. The chain uses SHAKE-256 for hash binding.
use alloc::vec::Vec;
use rvf_types::{ErrorCode, RvfError};
use crate::hash::shake256_256;
/// A single entry in a witness chain.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WitnessEntry {
/// Hash of the previous entry (zero for the first entry).
pub prev_hash: [u8; 32],
/// Hash of the action being witnessed.
pub action_hash: [u8; 32],
/// Nanosecond UNIX timestamp.
pub timestamp_ns: u64,
/// Witness type: 0x01=PROVENANCE, 0x02=COMPUTATION, etc.
pub witness_type: u8,
}
/// Size of one serialized witness entry: 32 + 32 + 8 + 1 = 73 bytes.
const ENTRY_SIZE: usize = 73;
/// Serialize a `WitnessEntry` into bytes.
fn encode_entry(entry: &WitnessEntry) -> [u8; ENTRY_SIZE] {
let mut buf = [0u8; ENTRY_SIZE];
buf[0..32].copy_from_slice(&entry.prev_hash);
buf[32..64].copy_from_slice(&entry.action_hash);
buf[64..72].copy_from_slice(&entry.timestamp_ns.to_le_bytes());
buf[72] = entry.witness_type;
buf
}
/// Deserialize a `WitnessEntry` from bytes.
///
/// # Errors
///
/// Returns `TruncatedSegment` if `data` is shorter than `ENTRY_SIZE` (73) bytes.
fn decode_entry(data: &[u8]) -> Result<WitnessEntry, RvfError> {
if data.len() < ENTRY_SIZE {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let mut prev_hash = [0u8; 32];
prev_hash.copy_from_slice(&data[0..32]);
let mut action_hash = [0u8; 32];
action_hash.copy_from_slice(&data[32..64]);
let timestamp_ns = u64::from_le_bytes(data[64..72].try_into().unwrap());
let witness_type = data[72];
Ok(WitnessEntry {
prev_hash,
action_hash,
timestamp_ns,
witness_type,
})
}
/// Create a witness chain from entries, linking each to the previous via hashes.
///
/// The first entry's `prev_hash` is set to all zeros (genesis).
/// Subsequent entries have `prev_hash` = SHAKE-256(previous entry bytes).
///
/// Returns the serialized chain as a byte vector.
pub fn create_witness_chain(entries: &[WitnessEntry]) -> Vec<u8> {
let mut chain = Vec::with_capacity(entries.len() * ENTRY_SIZE);
let mut prev_hash = [0u8; 32];
for entry in entries {
let mut linked = entry.clone();
linked.prev_hash = prev_hash;
let encoded = encode_entry(&linked);
prev_hash = shake256_256(&encoded);
chain.extend_from_slice(&encoded);
}
chain
}
/// Verify a witness chain's integrity.
///
/// Checks that each entry's `prev_hash` matches the SHAKE-256 hash of the
/// preceding entry. Returns the decoded entries if valid.
pub fn verify_witness_chain(data: &[u8]) -> Result<Vec<WitnessEntry>, RvfError> {
if data.is_empty() {
return Ok(Vec::new());
}
if !data.len().is_multiple_of(ENTRY_SIZE) {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let count = data.len() / ENTRY_SIZE;
let mut entries = Vec::with_capacity(count);
let mut expected_prev = [0u8; 32];
for i in 0..count {
let offset = i * ENTRY_SIZE;
let entry_bytes = &data[offset..offset + ENTRY_SIZE];
let entry = decode_entry(entry_bytes)?;
if entry.prev_hash != expected_prev {
return Err(RvfError::Code(ErrorCode::InvalidChecksum));
}
expected_prev = shake256_256(entry_bytes);
entries.push(entry);
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entries(n: usize) -> Vec<WitnessEntry> {
(0..n)
.map(|i| WitnessEntry {
prev_hash: [0u8; 32], // will be overwritten by create_witness_chain
action_hash: shake256_256(&[i as u8]),
timestamp_ns: 1_000_000_000 + i as u64,
witness_type: 0x01,
})
.collect()
}
#[test]
fn empty_chain() {
let chain = create_witness_chain(&[]);
assert!(chain.is_empty());
let result = verify_witness_chain(&chain).unwrap();
assert!(result.is_empty());
}
#[test]
fn single_entry_chain() {
let entries = make_entries(1);
let chain = create_witness_chain(&entries);
assert_eq!(chain.len(), ENTRY_SIZE);
let verified = verify_witness_chain(&chain).unwrap();
assert_eq!(verified.len(), 1);
assert_eq!(verified[0].prev_hash, [0u8; 32]);
}
#[test]
fn multi_entry_chain() {
let entries = make_entries(5);
let chain = create_witness_chain(&entries);
assert_eq!(chain.len(), 5 * ENTRY_SIZE);
let verified = verify_witness_chain(&chain).unwrap();
assert_eq!(verified.len(), 5);
for (i, entry) in verified.iter().enumerate() {
assert_eq!(entry.action_hash, entries[i].action_hash);
assert_eq!(entry.timestamp_ns, entries[i].timestamp_ns);
}
}
#[test]
fn tampered_chain_detected() {
let entries = make_entries(3);
let mut chain = create_witness_chain(&entries);
// Tamper with the second entry's action_hash byte
chain[ENTRY_SIZE + 32] ^= 0xFF;
let result = verify_witness_chain(&chain);
assert!(result.is_err());
}
#[test]
fn truncated_chain_detected() {
let entries = make_entries(2);
let chain = create_witness_chain(&entries);
let result = verify_witness_chain(&chain[..ENTRY_SIZE + 10]);
assert!(result.is_err());
}
#[test]
fn chain_links_are_correct() {
let entries = make_entries(3);
let chain = create_witness_chain(&entries);
let verified = verify_witness_chain(&chain).unwrap();
// First entry has zero prev_hash
assert_eq!(verified[0].prev_hash, [0u8; 32]);
// Second entry's prev_hash should equal hash of first entry's bytes
let first_bytes = &chain[0..ENTRY_SIZE];
let expected = shake256_256(first_bytes);
assert_eq!(verified[1].prev_hash, expected);
}
}