use blake3::Hasher;
pub const DOMAIN_TAG: &[u8] = b"tatara-process/v1alpha1\n";
#[must_use]
pub fn compose_root(
artifact: &str,
control: Option<&str>,
intent: &str,
previous: Option<&str>,
) -> String {
let mut h = Hasher::new();
h.update(DOMAIN_TAG);
h.update(artifact.as_bytes());
h.update(b"\n");
h.update(control.unwrap_or("").as_bytes());
h.update(b"\n");
h.update(intent.as_bytes());
h.update(b"\n");
h.update(previous.unwrap_or("").as_bytes());
crate::hash::hex_blake3_hash(&h.finalize())
}
#[must_use]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut acc: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
acc |= x ^ y;
}
acc == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_tag_matches_crate_name_and_version_bytes() {
let expected = format!("tatara-process/{}\n", crate::VERSION);
assert_eq!(DOMAIN_TAG, expected.as_bytes());
}
#[test]
fn domain_tag_ends_with_newline_separator() {
assert_eq!(DOMAIN_TAG.last(), Some(&b'\n'));
}
fn hand_authored_chain(
artifact: &str,
control: Option<&str>,
intent: &str,
previous: Option<&str>,
) -> String {
let mut h = Hasher::new();
h.update(DOMAIN_TAG);
h.update(artifact.as_bytes());
h.update(b"\n");
h.update(control.unwrap_or("").as_bytes());
h.update(b"\n");
h.update(intent.as_bytes());
h.update(b"\n");
h.update(previous.unwrap_or("").as_bytes());
hex::encode(h.finalize().as_bytes())
}
#[test]
fn compose_root_matches_pre_lift_hand_authored_chain() {
let cases: &[(&str, Option<&str>, &str, Option<&str>)] = &[
("aaaa", None, "iiii", None),
("aaaa", Some("cccc"), "iiii", None),
("aaaa", None, "iiii", Some("pppp")),
("aaaa", Some("cccc"), "iiii", Some("pppp")),
("", None, "", None),
("", Some(""), "", Some("")),
];
for (artifact, control, intent, previous) in cases {
assert_eq!(
compose_root(artifact, *control, intent, *previous),
hand_authored_chain(artifact, *control, intent, *previous),
"compose_root drifted from pre-lift hand-authored chain \
for inputs (artifact={artifact:?}, control={control:?}, \
intent={intent:?}, previous={previous:?})",
);
}
}
#[test]
fn compose_root_treats_empty_control_and_none_control_identically() {
let with_none = compose_root("art", None, "int", None);
let with_empty = compose_root("art", Some(""), "int", Some(""));
assert_eq!(with_none, with_empty);
}
#[test]
fn compose_root_is_deterministic_across_calls() {
let a = compose_root("art", Some("ctl"), "int", Some("prev"));
let b = compose_root("art", Some("ctl"), "int", Some("prev"));
assert_eq!(a, b);
}
#[test]
fn compose_root_differs_across_every_pillar() {
let base = compose_root("aaaa", Some("cccc"), "iiii", Some("pppp"));
assert_ne!(
base,
compose_root("BBBB", Some("cccc"), "iiii", Some("pppp")),
"artifact pillar swap failed to alter composed_root"
);
assert_ne!(
base,
compose_root("aaaa", Some("CCCC"), "iiii", Some("pppp")),
"control pillar swap failed to alter composed_root"
);
assert_ne!(
base,
compose_root("aaaa", Some("cccc"), "IIII", Some("pppp")),
"intent pillar swap failed to alter composed_root"
);
assert_ne!(
base,
compose_root("aaaa", Some("cccc"), "iiii", Some("PPPP")),
"previous pillar swap failed to alter composed_root"
);
}
#[test]
fn compose_root_output_is_lowercase_hex_of_blake3_length() {
let out = compose_root("a", None, "i", None);
assert_eq!(out.len(), 64);
assert!(out.chars().all(|c| c.is_ascii_hexdigit()));
assert!(out.chars().all(|c| !c.is_ascii_uppercase()));
}
fn hand_authored_ct_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut acc: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
acc |= x ^ y;
}
acc == 0
}
#[test]
fn constant_time_eq_matches_pre_lift_hand_authored_body() {
let cases: &[(&[u8], &[u8])] = &[
(b"", b""),
(b"", b"a"),
(b"a", b""),
(b"a", b"a"),
(b"a", b"b"),
(b"abcd", b"abcd"),
(b"abcd", b"abce"),
(b"abcd", b"abc"),
(b"abc", b"abcd"),
(b"\x00\x00\x00", b"\x00\x00\x00"),
(b"\xff\xff\xff", b"\xff\xff\xff"),
(b"\xff\xff\xff", b"\xff\xff\x00"),
];
for (a, b) in cases {
assert_eq!(
constant_time_eq(a, b),
hand_authored_ct_eq(a, b),
"constant_time_eq drifted from pre-lift hand-authored \
body for inputs (a={a:?}, b={b:?})",
);
}
}
#[test]
fn constant_time_eq_short_circuits_on_length_mismatch() {
assert!(!constant_time_eq(b"", b"a"));
assert!(!constant_time_eq(b"abc", b"abcd"));
assert!(!constant_time_eq(b"abcd", b"abc"));
}
#[test]
fn constant_time_eq_returns_true_only_on_full_byte_equality() {
assert!(constant_time_eq(b"", b""));
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abcdef", b"abcdeg"));
assert!(!constant_time_eq(b"Abcdef", b"abcdef"));
}
}