1use auths_core::witness::{EventHash, WitnessProvider};
40use auths_policy::{CanonicalCapability, DidParseError};
41use auths_verifier::PresentationVerdict;
42use auths_verifier::core::Attestation;
43use auths_verifier::types::CanonicalDid;
44use chrono::{DateTime, Utc};
45
46use crate::keri::KeyState;
47use crate::keri::event::EventReceipts;
48use crate::keri::types::Said;
49#[cfg(feature = "git-storage")]
50use crate::storage::receipts::{check_receipt_consistency, verify_receipt_signature};
51
52pub use auths_policy::{
54 CompileError, CompiledPolicy, Decision, EvalContext, Expr, Outcome, PolicyBuilder,
55 PolicyLimits, ReasonCode, SignerType, compile, compile_from_json, evaluate_strict,
56};
57
58pub fn context_from_attestation(
84 att: &Attestation,
85 now: DateTime<Utc>,
86) -> Result<EvalContext, DidParseError> {
87 let mut ctx = EvalContext::try_from_strings(now, &att.issuer, att.subject.as_ref())?;
88
89 ctx = ctx.revoked(att.is_revoked());
90
91 if let Some(expires_at) = att.expires_at {
92 ctx = ctx.expires_at(expires_at);
93 }
94
95 if let Some(ref delegated_by) = att.delegated_by {
96 if let Ok(did) = auths_policy::CanonicalDid::parse(delegated_by) {
98 ctx = ctx.delegated_by(did);
99 }
100 }
101
102 if let Some(ref st) = att.signer_type {
104 let policy_st = match st {
105 auths_verifier::core::SignerType::Human => auths_policy::SignerType::Human,
106 auths_verifier::core::SignerType::Agent => auths_policy::SignerType::Agent,
107 auths_verifier::core::SignerType::Workload => auths_policy::SignerType::Workload,
108 _ => auths_policy::SignerType::Workload,
109 };
110 ctx = ctx.signer_type(policy_st);
111 }
112
113 Ok(ctx)
114}
115
116#[allow(clippy::too_many_arguments)]
141pub fn context_from_delegated_member(
142 org_did: &str,
143 member_did: &str,
144 revoked: bool,
145 role: Option<&str>,
146 capabilities: &[String],
147 expires_at: Option<DateTime<Utc>>,
148 now: DateTime<Utc>,
149) -> Result<EvalContext, DidParseError> {
150 let mut ctx = EvalContext::try_from_strings(now, org_did, member_did)?;
151 ctx = ctx.revoked(revoked);
152
153 let caps: Vec<CanonicalCapability> = capabilities
154 .iter()
155 .filter_map(|c| CanonicalCapability::parse(c).ok())
156 .collect();
157 ctx = ctx.capabilities(caps);
158
159 if let Some(role) = role {
160 ctx = ctx.role(role.to_string());
161 }
162 if let Some(expires_at) = expires_at {
163 ctx = ctx.expires_at(expires_at);
164 }
165 if let Ok(did) = auths_policy::CanonicalDid::parse(org_did) {
166 ctx = ctx.delegated_by(did);
167 }
168
169 Ok(ctx)
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum CapsSource {
192 AgentScopeSeal,
194 Acdc,
196}
197
198impl CapsSource {
199 pub fn governing(agentscope_present: bool, acdc_present: bool) -> Option<CapsSource> {
211 match (acdc_present, agentscope_present) {
212 (true, _) => Some(CapsSource::Acdc),
213 (false, true) => Some(CapsSource::AgentScopeSeal),
214 (false, false) => None,
215 }
216 }
217}
218
219#[derive(Debug, thiserror::Error)]
227pub enum PolicyBridgeError {
228 #[error("no holder proof: presentation is not Valid, refusing to grant authority")]
231 NoHolderProof,
232 #[error("credential DID parse failed: {0}")]
234 Did(#[from] DidParseError),
235}
236
237pub fn context_from_credential(
265 presentation: &PresentationVerdict,
266 now: DateTime<Utc>,
267) -> Result<EvalContext, PolicyBridgeError> {
268 let PresentationVerdict::Valid {
269 issuer,
270 subject,
271 caps,
272 role,
273 expires_at,
274 } = presentation
275 else {
276 return Err(PolicyBridgeError::NoHolderProof);
277 };
278
279 let mut ctx = EvalContext::try_from_strings(now, issuer.as_str(), subject.as_str())?;
280 ctx = ctx.revoked(false);
281
282 let caps: Vec<CanonicalCapability> = caps
283 .iter()
284 .filter_map(|c| CanonicalCapability::parse(c.as_str()).ok())
285 .collect();
286 ctx = ctx.capabilities(caps);
287
288 if let Some(role) = role {
289 ctx = ctx.role(role.clone());
290 }
291 if let Some(expires_at) = expires_at {
292 ctx = ctx.expires_at(*expires_at);
293 }
294 if let Ok(did) = CanonicalDid::parse(issuer.as_str()) {
295 ctx = ctx.delegated_by(did);
296 }
297
298 Ok(ctx)
299}
300
301pub fn evaluate_compiled(
341 att: &Attestation,
342 policy: &CompiledPolicy,
343 now: DateTime<Utc>,
344) -> Result<Decision, DidParseError> {
345 let ctx = context_from_attestation(att, now)?;
346 Ok(evaluate_strict(policy, &ctx))
347}
348
349pub fn evaluate_with_witness(
377 identity: &KeyState,
378 att: &Attestation,
379 policy: &CompiledPolicy,
380 now: DateTime<Utc>,
381 local_head: EventHash,
382 witnesses: &[&dyn WitnessProvider],
383) -> Result<Decision, DidParseError> {
384 if witnesses.is_empty() {
385 return evaluate_compiled(att, policy, now);
386 }
387
388 let required_quorum = witnesses.first().map(|w| w.quorum()).unwrap_or(1);
389
390 if required_quorum == 0 {
391 return evaluate_compiled(att, policy, now);
392 }
393
394 let mut matching = 0;
395 let mut total_opinions = 0;
396
397 for witness in witnesses {
398 if let Some(head) = witness.observe_identity_head(&identity.prefix) {
399 total_opinions += 1;
400 if head == local_head {
401 matching += 1;
402 }
403 }
404 }
405
406 if total_opinions == 0 {
407 return evaluate_compiled(att, policy, now);
408 }
409
410 if matching < required_quorum {
411 return Ok(Decision::deny(
412 ReasonCode::WitnessQuorumNotMet,
413 format!(
414 "Witness quorum not met: {}/{} matching, {} required",
415 matching, total_opinions, required_quorum
416 ),
417 ));
418 }
419
420 evaluate_compiled(att, policy, now)
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
425pub enum ReceiptVerificationResult {
426 Valid,
428 InsufficientReceipts { required: usize, got: usize },
430 Duplicity { event_a: Said, event_b: Said },
432 InvalidSignature { witness_did: CanonicalDid },
434}
435
436pub trait WitnessKeyResolver: Send + Sync {
440 fn get_public_key(&self, witness_did: &str) -> Option<Vec<u8>>;
442}
443
444#[cfg(feature = "git-storage")]
468pub fn verify_receipts(
469 receipts: &EventReceipts,
470 threshold: usize,
471 key_resolver: Option<&dyn WitnessKeyResolver>,
472) -> ReceiptVerificationResult {
473 let unique = receipts.unique_witness_count();
475 if unique < threshold {
476 return ReceiptVerificationResult::InsufficientReceipts {
477 required: threshold,
478 got: unique,
479 };
480 }
481
482 if let Err(e) = check_receipt_consistency(&receipts.receipts) {
484 return ReceiptVerificationResult::Duplicity {
485 event_a: receipts.event_said.clone(),
486 event_b: Said::new_unchecked(format!("conflicting: {}", e)),
487 };
488 }
489
490 if let Some(resolver) = key_resolver {
495 for stored in &receipts.receipts {
496 let witness = stored.witness.as_str();
497 if let Some(public_key) = resolver.get_public_key(witness) {
498 let witness_curve = auths_crypto::did_key_decode(witness)
499 .map(|d| d.curve())
500 .unwrap_or_default();
501 let typed_pk =
502 match auths_verifier::decode_public_key_bytes(&public_key, witness_curve) {
503 Ok(pk) => pk,
504 Err(_) => {
505 #[allow(clippy::disallowed_methods)]
506 return ReceiptVerificationResult::InvalidSignature {
508 witness_did: CanonicalDid::new_unchecked(witness),
509 };
510 }
511 };
512 match verify_receipt_signature(&stored.signed.receipt, &typed_pk) {
513 Ok(true) => continue,
514 Ok(false) | Err(_) => {
515 return ReceiptVerificationResult::InvalidSignature {
516 #[allow(clippy::disallowed_methods)] witness_did: CanonicalDid::new_unchecked(witness),
518 };
519 }
520 }
521 }
522 }
525 }
526
527 ReceiptVerificationResult::Valid
528}
529
530#[cfg(feature = "git-storage")]
555#[allow(clippy::too_many_arguments)]
556pub fn evaluate_with_receipts(
557 identity: &KeyState,
558 att: &Attestation,
559 policy: &CompiledPolicy,
560 now: DateTime<Utc>,
561 local_head: EventHash,
562 witnesses: &[&dyn WitnessProvider],
563 receipts: &EventReceipts,
564 threshold: usize,
565 key_resolver: Option<&dyn WitnessKeyResolver>,
566) -> Result<Decision, DidParseError> {
567 match verify_receipts(receipts, threshold, key_resolver) {
568 ReceiptVerificationResult::Valid => {}
569 ReceiptVerificationResult::InsufficientReceipts { required, got } => {
570 return Ok(Decision::deny(
571 ReasonCode::WitnessQuorumNotMet,
572 format!(
573 "Insufficient receipts: {} required, {} present",
574 required, got
575 ),
576 ));
577 }
578 ReceiptVerificationResult::Duplicity { event_a, event_b } => {
579 return Ok(Decision::deny(
580 ReasonCode::WitnessQuorumNotMet,
581 format!("Duplicity detected: {} vs {}", event_a, event_b),
582 ));
583 }
584 ReceiptVerificationResult::InvalidSignature { witness_did } => {
585 return Ok(Decision::deny(
586 ReasonCode::WitnessQuorumNotMet,
587 format!("Invalid receipt signature from witness: {}", witness_did),
588 ));
589 }
590 }
591
592 evaluate_with_witness(identity, att, policy, now, local_head, witnesses)
593}
594
595#[cfg(test)]
596#[allow(clippy::disallowed_methods)]
597mod tests {
598 use super::*;
599 use auths_core::witness::NoOpWitness;
600 use auths_keri::{CesrKey, Prefix, Said, Threshold};
601 use auths_verifier::AttestationBuilder;
602 use chrono::Duration;
603
604 struct MockWitness {
606 head: Option<EventHash>,
607 quorum: usize,
608 }
609
610 impl WitnessProvider for MockWitness {
611 fn observe_identity_head(&self, _prefix: &Prefix) -> Option<EventHash> {
612 self.head
613 }
614
615 fn quorum(&self) -> usize {
616 self.quorum
617 }
618 }
619
620 fn make_key_state(prefix: &str) -> KeyState {
621 KeyState::from_inception(
622 Prefix::new_unchecked(prefix.to_string()),
623 vec![CesrKey::new_unchecked("DTestKey".to_string())],
624 vec![Said::new_unchecked("ENextCommitment".to_string())],
625 Threshold::Simple(1),
626 Threshold::Simple(1),
627 Said::new_unchecked("ETestSaid".to_string()),
628 vec![],
629 Threshold::Simple(0),
630 vec![],
631 )
632 }
633
634 fn make_attestation(
635 issuer: &str,
636 revoked_at: Option<DateTime<Utc>>,
637 expires_at: Option<DateTime<Utc>>,
638 ) -> Attestation {
639 AttestationBuilder::default()
640 .rid("test")
641 .issuer(issuer)
642 .subject("did:key:zSubject")
643 .revoked_at(revoked_at)
644 .expires_at(expires_at)
645 .build()
646 }
647
648 fn default_policy() -> CompiledPolicy {
649 PolicyBuilder::new().not_revoked().not_expired().build()
650 }
651
652 #[test]
653 fn context_from_attestation_basic() {
654 let att = make_attestation("did:keri:ETest", None, None);
655 let now = Utc::now();
656 let ctx = context_from_attestation(&att, now).unwrap();
657
658 assert_eq!(ctx.issuer.as_str(), "did:keri:ETest");
659 assert_eq!(ctx.subject.as_str(), "did:key:zSubject");
660 assert!(!ctx.revoked);
661 }
662
663 #[test]
664 fn context_from_attestation_has_no_capabilities_or_role() {
665 let att = make_attestation("did:keri:ETest", None, None);
669 let now = Utc::now();
670 let ctx = context_from_attestation(&att, now).unwrap();
671
672 assert!(
673 ctx.capabilities.is_empty(),
674 "attestation caps must not enter the policy context"
675 );
676 assert_eq!(
677 ctx.role, None,
678 "attestation role must not enter the policy context"
679 );
680 }
681
682 #[test]
683 fn caps_absent_without_valid_credential() {
684 let att = make_attestation("did:keri:ETestPrefix", None, None);
687 let policy = PolicyBuilder::new()
688 .not_revoked()
689 .require_capability("sign_commit")
690 .build();
691 let now = Utc::now();
692
693 let decision = evaluate_compiled(&att, &policy, now).unwrap();
694 assert_eq!(decision.outcome, Outcome::Deny);
695 assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
696 }
697
698 const CRED_ISSUER: &str = "did:keri:EIssuerCredential";
703 const CRED_SUBJECT: &str = "did:keri:ESubjectCredential";
704
705 fn valid_presentation(
707 caps: &[&str],
708 role: Option<&str>,
709 expires_at: Option<DateTime<Utc>>,
710 ) -> PresentationVerdict {
711 PresentationVerdict::Valid {
712 issuer: auths_verifier::IdentityDID::parse(CRED_ISSUER).expect("valid test issuer"),
713 subject: auths_verifier::CanonicalDid::parse(CRED_SUBJECT).expect("valid test subject"),
714 caps: caps
715 .iter()
716 .map(|c| auths_verifier::Capability::parse(c).expect("valid test capability"))
717 .collect(),
718 role: role.map(str::to_string),
719 expires_at,
720 }
721 }
722
723 #[test]
724 fn policy_reads_caps_from_credential() {
725 let presentation = valid_presentation(&["sign_commit"], Some("deployer"), None);
726 let now = Utc::now();
727
728 let ctx = context_from_credential(&presentation, now).unwrap();
729 assert_eq!(ctx.issuer.as_str(), CRED_ISSUER);
730 assert_eq!(ctx.subject.as_str(), CRED_SUBJECT);
731 assert!(!ctx.revoked);
732 assert_eq!(ctx.capabilities.len(), 1);
733 assert_eq!(ctx.capabilities[0].as_str(), "sign_commit");
734 assert_eq!(ctx.role.as_deref(), Some("deployer"));
735
736 let policy = PolicyBuilder::new()
737 .not_revoked()
738 .require_capability("sign_commit")
739 .build();
740 assert_eq!(evaluate_strict(&policy, &ctx).outcome, Outcome::Allow);
741 }
742
743 #[test]
744 fn raw_acdc_without_presentation_yields_no_authority() {
745 let now = Utc::now();
749 for verdict in [
750 PresentationVerdict::HolderNotCurrentKey,
751 PresentationVerdict::WrongAudience,
752 PresentationVerdict::NonceMismatchOrConsumed,
753 PresentationVerdict::Expired,
754 PresentationVerdict::SubjectKelInvalid,
755 PresentationVerdict::CredentialNotValid(
756 auths_verifier::CredentialVerdict::SaidMismatch,
757 ),
758 ] {
759 let result = context_from_credential(&verdict, now);
760 assert!(
761 matches!(result, Err(PolicyBridgeError::NoHolderProof)),
762 "non-Valid verdict {verdict:?} must fail closed, got {result:?}"
763 );
764 }
765 }
766
767 #[test]
768 fn capability_round_trips_into_acdc() {
769 use auths_keri::{AgentScope, decode_agent_scope, encode_agent_scope};
770 use auths_verifier::Capability;
771
772 let raw = "repo:foo-bar_baz";
774
775 let scope = AgentScope {
777 capabilities: vec![raw.to_string()],
778 expires_at: Some(99),
779 };
780 let encoded = encode_agent_scope("Eagent", &scope);
781 let (prefix, decoded) = decode_agent_scope(&encoded).unwrap();
782 assert_eq!(prefix, "Eagent");
783 assert_eq!(decoded.capabilities, vec![raw.to_string()]);
784
785 let acdc_capability_json = decoded.capabilities.join(",");
787 assert!(
788 !acdc_capability_json.contains(','),
789 "single cap stays comma-free; the join separator must not appear inside a cap"
790 );
791
792 for cap in acdc_capability_json.split(',') {
794 let canonical = CanonicalCapability::parse(cap).unwrap();
795 assert_eq!(canonical.as_str(), raw);
796 }
797
798 let att_cap = Capability::parse(raw).unwrap();
800 let canonical = CanonicalCapability::parse(&att_cap.to_string()).unwrap();
801 assert_eq!(canonical.as_str(), raw);
802
803 assert!(CanonicalCapability::parse("a,b").is_err());
805 }
806
807 #[test]
808 fn agentscope_seal_vs_acdc_precedence_documented() {
809 assert_eq!(CapsSource::governing(true, true), Some(CapsSource::Acdc));
812 assert_eq!(CapsSource::governing(false, true), Some(CapsSource::Acdc));
813 assert_eq!(
814 CapsSource::governing(true, false),
815 Some(CapsSource::AgentScopeSeal)
816 );
817 assert_eq!(CapsSource::governing(false, false), None);
818 }
819
820 #[test]
821 fn evaluate_compiled_allows_valid_attestation() {
822 let att = make_attestation("did:keri:ETestPrefix", None, None);
823 let policy = default_policy();
824 let now = Utc::now();
825
826 let decision = evaluate_compiled(&att, &policy, now).unwrap();
827 assert_eq!(decision.outcome, Outcome::Allow);
828 }
829
830 #[test]
831 fn evaluate_compiled_denies_revoked() {
832 let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None);
833 let policy = default_policy();
834 let now = Utc::now();
835
836 let decision = evaluate_compiled(&att, &policy, now).unwrap();
837 assert_eq!(decision.outcome, Outcome::Deny);
838 assert_eq!(decision.reason, ReasonCode::Revoked);
839 }
840
841 #[test]
842 fn evaluate_compiled_denies_expired() {
843 let past = Utc::now() - Duration::hours(1);
844 let att = make_attestation("did:keri:ETestPrefix", None, Some(past));
845 let policy = default_policy();
846 let now = Utc::now();
847
848 let decision = evaluate_compiled(&att, &policy, now).unwrap();
849 assert_eq!(decision.outcome, Outcome::Deny);
850 assert_eq!(decision.reason, ReasonCode::Expired);
851 }
852
853 #[test]
854 fn evaluate_compiled_allows_not_yet_expired() {
855 let future = Utc::now() + Duration::hours(1);
856 let att = make_attestation("did:keri:ETestPrefix", None, Some(future));
857 let policy = default_policy();
858 let now = Utc::now();
859
860 let decision = evaluate_compiled(&att, &policy, now).unwrap();
861 assert_eq!(decision.outcome, Outcome::Allow);
862 }
863
864 #[test]
865 fn evaluate_compiled_denies_issuer_mismatch() {
866 let att = make_attestation("did:keri:EWrongPrefix", None, None);
867 let policy = PolicyBuilder::new()
868 .not_revoked()
869 .require_issuer("did:keri:ETestPrefix")
870 .build();
871 let now = Utc::now();
872
873 let decision = evaluate_compiled(&att, &policy, now).unwrap();
874 assert_eq!(decision.outcome, Outcome::Deny);
875 assert_eq!(decision.reason, ReasonCode::IssuerMismatch);
876 }
877
878 #[test]
879 fn evaluate_compiled_denies_missing_capability() {
880 let att = make_attestation("did:keri:ETestPrefix", None, None);
881 let policy = PolicyBuilder::new()
882 .not_revoked()
883 .require_capability("sign_commit")
884 .build();
885 let now = Utc::now();
886
887 let decision = evaluate_compiled(&att, &policy, now).unwrap();
888 assert_eq!(decision.outcome, Outcome::Deny);
889 assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
890 }
891
892 #[test]
893 fn evaluate_compiled_allows_with_capability_from_credential() {
894 let presentation = valid_presentation(&["sign_commit"], None, None);
898 let policy = PolicyBuilder::new()
899 .not_revoked()
900 .require_capability("sign_commit")
901 .build();
902 let now = Utc::now();
903
904 let ctx = context_from_credential(&presentation, now).unwrap();
905 assert_eq!(evaluate_strict(&policy, &ctx).outcome, Outcome::Allow);
906 }
907
908 #[test]
909 fn evaluate_compiled_is_deterministic() {
910 let att = make_attestation("did:keri:ETestPrefix", None, None);
911 let policy = default_policy();
912 let now = Utc::now();
913
914 let decision1 = evaluate_compiled(&att, &policy, now).unwrap();
915 let decision2 = evaluate_compiled(&att, &policy, now).unwrap();
916
917 assert_eq!(decision1, decision2);
918 }
919
920 #[test]
925 fn evaluate_with_witness_no_witnesses_delegates() {
926 let identity = make_key_state("ETestPrefix");
927 let att = make_attestation("did:keri:ETestPrefix", None, None);
928 let policy = default_policy();
929 let now = Utc::now();
930 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
931
932 let decision =
933 evaluate_with_witness(&identity, &att, &policy, now, local_head, &[]).unwrap();
934
935 assert_eq!(decision.outcome, Outcome::Allow);
936 }
937
938 #[test]
939 fn evaluate_with_witness_noop_delegates() {
940 let identity = make_key_state("ETestPrefix");
941 let att = make_attestation("did:keri:ETestPrefix", None, None);
942 let policy = default_policy();
943 let now = Utc::now();
944 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
945
946 let noop = NoOpWitness;
947 let witnesses: &[&dyn WitnessProvider] = &[&noop];
948
949 let decision =
950 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
951
952 assert_eq!(decision.outcome, Outcome::Allow);
953 }
954
955 #[test]
956 fn evaluate_with_witness_mismatch_denies() {
957 let identity = make_key_state("ETestPrefix");
958 let att = make_attestation("did:keri:ETestPrefix", None, None);
959 let policy = default_policy();
960 let now = Utc::now();
961 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
962 let different_head =
963 EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
964
965 let witness = MockWitness {
966 head: Some(different_head),
967 quorum: 1,
968 };
969 let witnesses: &[&dyn WitnessProvider] = &[&witness];
970
971 let decision =
972 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
973
974 assert_eq!(decision.outcome, Outcome::Deny);
975 assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
976 }
977
978 #[test]
979 fn evaluate_with_witness_quorum_met_allows() {
980 let identity = make_key_state("ETestPrefix");
981 let att = make_attestation("did:keri:ETestPrefix", None, None);
982 let policy = default_policy();
983 let now = Utc::now();
984 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
985
986 let witness = MockWitness {
987 head: Some(local_head),
988 quorum: 1,
989 };
990 let witnesses: &[&dyn WitnessProvider] = &[&witness];
991
992 let decision =
993 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
994
995 assert_eq!(decision.outcome, Outcome::Allow);
996 }
997
998 #[test]
999 fn evaluate_with_witness_quorum_met_denies_when_policy_denies() {
1000 let identity = make_key_state("ETestPrefix");
1001 let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None); let policy = default_policy();
1003 let now = Utc::now();
1004 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1005
1006 let witness = MockWitness {
1007 head: Some(local_head),
1008 quorum: 1,
1009 };
1010 let witnesses: &[&dyn WitnessProvider] = &[&witness];
1011
1012 let decision =
1013 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1014
1015 assert_eq!(decision.outcome, Outcome::Deny);
1016 assert_eq!(decision.reason, ReasonCode::Revoked);
1017 }
1018
1019 #[test]
1020 fn evaluate_with_witness_multiple_witnesses_quorum() {
1021 let identity = make_key_state("ETestPrefix");
1022 let att = make_attestation("did:keri:ETestPrefix", None, None);
1023 let policy = default_policy();
1024 let now = Utc::now();
1025 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1026 let different_head =
1027 EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
1028
1029 let w1 = MockWitness {
1030 head: Some(local_head),
1031 quorum: 2,
1032 };
1033 let w2 = MockWitness {
1034 head: Some(local_head),
1035 quorum: 2,
1036 };
1037 let w3 = MockWitness {
1038 head: Some(different_head),
1039 quorum: 2,
1040 };
1041 let witnesses: &[&dyn WitnessProvider] = &[&w1, &w2, &w3];
1042
1043 let decision =
1044 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1045
1046 assert_eq!(decision.outcome, Outcome::Allow);
1047 }
1048
1049 #[test]
1050 fn evaluate_with_witness_no_opinions_delegates() {
1051 let identity = make_key_state("ETestPrefix");
1052 let att = make_attestation("did:keri:ETestPrefix", None, None);
1053 let policy = default_policy();
1054 let now = Utc::now();
1055 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1056
1057 let witness = MockWitness {
1058 head: None,
1059 quorum: 1,
1060 };
1061 let witnesses: &[&dyn WitnessProvider] = &[&witness];
1062
1063 let decision =
1064 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1065
1066 assert_eq!(decision.outcome, Outcome::Allow);
1067 }
1068
1069 fn make_test_receipt(
1074 event_said: &str,
1075 witness_did: &str,
1076 seq: u128,
1077 ) -> auths_core::witness::StoredReceipt {
1078 auths_core::witness::StoredReceipt {
1079 signed: auths_core::witness::SignedReceipt {
1080 receipt: auths_core::witness::Receipt {
1081 v: auths_keri::VersionString::placeholder(),
1082 t: auths_core::witness::ReceiptTag,
1083 d: Said::new_unchecked(event_said.to_string()),
1084 i: Prefix::new_unchecked("EController".to_string()),
1085 s: auths_keri::KeriSequence::new(seq),
1086 },
1087 signature: vec![],
1088 },
1089 witness: Prefix::new_unchecked(witness_did.to_string()),
1090 }
1091 }
1092
1093 #[test]
1094 fn verify_receipts_meets_threshold() {
1095 let receipts = EventReceipts::new(
1096 "ESAID123",
1097 vec![
1098 make_test_receipt("ESAID123", "did:key:w1", 0),
1099 make_test_receipt("ESAID123", "did:key:w2", 0),
1100 ],
1101 );
1102
1103 let result = verify_receipts(&receipts, 2, None);
1104 assert_eq!(result, ReceiptVerificationResult::Valid);
1105 }
1106
1107 #[test]
1108 fn verify_receipts_insufficient() {
1109 let receipts = EventReceipts::new(
1110 "ESAID123",
1111 vec![make_test_receipt("ESAID123", "did:key:w1", 0)],
1112 );
1113
1114 let result = verify_receipts(&receipts, 2, None);
1115 assert!(matches!(
1116 result,
1117 ReceiptVerificationResult::InsufficientReceipts {
1118 required: 2,
1119 got: 1
1120 }
1121 ));
1122 }
1123
1124 #[test]
1125 fn verify_receipts_duplicity() {
1126 let receipts = EventReceipts {
1127 event_said: Said::new_unchecked("ESAID_A".to_string()),
1128 receipts: vec![
1129 make_test_receipt("ESAID_A", "did:key:w1", 0),
1130 make_test_receipt("ESAID_B", "did:key:w2", 0), ],
1132 };
1133
1134 let result = verify_receipts(&receipts, 1, None);
1135 assert!(matches!(
1136 result,
1137 ReceiptVerificationResult::Duplicity { .. }
1138 ));
1139 }
1140
1141 #[test]
1142 fn evaluate_with_receipts_valid() {
1143 let identity = make_key_state("ETestPrefix");
1144 let att = make_attestation("did:keri:ETestPrefix", None, None);
1145 let policy = default_policy();
1146 let now = Utc::now();
1147 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1148 let receipts = EventReceipts::new(
1149 "ESAID",
1150 vec![
1151 make_test_receipt("ESAID", "did:key:w1", 0),
1152 make_test_receipt("ESAID", "did:key:w2", 0),
1153 ],
1154 );
1155
1156 let decision = evaluate_with_receipts(
1157 &identity,
1158 &att,
1159 &policy,
1160 now,
1161 local_head,
1162 &[],
1163 &receipts,
1164 2,
1165 None,
1166 )
1167 .unwrap();
1168
1169 assert_eq!(decision.outcome, Outcome::Allow);
1170 }
1171
1172 #[test]
1173 fn evaluate_with_receipts_insufficient_denies() {
1174 let identity = make_key_state("ETestPrefix");
1175 let att = make_attestation("did:keri:ETestPrefix", None, None);
1176 let policy = default_policy();
1177 let now = Utc::now();
1178 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1179 let receipts =
1180 EventReceipts::new("ESAID", vec![make_test_receipt("ESAID", "did:key:w1", 0)]);
1181
1182 let decision = evaluate_with_receipts(
1183 &identity,
1184 &att,
1185 &policy,
1186 now,
1187 local_head,
1188 &[],
1189 &receipts,
1190 2, None,
1192 )
1193 .unwrap();
1194
1195 assert_eq!(decision.outcome, Outcome::Deny);
1196 assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
1197 }
1198}