use sha2::{Digest, Sha256};
use crate::hash::{CANON_VERSION, CanonError, hash_chain};
use crate::model::FactEvent;
const ANCHOR_PREFIX: u8 = 0x02;
const BLOCK_LEN: usize = 64;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ChainAnchor {
pub event_count: u64,
pub head: Option<String>,
pub mac: String,
}
pub fn anchor_head(events: &[FactEvent], witness_key: &[u8]) -> Result<ChainAnchor, CanonError> {
let head = hash_chain(events)?.pop();
let event_count = events.len() as u64;
let mac = hex::encode(hmac_sha256(
witness_key,
&anchor_message(event_count, head.as_deref()),
));
Ok(ChainAnchor {
event_count,
head,
mac,
})
}
pub fn verify_anchor(
events: &[FactEvent],
anchor: &ChainAnchor,
witness_key: &[u8],
) -> Result<bool, CanonError> {
let head = hash_chain(events)?.pop();
let event_count = events.len() as u64;
let expected = hex::encode(hmac_sha256(
witness_key,
&anchor_message(event_count, head.as_deref()),
));
Ok(event_count == anchor.event_count
&& head == anchor.head
&& constant_time_eq(expected.as_bytes(), anchor.mac.as_bytes()))
}
fn anchor_message(count: u64, head: Option<&str>) -> Vec<u8> {
let mut message = vec![ANCHOR_PREFIX, CANON_VERSION];
message.extend_from_slice(&count.to_be_bytes());
match head {
None => message.push(0u8),
Some(head) => {
message.push(1u8);
message.extend_from_slice(&(head.len() as u64).to_be_bytes());
message.extend_from_slice(head.as_bytes());
}
}
message
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut block = [0u8; BLOCK_LEN];
if key.len() > BLOCK_LEN {
block[..32].copy_from_slice(&Sha256::digest(key));
} else {
block[..key.len()].copy_from_slice(key);
}
let mut ipad = [0x36u8; BLOCK_LEN];
let mut opad = [0x5cu8; BLOCK_LEN];
for ((byte, inner), outer) in block.iter().zip(ipad.iter_mut()).zip(opad.iter_mut()) {
*inner ^= byte;
*outer ^= byte;
}
let mut inner = Sha256::new();
inner.update(ipad);
inner.update(message);
let inner_digest = inner.finalize();
let mut outer = Sha256::new();
outer.update(opad);
outer.update(inner_digest);
let mut mac = [0u8; 32];
mac.copy_from_slice(&outer.finalize());
mac
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(feature = "signed-anchor")]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SignedTreeHead {
pub event_count: u64,
pub head: Option<String>,
pub signature: String,
}
#[cfg(feature = "signed-anchor")]
pub fn sign_head(
events: &[FactEvent],
signing_key: &ed25519_dalek::SigningKey,
) -> Result<SignedTreeHead, CanonError> {
use ed25519_dalek::Signer;
let head = hash_chain(events)?.pop();
let event_count = events.len() as u64;
let signature = signing_key.sign(&anchor_message(event_count, head.as_deref()));
Ok(SignedTreeHead {
event_count,
head,
signature: hex::encode(signature.to_bytes()),
})
}
#[cfg(feature = "signed-anchor")]
pub fn verify_signed_head(
events: &[FactEvent],
head: &SignedTreeHead,
verifying_key: &ed25519_dalek::VerifyingKey,
) -> Result<bool, CanonError> {
use ed25519_dalek::Signature;
let recomputed = hash_chain(events)?.pop();
let event_count = events.len() as u64;
if event_count != head.event_count || recomputed != head.head {
return Ok(false);
}
let Ok(bytes) = hex::decode(&head.signature) else {
return Ok(false);
};
let Ok(bytes) = <[u8; 64]>::try_from(bytes.as_slice()) else {
return Ok(false);
};
let signature = Signature::from_bytes(&bytes);
Ok(verifying_key
.verify_strict(
&anchor_message(event_count, recomputed.as_deref()),
&signature,
)
.is_ok())
}
#[cfg(test)]
mod tests {
use super::{ChainAnchor, anchor_head, hmac_sha256, verify_anchor};
use crate::ids::{ActorId, EvidenceId, FactEventId, FactId, SourceId, TimestampMillis};
use crate::model::{
Authority, AuthorityLevel, Confidence, Evidence, EvidenceKind, FactEvent, FactEventKind,
FactValue, Predicate, Provenance, Subject, Ttl,
};
const KEY: &[u8] = b"witness-secret-held-off-the-writer";
fn asserted(event_id: &str, value: &str) -> FactEvent {
FactEvent {
event_id: FactEventId::new(event_id).expect("event id"),
fact_id: FactId::new("fact:1").expect("fact id"),
kind: FactEventKind::Asserted,
subject: Subject::new("repo", "dent8").expect("subject"),
predicate: Predicate::new("database").expect("predicate"),
value: Some(FactValue::Text(value.to_string())),
confidence: Confidence::from_millis(900).expect("confidence"),
authority: Authority {
level: AuthorityLevel::High,
issuer: None,
scope: None,
},
ttl: Ttl::Never,
provenance: Provenance {
source: SourceId::new("source:test").expect("source"),
actor: ActorId::new("actor:test").expect("actor"),
tool: None,
run_id: None,
input_digest: None,
recorded_at: TimestampMillis::from_unix_millis(1),
attestation: None,
},
evidence: vec![Evidence {
id: EvidenceId::new("evidence:1").expect("evidence id"),
kind: EvidenceKind::UserStatement,
locator: "x".to_string(),
digest: None,
summary: None,
}],
observed_at: None,
valid_from: None,
valid_to: None,
}
}
#[test]
fn an_anchor_verifies_against_its_own_log() {
let events = [
asserted("event:0", "postgres"),
asserted("event:1", "redis"),
];
let anchor = anchor_head(&events, KEY).expect("anchor");
assert_eq!(anchor.event_count, 2);
assert!(anchor.head.is_some());
assert!(verify_anchor(&events, &anchor, KEY).expect("verify"));
}
#[test]
fn an_empty_log_anchors_and_verifies() {
let events: [FactEvent; 0] = [];
let anchor = anchor_head(&events, KEY).expect("anchor");
assert_eq!(anchor.event_count, 0);
assert!(anchor.head.is_none());
assert!(verify_anchor(&events, &anchor, KEY).expect("verify"));
}
#[test]
fn a_rehashed_forward_rewrite_is_caught_by_the_anchor() {
let original = [
asserted("event:0", "postgres"),
asserted("event:1", "redis"),
];
let anchor = anchor_head(&original, KEY).expect("anchor");
let rewritten = [
asserted("event:0", "postgres"),
asserted("event:1", "mysql"), ];
assert!(
!verify_anchor(&rewritten, &anchor, KEY).expect("verify"),
"a re-hashed-forward rewrite must fail anchor verification"
);
}
#[test]
fn the_wrong_witness_key_does_not_verify() {
let events = [asserted("event:0", "postgres")];
let anchor = anchor_head(&events, KEY).expect("anchor");
assert!(!verify_anchor(&events, &anchor, b"attacker-key").expect("verify"));
}
#[test]
fn a_truncated_log_is_caught_even_if_its_own_chain_is_valid() {
let full = [
asserted("event:0", "postgres"),
asserted("event:1", "redis"),
];
let anchor = anchor_head(&full, KEY).expect("anchor");
let truncated = [asserted("event:0", "postgres")];
assert!(!verify_anchor(&truncated, &anchor, KEY).expect("verify"));
}
#[test]
fn a_tampered_mac_does_not_verify() {
let events = [asserted("event:0", "postgres")];
let mut anchor = anchor_head(&events, KEY).expect("anchor");
anchor.mac = ChainAnchor {
event_count: anchor.event_count,
head: anchor.head.clone(),
mac: "00".repeat(32),
}
.mac;
assert!(!verify_anchor(&events, &anchor, KEY).expect("verify"));
}
#[test]
fn hmac_matches_a_known_rfc4231_vector() {
let key = [0x0bu8; 20];
let mac = hmac_sha256(&key, b"Hi There");
let expected = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7";
assert_eq!(hex::encode(mac), expected);
}
#[cfg(feature = "signed-anchor")]
#[test]
fn a_signed_tree_head_is_publicly_verifiable_and_tamper_detecting() {
use super::{sign_head, verify_signed_head};
use ed25519_dalek::SigningKey;
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
let verifying_key = signing_key.verifying_key();
let events = [
asserted("event:0", "postgres"),
asserted("event:1", "redis"),
];
let sth = sign_head(&events, &signing_key).expect("sign");
assert!(verify_signed_head(&events, &sth, &verifying_key).expect("verify"));
let rewritten = [
asserted("event:0", "postgres"),
asserted("event:1", "mysql"),
];
assert!(!verify_signed_head(&rewritten, &sth, &verifying_key).expect("verify"));
let attacker = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
assert!(!verify_signed_head(&events, &sth, &attacker).expect("verify"));
}
}