1use crate::body::Signature;
34use crate::receipt::{
35 is_canonical_ms_utc, ms_rfc3339, preimage_hash_of_object_with, verify_signature_over_hash_with,
36 ReceiptSigner,
37};
38use acdp_jcs::try_canonicalize_value;
39use acdp_primitives::error::AcdpError;
40use acdp_primitives::primitives::{ContentHash, CtxId, LineageId};
41use chrono::{DateTime, Utc};
42use serde::{Deserialize, Serialize};
43
44pub const LOG_LEAF_VERSION: &str = "acdp-log-leaf/1";
49
50pub const LOG_CHECKPOINT_VERSION: &str = "acdp-log/1";
54
55pub fn decode_sha256_hex(prefixed: &str) -> Result<[u8; 32], AcdpError> {
59 let hex_part = prefixed.strip_prefix("sha256:").ok_or_else(|| {
60 AcdpError::InvalidLogProof(format!(
61 "hash '{prefixed}' is not of the form 'sha256:<hex>' (RFC-ACDP-0012 §2)"
62 ))
63 })?;
64 if hex_part.len() != 64
65 || !hex_part
66 .bytes()
67 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
68 {
69 return Err(AcdpError::InvalidLogProof(format!(
70 "hash '{prefixed}' is not 'sha256:' + 64 lowercase hex digits (RFC-ACDP-0012 §2)"
71 )));
72 }
73 let bytes = hex::decode(hex_part)
74 .map_err(|e| AcdpError::InvalidLogProof(format!("hash '{prefixed}': {e}")))?;
75 bytes.try_into().map_err(|_| {
76 AcdpError::InvalidLogProof(format!("hash '{prefixed}' does not encode 32 bytes"))
77 })
78}
79
80pub fn encode_sha256_hex(digest: &[u8; 32]) -> String {
83 format!("sha256:{}", hex::encode(digest))
84}
85
86pub fn parse_log_id(log_id: &str) -> Result<(&str, &str), AcdpError> {
90 let (did, instance) = log_id.split_once("/log/").ok_or_else(|| {
91 AcdpError::InvalidLogProof(format!(
92 "log_id '{log_id}' is not '<registry_did>/log/<instance>' (RFC-ACDP-0012 §6)"
93 ))
94 })?;
95 let msi = did.strip_prefix("did:web:").ok_or_else(|| {
96 AcdpError::InvalidLogProof(format!(
97 "log_id '{log_id}' registry DID must be did:web (RFC-ACDP-0012 §6)"
98 ))
99 })?;
100 if msi.is_empty()
101 || !msi
102 .bytes()
103 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'%' | b':' | b'-'))
104 {
105 return Err(AcdpError::InvalidLogProof(format!(
106 "log_id '{log_id}' has a malformed did:web method-specific identifier"
107 )));
108 }
109 if instance.is_empty()
110 || instance.len() > 32
111 || !instance
112 .bytes()
113 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
114 {
115 return Err(AcdpError::InvalidLogProof(format!(
116 "log_id '{log_id}' instance component must match [a-z0-9-]{{1,32}} (RFC-ACDP-0012 §6)"
117 )));
118 }
119 Ok((did, instance))
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct LogLeaf {
137 pub leaf_version: String,
139 pub ctx_id: CtxId,
143 pub lineage_id: LineageId,
145 pub origin_registry: String,
149 #[serde(with = "ms_rfc3339")]
154 pub created_at: DateTime<Utc>,
155 pub content_hash: ContentHash,
157 pub key_fingerprint: String,
160 pub receipt_hash: String,
165}
166
167impl LogLeaf {
168 pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
172 let leaf = Self::deserialize(value)
173 .map_err(|e| AcdpError::InvalidLogProof(format!("log leaf does not parse: {e}")))?;
174 if leaf.leaf_version != LOG_LEAF_VERSION {
175 return Err(AcdpError::InvalidLogProof(format!(
176 "log leaf leaf_version '{}' ≠ '{LOG_LEAF_VERSION}' (RFC-ACDP-0012 §4)",
177 leaf.leaf_version
178 )));
179 }
180 let raw = value
181 .get("created_at")
182 .and_then(|v| v.as_str())
183 .ok_or_else(|| {
184 AcdpError::InvalidLogProof("log leaf created_at missing or not a string".into())
185 })?;
186 if !is_canonical_ms_utc(raw) {
187 return Err(AcdpError::InvalidLogProof(format!(
188 "log leaf created_at '{raw}' is not canonical millisecond-precision RFC 3339 \
189 UTC (`YYYY-MM-DDTHH:MM:SS.mmmZ`, RFC-ACDP-0012 §4)"
190 )));
191 }
192 Ok(leaf)
193 }
194
195 pub fn canonical_bytes(&self) -> Result<Vec<u8>, AcdpError> {
200 try_canonicalize_value(&serde_json::to_value(self)?)
201 }
202
203 pub fn leaf_hash(&self) -> Result<[u8; 32], AcdpError> {
205 Ok(acdp_crypto::merkle::leaf_hash(&self.canonical_bytes()?))
206 }
207
208 pub fn leaf_hash_hex(&self) -> Result<String, AcdpError> {
210 Ok(encode_sha256_hex(&self.leaf_hash()?))
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(deny_unknown_fields)]
229pub struct LogCheckpoint {
230 pub checkpoint_version: String,
232 pub log_id: String,
236 pub tree_size: u64,
239 pub root_hash: String,
241 #[serde(with = "ms_rfc3339")]
246 pub timestamp: DateTime<Utc>,
247 pub signature: Signature,
251}
252
253impl LogCheckpoint {
254 pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
260 let checkpoint = Self::deserialize(value).map_err(|e| {
261 AcdpError::InvalidLogProof(format!("log_checkpoint does not parse: {e}"))
262 })?;
263 if checkpoint.checkpoint_version != LOG_CHECKPOINT_VERSION {
264 return Err(AcdpError::InvalidLogProof(format!(
265 "log_checkpoint checkpoint_version '{}' ≠ '{LOG_CHECKPOINT_VERSION}' \
266 (RFC-ACDP-0012 §9.3 step 1)",
267 checkpoint.checkpoint_version
268 )));
269 }
270 parse_log_id(&checkpoint.log_id)?;
271 decode_sha256_hex(&checkpoint.root_hash)?;
272 let raw = value
273 .get("timestamp")
274 .and_then(|v| v.as_str())
275 .ok_or_else(|| {
276 AcdpError::InvalidLogProof(
277 "log_checkpoint timestamp missing or not a string".into(),
278 )
279 })?;
280 if !is_canonical_ms_utc(raw) {
281 return Err(AcdpError::InvalidLogProof(format!(
282 "log_checkpoint timestamp '{raw}' is not canonical millisecond-precision \
283 RFC 3339 UTC (`YYYY-MM-DDTHH:MM:SS.mmmZ`, RFC-ACDP-0012 §9.3 step 4)"
284 )));
285 }
286 checkpoint.registry_did()?;
289 Ok(checkpoint)
290 }
291
292 pub fn registry_did(&self) -> Result<&str, AcdpError> {
295 let (did, _instance) = parse_log_id(&self.log_id)?;
296 match self.signature.key_id.split_once('#') {
297 Some((key_did, frag)) if key_did == did && !frag.is_empty() => Ok(did),
298 _ => Err(AcdpError::InvalidLogProof(format!(
299 "log_checkpoint signature.key_id '{}' is not a DID URL under the log_id's \
300 registry DID '{did}' (RFC-ACDP-0012 §6)",
301 self.signature.key_id
302 ))),
303 }
304 }
305
306 pub fn preimage_hash_of_value(value: &serde_json::Value) -> Result<ContentHash, AcdpError> {
312 preimage_hash_of_object_with(value, "log_checkpoint", AcdpError::InvalidLogProof)
313 }
314
315 pub fn preimage_hash(&self) -> Result<ContentHash, AcdpError> {
320 Self::preimage_hash_of_value(&serde_json::to_value(self)?)
321 }
322
323 pub fn root_hash_bytes(&self) -> Result<[u8; 32], AcdpError> {
325 decode_sha256_hex(&self.root_hash)
326 }
327
328 pub fn verify_signature_with_key(
333 &self,
334 registry_pub_ed25519: Option<&[u8; 32]>,
335 registry_pub_p256_sec1: Option<&[u8]>,
336 ) -> Result<(), AcdpError> {
337 let hash = self.preimage_hash()?;
338 self.verify_signature_against_hash(&hash, registry_pub_ed25519, registry_pub_p256_sec1)
339 }
340
341 pub fn verify_signature_against_hash(
345 &self,
346 hash: &ContentHash,
347 registry_pub_ed25519: Option<&[u8; 32]>,
348 registry_pub_p256_sec1: Option<&[u8]>,
349 ) -> Result<(), AcdpError> {
350 verify_signature_over_hash_with(
351 &self.signature,
352 hash,
353 registry_pub_ed25519,
354 registry_pub_p256_sec1,
355 "log_checkpoint",
356 AcdpError::InvalidLogProof,
357 )
358 }
359
360 pub fn cross_check_registry_binding(
367 &self,
368 serving_authority: &str,
369 capabilities_registry_did: &str,
370 ) -> Result<(), AcdpError> {
371 let did = self.registry_did()?;
372 let expected_did = acdp_did::web::authority_to_did_web(serving_authority);
373 if did != expected_did {
374 return Err(AcdpError::InvalidLogProof(format!(
375 "log_checkpoint log_id registry DID '{did}' ≠ serving authority's DID \
376 '{expected_did}' (RFC-ACDP-0012 §9.3 step 3)"
377 )));
378 }
379 if did != capabilities_registry_did {
380 return Err(AcdpError::InvalidLogProof(format!(
381 "log_checkpoint log_id registry DID '{did}' ≠ capabilities.registry_did \
382 '{capabilities_registry_did}' (RFC-ACDP-0012 §9.3 step 3)"
383 )));
384 }
385 Ok(())
386 }
387
388 pub fn check_timestamp_skew(
394 &self,
395 now: DateTime<Utc>,
396 max_clock_skew: chrono::Duration,
397 ) -> Result<(), AcdpError> {
398 if self.timestamp.timestamp_subsec_nanos() % 1_000_000 != 0 {
399 return Err(AcdpError::InvalidLogProof(
400 "log_checkpoint timestamp is not millisecond-truncated (RFC-ACDP-0001 §5.3)".into(),
401 ));
402 }
403 if self.timestamp > now + max_clock_skew {
404 return Err(AcdpError::InvalidLogProof(format!(
405 "log_checkpoint timestamp '{}' is in the future beyond the {}s clock-skew \
406 allowance (consumer clock '{}') (RFC-ACDP-0012 §9.3 step 4)",
407 self.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
408 max_clock_skew.num_seconds(),
409 now.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
410 )));
411 }
412 Ok(())
413 }
414
415 pub fn age_at(&self, now: DateTime<Utc>) -> chrono::Duration {
418 now - self.timestamp
419 }
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
435#[serde(deny_unknown_fields)]
436pub struct LogInclusion {
437 pub log_id: String,
440 pub leaf_index: u64,
443 pub tree_size: u64,
446 pub inclusion_path: Vec<String>,
450 pub log_checkpoint: LogCheckpoint,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub leaf: Option<LogLeaf>,
460}
461
462impl LogInclusion {
463 pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
469 let inclusion = Self::deserialize(value).map_err(|e| {
470 AcdpError::InvalidLogProof(format!("log_inclusion does not parse: {e}"))
471 })?;
472 if inclusion.tree_size < 1 {
473 return Err(AcdpError::InvalidLogProof(
474 "log_inclusion tree_size must be >= 1 (an empty tree has no members)".into(),
475 ));
476 }
477 if let Some(cp) = value.get("log_checkpoint") {
481 LogCheckpoint::from_value(cp)?;
482 }
483 if let Some(leaf) = value.get("leaf") {
484 LogLeaf::from_value(leaf)?;
485 }
486 Ok(inclusion)
487 }
488
489 pub fn cross_check_binding(&self) -> Result<(), AcdpError> {
493 if self.tree_size != self.log_checkpoint.tree_size {
494 return Err(AcdpError::InvalidLogProof(format!(
495 "log_inclusion tree_size {} ≠ log_checkpoint.tree_size {} \
496 (RFC-ACDP-0012 §9.1 step 4)",
497 self.tree_size, self.log_checkpoint.tree_size
498 )));
499 }
500 if self.log_id != self.log_checkpoint.log_id {
501 return Err(AcdpError::InvalidLogProof(format!(
502 "log_inclusion log_id '{}' ≠ log_checkpoint.log_id '{}' \
503 (RFC-ACDP-0012 §9.1 step 4)",
504 self.log_id, self.log_checkpoint.log_id
505 )));
506 }
507 if self.leaf_index >= self.tree_size {
508 return Err(AcdpError::InvalidLogProof(format!(
509 "log_inclusion leaf_index {} is not < tree_size {} (RFC-ACDP-0012 §9.1 step 4)",
510 self.leaf_index, self.tree_size
511 )));
512 }
513 Ok(())
514 }
515
516 pub fn verify_leaf_hash(&self, leaf_hash: &[u8; 32]) -> Result<(), AcdpError> {
524 self.cross_check_binding()?;
525 let path = self
526 .inclusion_path
527 .iter()
528 .map(|p| decode_sha256_hex(p))
529 .collect::<Result<Vec<_>, _>>()?;
530 let root = self.log_checkpoint.root_hash_bytes()?;
531 if !acdp_crypto::merkle::verify_inclusion(
532 leaf_hash,
533 self.leaf_index,
534 self.tree_size,
535 &path,
536 &root,
537 ) {
538 return Err(AcdpError::InvalidLogProof(format!(
539 "inclusion_path for leaf_index {} does not fold to the checkpoint root_hash \
540 '{}' at tree_size {} (RFC-ACDP-0012 §9.1 step 6)",
541 self.leaf_index, self.log_checkpoint.root_hash, self.tree_size
542 )));
543 }
544 Ok(())
545 }
546
547 pub fn verify_reconstructed_leaf(&self, leaf: &LogLeaf) -> Result<(), AcdpError> {
551 self.verify_leaf_hash(&leaf.leaf_hash()?)
552 }
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
568#[serde(deny_unknown_fields)]
569pub struct LogConsistencyProof {
570 pub log_id: String,
573 pub first_tree_size: u64,
575 pub second_tree_size: u64,
577 pub consistency_path: Vec<String>,
580 pub log_checkpoint: LogCheckpoint,
582}
583
584impl LogConsistencyProof {
585 pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
589 let proof = Self::deserialize(value).map_err(|e| {
590 AcdpError::InvalidLogProof(format!("consistency proof does not parse: {e}"))
591 })?;
592 if let Some(cp) = value.get("log_checkpoint") {
593 LogCheckpoint::from_value(cp)?;
594 }
595 Ok(proof)
596 }
597
598 pub fn verify_against_first_root(&self, first_root_hash: &str) -> Result<(), AcdpError> {
609 if self.log_id != self.log_checkpoint.log_id {
610 return Err(AcdpError::InvalidLogProof(format!(
611 "consistency proof log_id '{}' ≠ log_checkpoint.log_id '{}' \
612 (consistency proofs exist only within one log_id, RFC-ACDP-0012 §7.4)",
613 self.log_id, self.log_checkpoint.log_id
614 )));
615 }
616 if self.second_tree_size != self.log_checkpoint.tree_size {
617 return Err(AcdpError::InvalidLogProof(format!(
618 "consistency proof second_tree_size {} ≠ log_checkpoint.tree_size {} \
619 (RFC-ACDP-0012 §8.2)",
620 self.second_tree_size, self.log_checkpoint.tree_size
621 )));
622 }
623 let first_root = decode_sha256_hex(first_root_hash)?;
624 let second_root = self.log_checkpoint.root_hash_bytes()?;
625 let path = self
626 .consistency_path
627 .iter()
628 .map(|p| decode_sha256_hex(p))
629 .collect::<Result<Vec<_>, _>>()?;
630 if !acdp_crypto::merkle::verify_consistency(
631 self.first_tree_size,
632 self.second_tree_size,
633 &path,
634 &first_root,
635 &second_root,
636 ) {
637 return Err(AcdpError::InvalidLogProof(format!(
638 "consistency_path does not prove the tree at size {} is a prefix of the tree \
639 at size {} — evidence of a logged-history rewrite; retain both checkpoints \
640 and this path (RFC-ACDP-0012 §9.2)",
641 self.first_tree_size, self.second_tree_size
642 )));
643 }
644 Ok(())
645 }
646}
647
648impl ReceiptSigner {
651 pub fn mint_log_checkpoint(
665 &self,
666 log_id: &str,
667 tree_size: u64,
668 root_hash: &str,
669 timestamp: DateTime<Utc>,
670 ) -> Result<LogCheckpoint, AcdpError> {
671 let (did, _instance) = parse_log_id(log_id)?;
672 if did != self.registry_did() {
673 return Err(AcdpError::SchemaViolation(format!(
674 "cannot mint a checkpoint for log_id '{log_id}' under registry_did '{}' \
675 (RFC-ACDP-0012 §6: the log_id embeds the registry's own DID)",
676 self.registry_did()
677 )));
678 }
679 decode_sha256_hex(root_hash)?;
680 let mut checkpoint = LogCheckpoint {
681 checkpoint_version: LOG_CHECKPOINT_VERSION.to_string(),
682 log_id: log_id.to_string(),
683 tree_size,
684 root_hash: root_hash.to_string(),
685 timestamp: acdp_primitives::time::trunc_ms(timestamp),
686 signature: Signature {
687 algorithm: self.signing_key().algorithm().into(),
688 key_id: self.key_id().to_string(),
689 value: String::new(), },
691 };
692 let hash = checkpoint.preimage_hash()?;
693 let (algorithm, value) = self.signing_key().sign_content_hash(&hash);
694 checkpoint.signature.algorithm = algorithm.into();
695 checkpoint.signature.value = value;
696 Ok(checkpoint)
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use acdp_crypto::SigningKey;
704
705 const REGISTRY_DID: &str = "did:web:registry.example.com";
706 const LOG_ID: &str = "did:web:registry.example.com/log/1";
707
708 fn signer() -> ReceiptSigner {
709 ReceiptSigner::new(
710 SigningKey::from_bytes(&[0x11u8; 32]),
711 REGISTRY_DID,
712 format!("{REGISTRY_DID}#receipt-key-1"),
713 )
714 .unwrap()
715 }
716
717 fn registry_pub() -> [u8; 32] {
718 SigningKey::from_bytes(&[0x11u8; 32]).verifying_key_bytes()
719 }
720
721 fn leaf(i: u8) -> LogLeaf {
722 let ctx_id =
723 format!("acdp://registry.example.com/00000000-0000-4000-8000-0000000000{i:02}");
724 LogLeaf {
725 leaf_version: LOG_LEAF_VERSION.into(),
726 lineage_id: acdp_crypto::derive_lineage_id(&CtxId(ctx_id.clone())),
727 ctx_id: CtxId(ctx_id),
728 origin_registry: "registry.example.com".into(),
729 created_at: chrono::DateTime::parse_from_rfc3339("2026-07-01T01:00:00.123Z")
730 .unwrap()
731 .with_timezone(&Utc),
732 content_hash: ContentHash(format!("sha256:{}", "b".repeat(64))),
733 key_fingerprint: format!("sha256:{}", "c".repeat(64)),
734 receipt_hash: format!("sha256:{}", "d".repeat(64)),
735 }
736 }
737
738 #[test]
739 fn leaf_closed_schema_and_version_enforced() {
740 let l = leaf(1);
741 let wire = serde_json::to_value(&l).unwrap();
742 assert_eq!(LogLeaf::from_value(&wire).unwrap(), l);
743
744 let mut extra = wire.clone();
745 extra
746 .as_object_mut()
747 .unwrap()
748 .insert("note".into(), serde_json::json!("x"));
749 assert!(matches!(
750 LogLeaf::from_value(&extra).unwrap_err(),
751 AcdpError::InvalidLogProof(_)
752 ));
753
754 let mut wrong = wire.clone();
755 wrong["leaf_version"] = serde_json::json!("acdp-log-leaf/2");
756 assert!(LogLeaf::from_value(&wrong).is_err());
757
758 let mut bad_ts = wire.clone();
759 bad_ts["created_at"] = serde_json::json!("2026-07-01T01:00:00Z");
760 assert!(LogLeaf::from_value(&bad_ts).is_err());
761 }
762
763 #[test]
764 fn checkpoint_mint_verify_round_trip() {
765 let root = encode_sha256_hex(&acdp_crypto::merkle_tree_hash(&[]));
766 let cp = signer()
767 .mint_log_checkpoint(LOG_ID, 0, &root, Utc::now())
768 .unwrap();
769 cp.verify_signature_with_key(Some(®istry_pub()), None)
770 .expect("freshly minted checkpoint must verify");
771 let wire = serde_json::to_value(&cp).unwrap();
772 let parsed = LogCheckpoint::from_value(&wire).unwrap();
773 let raw_hash = LogCheckpoint::preimage_hash_of_value(&wire).unwrap();
774 assert_eq!(raw_hash, cp.preimage_hash().unwrap());
775 parsed
776 .verify_signature_against_hash(&raw_hash, Some(®istry_pub()), None)
777 .unwrap();
778 parsed
779 .cross_check_registry_binding("registry.example.com", REGISTRY_DID)
780 .unwrap();
781 assert!(parsed
782 .cross_check_registry_binding("hostile.example", "did:web:hostile.example")
783 .is_err());
784 parsed
785 .check_timestamp_skew(Utc::now(), chrono::Duration::seconds(120))
786 .unwrap();
787
788 let mut tampered = wire.clone();
790 tampered["root_hash"] = serde_json::json!(format!("sha256:{}", "f".repeat(64)));
791 let t = LogCheckpoint::from_value(&tampered).unwrap();
792 let t_hash = LogCheckpoint::preimage_hash_of_value(&tampered).unwrap();
793 let err = t
794 .verify_signature_against_hash(&t_hash, Some(®istry_pub()), None)
795 .unwrap_err();
796 assert!(matches!(err, AcdpError::InvalidLogProof(_)), "got {err:?}");
797 }
798
799 #[test]
800 fn checkpoint_closed_schema_and_domain_separation() {
801 let root = encode_sha256_hex(&acdp_crypto::merkle_tree_hash(&[]));
802 let cp = signer()
803 .mint_log_checkpoint(LOG_ID, 0, &root, Utc::now())
804 .unwrap();
805 let mut wire = serde_json::to_value(&cp).unwrap();
806 wire.as_object_mut()
807 .unwrap()
808 .insert("witnesses".into(), serde_json::json!([]));
809 assert!(matches!(
810 LogCheckpoint::from_value(&wire).unwrap_err(),
811 AcdpError::InvalidLogProof(_)
812 ));
813
814 let mut wire = serde_json::to_value(&cp).unwrap();
815 wire["checkpoint_version"] = serde_json::json!("acdp-lhr/1");
816 assert!(LogCheckpoint::from_value(&wire).is_err());
817
818 let wire = serde_json::to_value(&cp).unwrap();
820 assert!(crate::receipt::LineageHeadReceipt::from_value(&wire).is_err());
821
822 assert!(signer()
824 .mint_log_checkpoint("did:web:evil.example.com/log/1", 0, &root, Utc::now())
825 .is_err());
826 assert!(signer()
827 .mint_log_checkpoint(
828 "did:web:registry.example.com/log/UPPER",
829 0,
830 &root,
831 Utc::now()
832 )
833 .is_err());
834 assert!(signer()
835 .mint_log_checkpoint(LOG_ID, 0, "sha256:short", Utc::now())
836 .is_err());
837 }
838
839 #[test]
842 fn inclusion_and_consistency_round_trip() {
843 let leaves: Vec<LogLeaf> = (0..5).map(leaf).collect();
844 let hashes: Vec<[u8; 32]> = leaves.iter().map(|l| l.leaf_hash().unwrap()).collect();
845 let root = acdp_crypto::merkle_tree_hash(&hashes);
846 let cp = signer()
847 .mint_log_checkpoint(LOG_ID, 5, &encode_sha256_hex(&root), Utc::now())
848 .unwrap();
849
850 for (i, l) in leaves.iter().enumerate() {
851 let path = acdp_crypto::inclusion_path(i, &hashes).unwrap();
852 let inclusion = LogInclusion {
853 log_id: LOG_ID.into(),
854 leaf_index: i as u64,
855 tree_size: 5,
856 inclusion_path: path.iter().map(encode_sha256_hex).collect(),
857 log_checkpoint: cp.clone(),
858 leaf: None,
859 };
860 inclusion.verify_reconstructed_leaf(l).unwrap();
861 let wire = serde_json::to_value(&inclusion).unwrap();
863 assert!(wire.get("leaf").is_none(), "absent, never null");
864 LogInclusion::from_value(&wire)
865 .unwrap()
866 .verify_leaf_hash(&hashes[i])
867 .unwrap();
868
869 if !inclusion.inclusion_path.is_empty() {
871 let mut bad = inclusion.clone();
872 let mut raw = decode_sha256_hex(&bad.inclusion_path[0]).unwrap();
873 raw[0] ^= 1;
874 bad.inclusion_path[0] = encode_sha256_hex(&raw);
875 let err = bad.verify_reconstructed_leaf(l).unwrap_err();
876 assert!(matches!(err, AcdpError::InvalidLogProof(_)), "got {err:?}");
877 }
878 }
879
880 let path = acdp_crypto::inclusion_path(0, &hashes).unwrap();
882 let mk = |leaf_index, tree_size, log_id: &str| LogInclusion {
883 log_id: log_id.into(),
884 leaf_index,
885 tree_size,
886 inclusion_path: path.iter().map(encode_sha256_hex).collect(),
887 log_checkpoint: cp.clone(),
888 leaf: None,
889 };
890 assert!(mk(0, 4, LOG_ID).verify_leaf_hash(&hashes[0]).is_err()); assert!(mk(5, 5, LOG_ID).verify_leaf_hash(&hashes[0]).is_err()); assert!(mk(0, 5, "did:web:registry.example.com/log/2")
893 .verify_leaf_hash(&hashes[0])
894 .is_err()); let first_root = acdp_crypto::merkle_tree_hash(&hashes[..3]);
898 let proof = LogConsistencyProof {
899 log_id: LOG_ID.into(),
900 first_tree_size: 3,
901 second_tree_size: 5,
902 consistency_path: acdp_crypto::consistency_proof(3, &hashes)
903 .unwrap()
904 .iter()
905 .map(encode_sha256_hex)
906 .collect(),
907 log_checkpoint: cp.clone(),
908 };
909 proof
910 .verify_against_first_root(&encode_sha256_hex(&first_root))
911 .unwrap();
912 let err = proof
914 .verify_against_first_root(&format!("sha256:{}", "e".repeat(64)))
915 .unwrap_err();
916 assert!(matches!(err, AcdpError::InvalidLogProof(_)), "got {err:?}");
917 }
918
919 #[test]
920 fn log_id_parsing() {
921 assert_eq!(
922 parse_log_id(LOG_ID).unwrap(),
923 ("did:web:registry.example.com", "1")
924 );
925 for bad in [
926 "did:web:registry.example.com", "did:key:z6Mk/log/1", "did:web:registry.example.com/log/", "did:web:registry.example.com/log/UP", &format!("did:web:r.example/log/{}", "a".repeat(33)), ] {
932 assert!(parse_log_id(bad).is_err(), "{bad} must be rejected");
933 }
934 }
935}