#[derive(Clone, Copy)]
pub struct ChainKey([u8; 32]);
impl ChainKey {
#[must_use]
pub fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl std::fmt::Debug for ChainKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ChainKey(..)")
}
}
#[derive(Clone, Copy)]
pub struct ChainHash([u8; 32]);
impl ChainHash {
#[must_use]
pub fn to_hex(self) -> String {
hex_encode(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, ChainError> {
hex_decode(s).map(Self).ok_or(ChainError::MalformedHash)
}
}
impl std::fmt::Debug for ChainHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ChainHash({})", self.to_hex())
}
}
impl PartialEq for ChainHash {
fn eq(&self, other: &Self) -> bool {
blake3::Hash::from(self.0) == blake3::Hash::from(other.0)
}
}
impl Eq for ChainHash {}
fn hex_encode(bytes: &[u8; 32]) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(64);
for b in bytes {
let _ = write!(out, "{b:02x}");
}
out
}
fn hex_decode(s: &str) -> Option<[u8; 32]> {
let s = s.trim();
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hi = (chunk[0] as char).to_digit(16)?;
let lo = (chunk[1] as char).to_digit(16)?;
out[i] = u8::try_from(hi * 16 + lo).ok()?;
}
Some(out)
}
#[must_use]
pub fn genesis(key: &ChainKey, domain: &str, file_identity: &[u8], key_epoch: u32) -> ChainHash {
let mut input = Vec::with_capacity(domain.len() + file_identity.len() + 4);
input.extend_from_slice(domain.as_bytes());
input.extend_from_slice(file_identity);
input.extend_from_slice(&key_epoch.to_le_bytes());
ChainHash(*blake3::keyed_hash(&key.0, &input).as_bytes())
}
#[must_use]
pub fn chain_next(key: &ChainKey, prev: &ChainHash, content: &[u8]) -> ChainHash {
let mut input = Vec::with_capacity(32 + content.len());
input.extend_from_slice(&prev.0);
input.extend_from_slice(content);
ChainHash(*blake3::keyed_hash(&key.0, &input).as_bytes())
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ChainError {
#[error("malformed chain hash (expected 64 hex characters)")]
MalformedHash,
#[error(
"chain hash mismatch at chained-entry index {index}: content was modified after being written"
)]
Mismatch {
index: u64,
},
#[error(
"chain is unverifiable: no known key epoch (current or previous rotation window) \
produces a valid link for this file — possibly re-keyed past the rotation window, \
or tampered"
)]
Unverifiable,
#[error("no chain key is available to verify a chained file")]
KeyUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyResolution {
Current,
Rekeyed(u32),
}
#[derive(Clone, Copy)]
pub struct ChainKeyRing {
current_epoch: u32,
current_key: ChainKey,
previous: Option<(u32, ChainKey)>,
}
impl std::fmt::Debug for ChainKeyRing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChainKeyRing")
.field("current_epoch", &self.current_epoch)
.field("previous_epoch", &self.previous.map(|(epoch, _)| epoch))
.finish_non_exhaustive()
}
}
impl ChainKeyRing {
#[must_use]
pub fn new(current_epoch: u32, current_key: ChainKey) -> Self {
Self {
current_epoch,
current_key,
previous: None,
}
}
#[must_use]
pub fn with_previous(mut self, epoch: u32, key: ChainKey) -> Self {
self.previous = Some((epoch, key));
self
}
#[must_use]
pub fn current_epoch(&self) -> u32 {
self.current_epoch
}
#[must_use]
pub fn current_key(&self) -> ChainKey {
self.current_key
}
fn candidates(&self) -> Vec<(u32, ChainKey, KeyResolution)> {
let mut out = vec![(self.current_epoch, self.current_key, KeyResolution::Current)];
if let Some((epoch, key)) = self.previous {
out.push((epoch, key, KeyResolution::Rekeyed(epoch)));
}
out
}
}
pub struct ChainVerifier {
key: ChainKey,
prev: ChainHash,
index: u64,
}
impl ChainVerifier {
#[must_use]
pub fn new(key: ChainKey, genesis: ChainHash) -> Self {
Self {
key,
prev: genesis,
index: 0,
}
}
pub fn verify_next(&mut self, content: &[u8], stored: &ChainHash) -> Result<(), ChainError> {
let expected = chain_next(&self.key, &self.prev, content);
if expected != *stored {
return Err(ChainError::Mismatch { index: self.index });
}
self.prev = expected;
self.index += 1;
Ok(())
}
#[must_use]
pub fn peek_next(&self, content: &[u8]) -> ChainHash {
chain_next(&self.key, &self.prev, content)
}
pub fn advance(&mut self, head: ChainHash) {
self.prev = head;
self.index += 1;
}
#[must_use]
pub fn head(&self) -> ChainHash {
self.prev
}
#[must_use]
pub fn index(&self) -> u64 {
self.index
}
}
pub struct ChainStreamVerifier {
domain: String,
file_identity: Vec<u8>,
candidates: Vec<(u32, ChainKey, KeyResolution)>,
resolved: Option<ChainVerifier>,
resolution: Option<KeyResolution>,
}
impl ChainStreamVerifier {
#[must_use]
pub fn new(
ring: &ChainKeyRing,
domain: impl Into<String>,
file_identity: impl Into<Vec<u8>>,
) -> Self {
Self {
domain: domain.into(),
file_identity: file_identity.into(),
candidates: ring.candidates(),
resolved: None,
resolution: None,
}
}
pub fn verify_next(&mut self, content: &[u8], stored: &ChainHash) -> Result<(), ChainError> {
if let Some(verifier) = self.resolved.as_mut() {
return verifier.verify_next(content, stored);
}
let mut survivor = None;
for (epoch, key, resolution) in &self.candidates {
let base = genesis(key, &self.domain, &self.file_identity, *epoch);
if chain_next(key, &base, content) == *stored {
survivor = Some((*epoch, *key, *resolution));
break;
}
}
let Some((epoch, key, resolution)) = survivor else {
return Err(ChainError::Unverifiable);
};
let base = genesis(&key, &self.domain, &self.file_identity, epoch);
let mut verifier = ChainVerifier::new(key, base);
verifier.verify_next(content, stored)?; self.resolved = Some(verifier);
self.resolution = Some(resolution);
self.candidates.clear();
Ok(())
}
#[must_use]
pub fn head(&self) -> Option<ChainHash> {
self.resolved.as_ref().map(ChainVerifier::head)
}
#[must_use]
pub fn resolution(&self) -> Option<KeyResolution> {
self.resolution
}
}
pub fn verify_chained_prefix(
ring: &ChainKeyRing,
domain: &str,
file_identity: &[u8],
entries: &[(Vec<u8>, ChainHash)],
) -> Result<(ChainHash, KeyResolution), ChainError> {
let (head, _checkpoint, resolution) =
verify_chained_prefix_with_checkpoint(ring, domain, file_identity, entries, u64::MAX)?;
Ok((head, resolution))
}
pub fn verify_chained_prefix_with_checkpoint(
ring: &ChainKeyRing,
domain: &str,
file_identity: &[u8],
entries: &[(Vec<u8>, ChainHash)],
checkpoint_index: u64,
) -> Result<(ChainHash, Option<ChainHash>, KeyResolution), ChainError> {
if entries.is_empty() {
return Ok((
genesis(&ring.current_key, domain, file_identity, ring.current_epoch),
None,
KeyResolution::Current,
));
}
let mut streaming = ChainStreamVerifier::new(ring, domain, file_identity.to_vec());
let mut checkpoint_head = None;
for (i, (content, stored)) in entries.iter().enumerate() {
streaming.verify_next(content, stored)?;
if i as u64 == checkpoint_index {
checkpoint_head = streaming.head();
}
}
let head = streaming
.head()
.unwrap_or_else(|| genesis(&ring.current_key, domain, file_identity, ring.current_epoch));
let resolution = streaming.resolution().unwrap_or(KeyResolution::Current);
Ok((head, checkpoint_head, resolution))
}
#[cfg(test)]
mod tests {
use super::*;
fn key(byte: u8) -> ChainKey {
ChainKey::new([byte; 32])
}
#[test]
fn hex_round_trip() {
let h = chain_next(&key(1), &genesis(&key(1), "d", b"f", 0), b"content");
let hex = h.to_hex();
assert_eq!(hex.len(), 64);
let back = ChainHash::from_hex(&hex).unwrap();
assert_eq!(h, back);
}
#[test]
fn from_hex_rejects_wrong_length() {
assert_eq!(ChainHash::from_hex("abc"), Err(ChainError::MalformedHash));
}
#[test]
fn from_hex_rejects_non_hex() {
let bad = "z".repeat(64);
assert_eq!(ChainHash::from_hex(&bad), Err(ChainError::MalformedHash));
}
#[test]
fn genesis_differs_per_domain() {
let a = genesis(&key(1), "domain-a", b"file", 0);
let b = genesis(&key(1), "domain-b", b"file", 0);
assert_ne!(a, b, "cross-subsystem genesis must differ");
}
#[test]
fn genesis_differs_per_file_identity() {
let a = genesis(&key(1), "d", b"file-a", 0);
let b = genesis(&key(1), "d", b"file-b", 0);
assert_ne!(a, b, "whole-file substitution must break at genesis");
}
#[test]
fn genesis_differs_per_epoch() {
let a = genesis(&key(1), "d", b"file", 0);
let b = genesis(&key(1), "d", b"file", 1);
assert_ne!(a, b, "key rotation must change genesis deterministically");
}
#[test]
fn verifier_detects_in_place_edit() {
let k = key(9);
let base = genesis(&k, "d", b"f", 0);
let mut writer = ChainVerifier::new(k, base);
let h0 = writer.peek_next(b"original");
writer.advance(h0);
let mut reader = ChainVerifier::new(k, base);
let err = reader.verify_next(b"tampered", &h0).unwrap_err();
assert_eq!(err, ChainError::Mismatch { index: 0 });
}
#[test]
fn verifier_detects_reorder() {
let k = key(3);
let base = genesis(&k, "d", b"f", 0);
let h0 = chain_next(&k, &base, b"a");
let h1 = chain_next(&k, &h0, b"b");
let _h2 = chain_next(&k, &h1, b"c");
let entries = vec![(b"a".to_vec(), h0), (b"c".to_vec(), h1)];
let ring = ChainKeyRing::new(0, k);
let err = verify_chained_prefix(&ring, "d", b"f", &entries).unwrap_err();
assert_eq!(err, ChainError::Mismatch { index: 1 });
}
#[test]
fn verify_chained_prefix_happy_path() {
let k = key(4);
let ring = ChainKeyRing::new(0, k);
let base = genesis(&k, "d", b"f", 0);
let h0 = chain_next(&k, &base, b"a");
let h1 = chain_next(&k, &h0, b"b");
let entries = vec![(b"a".to_vec(), h0), (b"b".to_vec(), h1)];
let (head, resolution) = verify_chained_prefix(&ring, "d", b"f", &entries).unwrap();
assert_eq!(head, h1);
assert_eq!(resolution, KeyResolution::Current);
}
#[test]
fn verify_chained_prefix_resolves_previous_epoch_as_rekeyed() {
let old_key = key(5);
let new_key = key(6);
let ring = ChainKeyRing::new(1, new_key).with_previous(0, old_key);
let base = genesis(&old_key, "d", b"f", 0);
let h0 = chain_next(&old_key, &base, b"a");
let entries = vec![(b"a".to_vec(), h0)];
let (_head, resolution) = verify_chained_prefix(&ring, "d", b"f", &entries).unwrap();
assert_eq!(resolution, KeyResolution::Rekeyed(0));
}
#[test]
fn verify_chained_prefix_unverifiable_when_no_epoch_matches() {
let ring = ChainKeyRing::new(0, key(1));
let wrong = genesis(&key(99), "d", b"f", 0);
let h0 = chain_next(&key(99), &wrong, b"a");
let entries = vec![(b"a".to_vec(), h0)];
let err = verify_chained_prefix(&ring, "d", b"f", &entries).unwrap_err();
assert_eq!(err, ChainError::Unverifiable);
}
#[test]
fn verify_chained_prefix_mismatch_after_correct_genesis_is_definite_tamper() {
let k = key(7);
let ring = ChainKeyRing::new(0, k);
let base = genesis(&k, "d", b"f", 0);
let h0 = chain_next(&k, &base, b"a");
let bogus = ChainHash(*blake3::hash(b"not a real chain link").as_bytes());
let entries = vec![(b"a".to_vec(), h0), (b"b".to_vec(), bogus)];
let err = verify_chained_prefix(&ring, "d", b"f", &entries).unwrap_err();
assert_eq!(err, ChainError::Mismatch { index: 1 });
}
#[test]
fn serde_json_value_maps_serialize_deterministically_whichever_backend_is_compiled_in() {
let mut map = serde_json::Map::new();
map.insert("zebra".to_owned(), serde_json::json!(1));
map.insert("alpha".to_owned(), serde_json::json!(2));
map.insert("mango".to_owned(), serde_json::json!(3));
let value = serde_json::Value::Object(map);
let first = serde_json::to_string(&value).unwrap();
let second = serde_json::to_string(&value).unwrap();
assert_eq!(
first, second,
"serde_json::Value must serialize deterministically for a fixed in-memory value, \
regardless of which backend (sorted BTreeMap or insertion-order IndexMap) is \
compiled in — this is the actual invariant canonicalization depends on, not sorted \
key order (see the corrected M1 note on this test)"
);
}
#[test]
fn round_trip_serialization_is_byte_identical() {
#[derive(serde::Serialize, serde::Deserialize)]
struct Fixture {
seq: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
chain: Option<String>,
payload: serde_json::Value,
}
let mut map = serde_json::Map::new();
map.insert("z".to_owned(), serde_json::json!("last"));
map.insert("a".to_owned(), serde_json::json!("first"));
map.insert(
"big".to_owned(),
serde_json::json!(9_007_199_254_740_993u64),
);
let original = Fixture {
seq: 5,
chain: None,
payload: serde_json::Value::Object(map),
};
let bytes1 = serde_json::to_vec(&original).unwrap();
let round_tripped: Fixture = serde_json::from_slice(&bytes1).unwrap();
let bytes2 = serde_json::to_vec(&round_tripped).unwrap();
assert_eq!(
bytes1, bytes2,
"serialize -> deserialize -> serialize must be byte-identical for canonicalization \
to be sound"
);
assert_eq!(
round_tripped.payload.get("big").unwrap(),
&serde_json::json!(9_007_199_254_740_993u64)
);
}
#[test]
fn chain_stream_verifier_matches_whole_slice_verification() {
let k = key(11);
let ring = ChainKeyRing::new(0, k);
let base = genesis(&k, "d", b"f", 0);
let h0 = chain_next(&k, &base, b"a");
let h1 = chain_next(&k, &h0, b"b");
let h2 = chain_next(&k, &h1, b"c");
let entries = vec![
(b"a".to_vec(), h0),
(b"b".to_vec(), h1),
(b"c".to_vec(), h2),
];
let (whole_head, whole_res) = verify_chained_prefix(&ring, "d", b"f", &entries).unwrap();
let mut streaming = ChainStreamVerifier::new(&ring, "d", b"f".to_vec());
for (content, stored) in &entries {
streaming.verify_next(content, stored).unwrap();
}
assert_eq!(streaming.head(), Some(whole_head));
assert_eq!(streaming.resolution(), Some(whole_res));
}
#[test]
fn verify_chained_prefix_with_checkpoint_captures_intermediate_head() {
let k = key(13);
let ring = ChainKeyRing::new(0, k);
let base = genesis(&k, "d", b"f", 0);
let h0 = chain_next(&k, &base, b"a");
let h1 = chain_next(&k, &h0, b"b");
let h2 = chain_next(&k, &h1, b"c");
let entries = vec![
(b"a".to_vec(), h0),
(b"b".to_vec(), h1),
(b"c".to_vec(), h2),
];
let (final_head, checkpoint, _res) =
verify_chained_prefix_with_checkpoint(&ring, "d", b"f", &entries, 1).unwrap();
assert_eq!(final_head, h2);
assert_eq!(checkpoint, Some(h1), "checkpoint at index 1 must be h1");
let (_final, out_of_range, _res) =
verify_chained_prefix_with_checkpoint(&ring, "d", b"f", &entries, 99).unwrap();
assert_eq!(out_of_range, None, "an out-of-range checkpoint is None");
}
#[test]
fn chain_stream_verifier_detects_tamper_mid_stream() {
let k = key(12);
let ring = ChainKeyRing::new(0, k);
let base = genesis(&k, "d", b"f", 0);
let h0 = chain_next(&k, &base, b"a");
let mut streaming = ChainStreamVerifier::new(&ring, "d", b"f".to_vec());
streaming.verify_next(b"a", &h0).unwrap();
let bogus = ChainHash(*blake3::hash(b"forged").as_bytes());
let err = streaming.verify_next(b"b", &bogus).unwrap_err();
assert_eq!(err, ChainError::Mismatch { index: 1 });
}
#[test]
fn chain_key_debug_does_not_leak_key_material() {
let k = key(0xAB);
let debug = format!("{k:?}");
assert!(
!debug.contains("171"),
"ChainKey Debug must not print key bytes"
);
assert_eq!(debug, "ChainKey(..)");
}
}