1use sha2::{Digest, Sha256};
29
30use crate::hash::{CANON_VERSION, CanonError, hash_chain};
31use crate::model::ClaimEvent;
32
33const ANCHOR_PREFIX: u8 = 0x02;
36
37const BLOCK_LEN: usize = 64;
39
40#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
45pub struct ChainAnchor {
46 pub event_count: u64,
47 pub head: Option<String>,
49 pub mac: String,
51}
52
53pub fn anchor_head(events: &[ClaimEvent], witness_key: &[u8]) -> Result<ChainAnchor, CanonError> {
57 let head = hash_chain(events)?.pop();
58 let event_count = events.len() as u64;
59 let mac = hex::encode(hmac_sha256(
60 witness_key,
61 &anchor_message(event_count, head.as_deref()),
62 ));
63 Ok(ChainAnchor {
64 event_count,
65 head,
66 mac,
67 })
68}
69
70pub fn verify_anchor(
79 events: &[ClaimEvent],
80 anchor: &ChainAnchor,
81 witness_key: &[u8],
82) -> Result<bool, CanonError> {
83 let head = hash_chain(events)?.pop();
84 let event_count = events.len() as u64;
85 let expected = hex::encode(hmac_sha256(
86 witness_key,
87 &anchor_message(event_count, head.as_deref()),
88 ));
89 Ok(event_count == anchor.event_count
90 && head == anchor.head
91 && constant_time_eq(expected.as_bytes(), anchor.mac.as_bytes()))
92}
93
94fn anchor_message(count: u64, head: Option<&str>) -> Vec<u8> {
98 let mut message = vec![ANCHOR_PREFIX, CANON_VERSION];
99 message.extend_from_slice(&count.to_be_bytes());
100 match head {
101 None => message.push(0u8),
102 Some(head) => {
103 message.push(1u8);
104 message.extend_from_slice(&(head.len() as u64).to_be_bytes());
105 message.extend_from_slice(head.as_bytes());
106 }
107 }
108 message
109}
110
111fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
114 let mut block = [0u8; BLOCK_LEN];
115 if key.len() > BLOCK_LEN {
116 block[..32].copy_from_slice(&Sha256::digest(key));
117 } else {
118 block[..key.len()].copy_from_slice(key);
119 }
120
121 let mut ipad = [0x36u8; BLOCK_LEN];
122 let mut opad = [0x5cu8; BLOCK_LEN];
123 for ((byte, inner), outer) in block.iter().zip(ipad.iter_mut()).zip(opad.iter_mut()) {
124 *inner ^= byte;
125 *outer ^= byte;
126 }
127
128 let mut inner = Sha256::new();
129 inner.update(ipad);
130 inner.update(message);
131 let inner_digest = inner.finalize();
132
133 let mut outer = Sha256::new();
134 outer.update(opad);
135 outer.update(inner_digest);
136
137 let mut mac = [0u8; 32];
138 mac.copy_from_slice(&outer.finalize());
139 mac
140}
141
142fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
144 if a.len() != b.len() {
145 return false;
146 }
147 let mut diff = 0u8;
148 for (x, y) in a.iter().zip(b) {
149 diff |= x ^ y;
150 }
151 diff == 0
152}
153
154#[cfg(feature = "signed-anchor")]
169#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct SignedTreeHead {
172 pub event_count: u64,
173 pub head: Option<String>,
175 pub signature: String,
177}
178
179#[cfg(feature = "signed-anchor")]
182pub fn sign_head(
183 events: &[ClaimEvent],
184 signing_key: &ed25519_dalek::SigningKey,
185) -> Result<SignedTreeHead, CanonError> {
186 use ed25519_dalek::Signer;
187
188 let head = hash_chain(events)?.pop();
189 let event_count = events.len() as u64;
190 let signature = signing_key.sign(&anchor_message(event_count, head.as_deref()));
191 Ok(SignedTreeHead {
192 event_count,
193 head,
194 signature: hex::encode(signature.to_bytes()),
195 })
196}
197
198#[cfg(feature = "signed-anchor")]
206pub fn verify_signed_head(
207 events: &[ClaimEvent],
208 head: &SignedTreeHead,
209 verifying_key: &ed25519_dalek::VerifyingKey,
210) -> Result<bool, CanonError> {
211 use ed25519_dalek::Signature;
212
213 let recomputed = hash_chain(events)?.pop();
214 let event_count = events.len() as u64;
215 if event_count != head.event_count || recomputed != head.head {
218 return Ok(false);
219 }
220 let Ok(bytes) = hex::decode(&head.signature) else {
221 return Ok(false);
222 };
223 let Ok(bytes) = <[u8; 64]>::try_from(bytes.as_slice()) else {
224 return Ok(false);
225 };
226 let signature = Signature::from_bytes(&bytes);
227 Ok(verifying_key
230 .verify_strict(
231 &anchor_message(event_count, recomputed.as_deref()),
232 &signature,
233 )
234 .is_ok())
235}
236
237#[cfg(test)]
238mod tests {
239 use super::{ChainAnchor, anchor_head, hmac_sha256, verify_anchor};
240 use crate::ids::{ActorId, ClaimEventId, ClaimId, EvidenceId, SourceId, TimestampMillis};
241 use crate::model::{
242 Authority, AuthorityLevel, ClaimEvent, ClaimEventKind, ClaimValue, Confidence, EntityRef,
243 Evidence, EvidenceKind, Predicate, Provenance, Ttl,
244 };
245
246 const KEY: &[u8] = b"witness-secret-held-off-the-writer";
247
248 fn asserted(event_id: &str, value: &str) -> ClaimEvent {
249 ClaimEvent {
250 event_id: ClaimEventId::new(event_id).expect("event id"),
251 claim_id: ClaimId::new("claim:1").expect("claim id"),
252 kind: ClaimEventKind::Asserted,
253 subject: EntityRef::new("repo", "dent8").expect("entity"),
254 predicate: Predicate::new("database").expect("predicate"),
255 value: Some(ClaimValue::Text(value.to_string())),
256 confidence: Confidence::from_millis(900).expect("confidence"),
257 authority: Authority {
258 level: AuthorityLevel::High,
259 issuer: None,
260 scope: None,
261 },
262 ttl: Ttl::Never,
263 provenance: Provenance {
264 source: SourceId::new("source:test").expect("source"),
265 actor: ActorId::new("actor:test").expect("actor"),
266 tool: None,
267 run_id: None,
268 input_digest: None,
269 recorded_at: TimestampMillis::from_unix_millis(1),
270 attestation: None,
271 },
272 evidence: vec![Evidence {
273 id: EvidenceId::new("evidence:1").expect("evidence id"),
274 kind: EvidenceKind::UserStatement,
275 locator: "x".to_string(),
276 digest: None,
277 summary: None,
278 }],
279 observed_at: None,
280 valid_from: None,
281 valid_to: None,
282 }
283 }
284
285 #[test]
286 fn an_anchor_verifies_against_its_own_log() {
287 let events = [
288 asserted("event:0", "postgres"),
289 asserted("event:1", "redis"),
290 ];
291 let anchor = anchor_head(&events, KEY).expect("anchor");
292 assert_eq!(anchor.event_count, 2);
293 assert!(anchor.head.is_some());
294 assert!(verify_anchor(&events, &anchor, KEY).expect("verify"));
295 }
296
297 #[test]
298 fn an_empty_log_anchors_and_verifies() {
299 let events: [ClaimEvent; 0] = [];
300 let anchor = anchor_head(&events, KEY).expect("anchor");
301 assert_eq!(anchor.event_count, 0);
302 assert!(anchor.head.is_none());
303 assert!(verify_anchor(&events, &anchor, KEY).expect("verify"));
304 }
305
306 #[test]
307 fn a_rehashed_forward_rewrite_is_caught_by_the_anchor() {
308 let original = [
313 asserted("event:0", "postgres"),
314 asserted("event:1", "redis"),
315 ];
316 let anchor = anchor_head(&original, KEY).expect("anchor");
317
318 let rewritten = [
319 asserted("event:0", "postgres"),
320 asserted("event:1", "mysql"), ];
322 assert!(
325 !verify_anchor(&rewritten, &anchor, KEY).expect("verify"),
326 "a re-hashed-forward rewrite must fail anchor verification"
327 );
328 }
329
330 #[test]
331 fn the_wrong_witness_key_does_not_verify() {
332 let events = [asserted("event:0", "postgres")];
333 let anchor = anchor_head(&events, KEY).expect("anchor");
334 assert!(!verify_anchor(&events, &anchor, b"attacker-key").expect("verify"));
335 }
336
337 #[test]
338 fn a_truncated_log_is_caught_even_if_its_own_chain_is_valid() {
339 let full = [
342 asserted("event:0", "postgres"),
343 asserted("event:1", "redis"),
344 ];
345 let anchor = anchor_head(&full, KEY).expect("anchor");
346 let truncated = [asserted("event:0", "postgres")];
347 assert!(!verify_anchor(&truncated, &anchor, KEY).expect("verify"));
348 }
349
350 #[test]
351 fn a_tampered_mac_does_not_verify() {
352 let events = [asserted("event:0", "postgres")];
353 let mut anchor = anchor_head(&events, KEY).expect("anchor");
354 anchor.mac = ChainAnchor {
355 event_count: anchor.event_count,
356 head: anchor.head.clone(),
357 mac: "00".repeat(32),
358 }
359 .mac;
360 assert!(!verify_anchor(&events, &anchor, KEY).expect("verify"));
361 }
362
363 #[test]
364 fn hmac_matches_a_known_rfc4231_vector() {
365 let key = [0x0bu8; 20];
367 let mac = hmac_sha256(&key, b"Hi There");
368 let expected = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7";
369 assert_eq!(hex::encode(mac), expected);
370 }
371
372 #[cfg(feature = "signed-anchor")]
373 #[test]
374 fn a_signed_tree_head_is_publicly_verifiable_and_tamper_detecting() {
375 use super::{sign_head, verify_signed_head};
376 use ed25519_dalek::SigningKey;
377
378 let signing_key = SigningKey::from_bytes(&[7u8; 32]);
379 let verifying_key = signing_key.verifying_key();
380 let events = [
381 asserted("event:0", "postgres"),
382 asserted("event:1", "redis"),
383 ];
384 let sth = sign_head(&events, &signing_key).expect("sign");
385
386 assert!(verify_signed_head(&events, &sth, &verifying_key).expect("verify"));
388
389 let rewritten = [
392 asserted("event:0", "postgres"),
393 asserted("event:1", "mysql"),
394 ];
395 assert!(!verify_signed_head(&rewritten, &sth, &verifying_key).expect("verify"));
396
397 let attacker = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
399 assert!(!verify_signed_head(&events, &sth, &attacker).expect("verify"));
400 }
401}