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: &[auths_keri::Capability],
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.as_str()).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 ..
275 } = presentation
276 else {
277 return Err(PolicyBridgeError::NoHolderProof);
278 };
279
280 let mut ctx = EvalContext::try_from_strings(now, issuer.as_str(), subject.as_str())?;
281 ctx = ctx.revoked(false);
282
283 let caps: Vec<CanonicalCapability> = caps
284 .iter()
285 .filter_map(|c| CanonicalCapability::parse(c.as_str()).ok())
286 .collect();
287 ctx = ctx.capabilities(caps);
288
289 if let Some(role) = role {
290 ctx = ctx.role(role.clone());
291 }
292 if let Some(expires_at) = expires_at {
293 ctx = ctx.expires_at(*expires_at);
294 }
295 if let Ok(did) = CanonicalDid::parse(issuer.as_str()) {
296 ctx = ctx.delegated_by(did);
297 }
298
299 Ok(ctx)
300}
301
302pub fn evaluate_compiled(
342 att: &Attestation,
343 policy: &CompiledPolicy,
344 now: DateTime<Utc>,
345) -> Result<Decision, DidParseError> {
346 let ctx = context_from_attestation(att, now)?;
347 Ok(evaluate_strict(policy, &ctx))
348}
349
350pub fn evaluate_with_witness(
378 identity: &KeyState,
379 att: &Attestation,
380 policy: &CompiledPolicy,
381 now: DateTime<Utc>,
382 local_head: EventHash,
383 witnesses: &[&dyn WitnessProvider],
384) -> Result<Decision, DidParseError> {
385 if witnesses.is_empty() {
386 return evaluate_compiled(att, policy, now);
387 }
388
389 let required_quorum = witnesses.first().map(|w| w.quorum()).unwrap_or(1);
390
391 if required_quorum == 0 {
392 return evaluate_compiled(att, policy, now);
393 }
394
395 let mut matching = 0;
396 let mut total_opinions = 0;
397
398 for witness in witnesses {
399 if let Some(head) = witness.observe_identity_head(&identity.prefix) {
400 total_opinions += 1;
401 if head == local_head {
402 matching += 1;
403 }
404 }
405 }
406
407 if total_opinions == 0 {
408 return evaluate_compiled(att, policy, now);
409 }
410
411 if matching < required_quorum {
412 return Ok(Decision::deny(
413 ReasonCode::WitnessQuorumNotMet,
414 format!(
415 "Witness quorum not met: {}/{} matching, {} required",
416 matching, total_opinions, required_quorum
417 ),
418 ));
419 }
420
421 evaluate_compiled(att, policy, now)
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
426pub enum ReceiptVerificationResult {
427 Valid,
429 InsufficientReceipts { required: usize, got: usize },
431 Duplicity { event_a: Said, event_b: Said },
433 InvalidSignature { witness_did: CanonicalDid },
435}
436
437pub trait WitnessKeyResolver: Send + Sync {
441 fn get_public_key(&self, witness_did: &str) -> Option<Vec<u8>>;
443}
444
445#[cfg(feature = "git-storage")]
469pub fn verify_receipts(
470 receipts: &EventReceipts,
471 threshold: usize,
472 key_resolver: Option<&dyn WitnessKeyResolver>,
473) -> ReceiptVerificationResult {
474 let unique = receipts.unique_witness_count();
476 if unique < threshold {
477 return ReceiptVerificationResult::InsufficientReceipts {
478 required: threshold,
479 got: unique,
480 };
481 }
482
483 if let Err(e) = check_receipt_consistency(&receipts.receipts) {
485 return ReceiptVerificationResult::Duplicity {
486 event_a: receipts.event_said.clone(),
487 event_b: Said::new_unchecked(format!("conflicting: {}", e)),
488 };
489 }
490
491 if let Some(resolver) = key_resolver {
496 for stored in &receipts.receipts {
497 let witness = stored.witness.as_str();
498 if let Some(public_key) = resolver.get_public_key(witness) {
499 let witness_curve = auths_crypto::did_key_decode(witness)
500 .map(|d| d.curve())
501 .unwrap_or_default();
502 let typed_pk =
503 match auths_verifier::decode_public_key_bytes(&public_key, witness_curve) {
504 Ok(pk) => pk,
505 Err(_) => {
506 #[allow(clippy::disallowed_methods)]
507 return ReceiptVerificationResult::InvalidSignature {
509 witness_did: CanonicalDid::new_unchecked(witness),
510 };
511 }
512 };
513 match verify_receipt_signature(&stored.signed.receipt, &typed_pk) {
514 Ok(true) => continue,
515 Ok(false) | Err(_) => {
516 return ReceiptVerificationResult::InvalidSignature {
517 #[allow(clippy::disallowed_methods)] witness_did: CanonicalDid::new_unchecked(witness),
519 };
520 }
521 }
522 }
523 }
526 }
527
528 ReceiptVerificationResult::Valid
529}
530
531#[cfg(feature = "git-storage")]
556#[allow(clippy::too_many_arguments)]
557pub fn evaluate_with_receipts(
558 identity: &KeyState,
559 att: &Attestation,
560 policy: &CompiledPolicy,
561 now: DateTime<Utc>,
562 local_head: EventHash,
563 witnesses: &[&dyn WitnessProvider],
564 receipts: &EventReceipts,
565 threshold: usize,
566 key_resolver: Option<&dyn WitnessKeyResolver>,
567) -> Result<Decision, DidParseError> {
568 match verify_receipts(receipts, threshold, key_resolver) {
569 ReceiptVerificationResult::Valid => {}
570 ReceiptVerificationResult::InsufficientReceipts { required, got } => {
571 return Ok(Decision::deny(
572 ReasonCode::WitnessQuorumNotMet,
573 format!(
574 "Insufficient receipts: {} required, {} present",
575 required, got
576 ),
577 ));
578 }
579 ReceiptVerificationResult::Duplicity { event_a, event_b } => {
580 return Ok(Decision::deny(
581 ReasonCode::WitnessQuorumNotMet,
582 format!("Duplicity detected: {} vs {}", event_a, event_b),
583 ));
584 }
585 ReceiptVerificationResult::InvalidSignature { witness_did } => {
586 return Ok(Decision::deny(
587 ReasonCode::WitnessQuorumNotMet,
588 format!("Invalid receipt signature from witness: {}", witness_did),
589 ));
590 }
591 }
592
593 evaluate_with_witness(identity, att, policy, now, local_head, witnesses)
594}
595
596#[cfg(test)]
597#[allow(clippy::disallowed_methods)]
598mod tests {
599 use super::*;
600 use auths_core::witness::NoOpWitness;
601 use auths_keri::{CesrKey, Prefix, Said, Threshold};
602 use auths_verifier::AttestationBuilder;
603 use chrono::Duration;
604
605 struct MockWitness {
607 head: Option<EventHash>,
608 quorum: usize,
609 }
610
611 impl WitnessProvider for MockWitness {
612 fn observe_identity_head(&self, _prefix: &Prefix) -> Option<EventHash> {
613 self.head
614 }
615
616 fn quorum(&self) -> usize {
617 self.quorum
618 }
619 }
620
621 fn make_key_state(prefix: &str) -> KeyState {
622 KeyState::from_inception(
623 Prefix::new_unchecked(prefix.to_string()),
624 vec![CesrKey::new_unchecked("DTestKey".to_string())],
625 vec![Said::new_unchecked("ENextCommitment".to_string())],
626 Threshold::Simple(1),
627 Threshold::Simple(1),
628 Said::new_unchecked("ETestSaid".to_string()),
629 vec![],
630 Threshold::Simple(0),
631 vec![],
632 )
633 }
634
635 fn make_attestation(
636 issuer: &str,
637 revoked_at: Option<DateTime<Utc>>,
638 expires_at: Option<DateTime<Utc>>,
639 ) -> Attestation {
640 AttestationBuilder::default()
641 .rid("test")
642 .issuer(issuer)
643 .subject("did:key:zSubject")
644 .revoked_at(revoked_at)
645 .expires_at(expires_at)
646 .build()
647 }
648
649 fn default_policy() -> CompiledPolicy {
650 PolicyBuilder::new().not_revoked().not_expired().build()
651 }
652
653 #[test]
654 fn context_from_attestation_basic() {
655 let att = make_attestation("did:keri:ETest", None, None);
656 let now = Utc::now();
657 let ctx = context_from_attestation(&att, now).unwrap();
658
659 assert_eq!(ctx.issuer.as_str(), "did:keri:ETest");
660 assert_eq!(ctx.subject.as_str(), "did:key:zSubject");
661 assert!(!ctx.revoked);
662 }
663
664 #[test]
665 fn context_from_attestation_has_no_capabilities_or_role() {
666 let att = make_attestation("did:keri:ETest", None, None);
670 let now = Utc::now();
671 let ctx = context_from_attestation(&att, now).unwrap();
672
673 assert!(
674 ctx.capabilities.is_empty(),
675 "attestation caps must not enter the policy context"
676 );
677 assert_eq!(
678 ctx.role, None,
679 "attestation role must not enter the policy context"
680 );
681 }
682
683 #[test]
684 fn caps_absent_without_valid_credential() {
685 let att = make_attestation("did:keri:ETestPrefix", None, None);
688 let policy = PolicyBuilder::new()
689 .not_revoked()
690 .require_capability("sign_commit")
691 .build();
692 let now = Utc::now();
693
694 let decision = evaluate_compiled(&att, &policy, now).unwrap();
695 assert_eq!(decision.outcome, Outcome::Deny);
696 assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
697 }
698
699 const CRED_ISSUER: &str = "did:keri:EIssuerCredential";
704 const CRED_SUBJECT: &str = "did:keri:ESubjectCredential";
705
706 fn valid_presentation(
708 caps: &[&str],
709 role: Option<&str>,
710 expires_at: Option<DateTime<Utc>>,
711 ) -> PresentationVerdict {
712 PresentationVerdict::Valid {
713 issuer: auths_verifier::IdentityDID::parse(CRED_ISSUER).expect("valid test issuer"),
714 subject: auths_verifier::CanonicalDid::parse(CRED_SUBJECT).expect("valid test subject"),
715 subject_root: auths_verifier::CanonicalDid::parse(CRED_SUBJECT)
716 .expect("valid test subject"),
717 caps: caps
718 .iter()
719 .map(|c| auths_verifier::Capability::parse(c).expect("valid test capability"))
720 .collect(),
721 role: role.map(str::to_string),
722 expires_at,
723 freshness: auths_verifier::Freshness::Unknown,
724 as_of: 0,
725 }
726 }
727
728 #[test]
729 fn policy_reads_caps_from_credential() {
730 let presentation = valid_presentation(&["sign_commit"], Some("deployer"), None);
731 let now = Utc::now();
732
733 let ctx = context_from_credential(&presentation, now).unwrap();
734 assert_eq!(ctx.issuer.as_str(), CRED_ISSUER);
735 assert_eq!(ctx.subject.as_str(), CRED_SUBJECT);
736 assert!(!ctx.revoked);
737 assert_eq!(ctx.capabilities.len(), 1);
738 assert_eq!(ctx.capabilities[0].as_str(), "sign_commit");
739 assert_eq!(ctx.role.as_deref(), Some("deployer"));
740
741 let policy = PolicyBuilder::new()
742 .not_revoked()
743 .require_capability("sign_commit")
744 .build();
745 assert_eq!(evaluate_strict(&policy, &ctx).outcome, Outcome::Allow);
746 }
747
748 #[test]
749 fn raw_acdc_without_presentation_yields_no_authority() {
750 let now = Utc::now();
754 for verdict in [
755 PresentationVerdict::HolderNotCurrentKey,
756 PresentationVerdict::WrongAudience,
757 PresentationVerdict::NonceMismatchOrConsumed,
758 PresentationVerdict::Expired,
759 PresentationVerdict::SubjectKelInvalid,
760 PresentationVerdict::CredentialNotValid(
761 auths_verifier::CredentialVerdict::SaidMismatch,
762 ),
763 ] {
764 let result = context_from_credential(&verdict, now);
765 assert!(
766 matches!(result, Err(PolicyBridgeError::NoHolderProof)),
767 "non-Valid verdict {verdict:?} must fail closed, got {result:?}"
768 );
769 }
770 }
771
772 #[test]
773 fn capability_round_trips_into_acdc() {
774 use auths_keri::{AgentScope, decode_agent_scope, encode_agent_scope};
775 use auths_verifier::Capability;
776
777 let raw = "repo:foo-bar_baz";
779
780 let scope = AgentScope {
782 capabilities: vec![Capability::parse(raw).unwrap()],
783 expires_at: Some(99),
784 };
785 let encoded = encode_agent_scope("Eagent", &scope);
786 let (prefix, decoded) = decode_agent_scope(&encoded).unwrap();
787 assert_eq!(prefix, "Eagent");
788 assert_eq!(decoded.capabilities, vec![Capability::parse(raw).unwrap()]);
789
790 let acdc_capability_json = decoded
792 .capabilities
793 .iter()
794 .map(|c| c.as_str())
795 .collect::<Vec<_>>()
796 .join(",");
797 assert!(
798 !acdc_capability_json.contains(','),
799 "single cap stays comma-free; the join separator must not appear inside a cap"
800 );
801
802 for cap in acdc_capability_json.split(',') {
804 let canonical = CanonicalCapability::parse(cap).unwrap();
805 assert_eq!(canonical.as_str(), raw);
806 }
807
808 let att_cap = Capability::parse(raw).unwrap();
810 let canonical = CanonicalCapability::parse(&att_cap.to_string()).unwrap();
811 assert_eq!(canonical.as_str(), raw);
812
813 assert!(CanonicalCapability::parse("a,b").is_err());
815 }
816
817 #[test]
818 fn agentscope_seal_vs_acdc_precedence_documented() {
819 assert_eq!(CapsSource::governing(true, true), Some(CapsSource::Acdc));
822 assert_eq!(CapsSource::governing(false, true), Some(CapsSource::Acdc));
823 assert_eq!(
824 CapsSource::governing(true, false),
825 Some(CapsSource::AgentScopeSeal)
826 );
827 assert_eq!(CapsSource::governing(false, false), None);
828 }
829
830 #[test]
831 fn evaluate_compiled_allows_valid_attestation() {
832 let att = make_attestation("did:keri:ETestPrefix", None, 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::Allow);
838 }
839
840 #[test]
841 fn evaluate_compiled_denies_revoked() {
842 let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None);
843 let policy = default_policy();
844 let now = Utc::now();
845
846 let decision = evaluate_compiled(&att, &policy, now).unwrap();
847 assert_eq!(decision.outcome, Outcome::Deny);
848 assert_eq!(decision.reason, ReasonCode::Revoked);
849 }
850
851 #[test]
852 fn evaluate_compiled_denies_expired() {
853 let past = Utc::now() - Duration::hours(1);
854 let att = make_attestation("did:keri:ETestPrefix", None, Some(past));
855 let policy = default_policy();
856 let now = Utc::now();
857
858 let decision = evaluate_compiled(&att, &policy, now).unwrap();
859 assert_eq!(decision.outcome, Outcome::Deny);
860 assert_eq!(decision.reason, ReasonCode::Expired);
861 }
862
863 #[test]
864 fn evaluate_compiled_allows_not_yet_expired() {
865 let future = Utc::now() + Duration::hours(1);
866 let att = make_attestation("did:keri:ETestPrefix", None, Some(future));
867 let policy = default_policy();
868 let now = Utc::now();
869
870 let decision = evaluate_compiled(&att, &policy, now).unwrap();
871 assert_eq!(decision.outcome, Outcome::Allow);
872 }
873
874 #[test]
875 fn evaluate_compiled_denies_issuer_mismatch() {
876 let att = make_attestation("did:keri:EWrongPrefix", None, None);
877 let policy = PolicyBuilder::new()
878 .not_revoked()
879 .require_issuer("did:keri:ETestPrefix")
880 .build();
881 let now = Utc::now();
882
883 let decision = evaluate_compiled(&att, &policy, now).unwrap();
884 assert_eq!(decision.outcome, Outcome::Deny);
885 assert_eq!(decision.reason, ReasonCode::IssuerMismatch);
886 }
887
888 #[test]
889 fn evaluate_compiled_denies_missing_capability() {
890 let att = make_attestation("did:keri:ETestPrefix", None, None);
891 let policy = PolicyBuilder::new()
892 .not_revoked()
893 .require_capability("sign_commit")
894 .build();
895 let now = Utc::now();
896
897 let decision = evaluate_compiled(&att, &policy, now).unwrap();
898 assert_eq!(decision.outcome, Outcome::Deny);
899 assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
900 }
901
902 #[test]
903 fn evaluate_compiled_allows_with_capability_from_credential() {
904 let presentation = valid_presentation(&["sign_commit"], None, None);
908 let policy = PolicyBuilder::new()
909 .not_revoked()
910 .require_capability("sign_commit")
911 .build();
912 let now = Utc::now();
913
914 let ctx = context_from_credential(&presentation, now).unwrap();
915 assert_eq!(evaluate_strict(&policy, &ctx).outcome, Outcome::Allow);
916 }
917
918 #[test]
919 fn evaluate_compiled_is_deterministic() {
920 let att = make_attestation("did:keri:ETestPrefix", None, None);
921 let policy = default_policy();
922 let now = Utc::now();
923
924 let decision1 = evaluate_compiled(&att, &policy, now).unwrap();
925 let decision2 = evaluate_compiled(&att, &policy, now).unwrap();
926
927 assert_eq!(decision1, decision2);
928 }
929
930 #[test]
935 fn evaluate_with_witness_no_witnesses_delegates() {
936 let identity = make_key_state("ETestPrefix");
937 let att = make_attestation("did:keri:ETestPrefix", None, None);
938 let policy = default_policy();
939 let now = Utc::now();
940 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
941
942 let decision =
943 evaluate_with_witness(&identity, &att, &policy, now, local_head, &[]).unwrap();
944
945 assert_eq!(decision.outcome, Outcome::Allow);
946 }
947
948 #[test]
949 fn evaluate_with_witness_noop_delegates() {
950 let identity = make_key_state("ETestPrefix");
951 let att = make_attestation("did:keri:ETestPrefix", None, None);
952 let policy = default_policy();
953 let now = Utc::now();
954 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
955
956 let noop = NoOpWitness;
957 let witnesses: &[&dyn WitnessProvider] = &[&noop];
958
959 let decision =
960 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
961
962 assert_eq!(decision.outcome, Outcome::Allow);
963 }
964
965 #[test]
966 fn evaluate_with_witness_mismatch_denies() {
967 let identity = make_key_state("ETestPrefix");
968 let att = make_attestation("did:keri:ETestPrefix", None, None);
969 let policy = default_policy();
970 let now = Utc::now();
971 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
972 let different_head =
973 EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
974
975 let witness = MockWitness {
976 head: Some(different_head),
977 quorum: 1,
978 };
979 let witnesses: &[&dyn WitnessProvider] = &[&witness];
980
981 let decision =
982 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
983
984 assert_eq!(decision.outcome, Outcome::Deny);
985 assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
986 }
987
988 #[test]
989 fn evaluate_with_witness_quorum_met_allows() {
990 let identity = make_key_state("ETestPrefix");
991 let att = make_attestation("did:keri:ETestPrefix", None, None);
992 let policy = default_policy();
993 let now = Utc::now();
994 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
995
996 let witness = MockWitness {
997 head: Some(local_head),
998 quorum: 1,
999 };
1000 let witnesses: &[&dyn WitnessProvider] = &[&witness];
1001
1002 let decision =
1003 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1004
1005 assert_eq!(decision.outcome, Outcome::Allow);
1006 }
1007
1008 #[test]
1009 fn evaluate_with_witness_quorum_met_denies_when_policy_denies() {
1010 let identity = make_key_state("ETestPrefix");
1011 let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None); let policy = default_policy();
1013 let now = Utc::now();
1014 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1015
1016 let witness = MockWitness {
1017 head: Some(local_head),
1018 quorum: 1,
1019 };
1020 let witnesses: &[&dyn WitnessProvider] = &[&witness];
1021
1022 let decision =
1023 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1024
1025 assert_eq!(decision.outcome, Outcome::Deny);
1026 assert_eq!(decision.reason, ReasonCode::Revoked);
1027 }
1028
1029 #[test]
1030 fn evaluate_with_witness_multiple_witnesses_quorum() {
1031 let identity = make_key_state("ETestPrefix");
1032 let att = make_attestation("did:keri:ETestPrefix", None, None);
1033 let policy = default_policy();
1034 let now = Utc::now();
1035 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1036 let different_head =
1037 EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
1038
1039 let w1 = MockWitness {
1040 head: Some(local_head),
1041 quorum: 2,
1042 };
1043 let w2 = MockWitness {
1044 head: Some(local_head),
1045 quorum: 2,
1046 };
1047 let w3 = MockWitness {
1048 head: Some(different_head),
1049 quorum: 2,
1050 };
1051 let witnesses: &[&dyn WitnessProvider] = &[&w1, &w2, &w3];
1052
1053 let decision =
1054 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1055
1056 assert_eq!(decision.outcome, Outcome::Allow);
1057 }
1058
1059 #[test]
1060 fn evaluate_with_witness_no_opinions_delegates() {
1061 let identity = make_key_state("ETestPrefix");
1062 let att = make_attestation("did:keri:ETestPrefix", None, None);
1063 let policy = default_policy();
1064 let now = Utc::now();
1065 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1066
1067 let witness = MockWitness {
1068 head: None,
1069 quorum: 1,
1070 };
1071 let witnesses: &[&dyn WitnessProvider] = &[&witness];
1072
1073 let decision =
1074 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
1075
1076 assert_eq!(decision.outcome, Outcome::Allow);
1077 }
1078
1079 fn make_test_receipt(
1084 event_said: &str,
1085 witness_did: &str,
1086 seq: u128,
1087 ) -> auths_core::witness::StoredReceipt {
1088 auths_core::witness::StoredReceipt {
1089 signed: auths_core::witness::SignedReceipt {
1090 receipt: auths_core::witness::Receipt {
1091 v: auths_keri::VersionString::placeholder(),
1092 t: auths_core::witness::ReceiptTag,
1093 d: Said::new_unchecked(event_said.to_string()),
1094 i: Prefix::new_unchecked("EController".to_string()),
1095 s: auths_keri::KeriSequence::new(seq),
1096 },
1097 signature: vec![],
1098 },
1099 witness: Prefix::new_unchecked(witness_did.to_string()),
1100 }
1101 }
1102
1103 #[test]
1104 fn verify_receipts_meets_threshold() {
1105 let receipts = EventReceipts::new(
1106 "ESAID123",
1107 vec![
1108 make_test_receipt("ESAID123", "did:key:w1", 0),
1109 make_test_receipt("ESAID123", "did:key:w2", 0),
1110 ],
1111 );
1112
1113 let result = verify_receipts(&receipts, 2, None);
1114 assert_eq!(result, ReceiptVerificationResult::Valid);
1115 }
1116
1117 #[test]
1118 fn verify_receipts_insufficient() {
1119 let receipts = EventReceipts::new(
1120 "ESAID123",
1121 vec![make_test_receipt("ESAID123", "did:key:w1", 0)],
1122 );
1123
1124 let result = verify_receipts(&receipts, 2, None);
1125 assert!(matches!(
1126 result,
1127 ReceiptVerificationResult::InsufficientReceipts {
1128 required: 2,
1129 got: 1
1130 }
1131 ));
1132 }
1133
1134 #[test]
1135 fn verify_receipts_duplicity() {
1136 let receipts = EventReceipts {
1137 event_said: Said::new_unchecked("ESAID_A".to_string()),
1138 receipts: vec![
1139 make_test_receipt("ESAID_A", "did:key:w1", 0),
1140 make_test_receipt("ESAID_B", "did:key:w2", 0), ],
1142 };
1143
1144 let result = verify_receipts(&receipts, 1, None);
1145 assert!(matches!(
1146 result,
1147 ReceiptVerificationResult::Duplicity { .. }
1148 ));
1149 }
1150
1151 #[test]
1152 fn evaluate_with_receipts_valid() {
1153 let identity = make_key_state("ETestPrefix");
1154 let att = make_attestation("did:keri:ETestPrefix", None, None);
1155 let policy = default_policy();
1156 let now = Utc::now();
1157 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1158 let receipts = EventReceipts::new(
1159 "ESAID",
1160 vec![
1161 make_test_receipt("ESAID", "did:key:w1", 0),
1162 make_test_receipt("ESAID", "did:key:w2", 0),
1163 ],
1164 );
1165
1166 let decision = evaluate_with_receipts(
1167 &identity,
1168 &att,
1169 &policy,
1170 now,
1171 local_head,
1172 &[],
1173 &receipts,
1174 2,
1175 None,
1176 )
1177 .unwrap();
1178
1179 assert_eq!(decision.outcome, Outcome::Allow);
1180 }
1181
1182 #[test]
1183 fn evaluate_with_receipts_insufficient_denies() {
1184 let identity = make_key_state("ETestPrefix");
1185 let att = make_attestation("did:keri:ETestPrefix", None, None);
1186 let policy = default_policy();
1187 let now = Utc::now();
1188 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
1189 let receipts =
1190 EventReceipts::new("ESAID", vec![make_test_receipt("ESAID", "did:key:w1", 0)]);
1191
1192 let decision = evaluate_with_receipts(
1193 &identity,
1194 &att,
1195 &policy,
1196 now,
1197 local_head,
1198 &[],
1199 &receipts,
1200 2, None,
1202 )
1203 .unwrap();
1204
1205 assert_eq!(decision.outcome, Outcome::Deny);
1206 assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
1207 }
1208}