1use sha2::{Digest, Sha256};
40
41use crate::model::FactEvent;
42
43pub const CANON_VERSION: u8 = 2;
58
59const LEAF_PREFIX: u8 = 0x00;
62
63const DIGEST_LEN: usize = 32;
65
66pub fn canonical_bytes(event: &FactEvent) -> Result<Vec<u8>, CanonError> {
70 let value = serde_json::to_value(event).map_err(CanonError::Serialize)?;
73 serde_json::to_vec(&value).map_err(CanonError::Serialize)
74}
75
76const ATTESTATION_DOMAIN: &[u8] = b"dent8.event-attestation.v1\0";
80
81pub fn attestation_message(event: &FactEvent) -> Result<Vec<u8>, CanonError> {
88 let mut unattested = event.clone();
89 unattested.provenance.attestation = None;
90 let body = canonical_bytes(&unattested)?;
91 let mut message = Vec::with_capacity(ATTESTATION_DOMAIN.len() + 8 + body.len());
92 message.extend_from_slice(ATTESTATION_DOMAIN);
93 message.extend_from_slice(&(body.len() as u64).to_be_bytes());
94 message.extend_from_slice(&body);
95 Ok(message)
96}
97
98pub fn event_hash(event: &FactEvent, previous: Option<&str>) -> Result<String, CanonError> {
102 let canonical = canonical_bytes(event)?;
103 let previous = previous.map(decode_digest).transpose()?;
104 Ok(hash_leaf(&canonical, previous.as_ref()))
105}
106
107fn decode_digest(hex_str: &str) -> Result<[u8; DIGEST_LEN], CanonError> {
110 let mut out = [0u8; DIGEST_LEN];
111 hex::decode_to_slice(hex_str, &mut out)
112 .map_err(|_| CanonError::InvalidPreviousHash(hex_str.to_string()))?;
113 Ok(out)
114}
115
116#[must_use]
121fn hash_leaf(canonical: &[u8], previous: Option<&[u8; DIGEST_LEN]>) -> String {
122 let mut hasher = Sha256::new();
123 hasher.update([LEAF_PREFIX, CANON_VERSION]);
124 hasher.update((canonical.len() as u64).to_be_bytes());
125 hasher.update(canonical);
126 match previous {
127 None => hasher.update([0u8]),
128 Some(previous) => {
129 hasher.update([1u8]);
130 hasher.update(previous);
131 }
132 }
133 hex::encode(hasher.finalize())
134}
135
136pub fn hash_chain(events: &[FactEvent]) -> Result<Vec<String>, CanonError> {
140 let mut hashes = Vec::with_capacity(events.len());
141 let mut previous: Option<String> = None;
142 for event in events {
143 let hash = event_hash(event, previous.as_deref())?;
144 previous = Some(hash.clone());
145 hashes.push(hash);
146 }
147 Ok(hashes)
148}
149
150#[derive(Debug)]
153pub enum CanonError {
154 Serialize(serde_json::Error),
156 InvalidPreviousHash(String),
158}
159
160impl std::fmt::Display for CanonError {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 Self::Serialize(error) => write!(f, "canonicalization failed: {error}"),
164 Self::InvalidPreviousHash(value) => {
165 write!(f, "previous hash is not a 64-char hex digest: {value:?}")
166 }
167 }
168 }
169}
170
171impl std::error::Error for CanonError {
172 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
173 match self {
174 Self::Serialize(error) => Some(error),
175 Self::InvalidPreviousHash(_) => None,
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{CanonError, canonical_bytes, event_hash, hash_chain};
183 use crate::ids::{ActorId, EvidenceId, FactEventId, FactId, SourceId, TimestampMillis};
184 use crate::model::{
185 Authority, AuthorityLevel, Confidence, Evidence, EvidenceKind, FactEvent, FactEventKind,
186 FactValue, Predicate, Provenance, Subject, SupersessionReason, Ttl,
187 };
188
189 fn event(event_id: &str, kind: FactEventKind, value: Option<FactValue>) -> FactEvent {
190 FactEvent {
191 event_id: FactEventId::new(event_id).expect("event id"),
192 fact_id: FactId::new("fact:1").expect("fact id"),
193 kind,
194 subject: Subject::new("repo", "dent8").expect("subject"),
195 predicate: Predicate::new("uses_database").expect("predicate"),
196 value,
197 confidence: Confidence::from_millis(900).expect("confidence"),
198 authority: Authority {
199 level: AuthorityLevel::High,
200 issuer: None,
201 scope: None,
202 },
203 ttl: Ttl::Never,
204 provenance: Provenance {
205 source: SourceId::new("source:test").expect("source"),
206 actor: ActorId::new("actor:test").expect("actor"),
207 tool: None,
208 run_id: None,
209 input_digest: None,
210 recorded_at: TimestampMillis::from_unix_millis(1),
211 attestation: None,
212 },
213 evidence: vec![Evidence {
214 id: EvidenceId::new("evidence:1").expect("evidence id"),
215 kind: EvidenceKind::UserStatement,
216 locator: "x".to_string(),
217 digest: None,
218 summary: None,
219 }],
220 observed_at: None,
221 valid_from: None,
222 valid_to: None,
223 }
224 }
225
226 fn asserted(event_id: &str) -> FactEvent {
227 event(
228 event_id,
229 FactEventKind::Asserted,
230 Some(FactValue::Text("postgres".to_string())),
231 )
232 }
233
234 #[test]
235 fn canonicalization_is_deterministic() {
236 let e = asserted("event:1");
237 assert_eq!(canonical_bytes(&e).unwrap(), canonical_bytes(&e).unwrap());
238 }
239
240 #[test]
241 fn canonicalization_is_key_order_independent() {
242 let e = asserted("event:1");
248 let declaration_order = serde_json::to_vec(&e).expect("serialize");
249 let canonical = canonical_bytes(&e).expect("canonicalize");
250 assert_ne!(
251 declaration_order, canonical,
252 "to_value did not reorder keys"
253 );
254
255 let reparsed: FactEvent = serde_json::from_slice(&declaration_order).expect("deserialize");
256 assert_eq!(
257 canonical_bytes(&reparsed).expect("re-canonicalize"),
258 canonical
259 );
260 }
261
262 #[test]
263 fn embedded_json_is_canonicalized_so_equal_values_hash_equally() {
264 let json_event = |raw: &str| {
265 event(
266 "event:1",
267 FactEventKind::Asserted,
268 Some(FactValue::json(raw).expect("valid json")),
269 )
270 };
271 let a = json_event("{ \"b\": 2, \"a\": 1 }");
273 let b = json_event("{\"a\":1,\n \"b\":2}");
274 assert_eq!(canonical_bytes(&a).unwrap(), canonical_bytes(&b).unwrap());
275 assert_eq!(event_hash(&a, None).unwrap(), event_hash(&b, None).unwrap());
276
277 let c = json_event(r#"{"a": 2, "b": 1}"#);
279 assert_ne!(event_hash(&a, None).unwrap(), event_hash(&c, None).unwrap());
280 }
281
282 #[test]
283 fn a_float_json_event_survives_a_serde_round_trip_unchanged() {
284 let e = event(
289 "event:1",
290 FactEventKind::Asserted,
291 Some(FactValue::json(r#"{"ratio": 13e300, "p": 0.1}"#).expect("json")),
292 );
293 let original = canonical_bytes(&e).expect("canonicalize");
294 let reloaded: FactEvent = serde_json::from_slice(&original).expect("deserialize");
295 assert_eq!(
296 canonical_bytes(&reloaded).expect("re-canonicalize"),
297 original
298 );
299 assert_eq!(
300 event_hash(&reloaded, None).unwrap(),
301 event_hash(&e, None).unwrap()
302 );
303 }
304
305 #[test]
306 fn canonicalization_round_trips_through_serde() {
307 let e = asserted("event:1");
309 let bytes = canonical_bytes(&e).expect("canonicalize");
310 let decoded: FactEvent = serde_json::from_slice(&bytes).expect("deserialize");
311 assert_eq!(decoded, e);
312 assert_eq!(canonical_bytes(&decoded).expect("re-canonicalize"), bytes);
313 }
314
315 #[test]
316 fn distinct_events_hash_differently() {
317 let a = event_hash(&asserted("event:1"), None).unwrap();
318 let b = event_hash(&asserted("event:2"), None).unwrap();
319 assert_ne!(a, b);
320 assert_eq!(a.len(), 64);
322 }
323
324 #[test]
325 fn genesis_is_unambiguous_and_previous_is_validated() {
326 let e = asserted("event:1");
327 let genesis = event_hash(&e, None).unwrap();
329 assert_eq!(genesis.len(), 64);
330 assert!(matches!(
333 event_hash(&e, Some("")),
334 Err(CanonError::InvalidPreviousHash(_))
335 ));
336 assert!(matches!(
337 event_hash(&e, Some("not-hex")),
338 Err(CanonError::InvalidPreviousHash(_))
339 ));
340 let chained = event_hash(&e, Some(&genesis)).unwrap();
342 assert_ne!(genesis, chained);
343 }
344
345 #[test]
346 fn the_chain_links_each_event_to_the_previous() {
347 let first = asserted("event:1");
348 let second = event(
349 "event:2",
350 FactEventKind::Superseded {
351 by: FactId::new("fact:2").expect("fact id"),
352 reason: SupersessionReason::NewerObservation,
353 },
354 None,
355 );
356
357 let unchained = event_hash(&second, None).unwrap();
358 let chained = event_hash(&second, Some(&event_hash(&first, None).unwrap())).unwrap();
359 assert_ne!(unchained, chained);
360 }
361
362 #[test]
363 fn tampering_with_an_event_breaks_the_chain_from_that_point() {
364 let events = [
365 asserted("event:1"),
366 asserted("event:2"),
367 asserted("event:3"),
368 ];
369 let original = hash_chain(&events).expect("chain");
370
371 let tampered = [
373 asserted("event:1"),
374 asserted("event:CHANGED"),
375 asserted("event:3"),
376 ];
377 let recomputed = hash_chain(&tampered).expect("chain");
378
379 assert_eq!(recomputed[0], original[0]); assert_ne!(recomputed[1], original[1]); assert_ne!(recomputed[2], original[2]); }
383
384 #[test]
385 fn attestation_field_is_skipped_when_none_so_v1_bytes_are_stable() {
386 let unattested = event(
390 "event:1",
391 FactEventKind::Asserted,
392 Some(FactValue::Text("postgres".into())),
393 );
394 let bytes = canonical_bytes(&unattested).expect("canonical bytes");
395 assert!(
396 !String::from_utf8(bytes)
397 .expect("utf8")
398 .contains("attestation"),
399 "None attestation must not appear in canonical bytes"
400 );
401
402 let mut attested = unattested.clone();
404 attested.provenance.attestation = Some(crate::model::WriteAttestation {
405 algorithm: crate::model::AttestationAlgorithm::Ed25519,
406 public_key: "aa".repeat(32),
407 signature: "bb".repeat(64),
408 });
409 assert_ne!(
410 canonical_bytes(&attested).expect("canonical bytes"),
411 canonical_bytes(&unattested).expect("canonical bytes"),
412 );
413 assert_ne!(
414 event_hash(&attested, None).expect("hash"),
415 event_hash(&unattested, None).expect("hash"),
416 );
417 }
418
419 #[test]
420 fn attestation_message_strips_the_attestation_itself() {
421 let unattested = event(
425 "event:1",
426 FactEventKind::Asserted,
427 Some(FactValue::Text("postgres".into())),
428 );
429 let mut attested = unattested.clone();
430 attested.provenance.attestation = Some(crate::model::WriteAttestation {
431 algorithm: crate::model::AttestationAlgorithm::Ed25519,
432 public_key: "aa".repeat(32),
433 signature: "bb".repeat(64),
434 });
435 let message_unattested =
436 super::attestation_message(&unattested).expect("attestation message");
437 let message_attested = super::attestation_message(&attested).expect("attestation message");
438 assert_eq!(message_unattested, message_attested);
439
440 let other = event(
442 "event:1",
443 FactEventKind::Asserted,
444 Some(FactValue::Text("mysql".into())),
445 );
446 assert_ne!(
447 super::attestation_message(&other).expect("attestation message"),
448 message_unattested
449 );
450
451 assert!(message_unattested.starts_with(b"dent8.event-attestation.v1\0"));
453 }
454
455 #[test]
456 fn attestation_round_trips_through_serde() {
457 let mut attested = event(
458 "event:1",
459 FactEventKind::Asserted,
460 Some(FactValue::Text("postgres".into())),
461 );
462 attested.provenance.attestation = Some(crate::model::WriteAttestation {
463 algorithm: crate::model::AttestationAlgorithm::Ed25519,
464 public_key: "aa".repeat(32),
465 signature: "bb".repeat(64),
466 });
467 let json = serde_json::to_string(&attested).expect("serialize");
468 assert!(json.contains("\"algorithm\":\"ed25519\""));
469 let back: FactEvent = serde_json::from_str(&json).expect("deserialize");
470 assert_eq!(back, attested);
471
472 let forged = json.replace("\"ed25519\"", "\"none\"");
474 assert!(serde_json::from_str::<FactEvent>(&forged).is_err());
475 }
476}