use spine::{Hasher, constant_time_eq};
use crate::root::{combined_root, validate_committed_epochs};
pub struct TrustedBindingRoot<'a> {
pub alg_id: u64,
pub hasher: &'a dyn Hasher,
pub root: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingProof {
pub member_roots: Vec<(u64, Vec<u8>)>,
pub alg_epochs: Vec<(u64, Vec<(u64, u64)>)>,
}
impl BindingProof {
#[must_use]
pub fn produce(
member_roots: Vec<(u64, Vec<u8>)>,
alg_epochs: Vec<(u64, Vec<(u64, u64)>)>,
) -> Self {
Self {
member_roots,
alg_epochs,
}
}
#[must_use]
pub fn verify(&self, trusted: &[TrustedBindingRoot<'_>], tree_size: u64, arity: u64) -> bool {
if trusted.is_empty() {
return false;
}
if !validate_committed_epochs(&self.alg_epochs, tree_size) {
return false;
}
if self.member_roots.windows(2).any(|w| w[0].0 >= w[1].0) {
return false;
}
for t in trusted {
if !self.member_roots.iter().any(|(id, _)| *id == t.alg_id) {
return false;
}
}
trusted.iter().all(|t| {
let computed = combined_root(
t.hasher,
&self.member_roots,
&self.alg_epochs,
tree_size,
arity,
);
constant_time_eq(&computed, t.root)
})
}
}
#[cfg(test)]
mod tests {
use sha2::{Digest as _, Sha256};
use super::*;
const MAX: u64 = u64::MAX;
#[derive(Debug)]
struct Sha256Hasher;
impl Hasher for Sha256Hasher {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
for c in children {
h.update(c);
}
h.finalize().to_vec()
}
fn empty(&self) -> Vec<u8> {
Sha256::digest(b"").to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn clone_box(&self) -> Box<dyn Hasher> {
Box::new(Sha256Hasher)
}
}
#[derive(Debug)]
struct PrefixedSha256Hasher;
impl PrefixedSha256Hasher {
const PREFIX: &'static [u8] = b"alg-B:";
}
impl Hasher for PrefixedSha256Hasher {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
self.hash(data)
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut buf = Vec::new();
for c in children {
buf.extend_from_slice(c);
}
self.hash(&buf)
}
fn empty(&self) -> Vec<u8> {
self.hash(b"")
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
let mut h = Sha256::new();
h.update(Self::PREFIX);
h.update(data);
h.finalize().to_vec()
}
fn clone_box(&self) -> Box<dyn Hasher> {
Box::new(PrefixedSha256Hasher)
}
}
fn honest_setup() -> (
BindingProof,
Sha256Hasher,
PrefixedSha256Hasher,
Vec<u8>,
Vec<u8>,
u64,
) {
let member_roots = vec![(0u64, vec![0xAA; 32]), (1u64, vec![0xBB; 32])];
let alg_epochs = vec![(0u64, vec![(0u64, MAX)]), (1u64, vec![(0u64, MAX)])];
let tree_size = 4;
let proof = BindingProof::produce(member_roots.clone(), alg_epochs.clone());
let h_a = Sha256Hasher;
let h_b = PrefixedSha256Hasher;
let br_a = combined_root(&h_a, &member_roots, &alg_epochs, tree_size, 2);
let br_b = combined_root(&h_b, &member_roots, &alg_epochs, tree_size, 2);
(proof, h_a, h_b, br_a, br_b, tree_size)
}
#[test]
fn consistent_trusted_set_verifies() {
let (proof, h_a, h_b, br_a, br_b, sz) = honest_setup();
let trusted = vec![
TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
},
TrustedBindingRoot {
alg_id: 1,
hasher: &h_b,
root: &br_b,
},
];
assert!(proof.verify(&trusted, sz, 2));
}
#[test]
fn single_algorithm_verifies() {
let (proof, h_a, _h_b, br_a, _br_b, sz) = honest_setup();
let trusted = vec![TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
}];
assert!(proof.verify(&trusted, sz, 2));
}
#[test]
fn promoted_binding_root_verifies() {
let member_roots = vec![(0u64, vec![0x42; 32])];
let alg_epochs = vec![(0u64, vec![(0u64, MAX)])];
let proof = BindingProof::produce(member_roots.clone(), alg_epochs.clone());
let h_a = Sha256Hasher;
let br_a = member_roots[0].1.clone();
assert_eq!(br_a, combined_root(&h_a, &member_roots, &alg_epochs, 4, 2));
let trusted = vec![TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
}];
assert!(proof.verify(&trusted, 4, 2));
}
#[test]
fn forged_binding_root_rejected() {
let (proof, h_a, h_b, br_a, _br_b, sz) = honest_setup();
let forged = vec![0xFFu8; 32];
let trusted = vec![
TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
},
TrustedBindingRoot {
alg_id: 1,
hasher: &h_b,
root: &forged,
},
];
assert!(!proof.verify(&trusted, sz, 2));
}
#[test]
fn inconsistent_member_root_rejected() {
let (proof, h_a, h_b, br_a, _br_b, sz) = honest_setup();
let other_roots = vec![(0u64, vec![0xAA; 32]), (1u64, vec![0xCC; 32])];
let other_epochs = vec![(0u64, vec![(0u64, MAX)]), (1u64, vec![(0u64, MAX)])];
let br_b_other = combined_root(&h_b, &other_roots, &other_epochs, 4, 2);
let trusted = vec![
TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
},
TrustedBindingRoot {
alg_id: 1,
hasher: &h_b,
root: &br_b_other,
},
];
assert!(!proof.verify(&trusted, sz, 2));
}
#[test]
fn wrong_hasher_rejected() {
let (proof, _h_a, h_b, br_a, _br_b, sz) = honest_setup();
let trusted = vec![TrustedBindingRoot {
alg_id: 0,
hasher: &h_b,
root: &br_a,
}];
assert!(!proof.verify(&trusted, sz, 2));
}
#[test]
fn empty_trusted_set_rejected() {
let (proof, _h_a, _h_b, _br_a, _br_b, sz) = honest_setup();
assert!(!proof.verify(&[], sz, 2));
}
#[test]
fn malformed_timeline_rejected() {
let member_roots = vec![(0u64, vec![0xAA; 32])];
let bad_epochs = vec![(0u64, vec![(0u64, 5u64), (4u64, MAX)])];
let proof = BindingProof::produce(member_roots.clone(), bad_epochs.clone());
let h_a = Sha256Hasher;
let br_a = combined_root(&h_a, &member_roots, &bad_epochs, 10, 2);
let trusted = vec![TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
}];
assert!(!proof.verify(&trusted, 10, 2));
}
#[test]
fn unsorted_member_roots_rejected() {
let member_roots = vec![(1u64, vec![0xBB; 32]), (0u64, vec![0xAA; 32])];
let alg_epochs = vec![(0u64, vec![(0u64, MAX)]), (1u64, vec![(0u64, MAX)])];
let proof = BindingProof::produce(member_roots.clone(), alg_epochs.clone());
let h_a = Sha256Hasher;
let br_a = combined_root(&h_a, &member_roots, &alg_epochs, 4, 2);
let trusted = vec![TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
}];
assert!(!proof.verify(&trusted, 4, 2));
}
#[test]
fn trusted_root_without_member_root_rejected() {
let (proof, h_a, _h_b, br_a, _br_b, sz) = honest_setup();
let trusted = vec![TrustedBindingRoot {
alg_id: 2,
hasher: &h_a,
root: &br_a,
}];
assert!(!proof.verify(&trusted, sz, 2));
}
#[test]
fn verify_reads_only_digests() {
let (mut proof, h_a, h_b, br_a, br_b, sz) = honest_setup();
let trusted = vec![
TrustedBindingRoot {
alg_id: 0,
hasher: &h_a,
root: &br_a,
},
TrustedBindingRoot {
alg_id: 1,
hasher: &h_b,
root: &br_b,
},
];
assert!(proof.verify(&trusted, sz, 2));
proof.member_roots[0].1[0] ^= 0x01;
assert!(!proof.verify(&trusted, sz, 2));
}
}