1use std::collections::BTreeSet;
33
34use exo_authority::permission::Permission;
35use exo_core::{Did, Hash256, PublicKey, Signature, Timestamp, crypto, hash::hash_structured};
36use serde::{Deserialize, Serialize};
37
38use crate::{
39 credential::{
40 AVC_SCHEMA_VERSION, AuthorityScope, AutonomousVolitionCredential, AvcConstraints, DataClass,
41 },
42 error::AvcError,
43 receipt::AvcTrustReceipt,
44 registry::AvcRegistryRead,
45};
46
47pub const AVC_HUMAN_APPROVAL_SIGNING_DOMAIN: &str = "exo.avc.human-approval.v1";
49pub const AVC_ACTION_SIGNING_DOMAIN: &str = "exo.avc.action.v1";
51pub const AVC_ACTION_COMMITMENT_DOMAIN: &str = "exo.avc.action.commitment.v1";
53pub const AVC_ACTION_DESCRIPTOR_DOMAIN: &str = "exo.avc.action.descriptor.v1";
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61pub enum AvcDecision {
62 Allow,
63 Deny,
64 HumanApprovalRequired,
65 ChallengeRequired,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
69pub enum AvcReasonCode {
70 Valid,
71 InvalidSignature,
72 InvalidIssuer,
73 InvalidSubject,
74 InvalidHolder,
75 Expired,
76 NotYetValid,
77 Revoked,
78 Suspended,
79 Quarantined,
80 AuthorityChainMissing,
81 AuthorityChainInvalid,
82 ScopeWidening,
83 PermissionDenied,
84 ToolDenied,
85 CounterpartyDenied,
86 DataClassDenied,
87 BudgetExceeded,
88 RiskExceeded,
89 HumanApprovalMissing,
90 HumanApprovalInvalid,
91 HumanApprovalExpired,
92 DelegationNotAllowed,
93 ConsentMissing,
94 PolicyMissing,
95 MalformedCredential,
96 ForbiddenAction,
97 OutsideTimeWindow,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct AvcActionRequest {
106 pub action_id: Hash256,
107 pub actor_did: Did,
108 pub requested_permission: Permission,
109 pub tool: Option<String>,
110 pub target_did: Option<Did>,
111 pub data_class: Option<DataClass>,
112 pub estimated_budget_minor_units: Option<u64>,
113 pub estimated_risk_bp: Option<u32>,
114 #[serde(default)]
115 pub human_approval: Option<AvcHumanApproval>,
116 pub requires_human_approval: bool,
117 pub action_name: Option<String>,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct AvcActionDescriptor {
123 pub schema_version: u16,
124 pub action_id: Hash256,
125 pub actor_did: Did,
126 pub requested_permission: Permission,
127 pub tool: Option<String>,
128 pub target_did: Option<Did>,
129 pub data_class: Option<DataClass>,
130 pub estimated_budget_minor_units: Option<u64>,
131 pub estimated_risk_bp: Option<u32>,
132 pub requires_human_approval: bool,
133 pub human_approval_present: bool,
134 pub action_name: Option<String>,
135}
136
137impl AvcActionDescriptor {
138 #[must_use]
139 pub fn from_action(action: &AvcActionRequest) -> Self {
140 Self {
141 schema_version: AVC_SCHEMA_VERSION,
142 action_id: action.action_id,
143 actor_did: action.actor_did.clone(),
144 requested_permission: action.requested_permission,
145 tool: action.tool.clone(),
146 target_did: action.target_did.clone(),
147 data_class: action.data_class.clone(),
148 estimated_budget_minor_units: action.estimated_budget_minor_units,
149 estimated_risk_bp: action.estimated_risk_bp,
150 requires_human_approval: action.requires_human_approval,
151 human_approval_present: action.human_approval.is_some(),
152 action_name: action.action_name.clone(),
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct AvcHumanApproval {
159 pub approver_did: Did,
160 pub approved_at: Timestamp,
161 pub expires_at: Option<Timestamp>,
162 pub signature: Signature,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct AvcValidationRequest {
167 pub credential: AutonomousVolitionCredential,
168 pub action: Option<AvcActionRequest>,
169 pub now: Timestamp,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct AvcValidationResult {
174 pub credential_id: Hash256,
175 pub decision: AvcDecision,
176 pub reason_codes: Vec<AvcReasonCode>,
177 pub normalized_holder_did: Did,
178 pub valid_until: Option<Timestamp>,
179 pub receipt: Option<AvcTrustReceipt>,
180}
181
182#[derive(Serialize)]
183struct HumanApprovalSigningPayload<'a> {
184 domain: &'static str,
185 schema_version: u16,
186 credential_id: &'a Hash256,
187 action_id: &'a Hash256,
188 actor_did: &'a Did,
189 requested_permission: &'a Permission,
190 tool: Option<&'a String>,
191 target_did: Option<&'a Did>,
192 data_class: Option<&'a DataClass>,
193 estimated_budget_minor_units: Option<u64>,
194 estimated_risk_bp: Option<u32>,
195 action_name: Option<&'a String>,
196 approver_did: &'a Did,
197 approved_at: &'a Timestamp,
198 expires_at: Option<&'a Timestamp>,
199}
200
201#[derive(Serialize)]
202struct AvcActionSigningPayload<'a> {
203 domain: &'static str,
204 schema_version: u16,
205 credential_id: &'a Hash256,
206 action: &'a AvcActionRequest,
207 validation_now: &'a Timestamp,
208}
209
210#[derive(Serialize)]
211struct AvcActionCommitmentPayload<'a> {
212 domain: &'static str,
213 schema_version: u16,
214 credential_id: &'a Hash256,
215 action: &'a AvcActionRequest,
216 validation_now: &'a Timestamp,
217}
218
219#[derive(Serialize)]
220struct AvcActionDescriptorPayload<'a> {
221 domain: &'static str,
222 descriptor: &'a AvcActionDescriptor,
223}
224
225pub fn validate_avc<R: AvcRegistryRead>(
239 request: &AvcValidationRequest,
240 registry: &R,
241) -> Result<AvcValidationResult, AvcError> {
242 let credential = &request.credential;
243 let credential_id = credential.id()?;
244 let normalized_holder_did = credential.effective_holder().clone();
245 let mut reasons: BTreeSet<AvcReasonCode> = BTreeSet::new();
246 let mut human_approval_required = false;
247
248 if credential.created_at > request.now {
250 reasons.insert(AvcReasonCode::NotYetValid);
251 }
252 if let Some(expires) = credential.expires_at {
253 if expires <= request.now {
254 reasons.insert(AvcReasonCode::Expired);
255 }
256 }
257 if let Some(window) = &credential.constraints.allowed_time_window {
258 if !window.contains(&request.now) {
259 reasons.insert(AvcReasonCode::OutsideTimeWindow);
260 }
261 }
262
263 if credential.signature.is_empty() {
265 reasons.insert(AvcReasonCode::InvalidSignature);
266 } else {
267 match registry.resolve_public_key(&credential.issuer_did) {
268 None => {
269 reasons.insert(AvcReasonCode::InvalidIssuer);
270 }
271 Some(pubkey) => {
272 if !verify_signature(credential, &pubkey)? {
273 reasons.insert(AvcReasonCode::InvalidSignature);
274 }
275 }
276 }
277 }
278
279 if credential.issuer_did != credential.principal_did {
281 match &credential.authority_chain {
282 None => {
283 reasons.insert(AvcReasonCode::AuthorityChainMissing);
284 }
285 Some(chain_ref) => {
286 if !registry.authority_chain_valid(&chain_ref.chain_hash, &request.now) {
287 reasons.insert(AvcReasonCode::AuthorityChainInvalid);
288 }
289 }
290 }
291 }
292 enforce_registered_issuer_grant(credential, registry, &mut reasons);
293
294 if registry.is_revoked(&credential_id) {
296 reasons.insert(AvcReasonCode::Revoked);
297 }
298
299 for consent_ref in &credential.consent_refs {
301 if consent_ref.required && !registry.consent_ref_exists(&consent_ref.consent_id) {
302 reasons.insert(AvcReasonCode::ConsentMissing);
303 }
304 }
305 for policy_ref in &credential.policy_refs {
306 if policy_ref.required
307 && !registry.policy_ref_exists(&policy_ref.policy_id, policy_ref.policy_version)
308 {
309 reasons.insert(AvcReasonCode::PolicyMissing);
310 }
311 }
312
313 if let Some(action) = &request.action {
315 evaluate_action(
316 credential,
317 action,
318 &normalized_holder_did,
319 registry,
320 &request.now,
321 &mut reasons,
322 &mut human_approval_required,
323 )?;
324 }
325
326 let mut sorted: Vec<AvcReasonCode> = reasons.into_iter().collect();
327 let decision = if sorted.is_empty() {
328 sorted.push(AvcReasonCode::Valid);
329 AvcDecision::Allow
330 } else if human_approval_required
331 && reasons_are_only(&sorted, AvcReasonCode::HumanApprovalMissing)
332 {
333 AvcDecision::HumanApprovalRequired
334 } else {
335 AvcDecision::Deny
336 };
337
338 Ok(AvcValidationResult {
339 credential_id,
340 decision,
341 reason_codes: sorted,
342 normalized_holder_did,
343 valid_until: credential.expires_at,
344 receipt: None,
345 })
346}
347
348fn reasons_are_only(reasons: &[AvcReasonCode], expected: AvcReasonCode) -> bool {
349 reasons.len() == 1 && reasons[0] == expected
350}
351
352fn verify_signature(
353 credential: &AutonomousVolitionCredential,
354 pubkey: &PublicKey,
355) -> Result<bool, AvcError> {
356 let payload = credential.signing_payload()?;
361 Ok(crypto::verify(&payload, &credential.signature, pubkey))
362}
363
364fn enforce_registered_issuer_grant<R: AvcRegistryRead>(
365 credential: &AutonomousVolitionCredential,
366 registry: &R,
367 reasons: &mut BTreeSet<AvcReasonCode>,
368) {
369 let Some(granted_permissions) =
370 registry.resolve_issuer_permission_grant(&credential.issuer_did)
371 else {
372 return;
373 };
374 if credential
375 .authority_scope
376 .permissions
377 .iter()
378 .any(|permission| !granted_permissions.contains(permission))
379 {
380 reasons.insert(AvcReasonCode::ScopeWidening);
381 }
382}
383
384pub fn human_approval_signature_payload(
395 credential: &AutonomousVolitionCredential,
396 action: &AvcActionRequest,
397 approval: &AvcHumanApproval,
398) -> Result<Vec<u8>, AvcError> {
399 let credential_id = credential.id()?;
400 let payload = HumanApprovalSigningPayload {
401 domain: AVC_HUMAN_APPROVAL_SIGNING_DOMAIN,
402 schema_version: AVC_SCHEMA_VERSION,
403 credential_id: &credential_id,
404 action_id: &action.action_id,
405 actor_did: &action.actor_did,
406 requested_permission: &action.requested_permission,
407 tool: action.tool.as_ref(),
408 target_did: action.target_did.as_ref(),
409 data_class: action.data_class.as_ref(),
410 estimated_budget_minor_units: action.estimated_budget_minor_units,
411 estimated_risk_bp: action.estimated_risk_bp,
412 action_name: action.action_name.as_ref(),
413 approver_did: &approval.approver_did,
414 approved_at: &approval.approved_at,
415 expires_at: approval.expires_at.as_ref(),
416 };
417 let mut buf = Vec::new();
418 ciborium::ser::into_writer(&payload, &mut buf)?;
419 Ok(buf)
420}
421
422pub fn avc_action_signature_payload(
431 credential: &AutonomousVolitionCredential,
432 action: &AvcActionRequest,
433 validation_now: &Timestamp,
434) -> Result<Vec<u8>, AvcError> {
435 let credential_id = credential.id()?;
436 let payload = AvcActionSigningPayload {
437 domain: AVC_ACTION_SIGNING_DOMAIN,
438 schema_version: AVC_SCHEMA_VERSION,
439 credential_id: &credential_id,
440 action,
441 validation_now,
442 };
443 let mut buf = Vec::new();
444 ciborium::ser::into_writer(&payload, &mut buf)?;
445 Ok(buf)
446}
447
448pub fn avc_action_commitment_hash(
457 credential: &AutonomousVolitionCredential,
458 action: &AvcActionRequest,
459 validation_now: &Timestamp,
460) -> Result<Hash256, AvcError> {
461 let credential_id = credential.id()?;
462 let payload = AvcActionCommitmentPayload {
463 domain: AVC_ACTION_COMMITMENT_DOMAIN,
464 schema_version: AVC_SCHEMA_VERSION,
465 credential_id: &credential_id,
466 action,
467 validation_now,
468 };
469 hash_structured(&payload).map_err(AvcError::from)
470}
471
472pub fn avc_action_descriptor_hash(descriptor: &AvcActionDescriptor) -> Result<Hash256, AvcError> {
481 hash_structured(&AvcActionDescriptorPayload {
482 domain: AVC_ACTION_DESCRIPTOR_DOMAIN,
483 descriptor,
484 })
485 .map_err(AvcError::from)
486}
487
488fn evaluate_action<R: AvcRegistryRead>(
489 credential: &AutonomousVolitionCredential,
490 action: &AvcActionRequest,
491 normalized_holder: &Did,
492 registry: &R,
493 now: &Timestamp,
494 reasons: &mut BTreeSet<AvcReasonCode>,
495 human_approval_required: &mut bool,
496) -> Result<(), AvcError> {
497 if action.actor_did != *normalized_holder && action.actor_did != credential.subject_did {
498 reasons.insert(AvcReasonCode::InvalidHolder);
499 }
500
501 if !credential
502 .authority_scope
503 .permissions
504 .contains(&action.requested_permission)
505 {
506 reasons.insert(AvcReasonCode::PermissionDenied);
507 }
508
509 enforce_tool(&credential.authority_scope, action, reasons);
510 enforce_data_class(&credential.authority_scope, action, reasons);
511 enforce_counterparty(&credential.authority_scope, action, reasons);
512 enforce_budget(&credential.constraints, action, reasons);
513 enforce_risk(
514 credential,
515 &credential.constraints,
516 action,
517 registry,
518 now,
519 reasons,
520 human_approval_required,
521 )?;
522 enforce_forbidden_action(&credential.constraints, action, reasons);
523 Ok(())
524}
525
526fn enforce_tool(
527 scope: &AuthorityScope,
528 action: &AvcActionRequest,
529 reasons: &mut BTreeSet<AvcReasonCode>,
530) {
531 let Some(tool) = &action.tool else {
532 return;
533 };
534 if scope.tools.is_empty() || !scope.tools.iter().any(|t| t == tool) {
535 reasons.insert(AvcReasonCode::ToolDenied);
536 }
537}
538
539fn enforce_data_class(
540 scope: &AuthorityScope,
541 action: &AvcActionRequest,
542 reasons: &mut BTreeSet<AvcReasonCode>,
543) {
544 let Some(class) = &action.data_class else {
545 return;
546 };
547 if !scope.data_classes.iter().any(|c| c == class) {
548 reasons.insert(AvcReasonCode::DataClassDenied);
549 }
550}
551
552fn enforce_counterparty(
553 scope: &AuthorityScope,
554 action: &AvcActionRequest,
555 reasons: &mut BTreeSet<AvcReasonCode>,
556) {
557 let Some(target) = &action.target_did else {
558 return;
559 };
560 if !scope.counterparties.is_empty() && !scope.counterparties.iter().any(|d| d == target) {
561 reasons.insert(AvcReasonCode::CounterpartyDenied);
562 }
563}
564
565fn enforce_budget(
566 constraints: &AvcConstraints,
567 action: &AvcActionRequest,
568 reasons: &mut BTreeSet<AvcReasonCode>,
569) {
570 if let (Some(cap), Some(estimate)) = (
571 constraints.max_budget_minor_units,
572 action.estimated_budget_minor_units,
573 ) {
574 if estimate > cap {
575 reasons.insert(AvcReasonCode::BudgetExceeded);
576 }
577 }
578}
579
580fn enforce_risk<R: AvcRegistryRead>(
581 credential: &AutonomousVolitionCredential,
582 constraints: &AvcConstraints,
583 action: &AvcActionRequest,
584 registry: &R,
585 now: &Timestamp,
586 reasons: &mut BTreeSet<AvcReasonCode>,
587 human_approval_required: &mut bool,
588) -> Result<(), AvcError> {
589 let risk_threshold_requires_approval = if let (Some(threshold), Some(estimate)) =
590 (constraints.approval_threshold_bp, action.estimated_risk_bp)
591 {
592 estimate >= threshold
593 } else {
594 false
595 };
596 if let (Some(cap), Some(estimate)) = (constraints.max_action_risk_bp, action.estimated_risk_bp)
597 {
598 if estimate > cap {
599 reasons.insert(AvcReasonCode::RiskExceeded);
600 }
601 }
602
603 let approval_required = constraints.human_approval_required || risk_threshold_requires_approval;
604 if approval_required {
605 *human_approval_required = true;
606 }
607 if approval_required || action.human_approval.is_some() {
608 match verify_human_approval(credential, action, registry, now)? {
609 Ok(()) => {}
610 Err(reason) => {
611 reasons.insert(reason);
612 }
613 }
614 }
615 Ok(())
616}
617
618fn verify_human_approval<R: AvcRegistryRead>(
619 credential: &AutonomousVolitionCredential,
620 action: &AvcActionRequest,
621 registry: &R,
622 now: &Timestamp,
623) -> Result<Result<(), AvcReasonCode>, AvcError> {
624 let Some(approval) = &action.human_approval else {
625 return Ok(Err(AvcReasonCode::HumanApprovalMissing));
626 };
627 if approval.signature.is_empty() || approval.approved_at > *now {
628 return Ok(Err(AvcReasonCode::HumanApprovalInvalid));
629 }
630 if let Some(expires_at) = approval.expires_at {
631 if expires_at <= approval.approved_at {
632 return Ok(Err(AvcReasonCode::HumanApprovalInvalid));
633 }
634 if expires_at <= *now {
635 return Ok(Err(AvcReasonCode::HumanApprovalExpired));
636 }
637 }
638
639 let Some(public_key) = registry.resolve_human_approval_key(&approval.approver_did) else {
640 return Ok(Err(AvcReasonCode::HumanApprovalInvalid));
641 };
642 let payload = human_approval_signature_payload(credential, action, approval)?;
643 if crypto::verify(&payload, &approval.signature, &public_key) {
644 Ok(Ok(()))
645 } else {
646 Ok(Err(AvcReasonCode::HumanApprovalInvalid))
647 }
648}
649
650fn enforce_forbidden_action(
651 constraints: &AvcConstraints,
652 action: &AvcActionRequest,
653 reasons: &mut BTreeSet<AvcReasonCode>,
654) {
655 let Some(name) = &action.action_name else {
656 return;
657 };
658 if constraints.forbidden_actions.iter().any(|a| a == name) {
659 reasons.insert(AvcReasonCode::ForbiddenAction);
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use exo_core::crypto::KeyPair;
666
667 use super::*;
668 use crate::{
669 credential::{
670 AVC_SCHEMA_VERSION, AuthorityChainRef, AvcConstraints, AvcDraft, AvcSubjectKind,
671 ConsentRef, PolicyRef, TimeWindow, issue_avc, test_support::*,
672 },
673 registry::{AvcRegistryWrite, InMemoryAvcRegistry},
674 revocation::{AvcRevocationReason, revoke_avc},
675 };
676
677 const ISSUER_SEED: [u8; 32] = [0x11; 32];
678 const HUMAN_APPROVER_SEED: [u8; 32] = [0x44; 32];
679
680 fn issuer_keypair() -> KeyPair {
681 KeyPair::from_secret_bytes(ISSUER_SEED).expect("valid seed")
682 }
683
684 fn human_approver_keypair() -> KeyPair {
685 KeyPair::from_secret_bytes(HUMAN_APPROVER_SEED).expect("valid seed")
686 }
687
688 struct Harness {
690 registry: InMemoryAvcRegistry,
691 }
692
693 impl Harness {
694 fn new() -> Self {
695 let mut registry = InMemoryAvcRegistry::new();
696 registry.put_public_key(did("issuer"), issuer_keypair().public);
697 Self { registry }
698 }
699
700 fn issue(&self, draft: AvcDraft) -> AutonomousVolitionCredential {
701 issue_avc(draft, |bytes| issuer_keypair().sign(bytes)).unwrap()
702 }
703 }
704
705 fn baseline_request(
706 cred: AutonomousVolitionCredential,
707 now: Timestamp,
708 ) -> AvcValidationRequest {
709 AvcValidationRequest {
710 credential: cred,
711 action: None,
712 now,
713 }
714 }
715
716 fn baseline_action(actor: Did) -> AvcActionRequest {
717 AvcActionRequest {
718 action_id: h256(0x55),
719 actor_did: actor,
720 requested_permission: Permission::Read,
721 tool: None,
722 target_did: None,
723 data_class: None,
724 estimated_budget_minor_units: None,
725 estimated_risk_bp: None,
726 human_approval: None,
727 requires_human_approval: false,
728 action_name: None,
729 }
730 }
731
732 fn attach_signed_human_approval(
733 credential: &AutonomousVolitionCredential,
734 action: &mut AvcActionRequest,
735 approver_did: Did,
736 approved_at: Timestamp,
737 expires_at: Option<Timestamp>,
738 approver_keypair: &KeyPair,
739 ) {
740 action.human_approval = Some(AvcHumanApproval {
741 approver_did,
742 approved_at,
743 expires_at,
744 signature: Signature::empty(),
745 });
746 let payload = human_approval_signature_payload(
747 credential,
748 action,
749 action
750 .human_approval
751 .as_ref()
752 .expect("approval placeholder"),
753 )
754 .expect("canonical approval payload");
755 action
756 .human_approval
757 .as_mut()
758 .expect("approval placeholder")
759 .signature = approver_keypair.sign(&payload);
760 }
761
762 #[test]
763 fn valid_credential_allows() {
764 let h = Harness::new();
765 let cred = h.issue(baseline_draft());
766 let request = baseline_request(cred, ts(1_500_000));
767 let result = validate_avc(&request, &h.registry).unwrap();
768 assert_eq!(result.decision, AvcDecision::Allow);
769 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
770 }
771
772 #[test]
773 fn allows_credential_when_issuer_has_no_registered_grant() {
774 let h = Harness::new();
775 let mut draft = baseline_draft();
776 draft.authority_scope.permissions = vec![Permission::Read, Permission::Write];
777 let cred = h.issue(draft);
778
779 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
780
781 assert_eq!(result.decision, AvcDecision::Allow);
782 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
783 }
784
785 #[test]
786 fn allows_credential_scope_within_registered_issuer_grant() {
787 let mut h = Harness::new();
788 h.registry
789 .put_issuer_permission_grant(did("issuer"), vec![Permission::Read]);
790 let mut draft = baseline_draft();
791 draft.authority_scope.permissions = vec![Permission::Read];
792 let cred = h.issue(draft);
793
794 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
795
796 assert_eq!(result.decision, AvcDecision::Allow);
797 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
798 }
799
800 #[test]
801 fn allows_credential_scope_with_duplicate_registered_grant_entries() {
802 let mut h = Harness::new();
803 h.registry.put_issuer_permission_grant(
804 did("issuer"),
805 vec![Permission::Write, Permission::Read, Permission::Write],
806 );
807 let mut draft = baseline_draft();
808 draft.authority_scope.permissions = vec![Permission::Read, Permission::Write];
809 let cred = h.issue(draft);
810
811 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
812
813 assert_eq!(result.decision, AvcDecision::Allow);
814 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
815 }
816
817 #[test]
818 fn action_signature_payload_is_domain_separated_and_context_bound() {
819 let h = Harness::new();
820 let cred = h.issue(baseline_draft());
821 let action = baseline_action(cred.subject_did.clone());
822 let payload_one = avc_action_signature_payload(&cred, &action, &ts(1_500_000)).unwrap();
823 let payload_two = avc_action_signature_payload(&cred, &action, &ts(1_500_001)).unwrap();
824 let needle = AVC_ACTION_SIGNING_DOMAIN.as_bytes();
825
826 assert!(payload_one.windows(needle.len()).any(|w| w == needle));
827 assert_ne!(payload_one, payload_two);
828 }
829
830 #[test]
831 fn denies_unknown_issuer_key() {
832 let h = Harness::new();
833 let mut draft = baseline_draft();
834 draft.issuer_did = did("ghost");
835 draft.principal_did = did("ghost"); let cred = h.issue(draft);
837 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
838 assert_eq!(result.decision, AvcDecision::Deny);
839 assert!(result.reason_codes.contains(&AvcReasonCode::InvalidIssuer));
840 }
841
842 #[test]
843 fn denies_empty_signature() {
844 let h = Harness::new();
845 let mut cred = h.issue(baseline_draft());
846 cred.signature = Signature::empty();
847 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
848 assert_eq!(result.decision, AvcDecision::Deny);
849 assert!(
850 result
851 .reason_codes
852 .contains(&AvcReasonCode::InvalidSignature)
853 );
854 }
855
856 #[test]
857 fn denies_invalid_signature_when_payload_tampered() {
858 let h = Harness::new();
859 let mut cred = h.issue(baseline_draft());
860 cred.delegated_intent.purpose = "tampered".into();
862 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
863 assert_eq!(result.decision, AvcDecision::Deny);
864 assert!(
865 result
866 .reason_codes
867 .contains(&AvcReasonCode::InvalidSignature)
868 );
869 }
870
871 #[test]
872 fn denies_wrong_key_signature() {
873 let h = Harness::new();
874 let other = KeyPair::from_secret_bytes([0x99; 32]).unwrap();
875 let mut cred = h.issue(baseline_draft());
876 let payload = cred.signing_payload().unwrap();
878 cred.signature = other.sign(&payload);
879 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
880 assert_eq!(result.decision, AvcDecision::Deny);
881 assert!(
882 result
883 .reason_codes
884 .contains(&AvcReasonCode::InvalidSignature)
885 );
886 }
887
888 #[test]
889 fn denies_expired_credential() {
890 let h = Harness::new();
891 let cred = h.issue(baseline_draft());
892 let result = validate_avc(&baseline_request(cred, ts(3_000_000)), &h.registry).unwrap();
893 assert_eq!(result.decision, AvcDecision::Deny);
894 assert!(result.reason_codes.contains(&AvcReasonCode::Expired));
895 }
896
897 #[test]
898 fn denies_not_yet_valid_credential() {
899 let h = Harness::new();
900 let cred = h.issue(baseline_draft());
901 let result = validate_avc(&baseline_request(cred, ts(0)), &h.registry).unwrap();
902 assert_eq!(result.decision, AvcDecision::Deny);
903 assert!(result.reason_codes.contains(&AvcReasonCode::NotYetValid));
904 }
905
906 #[test]
907 fn denies_outside_time_window() {
908 let h = Harness::new();
909 let mut draft = baseline_draft();
910 draft.constraints.allowed_time_window = Some(TimeWindow {
911 not_before: ts(1_400_000),
912 not_after: ts(1_450_000),
913 });
914 let cred = h.issue(draft);
915 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
916 assert!(
917 result
918 .reason_codes
919 .contains(&AvcReasonCode::OutsideTimeWindow)
920 );
921 }
922
923 #[test]
924 fn denies_revoked_credential() {
925 let mut h = Harness::new();
926 let cred = h.issue(baseline_draft());
927 let id = cred.id().unwrap();
928 h.registry.put_credential(cred.clone()).unwrap();
929 let revocation = revoke_avc(
930 id,
931 did("issuer"),
932 AvcRevocationReason::IssuerRevoked,
933 ts(1_250_000),
934 |bytes| issuer_keypair().sign(bytes),
935 )
936 .unwrap();
937 h.registry.put_revocation(revocation).unwrap();
938 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
939 assert_eq!(result.decision, AvcDecision::Deny);
940 assert!(result.reason_codes.contains(&AvcReasonCode::Revoked));
941 }
942
943 #[test]
944 fn denies_missing_authority_chain_when_issuer_differs_from_principal() {
945 let h = Harness::new();
946 let mut draft = baseline_draft();
947 draft.principal_did = did("principal");
948 let cred = h.issue(draft);
950 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
951 assert!(
952 result
953 .reason_codes
954 .contains(&AvcReasonCode::AuthorityChainMissing)
955 );
956 }
957
958 #[test]
959 fn denies_invalid_authority_chain_hash() {
960 let h = Harness::new();
961 let mut draft = baseline_draft();
962 draft.principal_did = did("principal");
963 draft.authority_chain = Some(AuthorityChainRef {
964 chain_hash: h256(0xDE),
965 });
966 let cred = h.issue(draft);
967 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
968 assert!(
969 result
970 .reason_codes
971 .contains(&AvcReasonCode::AuthorityChainInvalid)
972 );
973 }
974
975 #[test]
976 fn accepts_valid_authority_chain_hash() {
977 let mut h = Harness::new();
978 let mut draft = baseline_draft();
979 draft.principal_did = did("principal");
980 draft.authority_chain = Some(AuthorityChainRef {
981 chain_hash: h256(0xDE),
982 });
983 h.registry.mark_authority_chain_valid(h256(0xDE));
984 let cred = h.issue(draft);
985 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
986 assert_eq!(result.decision, AvcDecision::Allow);
987 }
988
989 #[test]
990 fn denies_missing_required_consent_ref() {
991 let h = Harness::new();
992 let mut draft = baseline_draft();
993 draft.consent_refs = vec![ConsentRef {
994 consent_id: h256(0xC0),
995 required: true,
996 }];
997 let cred = h.issue(draft);
998 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
999 assert!(result.reason_codes.contains(&AvcReasonCode::ConsentMissing));
1000 }
1001
1002 #[test]
1003 fn allows_when_optional_consent_ref_missing() {
1004 let h = Harness::new();
1005 let mut draft = baseline_draft();
1006 draft.consent_refs = vec![ConsentRef {
1007 consent_id: h256(0xC0),
1008 required: false,
1009 }];
1010 let cred = h.issue(draft);
1011 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1012 assert_eq!(result.decision, AvcDecision::Allow);
1013 }
1014
1015 #[test]
1016 fn denies_missing_required_policy_ref() {
1017 let h = Harness::new();
1018 let mut draft = baseline_draft();
1019 draft.policy_refs = vec![PolicyRef {
1020 policy_id: h256(0xB1),
1021 policy_version: 2,
1022 required: true,
1023 }];
1024 let cred = h.issue(draft);
1025 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1026 assert!(result.reason_codes.contains(&AvcReasonCode::PolicyMissing));
1027 }
1028
1029 #[test]
1030 fn denies_actor_mismatch() {
1031 let h = Harness::new();
1032 let cred = h.issue(baseline_draft());
1033 let mut request = baseline_request(cred, ts(1_500_000));
1034 request.action = Some(baseline_action(did("imposter")));
1035 let result = validate_avc(&request, &h.registry).unwrap();
1036 assert!(result.reason_codes.contains(&AvcReasonCode::InvalidHolder));
1037 }
1038
1039 #[test]
1040 fn denies_permission_outside_scope() {
1041 let h = Harness::new();
1042 let cred = h.issue(baseline_draft());
1043 let actor = cred.subject_did.clone();
1044 let mut action = baseline_action(actor);
1045 action.requested_permission = Permission::Govern;
1046 let mut request = baseline_request(cred, ts(1_500_000));
1047 request.action = Some(action);
1048 let result = validate_avc(&request, &h.registry).unwrap();
1049 assert!(
1050 result
1051 .reason_codes
1052 .contains(&AvcReasonCode::PermissionDenied)
1053 );
1054 }
1055
1056 #[test]
1057 fn denies_credential_scope_wider_than_registered_issuer_grant() {
1058 let mut h = Harness::new();
1059 h.registry.put_issuer_permission_grant(
1060 did("issuer"),
1061 vec![
1062 Permission::Read,
1063 Permission::Write,
1064 Permission::Execute,
1065 Permission::Delegate,
1066 ],
1067 );
1068 let mut draft = baseline_draft();
1069 draft.authority_scope.permissions = vec![Permission::Govern];
1070 let cred = h.issue(draft);
1071 let actor = cred.subject_did.clone();
1072 let mut action = baseline_action(actor);
1073 action.requested_permission = Permission::Govern;
1074 let mut request = baseline_request(cred, ts(1_500_000));
1075 request.action = Some(action);
1076
1077 let result = validate_avc(&request, &h.registry).unwrap();
1078
1079 assert_eq!(result.decision, AvcDecision::Deny);
1080 assert!(
1081 result.reason_codes.contains(&AvcReasonCode::ScopeWidening),
1082 "root issuer grants must cap credential-declared permissions"
1083 );
1084 }
1085
1086 #[test]
1087 fn denies_any_credential_permission_outside_registered_issuer_grant() {
1088 let mut h = Harness::new();
1089 h.registry
1090 .put_issuer_permission_grant(did("issuer"), vec![Permission::Read]);
1091 let mut draft = baseline_draft();
1092 draft.authority_scope.permissions = vec![Permission::Read, Permission::Write];
1093 let cred = h.issue(draft);
1094
1095 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1096
1097 assert_eq!(result.decision, AvcDecision::Deny);
1098 assert!(
1099 result.reason_codes.contains(&AvcReasonCode::ScopeWidening),
1100 "any credential permission outside the issuer grant must fail closed"
1101 );
1102 }
1103
1104 #[test]
1105 fn denies_tool_outside_scope() {
1106 let h = Harness::new();
1107 let cred = h.issue(baseline_draft());
1108 let actor = cred.subject_did.clone();
1109 let mut action = baseline_action(actor);
1110 action.tool = Some("ungoverned".into());
1111 let mut request = baseline_request(cred, ts(1_500_000));
1112 request.action = Some(action);
1113 let result = validate_avc(&request, &h.registry).unwrap();
1114 assert!(result.reason_codes.contains(&AvcReasonCode::ToolDenied));
1115 }
1116
1117 #[test]
1118 fn empty_tool_scope_denies_any_tool_action() {
1119 let h = Harness::new();
1120 let mut draft = baseline_draft();
1121 draft.authority_scope.tools = vec![];
1122 let cred = h.issue(draft);
1123 let actor = cred.subject_did.clone();
1124 let mut action = baseline_action(actor);
1125 action.tool = Some("anything".into());
1126 let mut request = baseline_request(cred, ts(1_500_000));
1127 request.action = Some(action);
1128 let result = validate_avc(&request, &h.registry).unwrap();
1129 assert!(result.reason_codes.contains(&AvcReasonCode::ToolDenied));
1130 }
1131
1132 #[test]
1133 fn empty_tool_scope_allows_action_without_tool() {
1134 let h = Harness::new();
1135 let mut draft = baseline_draft();
1136 draft.authority_scope.tools = vec![];
1137 let cred = h.issue(draft);
1138 let actor = cred.subject_did.clone();
1139 let action = baseline_action(actor);
1140 let mut request = baseline_request(cred, ts(1_500_000));
1141 request.action = Some(action);
1142 let result = validate_avc(&request, &h.registry).unwrap();
1143 assert_eq!(result.decision, AvcDecision::Allow);
1144 }
1145
1146 #[test]
1147 fn denies_data_class_outside_scope() {
1148 let h = Harness::new();
1149 let cred = h.issue(baseline_draft());
1150 let actor = cred.subject_did.clone();
1151 let mut action = baseline_action(actor);
1152 action.data_class = Some(DataClass::SensitivePersonalData);
1153 let mut request = baseline_request(cred, ts(1_500_000));
1154 request.action = Some(action);
1155 let result = validate_avc(&request, &h.registry).unwrap();
1156 assert!(
1157 result
1158 .reason_codes
1159 .contains(&AvcReasonCode::DataClassDenied)
1160 );
1161 }
1162
1163 #[test]
1164 fn denies_counterparty_when_allowlist_present() {
1165 let h = Harness::new();
1166 let mut draft = baseline_draft();
1167 draft.authority_scope.counterparties = vec![did("approved-cp")];
1168 let cred = h.issue(draft);
1169 let actor = cred.subject_did.clone();
1170 let mut action = baseline_action(actor);
1171 action.target_did = Some(did("malicious-cp"));
1172 let mut request = baseline_request(cred, ts(1_500_000));
1173 request.action = Some(action);
1174 let result = validate_avc(&request, &h.registry).unwrap();
1175 assert!(
1176 result
1177 .reason_codes
1178 .contains(&AvcReasonCode::CounterpartyDenied)
1179 );
1180 }
1181
1182 #[test]
1183 fn empty_counterparty_list_allows_any_target() {
1184 let h = Harness::new();
1185 let cred = h.issue(baseline_draft());
1186 let actor = cred.subject_did.clone();
1187 let mut action = baseline_action(actor);
1188 action.target_did = Some(did("any"));
1189 let mut request = baseline_request(cred, ts(1_500_000));
1190 request.action = Some(action);
1191 let result = validate_avc(&request, &h.registry).unwrap();
1192 assert_eq!(result.decision, AvcDecision::Allow);
1193 }
1194
1195 #[test]
1196 fn denies_budget_exceeded() {
1197 let h = Harness::new();
1198 let mut draft = baseline_draft();
1199 draft.constraints.max_budget_minor_units = Some(1_000);
1200 let cred = h.issue(draft);
1201 let actor = cred.subject_did.clone();
1202 let mut action = baseline_action(actor);
1203 action.estimated_budget_minor_units = Some(2_000);
1204 let mut request = baseline_request(cred, ts(1_500_000));
1205 request.action = Some(action);
1206 let result = validate_avc(&request, &h.registry).unwrap();
1207 assert!(result.reason_codes.contains(&AvcReasonCode::BudgetExceeded));
1208 }
1209
1210 #[test]
1211 fn in_scope_action_at_budget_and_risk_caps_allows() {
1212 let h = Harness::new();
1213 let mut draft = baseline_draft();
1214 draft.authority_scope.counterparties = vec![did("approved-cp")];
1215 draft.constraints.max_budget_minor_units = Some(1_000);
1216 draft.constraints.max_action_risk_bp = Some(1_000);
1217 let cred = h.issue(draft);
1218 let actor = cred.subject_did.clone();
1219 let mut action = baseline_action(actor);
1220 action.tool = Some("alpha".into());
1221 action.data_class = Some(DataClass::Public);
1222 action.target_did = Some(did("approved-cp"));
1223 action.estimated_budget_minor_units = Some(1_000);
1224 action.estimated_risk_bp = Some(1_000);
1225 let mut request = baseline_request(cred, ts(1_500_000));
1226 request.action = Some(action);
1227 let result = validate_avc(&request, &h.registry).unwrap();
1228 assert_eq!(result.decision, AvcDecision::Allow);
1229 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1230 }
1231
1232 #[test]
1233 fn in_scope_action_with_allowed_tool_allows() {
1234 let h = Harness::new();
1235 let cred = h.issue(baseline_draft());
1236 let actor = cred.subject_did.clone();
1237 let mut action = baseline_action(actor);
1238 action.tool = Some("alpha".into());
1239 let mut request = baseline_request(cred, ts(1_500_000));
1240 request.action = Some(action);
1241 let result = validate_avc(&request, &h.registry).unwrap();
1242 assert_eq!(result.decision, AvcDecision::Allow);
1243 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1244 }
1245
1246 #[test]
1247 fn non_expiring_credential_allows_explicit_holder_action() {
1248 let h = Harness::new();
1249 let mut draft = baseline_draft();
1250 draft.holder_did = Some(did("holder"));
1251 draft.expires_at = None;
1252 let cred = h.issue(draft);
1253 let mut request = baseline_request(cred, ts(1_500_000));
1254 request.action = Some(baseline_action(did("holder")));
1255 let result = validate_avc(&request, &h.registry).unwrap();
1256 assert_eq!(result.decision, AvcDecision::Allow);
1257 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1258 assert_eq!(result.normalized_holder_did, did("holder"));
1259 assert_eq!(result.valid_until, None);
1260 }
1261
1262 #[test]
1263 fn subject_actor_remains_valid_when_holder_is_explicit() {
1264 let h = Harness::new();
1265 let mut draft = baseline_draft();
1266 draft.holder_did = Some(did("holder"));
1267 let cred = h.issue(draft);
1268 let mut request = baseline_request(cred, ts(1_500_000));
1269 request.action = Some(baseline_action(did("agent")));
1270 let result = validate_avc(&request, &h.registry).unwrap();
1271 assert_eq!(result.decision, AvcDecision::Allow);
1272 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1273 assert_eq!(result.normalized_holder_did, did("holder"));
1274 }
1275
1276 #[test]
1277 fn risk_at_approval_threshold_requires_human_approval() {
1278 let h = Harness::new();
1279 let mut draft = baseline_draft();
1280 draft.constraints.max_action_risk_bp = Some(10_000);
1281 draft.constraints.approval_threshold_bp = Some(5_000);
1282 let cred = h.issue(draft);
1283 let actor = cred.subject_did.clone();
1284 let mut action = baseline_action(actor);
1285 action.estimated_risk_bp = Some(5_000);
1286 let mut request = baseline_request(cred, ts(1_500_000));
1287 request.action = Some(action);
1288 let result = validate_avc(&request, &h.registry).unwrap();
1289 assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1290 assert_eq!(
1291 result.reason_codes,
1292 vec![AvcReasonCode::HumanApprovalMissing]
1293 );
1294 }
1295
1296 #[test]
1297 fn denies_risk_exceeded() {
1298 let h = Harness::new();
1299 let mut draft = baseline_draft();
1300 draft.constraints.max_action_risk_bp = Some(1_000);
1301 let cred = h.issue(draft);
1302 let actor = cred.subject_did.clone();
1303 let mut action = baseline_action(actor);
1304 action.estimated_risk_bp = Some(5_000);
1305 let mut request = baseline_request(cred, ts(1_500_000));
1306 request.action = Some(action);
1307 let result = validate_avc(&request, &h.registry).unwrap();
1308 assert!(result.reason_codes.contains(&AvcReasonCode::RiskExceeded));
1309 }
1310
1311 #[test]
1312 fn risk_above_threshold_returns_human_approval_required() {
1313 let h = Harness::new();
1314 let mut draft = baseline_draft();
1315 draft.constraints.max_action_risk_bp = Some(10_000);
1316 draft.constraints.approval_threshold_bp = Some(5_000);
1317 let cred = h.issue(draft);
1318 let actor = cred.subject_did.clone();
1319 let mut action = baseline_action(actor);
1320 action.estimated_risk_bp = Some(7_500);
1321 let mut request = baseline_request(cred, ts(1_500_000));
1322 request.action = Some(action);
1323 let result = validate_avc(&request, &h.registry).unwrap();
1324 assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1325 assert_eq!(
1326 result.reason_codes,
1327 vec![AvcReasonCode::HumanApprovalMissing]
1328 );
1329 }
1330
1331 #[test]
1332 fn risk_above_threshold_ignores_caller_approval_flag() {
1333 let h = Harness::new();
1334 let mut draft = baseline_draft();
1335 draft.constraints.max_action_risk_bp = Some(10_000);
1336 draft.constraints.approval_threshold_bp = Some(5_000);
1337 let cred = h.issue(draft);
1338 let actor = cred.subject_did.clone();
1339 let mut action = baseline_action(actor);
1340 action.estimated_risk_bp = Some(7_500);
1341 action.requires_human_approval = true;
1342 let mut request = baseline_request(cred, ts(1_500_000));
1343 request.action = Some(action);
1344 let result = validate_avc(&request, &h.registry).unwrap();
1345 assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1346 assert_eq!(
1347 result.reason_codes,
1348 vec![AvcReasonCode::HumanApprovalMissing]
1349 );
1350 }
1351
1352 #[test]
1353 fn credential_human_approval_required_blocks_action_without_evidence() {
1354 let h = Harness::new();
1355 let mut draft = baseline_draft();
1356 draft.constraints.human_approval_required = true;
1357 let cred = h.issue(draft);
1358 let actor = cred.subject_did.clone();
1359 let action = baseline_action(actor);
1360 let mut request = baseline_request(cred, ts(1_500_000));
1361 request.action = Some(action);
1362 let result = validate_avc(&request, &h.registry).unwrap();
1363 assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1364 assert_eq!(
1365 result.reason_codes,
1366 vec![AvcReasonCode::HumanApprovalMissing]
1367 );
1368 }
1369
1370 #[test]
1371 fn signed_human_approval_satisfies_credential_requirement() {
1372 let mut h = Harness::new();
1373 let approver_keypair = human_approver_keypair();
1374 let approver_did = did("human-approver");
1375 h.registry
1376 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1377 let mut draft = baseline_draft();
1378 draft.constraints.human_approval_required = true;
1379 let cred = h.issue(draft);
1380 let actor = cred.subject_did.clone();
1381 let mut action = baseline_action(actor);
1382 attach_signed_human_approval(
1383 &cred,
1384 &mut action,
1385 approver_did,
1386 ts(1_400_000),
1387 Some(ts(1_900_000)),
1388 &approver_keypair,
1389 );
1390 let mut request = baseline_request(cred, ts(1_500_000));
1391 request.action = Some(action);
1392 let result = validate_avc(&request, &h.registry).unwrap();
1393 assert_eq!(result.decision, AvcDecision::Allow);
1394 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1395 }
1396
1397 #[test]
1398 fn signed_human_approval_satisfies_risk_threshold() {
1399 let mut h = Harness::new();
1400 let approver_keypair = human_approver_keypair();
1401 let approver_did = did("human-approver");
1402 h.registry
1403 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1404 let mut draft = baseline_draft();
1405 draft.constraints.max_action_risk_bp = Some(10_000);
1406 draft.constraints.approval_threshold_bp = Some(5_000);
1407 let cred = h.issue(draft);
1408 let actor = cred.subject_did.clone();
1409 let mut action = baseline_action(actor);
1410 action.estimated_risk_bp = Some(7_500);
1411 attach_signed_human_approval(
1412 &cred,
1413 &mut action,
1414 approver_did,
1415 ts(1_400_000),
1416 None,
1417 &approver_keypair,
1418 );
1419 let mut request = baseline_request(cred, ts(1_500_000));
1420 request.action = Some(action);
1421 let result = validate_avc(&request, &h.registry).unwrap();
1422 assert_eq!(result.decision, AvcDecision::Allow);
1423 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1424 }
1425
1426 #[test]
1427 fn valid_optional_human_approval_evidence_allows_unrequired_action() {
1428 let mut h = Harness::new();
1429 let approver_keypair = human_approver_keypair();
1430 let approver_did = did("human-approver");
1431 h.registry
1432 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1433 let cred = h.issue(baseline_draft());
1434 let actor = cred.subject_did.clone();
1435 let mut action = baseline_action(actor);
1436 attach_signed_human_approval(
1437 &cred,
1438 &mut action,
1439 approver_did,
1440 ts(1_400_000),
1441 Some(ts(1_900_000)),
1442 &approver_keypair,
1443 );
1444 let mut request = baseline_request(cred, ts(1_500_000));
1445 request.action = Some(action);
1446 let result = validate_avc(&request, &h.registry).unwrap();
1447 assert_eq!(result.decision, AvcDecision::Allow);
1448 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1449 }
1450
1451 #[test]
1452 fn human_approval_from_untrusted_approver_is_invalid() {
1453 let h = Harness::new();
1454 let approver_keypair = human_approver_keypair();
1455 let approver_did = did("human-approver");
1456 let mut draft = baseline_draft();
1457 draft.constraints.human_approval_required = true;
1458 let cred = h.issue(draft);
1459 let actor = cred.subject_did.clone();
1460 let mut action = baseline_action(actor);
1461 attach_signed_human_approval(
1462 &cred,
1463 &mut action,
1464 approver_did,
1465 ts(1_400_000),
1466 Some(ts(1_900_000)),
1467 &approver_keypair,
1468 );
1469 let mut request = baseline_request(cred, ts(1_500_000));
1470 request.action = Some(action);
1471 let result = validate_avc(&request, &h.registry).unwrap();
1472 assert_eq!(result.decision, AvcDecision::Deny);
1473 assert_eq!(
1474 result.reason_codes,
1475 vec![AvcReasonCode::HumanApprovalInvalid]
1476 );
1477 }
1478
1479 #[test]
1480 fn issuer_public_key_alone_does_not_authorize_human_approval() {
1481 let h = Harness::new();
1482 let issuer_keypair = issuer_keypair();
1483 let mut draft = baseline_draft();
1484 draft.constraints.human_approval_required = true;
1485 let cred = h.issue(draft);
1486 let actor = cred.subject_did.clone();
1487 let mut action = baseline_action(actor);
1488 attach_signed_human_approval(
1489 &cred,
1490 &mut action,
1491 did("issuer"),
1492 ts(1_400_000),
1493 Some(ts(1_900_000)),
1494 &issuer_keypair,
1495 );
1496 let mut request = baseline_request(cred, ts(1_500_000));
1497 request.action = Some(action);
1498 let result = validate_avc(&request, &h.registry).unwrap();
1499 assert_eq!(result.decision, AvcDecision::Deny);
1500 assert_eq!(
1501 result.reason_codes,
1502 vec![AvcReasonCode::HumanApprovalInvalid]
1503 );
1504 }
1505
1506 #[test]
1507 fn optional_human_approval_evidence_must_still_verify() {
1508 let h = Harness::new();
1509 let approver_keypair = human_approver_keypair();
1510 let cred = h.issue(baseline_draft());
1511 let actor = cred.subject_did.clone();
1512 let mut action = baseline_action(actor);
1513 attach_signed_human_approval(
1514 &cred,
1515 &mut action,
1516 did("human-approver"),
1517 ts(1_400_000),
1518 Some(ts(1_900_000)),
1519 &approver_keypair,
1520 );
1521 let mut request = baseline_request(cred, ts(1_500_000));
1522 request.action = Some(action);
1523 let result = validate_avc(&request, &h.registry).unwrap();
1524 assert_eq!(result.decision, AvcDecision::Deny);
1525 assert_eq!(
1526 result.reason_codes,
1527 vec![AvcReasonCode::HumanApprovalInvalid]
1528 );
1529 }
1530
1531 #[test]
1532 fn human_approval_signature_binds_action_fields() {
1533 let mut h = Harness::new();
1534 let approver_keypair = human_approver_keypair();
1535 let approver_did = did("human-approver");
1536 h.registry
1537 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1538 let mut draft = baseline_draft();
1539 draft.constraints.max_action_risk_bp = Some(10_000);
1540 draft.constraints.approval_threshold_bp = Some(5_000);
1541 let cred = h.issue(draft);
1542 let actor = cred.subject_did.clone();
1543 let mut action = baseline_action(actor);
1544 action.estimated_risk_bp = Some(7_500);
1545 attach_signed_human_approval(
1546 &cred,
1547 &mut action,
1548 approver_did,
1549 ts(1_400_000),
1550 None,
1551 &approver_keypair,
1552 );
1553 action.estimated_risk_bp = Some(7_501);
1554 let mut request = baseline_request(cred, ts(1_500_000));
1555 request.action = Some(action);
1556 let result = validate_avc(&request, &h.registry).unwrap();
1557 assert_eq!(result.decision, AvcDecision::Deny);
1558 assert_eq!(
1559 result.reason_codes,
1560 vec![AvcReasonCode::HumanApprovalInvalid]
1561 );
1562 }
1563
1564 #[test]
1565 fn expired_human_approval_is_rejected() {
1566 let mut h = Harness::new();
1567 let approver_keypair = human_approver_keypair();
1568 let approver_did = did("human-approver");
1569 h.registry
1570 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1571 let mut draft = baseline_draft();
1572 draft.constraints.human_approval_required = true;
1573 let cred = h.issue(draft);
1574 let actor = cred.subject_did.clone();
1575 let mut action = baseline_action(actor);
1576 attach_signed_human_approval(
1577 &cred,
1578 &mut action,
1579 approver_did,
1580 ts(1_300_000),
1581 Some(ts(1_400_000)),
1582 &approver_keypair,
1583 );
1584 let mut request = baseline_request(cred, ts(1_500_000));
1585 request.action = Some(action);
1586 let result = validate_avc(&request, &h.registry).unwrap();
1587 assert_eq!(result.decision, AvcDecision::Deny);
1588 assert_eq!(
1589 result.reason_codes,
1590 vec![AvcReasonCode::HumanApprovalExpired]
1591 );
1592 }
1593
1594 #[test]
1595 fn human_approval_with_empty_signature_is_invalid() {
1596 let mut h = Harness::new();
1597 let approver_keypair = human_approver_keypair();
1598 let approver_did = did("human-approver");
1599 h.registry
1600 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1601 let mut draft = baseline_draft();
1602 draft.constraints.human_approval_required = true;
1603 let cred = h.issue(draft);
1604 let actor = cred.subject_did.clone();
1605 let mut action = baseline_action(actor);
1606 action.human_approval = Some(AvcHumanApproval {
1607 approver_did,
1608 approved_at: ts(1_400_000),
1609 expires_at: Some(ts(1_900_000)),
1610 signature: Signature::empty(),
1611 });
1612 let mut request = baseline_request(cred, ts(1_500_000));
1613 request.action = Some(action);
1614 let result = validate_avc(&request, &h.registry).unwrap();
1615 assert_eq!(result.decision, AvcDecision::Deny);
1616 assert_eq!(
1617 result.reason_codes,
1618 vec![AvcReasonCode::HumanApprovalInvalid]
1619 );
1620 }
1621
1622 #[test]
1623 fn human_approval_expiring_at_approval_time_is_invalid() {
1624 let mut h = Harness::new();
1625 let approver_keypair = human_approver_keypair();
1626 let approver_did = did("human-approver");
1627 h.registry
1628 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1629 let mut draft = baseline_draft();
1630 draft.constraints.human_approval_required = true;
1631 let cred = h.issue(draft);
1632 let actor = cred.subject_did.clone();
1633 let mut action = baseline_action(actor);
1634 attach_signed_human_approval(
1635 &cred,
1636 &mut action,
1637 approver_did,
1638 ts(1_400_000),
1639 Some(ts(1_400_000)),
1640 &approver_keypair,
1641 );
1642 let mut request = baseline_request(cred, ts(1_500_000));
1643 request.action = Some(action);
1644 let result = validate_avc(&request, &h.registry).unwrap();
1645 assert_eq!(result.decision, AvcDecision::Deny);
1646 assert_eq!(
1647 result.reason_codes,
1648 vec![AvcReasonCode::HumanApprovalInvalid]
1649 );
1650 }
1651
1652 #[test]
1653 fn human_approval_expiring_at_now_is_expired() {
1654 let mut h = Harness::new();
1655 let approver_keypair = human_approver_keypair();
1656 let approver_did = did("human-approver");
1657 h.registry
1658 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1659 let mut draft = baseline_draft();
1660 draft.constraints.human_approval_required = true;
1661 let cred = h.issue(draft);
1662 let actor = cred.subject_did.clone();
1663 let mut action = baseline_action(actor);
1664 attach_signed_human_approval(
1665 &cred,
1666 &mut action,
1667 approver_did,
1668 ts(1_400_000),
1669 Some(ts(1_500_000)),
1670 &approver_keypair,
1671 );
1672 let mut request = baseline_request(cred, ts(1_500_000));
1673 request.action = Some(action);
1674 let result = validate_avc(&request, &h.registry).unwrap();
1675 assert_eq!(result.decision, AvcDecision::Deny);
1676 assert_eq!(
1677 result.reason_codes,
1678 vec![AvcReasonCode::HumanApprovalExpired]
1679 );
1680 }
1681
1682 #[test]
1683 fn human_approval_with_future_approval_time_is_invalid() {
1684 let mut h = Harness::new();
1685 let approver_keypair = human_approver_keypair();
1686 let approver_did = did("human-approver");
1687 h.registry
1688 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1689 let mut draft = baseline_draft();
1690 draft.constraints.human_approval_required = true;
1691 let cred = h.issue(draft);
1692 let actor = cred.subject_did.clone();
1693 let mut action = baseline_action(actor);
1694 attach_signed_human_approval(
1695 &cred,
1696 &mut action,
1697 approver_did,
1698 ts(1_600_000),
1699 Some(ts(1_900_000)),
1700 &approver_keypair,
1701 );
1702 let mut request = baseline_request(cred, ts(1_500_000));
1703 request.action = Some(action);
1704 let result = validate_avc(&request, &h.registry).unwrap();
1705 assert_eq!(result.decision, AvcDecision::Deny);
1706 assert_eq!(
1707 result.reason_codes,
1708 vec![AvcReasonCode::HumanApprovalInvalid]
1709 );
1710 }
1711
1712 #[test]
1713 fn human_approval_expiring_before_approval_time_is_invalid() {
1714 let mut h = Harness::new();
1715 let approver_keypair = human_approver_keypair();
1716 let approver_did = did("human-approver");
1717 h.registry
1718 .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1719 let mut draft = baseline_draft();
1720 draft.constraints.human_approval_required = true;
1721 let cred = h.issue(draft);
1722 let actor = cred.subject_did.clone();
1723 let mut action = baseline_action(actor);
1724 attach_signed_human_approval(
1725 &cred,
1726 &mut action,
1727 approver_did,
1728 ts(1_400_000),
1729 Some(ts(1_399_999)),
1730 &approver_keypair,
1731 );
1732 let mut request = baseline_request(cred, ts(1_500_000));
1733 request.action = Some(action);
1734 let result = validate_avc(&request, &h.registry).unwrap();
1735 assert_eq!(result.decision, AvcDecision::Deny);
1736 assert_eq!(
1737 result.reason_codes,
1738 vec![AvcReasonCode::HumanApprovalInvalid]
1739 );
1740 }
1741
1742 #[test]
1743 fn risk_below_approval_threshold_allows_without_human_approval() {
1744 let h = Harness::new();
1745 let mut draft = baseline_draft();
1746 draft.constraints.max_action_risk_bp = Some(10_000);
1747 draft.constraints.approval_threshold_bp = Some(5_000);
1748 let cred = h.issue(draft);
1749 let actor = cred.subject_did.clone();
1750 let mut action = baseline_action(actor);
1751 action.estimated_risk_bp = Some(4_999);
1752 let mut request = baseline_request(cred, ts(1_500_000));
1753 request.action = Some(action);
1754 let result = validate_avc(&request, &h.registry).unwrap();
1755 assert_eq!(result.decision, AvcDecision::Allow);
1756 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1757 }
1758
1759 #[test]
1760 fn risk_threshold_without_estimate_allows_without_human_approval() {
1761 let h = Harness::new();
1762 let mut draft = baseline_draft();
1763 draft.constraints.max_action_risk_bp = Some(10_000);
1764 draft.constraints.approval_threshold_bp = Some(5_000);
1765 let cred = h.issue(draft);
1766 let actor = cred.subject_did.clone();
1767 let action = baseline_action(actor);
1768 let mut request = baseline_request(cred, ts(1_500_000));
1769 request.action = Some(action);
1770 let result = validate_avc(&request, &h.registry).unwrap();
1771 assert_eq!(result.decision, AvcDecision::Allow);
1772 assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1773 }
1774
1775 #[test]
1776 fn denies_forbidden_action_name() {
1777 let h = Harness::new();
1778 let mut draft = baseline_draft();
1779 draft.constraints.forbidden_actions = vec!["payment.execute".into()];
1780 let cred = h.issue(draft);
1781 let actor = cred.subject_did.clone();
1782 let mut action = baseline_action(actor);
1783 action.action_name = Some("payment.execute".into());
1784 let mut request = baseline_request(cred, ts(1_500_000));
1785 request.action = Some(action);
1786 let result = validate_avc(&request, &h.registry).unwrap();
1787 assert!(
1788 result
1789 .reason_codes
1790 .contains(&AvcReasonCode::ForbiddenAction)
1791 );
1792 }
1793
1794 #[test]
1795 fn reason_codes_are_sorted_and_deduped() {
1796 let h = Harness::new();
1797 let mut draft = baseline_draft();
1799 draft.principal_did = did("principal"); draft.authority_scope.tools = vec![];
1802 let cred = h.issue(draft);
1803 let actor = cred.subject_did.clone();
1804 let mut action = baseline_action(actor);
1805 action.tool = Some("forbidden".into());
1806 action.requested_permission = Permission::Govern; let mut request = baseline_request(cred, ts(3_000_000)); request.action = Some(action);
1809 let result = validate_avc(&request, &h.registry).unwrap();
1810 assert_eq!(result.decision, AvcDecision::Deny);
1811
1812 let mut sorted = result.reason_codes.clone();
1813 sorted.sort();
1814 assert_eq!(sorted, result.reason_codes, "reason codes must be sorted");
1815
1816 let mut deduped = result.reason_codes.clone();
1817 deduped.dedup();
1818 assert_eq!(deduped, result.reason_codes, "reason codes must be deduped");
1819 }
1820
1821 #[test]
1822 fn validation_does_not_consult_payment_state() {
1823 let h = Harness::new();
1825 let cred = h.issue(baseline_draft());
1826 let r1 = validate_avc(&baseline_request(cred.clone(), ts(1_500_000)), &h.registry).unwrap();
1827 let r2 = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1828 assert_eq!(r1, r2);
1829 }
1830
1831 #[test]
1832 fn validation_request_round_trip_serializes() {
1833 let h = Harness::new();
1834 let cred = h.issue(baseline_draft());
1835 let request = baseline_request(cred, ts(1_500_000));
1836 let mut buf = Vec::new();
1837 ciborium::ser::into_writer(&request, &mut buf).unwrap();
1838 let decoded: AvcValidationRequest = ciborium::de::from_reader(buf.as_slice()).unwrap();
1839 assert_eq!(decoded, request);
1840 }
1841
1842 #[test]
1843 fn unsupported_subject_with_unknown_kind_still_allows() {
1844 let h = Harness::new();
1845 let mut draft = baseline_draft();
1846 draft.subject_kind = AvcSubjectKind::Unknown;
1847 let cred = h.issue(draft);
1848 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1849 assert_eq!(result.decision, AvcDecision::Allow);
1850 }
1851
1852 #[test]
1853 fn validation_request_now_inside_window_is_inclusive() {
1854 let h = Harness::new();
1855 let mut draft = baseline_draft();
1856 draft.constraints.allowed_time_window = Some(TimeWindow {
1857 not_before: ts(1_500_000),
1858 not_after: ts(1_500_000_000),
1859 });
1860 let cred = h.issue(draft);
1861 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1862 assert_eq!(result.decision, AvcDecision::Allow);
1863 }
1864
1865 #[test]
1866 fn confirms_schema_constant_is_one() {
1867 assert_eq!(AVC_SCHEMA_VERSION, 1);
1868 }
1869
1870 #[test]
1871 fn validation_with_only_constraints_passes_when_no_action() {
1872 let h = Harness::new();
1873 let mut draft = baseline_draft();
1874 draft.constraints = AvcConstraints {
1875 max_budget_minor_units: Some(1_000),
1876 currency_code: Some("USD".into()),
1877 max_action_risk_bp: Some(2_000),
1878 human_approval_required: false,
1879 approval_threshold_bp: Some(5_000),
1880 max_delegation_depth: 1,
1881 allowed_time_window: None,
1882 forbidden_actions: vec!["bad".into()],
1883 emergency_stop_refs: vec!["stop".into()],
1884 };
1885 let cred = h.issue(draft);
1886 let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1887 assert_eq!(result.decision, AvcDecision::Allow);
1888 }
1889}