1use auths_core::witness::{EventHash, WitnessProvider};
40use auths_policy::{CanonicalCapability, DidParseError, evaluate_strict};
41use auths_verifier::core::Attestation;
42use auths_verifier::types::DeviceDID;
43use chrono::{DateTime, Utc};
44
45use crate::keri::KeyState;
46use crate::keri::event::EventReceipts;
47use crate::keri::types::Said;
48#[cfg(feature = "git-storage")]
49use crate::storage::receipts::{check_receipt_consistency, verify_receipt_signature};
50
51pub use auths_policy::{
53 CompileError, CompiledPolicy, Decision, EvalContext, Expr, Outcome, PolicyBuilder,
54 PolicyLimits, ReasonCode, compile, compile_from_json,
55};
56
57pub fn context_from_attestation(
72 att: &Attestation,
73 now: DateTime<Utc>,
74) -> Result<EvalContext, DidParseError> {
75 let mut ctx = EvalContext::try_from_strings(now, &att.issuer, att.subject.as_ref())?;
76
77 ctx = ctx.revoked(att.is_revoked());
78
79 let caps: Vec<CanonicalCapability> = att
81 .capabilities
82 .iter()
83 .filter_map(|c| CanonicalCapability::parse(&c.to_string()).ok())
84 .collect();
85 ctx = ctx.capabilities(caps);
86
87 if let Some(expires_at) = att.expires_at {
88 ctx = ctx.expires_at(expires_at);
89 }
90
91 if let Some(ref role) = att.role {
92 ctx = ctx.role(role.to_string());
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
116pub fn evaluate_compiled(
156 att: &Attestation,
157 policy: &CompiledPolicy,
158 now: DateTime<Utc>,
159) -> Result<Decision, DidParseError> {
160 let ctx = context_from_attestation(att, now)?;
161 Ok(evaluate_strict(policy, &ctx))
162}
163
164pub fn evaluate_with_witness(
192 identity: &KeyState,
193 att: &Attestation,
194 policy: &CompiledPolicy,
195 now: DateTime<Utc>,
196 local_head: EventHash,
197 witnesses: &[&dyn WitnessProvider],
198) -> Result<Decision, DidParseError> {
199 if witnesses.is_empty() {
200 return evaluate_compiled(att, policy, now);
201 }
202
203 let required_quorum = witnesses.first().map(|w| w.quorum()).unwrap_or(1);
204
205 if required_quorum == 0 {
206 return evaluate_compiled(att, policy, now);
207 }
208
209 let mut matching = 0;
210 let mut total_opinions = 0;
211
212 for witness in witnesses {
213 if let Some(head) = witness.observe_identity_head(&identity.prefix) {
214 total_opinions += 1;
215 if head == local_head {
216 matching += 1;
217 }
218 }
219 }
220
221 if total_opinions == 0 {
222 return evaluate_compiled(att, policy, now);
223 }
224
225 if matching < required_quorum {
226 return Ok(Decision::deny(
227 ReasonCode::WitnessQuorumNotMet,
228 format!(
229 "Witness quorum not met: {}/{} matching, {} required",
230 matching, total_opinions, required_quorum
231 ),
232 ));
233 }
234
235 evaluate_compiled(att, policy, now)
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum ReceiptVerificationResult {
241 Valid,
243 InsufficientReceipts { required: usize, got: usize },
245 Duplicity { event_a: Said, event_b: Said },
247 InvalidSignature { witness_did: DeviceDID },
249}
250
251pub trait WitnessKeyResolver: Send + Sync {
255 fn get_public_key(&self, witness_did: &str) -> Option<Vec<u8>>;
257}
258
259#[cfg(feature = "git-storage")]
283pub fn verify_receipts(
284 receipts: &EventReceipts,
285 threshold: usize,
286 key_resolver: Option<&dyn WitnessKeyResolver>,
287) -> ReceiptVerificationResult {
288 let unique = receipts.unique_witness_count();
290 if unique < threshold {
291 return ReceiptVerificationResult::InsufficientReceipts {
292 required: threshold,
293 got: unique,
294 };
295 }
296
297 if let Err(e) = check_receipt_consistency(&receipts.receipts) {
299 return ReceiptVerificationResult::Duplicity {
300 event_a: receipts.event_said.clone(),
301 event_b: Said::new_unchecked(format!("conflicting: {}", e)),
302 };
303 }
304
305 if let Some(resolver) = key_resolver {
307 for receipt in &receipts.receipts {
308 if let Some(public_key) = resolver.get_public_key(&receipt.i) {
309 match verify_receipt_signature(receipt, &public_key) {
310 Ok(true) => continue,
311 Ok(false) => {
312 return ReceiptVerificationResult::InvalidSignature {
313 #[allow(clippy::disallowed_methods)] witness_did: DeviceDID::new_unchecked(&receipt.i),
315 };
316 }
317 Err(_) => {
318 return ReceiptVerificationResult::InvalidSignature {
319 #[allow(clippy::disallowed_methods)] witness_did: DeviceDID::new_unchecked(&receipt.i),
321 };
322 }
323 }
324 }
325 }
328 }
329
330 ReceiptVerificationResult::Valid
331}
332
333#[cfg(feature = "git-storage")]
358#[allow(clippy::too_many_arguments)]
359pub fn evaluate_with_receipts(
360 identity: &KeyState,
361 att: &Attestation,
362 policy: &CompiledPolicy,
363 now: DateTime<Utc>,
364 local_head: EventHash,
365 witnesses: &[&dyn WitnessProvider],
366 receipts: &EventReceipts,
367 threshold: usize,
368 key_resolver: Option<&dyn WitnessKeyResolver>,
369) -> Result<Decision, DidParseError> {
370 match verify_receipts(receipts, threshold, key_resolver) {
371 ReceiptVerificationResult::Valid => {}
372 ReceiptVerificationResult::InsufficientReceipts { required, got } => {
373 return Ok(Decision::deny(
374 ReasonCode::WitnessQuorumNotMet,
375 format!(
376 "Insufficient receipts: {} required, {} present",
377 required, got
378 ),
379 ));
380 }
381 ReceiptVerificationResult::Duplicity { event_a, event_b } => {
382 return Ok(Decision::deny(
383 ReasonCode::WitnessQuorumNotMet,
384 format!("Duplicity detected: {} vs {}", event_a, event_b),
385 ));
386 }
387 ReceiptVerificationResult::InvalidSignature { witness_did } => {
388 return Ok(Decision::deny(
389 ReasonCode::WitnessQuorumNotMet,
390 format!("Invalid receipt signature from witness: {}", witness_did),
391 ));
392 }
393 }
394
395 evaluate_with_witness(identity, att, policy, now, local_head, witnesses)
396}
397
398#[cfg(test)]
399#[allow(clippy::disallowed_methods)]
400mod tests {
401 use super::*;
402 use auths_core::witness::NoOpWitness;
403 use auths_verifier::AttestationBuilder;
404 use auths_verifier::core::Capability;
405 use auths_verifier::keri::{Prefix, Said};
406 use chrono::Duration;
407
408 struct MockWitness {
410 head: Option<EventHash>,
411 quorum: usize,
412 }
413
414 impl WitnessProvider for MockWitness {
415 fn observe_identity_head(&self, _prefix: &Prefix) -> Option<EventHash> {
416 self.head
417 }
418
419 fn quorum(&self) -> usize {
420 self.quorum
421 }
422 }
423
424 fn make_key_state(prefix: &str) -> KeyState {
425 KeyState {
426 prefix: Prefix::new_unchecked(prefix.to_string()),
427 sequence: 0,
428 current_keys: vec!["DTestKey".to_string()],
429 next_commitment: vec![],
430 last_event_said: Said::new_unchecked("ETestSaid".to_string()),
431 is_abandoned: false,
432 threshold: 1,
433 next_threshold: 1,
434 }
435 }
436
437 fn make_attestation(
438 issuer: &str,
439 revoked_at: Option<DateTime<Utc>>,
440 expires_at: Option<DateTime<Utc>>,
441 ) -> Attestation {
442 AttestationBuilder::default()
443 .rid("test")
444 .issuer(issuer)
445 .subject("did:key:zSubject")
446 .revoked_at(revoked_at)
447 .expires_at(expires_at)
448 .build()
449 }
450
451 fn default_policy() -> CompiledPolicy {
452 PolicyBuilder::new().not_revoked().not_expired().build()
453 }
454
455 #[test]
456 fn context_from_attestation_basic() {
457 let att = make_attestation("did:keri:ETest", None, None);
458 let now = Utc::now();
459 let ctx = context_from_attestation(&att, now).unwrap();
460
461 assert_eq!(ctx.issuer.as_str(), "did:keri:ETest");
462 assert_eq!(ctx.subject.as_str(), "did:key:zSubject");
463 assert!(!ctx.revoked);
464 }
465
466 #[test]
467 fn context_from_attestation_with_capabilities() {
468 let mut att = make_attestation("did:keri:ETest", None, None);
469 att.capabilities = vec![Capability::sign_commit()];
470 let now = Utc::now();
471 let ctx = context_from_attestation(&att, now).unwrap();
472
473 assert_eq!(ctx.capabilities.len(), 1);
474 assert_eq!(ctx.capabilities[0].as_str(), "sign_commit");
475 }
476
477 #[test]
478 fn context_from_attestation_with_role() {
479 let mut att = make_attestation("did:keri:ETest", None, None);
480 att.role = Some(auths_verifier::core::Role::Member);
481 let now = Utc::now();
482 let ctx = context_from_attestation(&att, now).unwrap();
483
484 assert_eq!(ctx.role.as_deref(), Some("member"));
485 }
486
487 #[test]
488 fn evaluate_compiled_allows_valid_attestation() {
489 let att = make_attestation("did:keri:ETestPrefix", None, None);
490 let policy = default_policy();
491 let now = Utc::now();
492
493 let decision = evaluate_compiled(&att, &policy, now).unwrap();
494 assert_eq!(decision.outcome, Outcome::Allow);
495 }
496
497 #[test]
498 fn evaluate_compiled_denies_revoked() {
499 let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None);
500 let policy = default_policy();
501 let now = Utc::now();
502
503 let decision = evaluate_compiled(&att, &policy, now).unwrap();
504 assert_eq!(decision.outcome, Outcome::Deny);
505 assert_eq!(decision.reason, ReasonCode::Revoked);
506 }
507
508 #[test]
509 fn evaluate_compiled_denies_expired() {
510 let past = Utc::now() - Duration::hours(1);
511 let att = make_attestation("did:keri:ETestPrefix", None, Some(past));
512 let policy = default_policy();
513 let now = Utc::now();
514
515 let decision = evaluate_compiled(&att, &policy, now).unwrap();
516 assert_eq!(decision.outcome, Outcome::Deny);
517 assert_eq!(decision.reason, ReasonCode::Expired);
518 }
519
520 #[test]
521 fn evaluate_compiled_allows_not_yet_expired() {
522 let future = Utc::now() + Duration::hours(1);
523 let att = make_attestation("did:keri:ETestPrefix", None, Some(future));
524 let policy = default_policy();
525 let now = Utc::now();
526
527 let decision = evaluate_compiled(&att, &policy, now).unwrap();
528 assert_eq!(decision.outcome, Outcome::Allow);
529 }
530
531 #[test]
532 fn evaluate_compiled_denies_issuer_mismatch() {
533 let att = make_attestation("did:keri:EWrongPrefix", None, None);
534 let policy = PolicyBuilder::new()
535 .not_revoked()
536 .require_issuer("did:keri:ETestPrefix")
537 .build();
538 let now = Utc::now();
539
540 let decision = evaluate_compiled(&att, &policy, now).unwrap();
541 assert_eq!(decision.outcome, Outcome::Deny);
542 assert_eq!(decision.reason, ReasonCode::IssuerMismatch);
543 }
544
545 #[test]
546 fn evaluate_compiled_denies_missing_capability() {
547 let att = make_attestation("did:keri:ETestPrefix", None, None);
548 let policy = PolicyBuilder::new()
549 .not_revoked()
550 .require_capability("sign_commit")
551 .build();
552 let now = Utc::now();
553
554 let decision = evaluate_compiled(&att, &policy, now).unwrap();
555 assert_eq!(decision.outcome, Outcome::Deny);
556 assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
557 }
558
559 #[test]
560 fn evaluate_compiled_allows_with_capability() {
561 let mut att = make_attestation("did:keri:ETestPrefix", None, None);
562 att.capabilities = vec![Capability::sign_commit()];
563 let policy = PolicyBuilder::new()
564 .not_revoked()
565 .require_capability("sign_commit")
566 .build();
567 let now = Utc::now();
568
569 let decision = evaluate_compiled(&att, &policy, now).unwrap();
570 assert_eq!(decision.outcome, Outcome::Allow);
571 }
572
573 #[test]
574 fn evaluate_compiled_is_deterministic() {
575 let att = make_attestation("did:keri:ETestPrefix", None, None);
576 let policy = default_policy();
577 let now = Utc::now();
578
579 let decision1 = evaluate_compiled(&att, &policy, now).unwrap();
580 let decision2 = evaluate_compiled(&att, &policy, now).unwrap();
581
582 assert_eq!(decision1, decision2);
583 }
584
585 #[test]
590 fn evaluate_with_witness_no_witnesses_delegates() {
591 let identity = make_key_state("ETestPrefix");
592 let att = make_attestation("did:keri:ETestPrefix", None, None);
593 let policy = default_policy();
594 let now = Utc::now();
595 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
596
597 let decision =
598 evaluate_with_witness(&identity, &att, &policy, now, local_head, &[]).unwrap();
599
600 assert_eq!(decision.outcome, Outcome::Allow);
601 }
602
603 #[test]
604 fn evaluate_with_witness_noop_delegates() {
605 let identity = make_key_state("ETestPrefix");
606 let att = make_attestation("did:keri:ETestPrefix", None, None);
607 let policy = default_policy();
608 let now = Utc::now();
609 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
610
611 let noop = NoOpWitness;
612 let witnesses: &[&dyn WitnessProvider] = &[&noop];
613
614 let decision =
615 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
616
617 assert_eq!(decision.outcome, Outcome::Allow);
618 }
619
620 #[test]
621 fn evaluate_with_witness_mismatch_denies() {
622 let identity = make_key_state("ETestPrefix");
623 let att = make_attestation("did:keri:ETestPrefix", None, None);
624 let policy = default_policy();
625 let now = Utc::now();
626 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
627 let different_head =
628 EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
629
630 let witness = MockWitness {
631 head: Some(different_head),
632 quorum: 1,
633 };
634 let witnesses: &[&dyn WitnessProvider] = &[&witness];
635
636 let decision =
637 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
638
639 assert_eq!(decision.outcome, Outcome::Deny);
640 assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
641 }
642
643 #[test]
644 fn evaluate_with_witness_quorum_met_allows() {
645 let identity = make_key_state("ETestPrefix");
646 let att = make_attestation("did:keri:ETestPrefix", None, None);
647 let policy = default_policy();
648 let now = Utc::now();
649 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
650
651 let witness = MockWitness {
652 head: Some(local_head),
653 quorum: 1,
654 };
655 let witnesses: &[&dyn WitnessProvider] = &[&witness];
656
657 let decision =
658 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
659
660 assert_eq!(decision.outcome, Outcome::Allow);
661 }
662
663 #[test]
664 fn evaluate_with_witness_quorum_met_denies_when_policy_denies() {
665 let identity = make_key_state("ETestPrefix");
666 let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None); let policy = default_policy();
668 let now = Utc::now();
669 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
670
671 let witness = MockWitness {
672 head: Some(local_head),
673 quorum: 1,
674 };
675 let witnesses: &[&dyn WitnessProvider] = &[&witness];
676
677 let decision =
678 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
679
680 assert_eq!(decision.outcome, Outcome::Deny);
681 assert_eq!(decision.reason, ReasonCode::Revoked);
682 }
683
684 #[test]
685 fn evaluate_with_witness_multiple_witnesses_quorum() {
686 let identity = make_key_state("ETestPrefix");
687 let att = make_attestation("did:keri:ETestPrefix", None, None);
688 let policy = default_policy();
689 let now = Utc::now();
690 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
691 let different_head =
692 EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
693
694 let w1 = MockWitness {
695 head: Some(local_head),
696 quorum: 2,
697 };
698 let w2 = MockWitness {
699 head: Some(local_head),
700 quorum: 2,
701 };
702 let w3 = MockWitness {
703 head: Some(different_head),
704 quorum: 2,
705 };
706 let witnesses: &[&dyn WitnessProvider] = &[&w1, &w2, &w3];
707
708 let decision =
709 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
710
711 assert_eq!(decision.outcome, Outcome::Allow);
712 }
713
714 #[test]
715 fn evaluate_with_witness_no_opinions_delegates() {
716 let identity = make_key_state("ETestPrefix");
717 let att = make_attestation("did:keri:ETestPrefix", None, None);
718 let policy = default_policy();
719 let now = Utc::now();
720 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
721
722 let witness = MockWitness {
723 head: None,
724 quorum: 1,
725 };
726 let witnesses: &[&dyn WitnessProvider] = &[&witness];
727
728 let decision =
729 evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
730
731 assert_eq!(decision.outcome, Outcome::Allow);
732 }
733
734 fn make_test_receipt(
739 event_said: &str,
740 witness_did: &str,
741 seq: u64,
742 ) -> auths_core::witness::Receipt {
743 auths_core::witness::Receipt {
744 v: auths_core::witness::KERI_VERSION.into(),
745 t: auths_core::witness::RECEIPT_TYPE.into(),
746 d: Said::new_unchecked(format!(
747 "E{}",
748 &event_said.chars().skip(1).take(10).collect::<String>()
749 )),
750 i: witness_did.to_string(),
751 s: seq,
752 a: Said::new_unchecked(event_said.to_string()),
753 sig: vec![0; 64],
754 }
755 }
756
757 #[test]
758 fn verify_receipts_meets_threshold() {
759 let receipts = EventReceipts::new(
760 "ESAID123",
761 vec![
762 make_test_receipt("ESAID123", "did:key:w1", 0),
763 make_test_receipt("ESAID123", "did:key:w2", 0),
764 ],
765 );
766
767 let result = verify_receipts(&receipts, 2, None);
768 assert_eq!(result, ReceiptVerificationResult::Valid);
769 }
770
771 #[test]
772 fn verify_receipts_insufficient() {
773 let receipts = EventReceipts::new(
774 "ESAID123",
775 vec![make_test_receipt("ESAID123", "did:key:w1", 0)],
776 );
777
778 let result = verify_receipts(&receipts, 2, None);
779 assert!(matches!(
780 result,
781 ReceiptVerificationResult::InsufficientReceipts {
782 required: 2,
783 got: 1
784 }
785 ));
786 }
787
788 #[test]
789 fn verify_receipts_duplicity() {
790 let receipts = EventReceipts {
791 event_said: Said::new_unchecked("ESAID_A".to_string()),
792 receipts: vec![
793 make_test_receipt("ESAID_A", "did:key:w1", 0),
794 make_test_receipt("ESAID_B", "did:key:w2", 0), ],
796 };
797
798 let result = verify_receipts(&receipts, 1, None);
799 assert!(matches!(
800 result,
801 ReceiptVerificationResult::Duplicity { .. }
802 ));
803 }
804
805 #[test]
806 fn evaluate_with_receipts_valid() {
807 let identity = make_key_state("ETestPrefix");
808 let att = make_attestation("did:keri:ETestPrefix", None, None);
809 let policy = default_policy();
810 let now = Utc::now();
811 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
812 let receipts = EventReceipts::new(
813 "ESAID",
814 vec![
815 make_test_receipt("ESAID", "did:key:w1", 0),
816 make_test_receipt("ESAID", "did:key:w2", 0),
817 ],
818 );
819
820 let decision = evaluate_with_receipts(
821 &identity,
822 &att,
823 &policy,
824 now,
825 local_head,
826 &[],
827 &receipts,
828 2,
829 None,
830 )
831 .unwrap();
832
833 assert_eq!(decision.outcome, Outcome::Allow);
834 }
835
836 #[test]
837 fn evaluate_with_receipts_insufficient_denies() {
838 let identity = make_key_state("ETestPrefix");
839 let att = make_attestation("did:keri:ETestPrefix", None, None);
840 let policy = default_policy();
841 let now = Utc::now();
842 let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
843 let receipts =
844 EventReceipts::new("ESAID", vec![make_test_receipt("ESAID", "did:key:w1", 0)]);
845
846 let decision = evaluate_with_receipts(
847 &identity,
848 &att,
849 &policy,
850 now,
851 local_head,
852 &[],
853 &receipts,
854 2, None,
856 )
857 .unwrap();
858
859 assert_eq!(decision.outcome, Outcome::Deny);
860 assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
861 }
862}