1use acdp_crypto::hash::{compute_content_hash, derive_lineage_id};
6use acdp_primitives::error::AcdpError;
7use acdp_types::{
8 body::Body,
9 capabilities::CapabilitiesDocument,
10 primitives::{AgentDid, ContentHash, ContextType, CtxId, LineageId},
11 publish::PublishRequest,
12 revocation::KeyRevocation,
13};
14
15#[derive(Debug)]
18pub struct ValidatedPublish {
19 pub recomputed_hash: ContentHash,
21}
22
23pub struct PublishValidator<'a> {
29 caps: &'a CapabilitiesDocument,
30 own_authority: Option<&'a str>,
31}
32
33impl<'a> PublishValidator<'a> {
34 pub fn new(caps: &'a CapabilitiesDocument) -> Self {
36 Self {
37 caps,
38 own_authority: None,
39 }
40 }
41
42 pub fn for_authority(caps: &'a CapabilitiesDocument, own_authority: &'a str) -> Self {
50 Self {
51 caps,
52 own_authority: Some(own_authority),
53 }
54 }
55
56 pub fn validate_post_schema(
75 &self,
76 req: &PublishRequest,
77 raw_body_bytes: usize,
78 ) -> Result<ValidatedPublish, AcdpError> {
79 acdp_validation::validate_publish_request(req)?;
88 self.validate_registry_limits_and_crypto(req, raw_body_bytes)
89 }
90
91 #[deprecated(
100 since = "0.1.0",
101 note = "Use validate_post_schema; this alias no longer skips runtime validation"
102 )]
103 pub fn validate_structural(
104 &self,
105 req: &PublishRequest,
106 raw_body_bytes: usize,
107 ) -> Result<ValidatedPublish, AcdpError> {
108 self.validate_post_schema(req, raw_body_bytes)
109 }
110
111 fn validate_registry_limits_and_crypto(
115 &self,
116 req: &PublishRequest,
117 raw_body_bytes: usize,
118 ) -> Result<ValidatedPublish, AcdpError> {
119 if raw_body_bytes as u64 > self.caps.limits.max_payload_bytes {
121 return Err(AcdpError::SchemaViolation(format!(
122 "payload {} bytes exceeds limit {}",
123 raw_body_bytes, self.caps.limits.max_payload_bytes
124 )));
125 }
126
127 for dr in &req.data_refs {
130 if let Some(emb) = &dr.embedded {
131 let decoded = acdp_validation::embedded_decoded_bytes(emb)?;
132 if decoded.len() as u64 > self.caps.limits.max_embedded_bytes {
133 return Err(AcdpError::EmbeddedTooLarge(format!(
134 "embedded data reference {} bytes exceeds {} limit",
135 decoded.len(),
136 self.caps.limits.max_embedded_bytes
137 )));
138 }
139 acdp_validation::verify_embedded_hash(dr)?;
142 }
143 }
144
145 let body_val = serde_json::to_value(req)?;
147 let recomputed = compute_content_hash(&body_val)?;
148 if recomputed != req.content_hash {
149 return Err(AcdpError::HashMismatch {
150 stored: req.content_hash.clone(),
151 recomputed: recomputed.clone(),
152 });
153 }
154
155 if !self
157 .caps
158 .supported_signature_algorithms
159 .iter()
160 .any(|a| a == &req.signature.algorithm)
161 {
162 return Err(AcdpError::SchemaViolation(format!(
163 "unsupported algorithm '{}'; registry supports {:?}",
164 req.signature.algorithm, self.caps.supported_signature_algorithms,
165 )));
166 }
167
168 let agent_method = req
175 .agent_id
176 .as_str()
177 .splitn(3, ':')
178 .take(2)
179 .collect::<Vec<_>>()
180 .join(":");
181 if !self.caps.supports_did_method(&agent_method) {
182 return Err(AcdpError::KeyResolution(format!(
183 "agent_id method '{agent_method}' is not in this registry's \
184 supported_did_methods {:?}",
185 self.caps.supported_did_methods
186 )));
187 }
188
189 let key_id = &req.signature.key_id;
191 let did_part = key_id.split_once('#').map(|(d, _)| d).ok_or_else(|| {
192 AcdpError::KeyResolution(format!("key_id '{key_id}' has no '#fragment'"))
193 })?;
194
195 if did_part != req.agent_id.as_str() {
196 return Err(AcdpError::KeyNotAuthorized(format!(
197 "key_id DID '{did_part}' ≠ agent_id '{}'",
198 req.agent_id
199 )));
200 }
201
202 if let (Some(own), Some(target)) = (self.own_authority, &req.supersedes) {
204 let target_authority = target.authority();
205 if target_authority != own {
206 return Err(AcdpError::SupersededTarget {
207 reason: acdp_primitives::error::SupersessionReason::CrossRegistrySupersessionUnsupported,
208 message: format!(
209 "supersedes target on '{target_authority}' rejected by '{own}'; \
210 v0.1.0 only allows same-registry supersession"
211 ),
212 });
213 }
214 }
215
216 if is_interim_key_revocation_form(&req.context_type)
226 && key_revocation_retirement_gate_applies(&self.caps.acdp_version)
227 {
228 return Err(AcdpError::SchemaViolation(format!(
229 "context_type '{}' (the interim key-revocation form) is retired for \
230 registries advertising acdp_version >= 0.5.0 (RFC-ACDP-0014 §10); \
231 publish using the standard 'key-revocation' context_type instead",
232 ContextType::KEY_REVOCATION_INTERIM
233 )));
234 }
235
236 if is_interim_key_revocation_form(&req.context_type)
252 && key_revocation_gate_applies(&self.caps.acdp_version)
253 {
254 KeyRevocation::check_not_self_signed_did_key_lenient(req)?;
255 self.check_revocation_controller_lenient(req)?;
256 }
257
258 if matches!(req.context_type, ContextType::KeyRevocation)
273 && key_revocation_gate_applies(&self.caps.acdp_version)
274 {
275 let revocation = KeyRevocation::from_publish_request(req)?;
276 self.check_revocation_controller(req, &revocation)?;
277 }
278
279 Ok(ValidatedPublish {
282 recomputed_hash: recomputed,
283 })
284 }
285
286 fn check_revocation_controller(
311 &self,
312 req: &PublishRequest,
313 revocation: &KeyRevocation,
314 ) -> Result<(), AcdpError> {
315 let controller_present = req
316 .metadata
317 .as_ref()
318 .and_then(|m| m.as_object())
319 .is_some_and(|m| m.contains_key("revoked_key_controller"));
320
321 let agent_is_registry = req.agent_id.as_str() == self.caps.registry_did;
322 let controller_differs = revocation.revoked_key_controller != req.agent_id;
323
324 if controller_present && controller_differs && !agent_is_registry {
325 return Err(AcdpError::SchemaViolation(format!(
327 "metadata.revoked_key_controller '{}' differs from agent_id '{}', but \
328 agent_id is not this registry's own DID ('{}'); a controller different \
329 from agent_id is only valid on a §6 registry-attested revocation \
330 (RFC-ACDP-0014 §4, §6)",
331 revocation.revoked_key_controller, req.agent_id, self.caps.registry_did
332 )));
333 }
334
335 if !controller_present && agent_is_registry {
336 return Err(AcdpError::SchemaViolation(format!(
338 "key-revocation published under this registry's own DID ('{}') has no \
339 metadata.revoked_key_controller; a registry-attested revocation MUST \
340 name the affected producer's DID as the controller (RFC-ACDP-0014 §4, §6)",
341 self.caps.registry_did
342 )));
343 }
344
345 Ok(())
346 }
347
348 fn check_revocation_controller_lenient(&self, req: &PublishRequest) -> Result<(), AcdpError> {
363 let controller_raw = req
364 .metadata
365 .as_ref()
366 .and_then(|m| m.as_object())
367 .and_then(|m| m.get("revoked_key_controller"))
368 .and_then(|v| v.as_str());
369
370 let agent_is_registry = req.agent_id.as_str() == self.caps.registry_did;
371
372 match controller_raw {
373 Some(raw) => {
374 let Ok(controller) = AgentDid::parse(raw) else {
375 return Ok(());
376 };
377 if controller != req.agent_id && !agent_is_registry {
378 return Err(AcdpError::SchemaViolation(format!(
380 "metadata.revoked_key_controller '{raw}' differs from agent_id '{}', \
381 but agent_id is not this registry's own DID ('{}'); a controller \
382 different from agent_id is only valid on a §6 registry-attested \
383 revocation (RFC-ACDP-0014 §5, §6)",
384 req.agent_id, self.caps.registry_did
385 )));
386 }
387 Ok(())
388 }
389 None => {
390 if agent_is_registry {
391 return Err(AcdpError::SchemaViolation(format!(
393 "key-revocation published under this registry's own DID ('{}') has \
394 no metadata.revoked_key_controller; a registry-attested revocation \
395 MUST name the affected producer's DID as the controller \
396 (RFC-ACDP-0014 §6)",
397 self.caps.registry_did
398 )));
399 }
400 Ok(())
401 }
402 }
403 }
404}
405
406fn is_well_formed_version(v: &str) -> bool {
413 let parts: Vec<&str> = v.split('.').collect();
414 parts.len() == 3
415 && parts
416 .iter()
417 .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
418}
419
420fn parse_major_minor_saturating(v: &str) -> (u64, u64) {
436 let mut parts = v.split('.');
437 let major = parts.next().unwrap_or("0").parse().unwrap_or(u64::MAX);
438 let minor = parts.next().unwrap_or("0").parse().unwrap_or(u64::MAX);
439 (major, minor)
440}
441
442pub fn key_revocation_gate_applies(acdp_version: &str) -> bool {
465 if !is_well_formed_version(acdp_version) {
466 return true;
467 }
468 let (major, minor) = parse_major_minor_saturating(acdp_version);
469 major > 0 || minor >= 3
470}
471
472fn is_interim_key_revocation_form(ct: &ContextType) -> bool {
477 matches!(ct, ContextType::Custom(s) if s == ContextType::KEY_REVOCATION_INTERIM)
478}
479
480fn key_revocation_retirement_gate_applies(acdp_version: &str) -> bool {
489 if !is_well_formed_version(acdp_version) {
490 return true;
491 }
492 let (major, minor) = parse_major_minor_saturating(acdp_version);
493 major > 0 || minor >= 5
494}
495
496fn advertises_0_5_0_or_higher(acdp_version: &str) -> bool {
513 if !is_well_formed_version(acdp_version) {
514 return false;
515 }
516 let (major, minor) = parse_major_minor_saturating(acdp_version);
517 major > 0 || minor >= 5
518}
519
520pub fn check_revocation_supersession(
611 prev: &Body,
612 req: &PublishRequest,
613 acdp_version: &str,
614) -> Result<(), AcdpError> {
615 if !prev.context_type.is_key_revocation() {
616 return Ok(());
619 }
620
621 if !req.context_type.is_key_revocation() {
622 if advertises_0_5_0_or_higher(acdp_version) {
637 return Err(AcdpError::SupersededTarget {
638 reason: acdp_primitives::error::SupersessionReason::RevocationTypeMismatch,
639 message: format!(
640 "ctx_id '{}' is a key-revocation context and MAY only be superseded by \
641 another key-revocation context (RFC-ACDP-0014 §4); the incoming publish \
642 from agent_id '{}' has type '{}'",
643 prev.ctx_id,
644 req.agent_id,
645 context_type_label(&req.context_type),
646 ),
647 });
648 }
649 return Err(AcdpError::SchemaViolation(format!(
650 "ctx_id '{}' is a key-revocation context and MAY only be superseded by \
651 another key-revocation context (RFC-ACDP-0014 §4); the incoming publish \
652 from agent_id '{}' has type '{}'",
653 prev.ctx_id,
654 req.agent_id,
655 context_type_label(&req.context_type),
656 )));
657 }
658
659 let prev_revocation = match KeyRevocation::from_body(prev) {
662 Ok(r) => r,
663 Err(_) => {
664 return Ok(());
682 }
683 };
684
685 let incoming_revocation = KeyRevocation::from_publish_request(req)?;
686
687 if prev_revocation.trust_class == incoming_revocation.trust_class {
688 Ok(())
691 } else {
692 Err(AcdpError::SchemaViolation(format!(
694 "ctx_id '{}' is a key-revocation with signer class {:?}; the incoming \
695 supersession from agent_id '{}' is a key-revocation with signer class {:?} \
696 — a revocation MAY only be superseded by another key-revocation from the \
697 same signer class (RFC-ACDP-0014 §4)",
698 prev.ctx_id, prev_revocation.trust_class, req.agent_id, incoming_revocation.trust_class,
699 )))
700 }
701}
702
703fn context_type_label(context_type: &acdp_types::primitives::ContextType) -> String {
707 serde_json::to_value(context_type)
708 .ok()
709 .and_then(|v| v.as_str().map(str::to_owned))
710 .unwrap_or_else(|| "<unrepresentable>".into())
711}
712
713pub fn assign_identifiers(
726 authority: &str,
727 supersedes: &Option<CtxId>,
728 first_version_ctx_id: Option<&CtxId>,
729 _validated: &ValidatedPublish,
730) -> Result<(CtxId, LineageId), AcdpError> {
731 let uuid = uuid::Uuid::new_v4();
732 let ctx_id = CtxId(format!("acdp://{authority}/{uuid}"));
733 let lineage_source: &CtxId = match (supersedes, first_version_ctx_id) {
734 (None, _) => &ctx_id,
735 (Some(_), Some(v1)) => v1,
736 (Some(_), None) => {
737 return Err(AcdpError::SchemaViolation(
738 "supersession assignment requires the v1 ctx_id to derive lineage_id".into(),
739 ));
740 }
741 };
742 let lineage_id = derive_lineage_id(lineage_source);
743 Ok((ctx_id, lineage_id))
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749 use acdp_crypto::SigningKey;
750 use acdp_producer::Producer;
751 use acdp_types::{
752 capabilities::Limits, primitives::Visibility, revocation::RevocationTrustClass,
753 };
754
755 fn test_caps() -> CapabilitiesDocument {
756 CapabilitiesDocument {
757 acdp_version: "0.1.0".into(),
758 registry_did: "did:web:registry.example.com".into(),
759 supported_signature_algorithms: vec!["ed25519".into()],
760 supported_did_methods: vec!["did:web".into()],
761 profiles: vec!["acdp-registry-core".into()],
762 limits: Limits {
763 max_payload_bytes: 1_048_576,
764 max_embedded_bytes: 65_536,
765 idempotency_key_ttl_seconds: None,
766 max_publish_per_minute: None,
767 },
768 read_authentication_methods: vec![],
769 anonymous_public_reads: true,
770 supports_idempotency_key: false,
771 extensions: Default::default(),
772 }
773 }
774
775 fn test_request() -> PublishRequest {
776 let key = SigningKey::from_bytes(&[0u8; 32]);
777 let p = Producer::new(
778 key,
779 AgentDid::new("did:web:agents.example.com:test-producer"),
780 "did:web:agents.example.com:test-producer#key-1",
781 );
782 p.publish_request()
783 .title("Golden test vector — minimal first version")
784 .context_type(ContextType::DataSnapshot)
785 .visibility(Visibility::Public)
786 .build()
787 .unwrap()
788 }
789
790 #[test]
791 fn happy_path_validates() {
792 let caps = test_caps();
793 let v = PublishValidator::new(&caps);
794 let req = test_request();
795 let raw_len = serde_json::to_vec(&req).unwrap().len();
796 v.validate_post_schema(&req, raw_len).unwrap();
797 }
798
799 #[test]
800 fn payload_too_large_rejected() {
801 let mut caps = test_caps();
802 caps.limits.max_payload_bytes = 10;
803 let v = PublishValidator::new(&caps);
804 let req = test_request();
805 let err = v.validate_post_schema(&req, 1024).unwrap_err();
806 assert!(matches!(err, AcdpError::SchemaViolation(_)));
807 }
808
809 #[test]
810 fn unsupported_algorithm_rejected() {
811 let mut caps = test_caps();
812 caps.supported_signature_algorithms = vec!["secp256k1".into()];
813 let v = PublishValidator::new(&caps);
814 let req = test_request();
815 let err = v.validate_post_schema(&req, 1024).unwrap_err();
816 assert!(matches!(err, AcdpError::SchemaViolation(_)));
817 }
818
819 #[test]
820 fn key_id_without_fragment_rejected() {
821 let caps = test_caps();
822 let v = PublishValidator::new(&caps);
823 let mut req = test_request();
824 req.signature.key_id = "did:web:agents.example.com:test-producer".into();
825 let err = v.validate_post_schema(&req, 1024).unwrap_err();
826 assert!(matches!(err, AcdpError::KeyResolution(_)));
827 }
828
829 #[test]
830 fn key_id_did_must_match_agent_id() {
831 let caps = test_caps();
832 let v = PublishValidator::new(&caps);
833 let mut req = test_request();
834 req.signature.key_id = "did:web:other.example.com:attacker#key-1".into();
835 let err = v.validate_post_schema(&req, 1024).unwrap_err();
836 assert!(matches!(err, AcdpError::KeyNotAuthorized(_)));
837 }
838
839 #[test]
840 fn tampered_hash_detected() {
841 let caps = test_caps();
842 let v = PublishValidator::new(&caps);
843 let mut req = test_request();
844 req.title = "tampered title".into();
845 let err = v.validate_post_schema(&req, 1024).unwrap_err();
846 assert!(matches!(err, AcdpError::HashMismatch { .. }));
847 }
848
849 #[test]
850 fn assign_identifiers_first_version_derives_lineage_from_new_id() {
851 let v = ValidatedPublish {
852 recomputed_hash: ContentHash("sha256:abcd".into()),
853 };
854 let (ctx_id, lineage_id) =
855 assign_identifiers("registry.example.com", &None, None, &v).unwrap();
856 let expected = derive_lineage_id(&ctx_id);
857 assert_eq!(lineage_id, expected);
858 }
859
860 #[test]
861 fn assign_identifiers_supersession_uses_v1_ctx_id() {
862 let v = ValidatedPublish {
863 recomputed_hash: ContentHash("sha256:abcd".into()),
864 };
865 let v1 = CtxId("acdp://registry.example.com/12345678-1234-4321-8123-123456781234".into());
866 let supersedes = Some(CtxId(
867 "acdp://registry.example.com/12345678-1234-4321-8123-123456781299".into(),
868 ));
869 let (_new_id, lineage_id) =
870 assign_identifiers("registry.example.com", &supersedes, Some(&v1), &v).unwrap();
871 assert_eq!(lineage_id, derive_lineage_id(&v1));
872 }
873
874 #[test]
875 fn cross_registry_supersession_rejected() {
876 let caps = test_caps();
877 let v = PublishValidator::for_authority(&caps, "registry.example.com");
878 let key = SigningKey::from_bytes(&[0u8; 32]);
880 let p = Producer::new(
881 key,
882 AgentDid::new("did:web:agents.example.com:test-producer"),
883 "did:web:agents.example.com:test-producer#key-1",
884 );
885 let other_reg =
886 CtxId("acdp://other.example.com/12345678-1234-4321-8123-123456781234".into());
887 let req = p
888 .supersede(other_reg)
889 .version(2)
890 .title("v2")
891 .context_type(ContextType::DataSnapshot)
892 .build()
893 .unwrap();
894 let raw_len = serde_json::to_vec(&req).unwrap().len();
895 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
896 match err {
897 AcdpError::SupersededTarget { reason, .. } => {
898 assert_eq!(
899 reason,
900 acdp_primitives::error::SupersessionReason::CrossRegistrySupersessionUnsupported
901 );
902 }
903 other => panic!("expected SupersededTarget, got {other:?}"),
904 }
905 }
906
907 #[test]
908 fn same_registry_supersession_passes_authority_check() {
909 let caps = test_caps();
910 let v = PublishValidator::for_authority(&caps, "registry.example.com");
911 let key = SigningKey::from_bytes(&[0u8; 32]);
912 let p = Producer::new(
913 key,
914 AgentDid::new("did:web:agents.example.com:test-producer"),
915 "did:web:agents.example.com:test-producer#key-1",
916 );
917 let same = CtxId("acdp://registry.example.com/12345678-1234-4321-8123-123456781234".into());
918 let req = p
919 .supersede(same)
920 .version(2)
921 .title("v2")
922 .context_type(ContextType::DataSnapshot)
923 .build()
924 .unwrap();
925 let raw_len = serde_json::to_vec(&req).unwrap().len();
926 v.validate_post_schema(&req, raw_len).unwrap();
927 }
928
929 #[test]
930 fn assign_identifiers_supersession_without_v1_id_rejected() {
931 let v = ValidatedPublish {
932 recomputed_hash: ContentHash("sha256:abcd".into()),
933 };
934 let supersedes = Some(CtxId("acdp://x/y".into()));
935 let err = assign_identifiers("registry.example.com", &supersedes, None, &v).unwrap_err();
936 assert!(matches!(err, AcdpError::SchemaViolation(_)));
937 }
938
939 fn test_caps_v030() -> CapabilitiesDocument {
942 CapabilitiesDocument {
943 acdp_version: "0.3.0".into(),
944 ..test_caps()
945 }
946 }
947
948 fn test_caps_v020() -> CapabilitiesDocument {
949 CapabilitiesDocument {
950 acdp_version: "0.2.0".into(),
951 ..test_caps()
952 }
953 }
954
955 fn test_caps_v050() -> CapabilitiesDocument {
956 CapabilitiesDocument {
957 acdp_version: "0.5.0".into(),
958 ..test_caps()
959 }
960 }
961
962 const REVOCATION_PRODUCER_DID: &str = "did:web:agents.example.com:test-producer";
963 const REVOCATION_OTHER_PRODUCER_DID: &str = "did:web:agents.example.com:other-producer";
964 const REVOCATION_FP: &str =
965 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
966 const REVOCATION_SINCE: &str = "2026-05-01T00:00:00.000Z";
967
968 fn valid_revocation_metadata() -> serde_json::Value {
969 serde_json::json!({
970 "revoked_key_fingerprint": REVOCATION_FP,
971 "compromised_since": REVOCATION_SINCE,
972 })
973 }
974
975 fn build_revocation_request(
980 agent_did: &str,
981 metadata: serde_json::Value,
982 acdp_version: &str,
983 ) -> PublishRequest {
984 let key = SigningKey::from_bytes(&[0u8; 32]);
985 let p = Producer::new(key, AgentDid::new(agent_did), format!("{agent_did}#key-1"));
986 p.publish_request()
987 .title("Key revocation test")
988 .context_type(ContextType::KeyRevocation)
989 .visibility(Visibility::Public)
990 .acdp_version(acdp_version)
991 .metadata(metadata)
992 .build()
993 .unwrap()
994 }
995
996 #[test]
999 fn revocation_arm1_absent_controller_accepted_at_0_3_0() {
1000 let caps = test_caps_v030();
1001 let v = PublishValidator::new(&caps);
1002 let req = build_revocation_request(
1003 REVOCATION_PRODUCER_DID,
1004 valid_revocation_metadata(),
1005 "0.3.0",
1006 );
1007 let raw_len = serde_json::to_vec(&req).unwrap().len();
1008 v.validate_post_schema(&req, raw_len).unwrap();
1009 }
1010
1011 #[test]
1014 fn revocation_arm2_explicit_matching_controller_accepted_at_0_3_0() {
1015 let caps = test_caps_v030();
1016 let v = PublishValidator::new(&caps);
1017 let mut meta = valid_revocation_metadata();
1018 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_PRODUCER_DID);
1019 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1020 let raw_len = serde_json::to_vec(&req).unwrap().len();
1021 v.validate_post_schema(&req, raw_len).unwrap();
1022 }
1023
1024 #[test]
1027 fn revocation_arm3_registry_attested_accepted_at_0_3_0() {
1028 let caps = test_caps_v030();
1029 let registry_did = caps.registry_did.clone();
1030 let v = PublishValidator::new(&caps);
1031 let mut meta = valid_revocation_metadata();
1032 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_PRODUCER_DID);
1033 let req = build_revocation_request(®istry_did, meta, "0.3.0");
1034 let raw_len = serde_json::to_vec(&req).unwrap().len();
1035 v.validate_post_schema(&req, raw_len).unwrap();
1036 }
1037
1038 #[test]
1041 fn revocation_arm4_mismatched_controller_rejected_at_0_3_0() {
1042 let caps = test_caps_v030();
1043 let v = PublishValidator::new(&caps);
1044 let mut meta = valid_revocation_metadata();
1045 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_OTHER_PRODUCER_DID);
1046 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1047 let raw_len = serde_json::to_vec(&req).unwrap().len();
1048 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1049 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1050 }
1051
1052 #[test]
1053 fn revocation_arm4_accepted_at_0_2_0_positive_control() {
1054 let caps = test_caps_v020();
1055 let v = PublishValidator::new(&caps);
1056 let mut meta = valid_revocation_metadata();
1057 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_OTHER_PRODUCER_DID);
1058 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1059 let raw_len = serde_json::to_vec(&req).unwrap().len();
1060 v.validate_post_schema(&req, raw_len).unwrap();
1061 }
1062
1063 #[test]
1070 fn revocation_arm5_absent_controller_under_registry_did_rejected_at_0_3_0() {
1071 let caps = test_caps_v030();
1072 let registry_did = caps.registry_did.clone();
1073 let v = PublishValidator::new(&caps);
1074 let req = build_revocation_request(®istry_did, valid_revocation_metadata(), "0.3.0");
1075 let raw_len = serde_json::to_vec(&req).unwrap().len();
1076 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1077 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1078 }
1079
1080 #[test]
1081 fn revocation_arm5_accepted_at_0_2_0_positive_control() {
1082 let caps = test_caps_v020();
1083 let registry_did = caps.registry_did.clone();
1084 let v = PublishValidator::new(&caps);
1085 let req = build_revocation_request(®istry_did, valid_revocation_metadata(), "0.3.0");
1086 let raw_len = serde_json::to_vec(&req).unwrap().len();
1087 v.validate_post_schema(&req, raw_len).unwrap();
1088 }
1089
1090 #[test]
1091 fn revocation_non_public_visibility_rejected_at_0_3_0() {
1092 let caps = test_caps_v030();
1093 let v = PublishValidator::new(&caps);
1094 let key = SigningKey::from_bytes(&[0u8; 32]);
1095 let p = Producer::new(
1096 key,
1097 AgentDid::new(REVOCATION_PRODUCER_DID),
1098 format!("{REVOCATION_PRODUCER_DID}#key-1"),
1099 );
1100 let req = p
1101 .publish_request()
1102 .title("Key revocation test")
1103 .context_type(ContextType::KeyRevocation)
1104 .visibility(Visibility::Restricted)
1105 .audience(vec![AgentDid::new(REVOCATION_PRODUCER_DID)])
1106 .acdp_version("0.3.0")
1107 .metadata(valid_revocation_metadata())
1108 .build()
1109 .unwrap();
1110 let raw_len = serde_json::to_vec(&req).unwrap().len();
1111 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1112 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1113 }
1114
1115 #[test]
1116 fn revocation_non_public_visibility_accepted_at_0_2_0_positive_control() {
1117 let caps = test_caps_v020();
1118 let v = PublishValidator::new(&caps);
1119 let key = SigningKey::from_bytes(&[0u8; 32]);
1120 let p = Producer::new(
1121 key,
1122 AgentDid::new(REVOCATION_PRODUCER_DID),
1123 format!("{REVOCATION_PRODUCER_DID}#key-1"),
1124 );
1125 let req = p
1126 .publish_request()
1127 .title("Key revocation test")
1128 .context_type(ContextType::KeyRevocation)
1129 .visibility(Visibility::Restricted)
1130 .audience(vec![AgentDid::new(REVOCATION_PRODUCER_DID)])
1131 .acdp_version("0.3.0")
1132 .metadata(valid_revocation_metadata())
1133 .build()
1134 .unwrap();
1135 let raw_len = serde_json::to_vec(&req).unwrap().len();
1136 v.validate_post_schema(&req, raw_len).unwrap();
1137 }
1138
1139 #[test]
1140 fn revocation_missing_fingerprint_rejected_at_0_3_0() {
1141 let caps = test_caps_v030();
1142 let v = PublishValidator::new(&caps);
1143 let mut meta = valid_revocation_metadata();
1144 meta.as_object_mut()
1145 .unwrap()
1146 .remove("revoked_key_fingerprint");
1147 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1148 let raw_len = serde_json::to_vec(&req).unwrap().len();
1149 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1150 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1151 }
1152
1153 #[test]
1154 fn revocation_missing_fingerprint_accepted_at_0_2_0_positive_control() {
1155 let caps = test_caps_v020();
1156 let v = PublishValidator::new(&caps);
1157 let mut meta = valid_revocation_metadata();
1158 meta.as_object_mut()
1159 .unwrap()
1160 .remove("revoked_key_fingerprint");
1161 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1162 let raw_len = serde_json::to_vec(&req).unwrap().len();
1163 v.validate_post_schema(&req, raw_len).unwrap();
1164 }
1165
1166 #[test]
1167 fn revocation_missing_compromised_since_rejected_at_0_3_0() {
1168 let caps = test_caps_v030();
1169 let v = PublishValidator::new(&caps);
1170 let mut meta = valid_revocation_metadata();
1171 meta.as_object_mut().unwrap().remove("compromised_since");
1172 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1173 let raw_len = serde_json::to_vec(&req).unwrap().len();
1174 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1175 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1176 }
1177
1178 #[test]
1179 fn revocation_missing_compromised_since_accepted_at_0_2_0_positive_control() {
1180 let caps = test_caps_v020();
1181 let v = PublishValidator::new(&caps);
1182 let mut meta = valid_revocation_metadata();
1183 meta.as_object_mut().unwrap().remove("compromised_since");
1184 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1185 let raw_len = serde_json::to_vec(&req).unwrap().len();
1186 v.validate_post_schema(&req, raw_len).unwrap();
1187 }
1188
1189 #[test]
1190 fn revocation_malformed_fingerprint_rejected_at_0_3_0() {
1191 let caps = test_caps_v030();
1192 let v = PublishValidator::new(&caps);
1193 let mut meta = valid_revocation_metadata();
1194 meta["revoked_key_fingerprint"] = serde_json::json!("not-a-fingerprint");
1195 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1196 let raw_len = serde_json::to_vec(&req).unwrap().len();
1197 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1198 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1199 }
1200
1201 #[test]
1202 fn revocation_malformed_fingerprint_accepted_at_0_2_0_positive_control() {
1203 let caps = test_caps_v020();
1204 let v = PublishValidator::new(&caps);
1205 let mut meta = valid_revocation_metadata();
1206 meta["revoked_key_fingerprint"] = serde_json::json!("not-a-fingerprint");
1207 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1208 let raw_len = serde_json::to_vec(&req).unwrap().len();
1209 v.validate_post_schema(&req, raw_len).unwrap();
1210 }
1211
1212 #[test]
1213 fn revocation_non_canonical_compromised_since_rejected_at_0_3_0() {
1214 let caps = test_caps_v030();
1215 let v = PublishValidator::new(&caps);
1216 let mut meta = valid_revocation_metadata();
1217 meta["compromised_since"] = serde_json::json!("2026-05-01T00:00:00Z");
1218 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1219 let raw_len = serde_json::to_vec(&req).unwrap().len();
1220 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1221 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1222 }
1223
1224 #[test]
1225 fn revocation_non_canonical_compromised_since_accepted_at_0_2_0_positive_control() {
1226 let caps = test_caps_v020();
1227 let v = PublishValidator::new(&caps);
1228 let mut meta = valid_revocation_metadata();
1229 meta["compromised_since"] = serde_json::json!("2026-05-01T00:00:00Z");
1230 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1231 let raw_len = serde_json::to_vec(&req).unwrap().len();
1232 v.validate_post_schema(&req, raw_len).unwrap();
1233 }
1234
1235 #[test]
1236 fn revocation_reason_over_limit_rejected_at_0_3_0() {
1237 let caps = test_caps_v030();
1238 let v = PublishValidator::new(&caps);
1239 let mut meta = valid_revocation_metadata();
1240 meta["reason"] =
1241 serde_json::json!("x".repeat(acdp_types::revocation::MAX_REASON_CHARS + 1));
1242 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1243 let raw_len = serde_json::to_vec(&req).unwrap().len();
1244 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1245 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1246 }
1247
1248 #[test]
1249 fn revocation_reason_over_limit_accepted_at_0_2_0_positive_control() {
1250 let caps = test_caps_v020();
1251 let v = PublishValidator::new(&caps);
1252 let mut meta = valid_revocation_metadata();
1253 meta["reason"] =
1254 serde_json::json!("x".repeat(acdp_types::revocation::MAX_REASON_CHARS + 1));
1255 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1256 let raw_len = serde_json::to_vec(&req).unwrap().len();
1257 v.validate_post_schema(&req, raw_len).unwrap();
1258 }
1259
1260 #[test]
1268 fn revocation_reason_at_exactly_the_limit_accepted_at_0_3_0() {
1269 let caps = test_caps_v030();
1270 let v = PublishValidator::new(&caps);
1271 let mut meta = valid_revocation_metadata();
1272 meta["reason"] = serde_json::json!("x".repeat(acdp_types::revocation::MAX_REASON_CHARS));
1273 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1274 let raw_len = serde_json::to_vec(&req).unwrap().len();
1275 v.validate_post_schema(&req, raw_len)
1276 .expect("a reason of exactly MAX_REASON_CHARS must be accepted, not rejected");
1277 }
1278
1279 #[test]
1284 fn revocation_gate_fails_closed_on_unparseable_acdp_version() {
1285 let mut caps = test_caps_v030();
1286 caps.acdp_version = "not-a-version".into();
1287 let v = PublishValidator::new(&caps);
1288 let mut meta = valid_revocation_metadata();
1289 meta.as_object_mut()
1290 .unwrap()
1291 .remove("revoked_key_fingerprint");
1292 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1293 let raw_len = serde_json::to_vec(&req).unwrap().len();
1294 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1295 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1296 }
1297
1298 #[test]
1299 fn revocation_gate_fails_closed_on_empty_acdp_version() {
1300 let mut caps = test_caps_v030();
1301 caps.acdp_version = "".into();
1302 let v = PublishValidator::new(&caps);
1303 let mut meta = valid_revocation_metadata();
1304 meta.as_object_mut()
1305 .unwrap()
1306 .remove("revoked_key_fingerprint");
1307 let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1308 let raw_len = serde_json::to_vec(&req).unwrap().len();
1309 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1310 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1311 }
1312
1313 #[test]
1316 fn non_key_revocation_body_unaffected_by_gate_at_0_3_0() {
1317 let caps = test_caps_v030();
1318 let v = PublishValidator::new(&caps);
1319 let req = test_request();
1320 let raw_len = serde_json::to_vec(&req).unwrap().len();
1321 v.validate_post_schema(&req, raw_len).unwrap();
1322 }
1323
1324 #[test]
1333 fn key_revocation_gate_truth_table() {
1334 let cases: &[(&str, bool)] = &[
1335 ("0.3x.0", true),
1338 ("0. 3.0", true),
1341 ("0.2.0 ", true),
1345 ("0.2.0;", true),
1346 ("0.x.1", true),
1348 ("0.\u{0663}.0", true),
1352 ("not-a-version", true),
1355 ("", true),
1356 ("0.3.0", true),
1358 ("0.4.0", true),
1359 ("1.0.0", true),
1360 ("0.2.9", false),
1362 ("0.2.0", false),
1363 ];
1364 for (input, expected) in cases {
1365 assert_eq!(
1366 key_revocation_gate_applies(input),
1367 *expected,
1368 "input {input:?} should gate {}",
1369 if *expected { "ON" } else { "OFF" }
1370 );
1371 }
1372 }
1373
1374 #[test]
1387 fn zero_five_zero_threshold_gates_truth_table() {
1388 let cases: &[(&str, bool, bool)] = &[
1389 ("0.5.0", true, true),
1391 ("0.4.9", false, false),
1392 ("1.0.0", true, true),
1393 ("0.5.1", true, true),
1394 ("0.05.0", true, true),
1399 ("000.005.000", true, true),
1400 ("0.5.0-alpha", true, false),
1404 ("0.5.0.1", true, false),
1407 ("99999999999999999999.0.0", true, true),
1415 ];
1416 for (input, retirement_expected, advertises_expected) in cases {
1417 assert_eq!(
1418 key_revocation_retirement_gate_applies(input),
1419 *retirement_expected,
1420 "§10 retirement gate: input {input:?} should gate {}",
1421 if *retirement_expected { "ON" } else { "OFF" }
1422 );
1423 assert_eq!(
1424 advertises_0_5_0_or_higher(input),
1425 *advertises_expected,
1426 "advertises_0_5_0_or_higher: input {input:?} should be {advertises_expected}"
1427 );
1428 }
1429 }
1430
1431 fn build_revocation_request_with_type(
1437 agent_did: &str,
1438 metadata: serde_json::Value,
1439 acdp_version: &str,
1440 context_type: ContextType,
1441 ) -> PublishRequest {
1442 let key = SigningKey::from_bytes(&[0u8; 32]);
1443 let p = Producer::new(key, AgentDid::new(agent_did), format!("{agent_did}#key-1"));
1444 p.publish_request()
1445 .title("Key revocation test (interim §10 type)")
1446 .context_type(context_type)
1447 .visibility(Visibility::Public)
1448 .acdp_version(acdp_version)
1449 .metadata(metadata)
1450 .build()
1451 .unwrap()
1452 }
1453
1454 #[test]
1465 fn revocation_interim_custom_type_valid_body_accepted_at_0_3_0() {
1466 let caps = test_caps_v030();
1467 let v = PublishValidator::new(&caps);
1468 let req = build_revocation_request_with_type(
1469 REVOCATION_PRODUCER_DID,
1470 valid_revocation_metadata(),
1471 "0.3.0",
1472 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1473 );
1474 let raw_len = serde_json::to_vec(&req).unwrap().len();
1475 v.validate_post_schema(&req, raw_len).unwrap();
1476 }
1477
1478 #[test]
1484 fn revocation_interim_custom_type_malformed_body_accepted_at_0_3_0() {
1485 let caps = test_caps_v030();
1486 let v = PublishValidator::new(&caps);
1487 let mut meta = valid_revocation_metadata();
1488 meta.as_object_mut()
1489 .unwrap()
1490 .remove("revoked_key_fingerprint");
1491 let req = build_revocation_request_with_type(
1492 REVOCATION_PRODUCER_DID,
1493 meta,
1494 "0.3.0",
1495 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1496 );
1497 let raw_len = serde_json::to_vec(&req).unwrap().len();
1498 v.validate_post_schema(&req, raw_len).unwrap();
1499 }
1500
1501 fn did_key_producer_fixture(seed: [u8; 32]) -> (SigningKey, String, String, String) {
1502 let key = SigningKey::from_bytes(&seed);
1503 let public_key = key.verifying_key_bytes();
1504 let did = acdp_did::key::did_key_from_ed25519(&public_key);
1505 let key_id = acdp_did::key::did_key_url(&did).unwrap();
1506 let fingerprint = acdp_crypto::fingerprint::fingerprint_ed25519(&public_key);
1507 (key, did, key_id, fingerprint)
1508 }
1509
1510 fn caps_v030_with_did_key() -> CapabilitiesDocument {
1511 CapabilitiesDocument {
1512 acdp_version: "0.3.0".into(),
1513 supported_did_methods: vec!["did:web".into(), "did:key".into()],
1514 ..test_caps()
1515 }
1516 }
1517
1518 #[test]
1528 fn revocation_interim_custom_type_did_key_self_signed_rejected_at_0_3_0() {
1529 let (key, did, key_id, fingerprint) = did_key_producer_fixture([9u8; 32]);
1530 let mut meta = valid_revocation_metadata();
1531 meta["revoked_key_fingerprint"] = serde_json::json!(fingerprint);
1532
1533 let req = Producer::new(key, AgentDid::new(&did), key_id)
1534 .publish_request()
1535 .title("self-signed interim revocation")
1536 .context_type(ContextType::Custom(
1537 ContextType::KEY_REVOCATION_INTERIM.into(),
1538 ))
1539 .visibility(Visibility::Public)
1540 .acdp_version("0.3.0")
1541 .metadata(meta)
1542 .build()
1543 .unwrap();
1544
1545 let caps = caps_v030_with_did_key();
1546 let v = PublishValidator::new(&caps);
1547 let raw_len = serde_json::to_vec(&req).unwrap().len();
1548 assert!(matches!(
1549 v.validate_post_schema(&req, raw_len),
1550 Err(AcdpError::KeyNotAuthorized(_))
1551 ));
1552 }
1553
1554 #[test]
1560 fn revocation_interim_custom_type_did_key_different_key_accepted_at_0_3_0() {
1561 let (key, did, key_id, _fingerprint) = did_key_producer_fixture([10u8; 32]);
1562 let meta = valid_revocation_metadata();
1565
1566 let req = Producer::new(key, AgentDid::new(&did), key_id)
1567 .publish_request()
1568 .title("non-self-signed interim revocation")
1569 .context_type(ContextType::Custom(
1570 ContextType::KEY_REVOCATION_INTERIM.into(),
1571 ))
1572 .visibility(Visibility::Public)
1573 .acdp_version("0.3.0")
1574 .metadata(meta)
1575 .build()
1576 .unwrap();
1577
1578 let caps = caps_v030_with_did_key();
1579 let v = PublishValidator::new(&caps);
1580 let raw_len = serde_json::to_vec(&req).unwrap().len();
1581 v.validate_post_schema(&req, raw_len)
1582 .expect("did:key signer whose fingerprint differs from the revoked key must pass");
1583 }
1584
1585 #[test]
1592 fn revocation_interim_custom_type_mismatched_controller_rejected_at_0_3_0() {
1593 let mut meta = valid_revocation_metadata();
1594 meta["revoked_key_controller"] = serde_json::json!("did:web:someone-else.example.com");
1595 let req = build_revocation_request_with_type(
1596 REVOCATION_PRODUCER_DID,
1597 meta,
1598 "0.3.0",
1599 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1600 );
1601 let caps = test_caps_v030();
1602 let v = PublishValidator::new(&caps);
1603 let raw_len = serde_json::to_vec(&req).unwrap().len();
1604 assert!(matches!(
1605 v.validate_post_schema(&req, raw_len),
1606 Err(AcdpError::SchemaViolation(_))
1607 ));
1608 }
1609
1610 #[test]
1614 fn revocation_interim_custom_type_registry_attested_controller_accepted_at_0_3_0() {
1615 let caps = test_caps_v030();
1616 let mut meta = valid_revocation_metadata();
1617 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_PRODUCER_DID);
1618 let req = build_revocation_request_with_type(
1619 &caps.registry_did,
1620 meta,
1621 "0.3.0",
1622 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1623 );
1624 let v = PublishValidator::new(&caps);
1625 let raw_len = serde_json::to_vec(&req).unwrap().len();
1626 v.validate_post_schema(&req, raw_len)
1627 .expect("registry-attested interim revocation with a named controller must pass");
1628 }
1629
1630 #[test]
1635 fn revocation_interim_custom_type_registry_attested_missing_controller_rejected_at_0_3_0() {
1636 let caps = test_caps_v030();
1637 let meta = valid_revocation_metadata();
1638 let req = build_revocation_request_with_type(
1639 &caps.registry_did,
1640 meta,
1641 "0.3.0",
1642 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1643 );
1644 let v = PublishValidator::new(&caps);
1645 let raw_len = serde_json::to_vec(&req).unwrap().len();
1646 assert!(matches!(
1647 v.validate_post_schema(&req, raw_len),
1648 Err(AcdpError::SchemaViolation(_))
1649 ));
1650 }
1651
1652 #[test]
1660 fn revocation_interim_custom_type_rejected_unconditionally_at_0_5_0() {
1661 let caps = test_caps_v050();
1662 let v = PublishValidator::new(&caps);
1663 let req = build_revocation_request_with_type(
1664 REVOCATION_PRODUCER_DID,
1665 valid_revocation_metadata(),
1666 "0.5.0",
1667 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1668 );
1669 let raw_len = serde_json::to_vec(&req).unwrap().len();
1670 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1671 assert!(
1672 matches!(err, AcdpError::SchemaViolation(_)),
1673 "the interim form must be rejected unconditionally at >= 0.5.0, got {err:?}"
1674 );
1675 }
1676
1677 #[test]
1683 fn interim_form_retirement_gate_fails_closed_on_malformed_acdp_version() {
1684 let mut caps = test_caps_v050();
1685 caps.acdp_version = "not-a-version".into();
1686 let v = PublishValidator::new(&caps);
1687 let req = build_revocation_request_with_type(
1688 REVOCATION_PRODUCER_DID,
1689 valid_revocation_metadata(),
1690 "0.5.0",
1691 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1692 );
1693 let raw_len = serde_json::to_vec(&req).unwrap().len();
1694 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1695 assert!(
1696 matches!(err, AcdpError::SchemaViolation(_)),
1697 "a malformed acdp_version must fail closed toward retiring the interim form, got {err:?}"
1698 );
1699 }
1700
1701 #[test]
1707 fn revocation_interim_custom_type_rejected_with_supersedes_at_0_5_0() {
1708 let caps = test_caps_v050();
1709 let v = PublishValidator::new(&caps);
1710 let key = SigningKey::from_bytes(&[0u8; 32]);
1711 let p = Producer::new(
1712 key,
1713 AgentDid::new(REVOCATION_PRODUCER_DID),
1714 format!("{REVOCATION_PRODUCER_DID}#key-1"),
1715 );
1716 let target =
1717 CtxId("acdp://registry.example.com/00000000-0000-4000-8000-000000000002".into());
1718 let req = p
1719 .supersede(target)
1720 .version(2)
1721 .title("Key revocation test (interim §10 type, v2 supersedes)")
1722 .context_type(ContextType::Custom(
1723 ContextType::KEY_REVOCATION_INTERIM.into(),
1724 ))
1725 .visibility(Visibility::Public)
1726 .acdp_version("0.5.0")
1727 .metadata(valid_revocation_metadata())
1728 .build()
1729 .unwrap();
1730 let raw_len = serde_json::to_vec(&req).unwrap().len();
1731 let err = v.validate_post_schema(&req, raw_len).unwrap_err();
1732 assert!(
1733 matches!(err, AcdpError::SchemaViolation(_)),
1734 "the interim form must be rejected even when it carries a supersedes target, got {err:?}"
1735 );
1736 }
1737
1738 #[test]
1743 fn revocation_standard_type_still_accepted_at_0_5_0() {
1744 let caps = test_caps_v050();
1745 let v = PublishValidator::new(&caps);
1746 let req = build_revocation_request(
1747 REVOCATION_PRODUCER_DID,
1748 valid_revocation_metadata(),
1749 "0.5.0",
1750 );
1751 let raw_len = serde_json::to_vec(&req).unwrap().len();
1752 v.validate_post_schema(&req, raw_len).expect(
1753 "the standard key-revocation type is not retired by §10, only the interim spelling is",
1754 );
1755 }
1756
1757 fn body_from_request(req: &PublishRequest) -> Body {
1765 Body::from_publish_request(
1766 req,
1767 CtxId("acdp://registry.example.com/00000000-0000-4000-8000-000000000001".into()),
1768 LineageId(format!("lin:sha256:{}", "0".repeat(64))),
1769 "registry.example.com",
1770 chrono::DateTime::parse_from_rfc3339("2026-05-01T00:00:00.000Z")
1771 .unwrap()
1772 .with_timezone(&chrono::Utc),
1773 )
1774 }
1775
1776 #[test]
1780 fn revocation_supersession_same_class_allowed_t_earlier() {
1781 let prev_req = build_revocation_request(
1782 REVOCATION_PRODUCER_DID,
1783 valid_revocation_metadata(), "0.3.0",
1785 );
1786 let prev = body_from_request(&prev_req);
1787
1788 let mut meta = valid_revocation_metadata();
1789 meta["compromised_since"] = serde_json::json!("2026-04-01T00:00:00.000Z"); let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1791
1792 check_revocation_supersession(&prev, &req, "0.3.0")
1793 .expect("same signer class supersession must be allowed regardless of T direction");
1794 }
1795
1796 #[test]
1800 fn revocation_supersession_different_class_rejected() {
1801 let prev_req = build_revocation_request(
1802 REVOCATION_PRODUCER_DID,
1803 valid_revocation_metadata(), "0.3.0",
1805 );
1806 let prev = body_from_request(&prev_req);
1807
1808 let registry_did = test_caps().registry_did;
1809 let mut meta = valid_revocation_metadata();
1810 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_PRODUCER_DID);
1811 let req = build_revocation_request(®istry_did, meta, "0.3.0"); let err = check_revocation_supersession(&prev, &req, "0.3.0").unwrap_err();
1814 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1815 }
1816
1817 #[test]
1822 fn revocation_superseded_by_non_revocation_rejected() {
1823 let prev_req = build_revocation_request(
1824 REVOCATION_PRODUCER_DID,
1825 valid_revocation_metadata(),
1826 "0.3.0",
1827 );
1828 let prev = body_from_request(&prev_req);
1829 let req = test_request(); let err = check_revocation_supersession(&prev, &req, "0.3.0").unwrap_err();
1832 assert!(matches!(err, AcdpError::SchemaViolation(_)));
1833 }
1834
1835 #[test]
1840 fn revocation_superseded_by_non_revocation_rejected_as_revocation_type_mismatch_at_0_5_0() {
1841 let prev_req = build_revocation_request(
1842 REVOCATION_PRODUCER_DID,
1843 valid_revocation_metadata(),
1844 "0.3.0",
1845 );
1846 let prev = body_from_request(&prev_req);
1847 let req = test_request(); let err = check_revocation_supersession(&prev, &req, "0.5.0").unwrap_err();
1850 assert!(
1851 matches!(
1852 err,
1853 AcdpError::SupersededTarget {
1854 reason: acdp_primitives::error::SupersessionReason::RevocationTypeMismatch,
1855 ..
1856 }
1857 ),
1858 "expected SupersededTarget/RevocationTypeMismatch at acdp_version >= 0.5.0, got {err:?}"
1859 );
1860 }
1861
1862 #[test]
1866 fn revocation_superseded_by_non_revocation_still_schema_violation_below_0_5_0() {
1867 let prev_req = build_revocation_request(
1868 REVOCATION_PRODUCER_DID,
1869 valid_revocation_metadata(),
1870 "0.3.0",
1871 );
1872 let prev = body_from_request(&prev_req);
1873 let req = test_request(); let err = check_revocation_supersession(&prev, &req, "0.4.9").unwrap_err();
1876 assert!(
1877 matches!(err, AcdpError::SchemaViolation(_)),
1878 "0.4.9 is below the 0.5.0 boundary; expected the unchanged SchemaViolation, got {err:?}"
1879 );
1880 }
1881
1882 #[test]
1890 fn revocation_superseded_by_non_revocation_rejected_as_revocation_type_mismatch_interim_predecessor_at_0_5_0(
1891 ) {
1892 let prev_req = build_revocation_request_with_type(
1893 REVOCATION_PRODUCER_DID,
1894 valid_revocation_metadata(),
1895 "0.3.0",
1896 ContextType::Custom(ContextType::KEY_REVOCATION_INTERIM.into()),
1897 );
1898 let prev = body_from_request(&prev_req);
1899 let req = test_request(); let err = check_revocation_supersession(&prev, &req, "0.5.0").unwrap_err();
1902 assert!(
1903 matches!(
1904 err,
1905 AcdpError::SupersededTarget {
1906 reason: acdp_primitives::error::SupersessionReason::RevocationTypeMismatch,
1907 ..
1908 }
1909 ),
1910 "an interim-typed predecessor must trigger the same rejection as a \
1911 standard-typed one, got {err:?}"
1912 );
1913 }
1914
1915 #[test]
1926 fn revocation_supersession_same_class_allowed_at_0_5_0() {
1927 let prev_req = build_revocation_request(
1928 REVOCATION_PRODUCER_DID,
1929 valid_revocation_metadata(), "0.5.0",
1931 );
1932 let prev = body_from_request(&prev_req);
1933
1934 let mut meta = valid_revocation_metadata();
1935 meta["compromised_since"] = serde_json::json!("2026-04-01T00:00:00.000Z"); let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.5.0");
1937
1938 check_revocation_supersession(&prev, &req, "0.5.0").expect(
1939 "a same-class key-revocation supersession must still be allowed at 0.5.0 — \
1940 the (0.5.0) rule targets non-revocation successors only",
1941 );
1942 }
1943
1944 #[test]
1949 fn non_revocation_predecessor_superseded_by_revocation_allowed() {
1950 let prev_req = test_request();
1951 let prev = body_from_request(&prev_req);
1952 let req = build_revocation_request(
1953 REVOCATION_PRODUCER_DID,
1954 valid_revocation_metadata(),
1955 "0.3.0",
1956 );
1957
1958 check_revocation_supersession(&prev, &req, "0.3.0")
1959 .expect("a non-revocation predecessor is out of scope for this §4 row");
1960 }
1961
1962 #[test]
1978 fn revocation_supersession_same_class_narrowing_t_allowed_at_publish() {
1979 let mut prev_meta = valid_revocation_metadata();
1980 prev_meta["compromised_since"] = serde_json::json!("2026-05-01T00:00:00.000Z");
1981 let prev_req = build_revocation_request(REVOCATION_PRODUCER_DID, prev_meta, "0.3.0");
1982 let prev = body_from_request(&prev_req);
1983
1984 let mut meta = valid_revocation_metadata();
1985 meta["compromised_since"] = serde_json::json!("2026-06-01T00:00:00.000Z"); let req = build_revocation_request(REVOCATION_PRODUCER_DID, meta, "0.3.0");
1987
1988 check_revocation_supersession(&prev, &req, "0.3.0").expect(
1989 "narrowing the compromise window (T moved later) is allowed at publish time; \
1990 this function enforces only type + signer class, not compromised_since \
1991 direction (RFC-ACDP-0014 §4:58)",
1992 );
1993 }
1994
1995 #[test]
2001 fn revocation_supersession_malformed_predecessor_skips_class_comparison() {
2002 let key = SigningKey::from_bytes(&[0u8; 32]);
2003 let p = Producer::new(
2004 key,
2005 AgentDid::new(REVOCATION_PRODUCER_DID),
2006 format!("{REVOCATION_PRODUCER_DID}#key-1"),
2007 );
2008 let prev_req = p
2009 .publish_request()
2010 .title("Malformed pre-0.3.0 key-revocation (no metadata)")
2011 .context_type(ContextType::KeyRevocation)
2012 .visibility(Visibility::Public)
2013 .acdp_version("0.2.0")
2014 .build()
2015 .unwrap();
2016 let prev = body_from_request(&prev_req);
2017 assert!(
2018 KeyRevocation::from_body(&prev).is_err(),
2019 "fixture must actually fail from_body, or this test proves nothing"
2020 );
2021
2022 let req = build_revocation_request(
2023 REVOCATION_OTHER_PRODUCER_DID,
2024 valid_revocation_metadata(),
2025 "0.3.0",
2026 );
2027
2028 check_revocation_supersession(&prev, &req, "0.3.0").expect(
2029 "arm 6b: a malformed predecessor skips the class comparison but a \
2030 well-formed key-revocation successor is still allowed",
2031 );
2032 }
2033
2034 #[test]
2041 fn revocation_supersession_malformed_predecessor_still_blocks_non_revocation_successor() {
2042 let key = SigningKey::from_bytes(&[0u8; 32]);
2043 let p = Producer::new(
2044 key,
2045 AgentDid::new(REVOCATION_PRODUCER_DID),
2046 format!("{REVOCATION_PRODUCER_DID}#key-1"),
2047 );
2048 let prev_req = p
2049 .publish_request()
2050 .title("Malformed pre-0.3.0 key-revocation (no metadata)")
2051 .context_type(ContextType::KeyRevocation)
2052 .visibility(Visibility::Public)
2053 .acdp_version("0.2.0")
2054 .build()
2055 .unwrap();
2056 let prev = body_from_request(&prev_req);
2057 assert!(
2058 KeyRevocation::from_body(&prev).is_err(),
2059 "fixture must actually fail from_body, or this test proves nothing"
2060 );
2061
2062 let req = test_request(); let err = check_revocation_supersession(&prev, &req, "0.3.0").unwrap_err();
2065 assert!(
2066 matches!(err, AcdpError::SchemaViolation(_)),
2067 "arm 3's type rule must still reject a non-revocation successor even when the \
2068 predecessor is malformed and the class comparison is skipped"
2069 );
2070 }
2071
2072 #[test]
2083 fn revocation_supersession_same_did_class_flip_rejected() {
2084 let caps = test_caps_v030();
2085 let v = PublishValidator::new(&caps);
2086 let registry_did = caps.registry_did.clone();
2087
2088 let mut prev_meta = valid_revocation_metadata();
2089 prev_meta["revoked_key_controller"] = serde_json::json!(registry_did);
2090 let prev_req = build_revocation_request(®istry_did, prev_meta, "0.3.0"); let prev_revocation = KeyRevocation::from_publish_request(&prev_req).unwrap();
2092 assert_eq!(
2093 prev_revocation.trust_class,
2094 RevocationTrustClass::ProducerSigned
2095 );
2096 v.check_revocation_controller(&prev_req, &prev_revocation)
2097 .expect("PREV fixture must be a legitimately publishable revocation");
2098 let prev = body_from_request(&prev_req);
2099
2100 let mut meta = valid_revocation_metadata();
2101 meta["revoked_key_controller"] = serde_json::json!(REVOCATION_PRODUCER_DID);
2102 let req = build_revocation_request(®istry_did, meta, "0.3.0"); let in_revocation = KeyRevocation::from_publish_request(&req).unwrap();
2104 assert_eq!(
2105 in_revocation.trust_class,
2106 RevocationTrustClass::RegistryAttested
2107 );
2108 v.check_revocation_controller(&req, &in_revocation)
2109 .expect("IN fixture must be a legitimately publishable revocation");
2110
2111 let err = check_revocation_supersession(&prev, &req, "0.3.0").unwrap_err();
2112 assert!(
2113 matches!(err, AcdpError::SchemaViolation(_)),
2114 "same agent_id on both sides must NOT be enough to allow this supersession — \
2115 the criterion is signer class, not DID"
2116 );
2117 }
2118
2119 #[test]
2127 fn revocation_supersession_cross_did_same_class_allowed() {
2128 let prev_req = build_revocation_request(
2129 REVOCATION_PRODUCER_DID,
2130 valid_revocation_metadata(), "0.3.0",
2132 );
2133 let prev = body_from_request(&prev_req);
2134
2135 let req = build_revocation_request(
2136 REVOCATION_OTHER_PRODUCER_DID, valid_revocation_metadata(), "0.3.0",
2139 );
2140
2141 check_revocation_supersession(&prev, &req, "0.3.0").expect(
2142 "cross-DID, same signer class (ProducerSigned) must be allowed — \
2143 RFC-ACDP-0014 §13 blesses cross-producer supersession; the criterion is \
2144 class, not DID",
2145 );
2146 }
2147}