Skip to main content

exo_avc/
validation.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! AVC validation — fail-closed adjudication of a credential and an
18//! optional action against a registry.
19//!
20//! Validation is **deterministic**: it consumes a `now` timestamp from
21//! the caller (no wall-clock reads), iterates registry data through
22//! `BTreeMap`/`BTreeSet`, and produces decisions whose reason codes are
23//! sorted and deduplicated.
24//!
25//! Validation is **fail-closed**: any unresolved key, missing required
26//! reference, malformed structural value, scope violation, expiration,
27//! or revocation produces an explicit `Deny` with reason codes describing
28//! the failure. Errors are reserved for transport-level failures (CBOR
29//! encoding, registry I/O) and must never silently translate into
30//! `Allow`.
31
32use 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
47/// Signing domain tag for AVC human approval evidence.
48pub const AVC_HUMAN_APPROVAL_SIGNING_DOMAIN: &str = "exo.avc.human-approval.v1";
49/// Signing domain tag for AVC subject action proofs.
50pub const AVC_ACTION_SIGNING_DOMAIN: &str = "exo.avc.action.v1";
51/// Signing domain tag for AVC receipt action commitments.
52pub const AVC_ACTION_COMMITMENT_DOMAIN: &str = "exo.avc.action.commitment.v1";
53/// Signing domain tag for canonical receipt action descriptors.
54pub const AVC_ACTION_DESCRIPTOR_DOMAIN: &str = "exo.avc.action.descriptor.v1";
55
56// ---------------------------------------------------------------------------
57// Decision / Reason
58// ---------------------------------------------------------------------------
59
60#[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// ---------------------------------------------------------------------------
101// Validation request / result
102// ---------------------------------------------------------------------------
103
104#[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    /// Free-form action name used to enforce `forbidden_actions`.
118    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
225// ---------------------------------------------------------------------------
226// Validation entry point
227// ---------------------------------------------------------------------------
228
229/// Validate a credential and optional action against a registry.
230///
231/// Decisions are deterministic: the same inputs always yield the same
232/// reason codes in the same order.
233///
234/// # Errors
235/// Returns [`AvcError::Serialization`] if the credential cannot be CBOR
236/// encoded for ID computation. All other failures flow as `Deny`
237/// decisions with reason codes.
238pub 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    // Structural checks first — these would otherwise misroute later checks.
249    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    // Signature: resolve issuer key and verify.
264    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    // Authority chain when issuer != principal.
280    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    // Revocation.
295    if registry.is_revoked(&credential_id) {
296        reasons.insert(AvcReasonCode::Revoked);
297    }
298
299    // Required consent / policy refs.
300    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    // Action fit.
314    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    // Caller ensures `signature.is_empty()` is false before invoking this
357    // helper (see validate_avc). `crypto::verify` itself returns `false`
358    // for `Signature::Empty` defensively, so an empty value here is
359    // simply rejected rather than producing a false positive.
360    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
384/// Compute the canonical signing payload for a human approval over a
385/// specific AVC credential/action pair.
386///
387/// The caller-provided `requires_human_approval` flag is deliberately
388/// excluded because it is not proof of approval. Authorization depends
389/// on this signed approval evidence and the trusted human-approver key
390/// registry instead.
391///
392/// # Errors
393/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
394pub 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
422/// Compute the canonical signing payload for a subject's action proof.
423///
424/// The payload binds the action to the AVC credential ID and the validation
425/// timestamp supplied to the node. This prevents a detached action signature
426/// from being replayed against a different credential or validation context.
427///
428/// # Errors
429/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
430pub 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
448/// Compute a deterministic commitment over the subject-signed action content.
449///
450/// The commitment binds the full action request to the content-addressed AVC
451/// credential ID and the validation timestamp used by the subject action
452/// signature. It does not claim external anchoring.
453///
454/// # Errors
455/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
456pub 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
472/// Compute a deterministic hash for the minimal action descriptor embedded in
473/// trust receipts. The descriptor is intentionally narrower than the signed
474/// action request: high-value proof material remains committed by
475/// [`avc_action_commitment_hash`], while the receipt carries enough canonical
476/// action meaning for court/audit reconstruction.
477///
478/// # Errors
479/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
480pub 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    /// Build a registry seeded with the issuer's public key.
689    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"); // ghost is also principal so authority chain not required
836        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        // Mutate after signing — payload no longer matches signature.
861        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        // Re-sign with a different key.
877        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        // No authority_chain supplied.
949        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        // Construct a credential that fails several checks at once.
1798        let mut draft = baseline_draft();
1799        draft.principal_did = did("principal"); // forces authority chain
1800        // Keep tool empty; action will request a tool.
1801        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; // not in scope
1807        let mut request = baseline_request(cred, ts(3_000_000)); // also expired
1808        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        // No quote/settlement registry exists; validation should still succeed.
1824        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}