Skip to main content

chio_kernel_core/
capability_verify.rs

1//! Pure capability verification.
2//!
3//! Given a `CapabilityToken`, a trusted-issuer key set, and a clock, this
4//! module answers: "is the signature valid, is the issuer trusted, and is
5//! the capability inside its validity window right now?". It does NOT
6//! check:
7//!
8//! - Revocation (stateful, lives in `chio-kernel::revocation_runtime`).
9//! - Delegation-chain lineage against the receipt store (IO-dependent).
10//! - Scope match against a request (use [`crate::scope::resolve_capability_grants`]).
11//! - DPoP subject binding (lives in `chio-kernel::dpop`).
12//!
13//! All four are orchestrated by `chio-kernel::ChioKernel::evaluate_tool_call_sync`,
14//! which calls into this module for the pure pieces and its own async/std
15//! plumbing for the rest.
16//!
17//! Verified-core boundary note:
18//! `formal/proof-manifest.toml` includes this module in the bounded verified
19//! core because it performs only issuer-trust, signature, and time-window
20//! checks over an in-memory capability token. Revocation stores, delegation
21//! lineage joins, and transport-bound subject proof remain excluded surfaces.
22
23use alloc::format;
24use alloc::string::{String, ToString};
25use alloc::vec::Vec;
26
27use chio_core_types::capability::{
28    aggregate_invocation::verify_aggregate_invocation_budget,
29    attenuation::{
30        validate_delegation_chain, validate_delegation_chain_with_trust_root, ScopeHash,
31    },
32    crypto_floor::{CapabilityCryptoFloor, CapabilityFloorVerifyError},
33    cumulative_approval::verify_cumulative_approval_constraints,
34    features::{CapabilityNegotiation, AGGREGATE_INVOCATION_BUDGET, CUMULATIVE_APPROVAL_BUDGET},
35    scope::ChioScope,
36    token::CapabilityToken,
37};
38use chio_core_types::crypto::PublicKey;
39use chio_core_types::error::Error as CoreError;
40
41use crate::budget_split::{BudgetRegistry, BudgetSplitError, NoopBudgetRegistry};
42use crate::clock::Clock;
43use crate::formal_core::{classify_time_window, TimeWindowStatus};
44use crate::normalized::{NormalizationError, NormalizedVerifiedCapability};
45
46/// The subset of a verified capability that portable callers actually need.
47///
48/// This deliberately excludes mutable kernel state (budget counters,
49/// revocation membership) and avoids returning a reference into the token
50/// so adapters that drop the token after verification can still act on
51/// the captured scope.
52#[derive(Debug, Clone)]
53pub struct VerifiedCapability {
54    /// The capability ID.
55    pub id: String,
56    /// The subject hex-encoded public key.
57    pub subject_hex: String,
58    /// The issuer hex-encoded public key.
59    pub issuer_hex: String,
60    /// The authorized scope.
61    pub scope: ChioScope,
62    /// `issued_at` timestamp (Unix seconds).
63    pub issued_at: u64,
64    /// `expires_at` timestamp (Unix seconds).
65    pub expires_at: u64,
66    /// The clock value used for time-bound enforcement.
67    pub evaluated_at: u64,
68}
69
70impl VerifiedCapability {
71    /// Project this verification result into the proof-facing normalized AST.
72    pub fn normalized(&self) -> Result<NormalizedVerifiedCapability, NormalizationError> {
73        NormalizedVerifiedCapability::try_from(self)
74    }
75}
76
77/// Errors raised by [`verify_capability`].
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum CapabilityError {
80    /// Issuer public key is not in the trusted set.
81    UntrustedIssuer,
82    /// Canonical-JSON signature did not verify against the issuer key.
83    InvalidSignature,
84    /// Signature material violates the configured crypto floor.
85    CryptoFloorRejected(String),
86    /// Token is not yet valid (clock is before `issued_at`).
87    NotYetValid,
88    /// Token has expired.
89    Expired,
90    /// Attenuated capability token violated the chain-binding rule.
91    /// `attenuation_proof.parent_scope_hash` did not match either the
92    /// issuer's trust-root scope hash (direct issue) or the last
93    /// delegation link's `scope_hash` (delegated chain).
94    AttenuationViolation(String),
95    /// Sibling-sum budget enforcement rejected this delegation.
96    BudgetSplitRejected(BudgetSplitError),
97    /// An internal invariant was violated (e.g. canonical-JSON failure).
98    Internal(String),
99}
100
101impl From<BudgetSplitError> for CapabilityError {
102    fn from(err: BudgetSplitError) -> Self {
103        CapabilityError::BudgetSplitRejected(err)
104    }
105}
106
107/// Verify the signature, issuer trust, and time-bounds of a capability token.
108///
109/// Returns a [`VerifiedCapability`] when all three checks succeed. Delegation
110/// chain validation, revocation lookup, and subject-binding checks are the
111/// caller's responsibility (see module docs).
112///
113/// This wrapper uses a [`NoopBudgetRegistry`] internally; callers that need
114/// sibling-sum budget enforcement must use [`verify_capability_with_floor`]
115/// directly with their own [`BudgetRegistry`] instance.
116pub fn verify_capability(
117    token: &CapabilityToken,
118    trusted_issuers: &[PublicKey],
119    clock: &dyn Clock,
120) -> Result<VerifiedCapability, CapabilityError> {
121    let mut budgets = NoopBudgetRegistry;
122    verify_capability_with_floor(
123        token,
124        trusted_issuers,
125        clock,
126        CapabilityCryptoFloor::AllowClassical,
127        &mut budgets,
128    )
129}
130
131/// Verify a capability token while enforcing the configured crypto floor and
132/// sibling-sum budget split.
133///
134/// This is the floor-aware entry point for kernels that load
135/// `policy.crypto_floor`. The default [`verify_capability`] wrapper uses
136/// [`CapabilityCryptoFloor::AllowClassical`] and a [`NoopBudgetRegistry`].
137///
138/// Sibling-sum enforcement: when the token carries a non-empty
139/// `delegation_chain`, the verifier asks `budgets` to admit the new child
140/// under the immediate parent (the last entry in the chain). The proposed
141/// share is `token.budget_share_bps.unwrap_or(MAX_BUDGET_SHARE_BPS)`: a
142/// missing field is interpreted as a request for the full parent share.
143/// The parent itself must already be registered in `budgets` from
144/// verifier-owned lineage or a parent snapshot. Unknown parents fail closed.
145/// Per-token validation has already enforced the `<= 10_000` cap by the time
146/// the token reaches this function.
147pub fn verify_capability_with_floor(
148    token: &CapabilityToken,
149    trusted_issuers: &[PublicKey],
150    clock: &dyn Clock,
151    crypto_floor: CapabilityCryptoFloor,
152    budgets: &mut dyn BudgetRegistry,
153) -> Result<VerifiedCapability, CapabilityError> {
154    let verified =
155        verify_capability_base(token, trusted_issuers, clock, crypto_floor, false, false)?;
156    admit_delegated_budget(token, budgets)?;
157    Ok(verified)
158}
159
160fn verify_capability_base(
161    token: &CapabilityToken,
162    trusted_issuers: &[PublicKey],
163    clock: &dyn Clock,
164    crypto_floor: CapabilityCryptoFloor,
165    aggregate_budget_enabled: bool,
166    cumulative_approval_enabled: bool,
167) -> Result<VerifiedCapability, CapabilityError> {
168    // Issuer trust check. The full kernel also trusts its own public key
169    // and the set returned by the capability authority; callers must
170    // provide the full trust set they care about.
171    if !trusted_issuers.contains(&token.issuer) {
172        return Err(CapabilityError::UntrustedIssuer);
173    }
174    if !aggregate_budget_enabled && token_uses_aggregate_budget(token) {
175        return Err(CapabilityError::AttenuationViolation(
176            "aggregate_invocation_budget was not negotiated".to_string(),
177        ));
178    }
179    if !cumulative_approval_enabled && token.scope.has_cumulative_approval() {
180        return Err(CapabilityError::AttenuationViolation(
181            "cumulative_approval_budget was not negotiated".to_string(),
182        ));
183    }
184
185    // Signature check.
186    match token.verify_signature_with_floor(crypto_floor) {
187        Ok(true) => {}
188        Ok(false) => return Err(CapabilityError::InvalidSignature),
189        Err(error @ CapabilityFloorVerifyError::RejectedByCryptoFloor { .. }) => {
190            return Err(CapabilityError::CryptoFloorRejected(error.to_string()));
191        }
192        Err(error @ CapabilityFloorVerifyError::AlgorithmMismatch { .. }) => {
193            return Err(CapabilityError::CryptoFloorRejected(error.to_string()));
194        }
195        Err(CapabilityFloorVerifyError::Crypto(error)) => {
196            return Err(CapabilityError::Internal(error.to_string()));
197        }
198    }
199
200    // Time-bound check.
201    let now = clock.now_unix_secs();
202    match classify_time_window(now, token.issued_at, token.expires_at) {
203        TimeWindowStatus::Valid => {}
204        TimeWindowStatus::NotYetValid => return Err(CapabilityError::NotYetValid),
205        TimeWindowStatus::Expired => return Err(CapabilityError::Expired),
206    }
207
208    Ok(VerifiedCapability {
209        id: token.id.clone(),
210        subject_hex: token.subject.to_hex(),
211        issuer_hex: token.issuer.to_hex(),
212        scope: token.scope.clone(),
213        issued_at: token.issued_at,
214        expires_at: token.expires_at,
215        evaluated_at: now,
216    })
217}
218
219pub(crate) fn admit_delegated_budget(
220    token: &CapabilityToken,
221    budgets: &mut dyn BudgetRegistry,
222) -> Result<(), CapabilityError> {
223    // Sibling-sum budget split. Only fires for tokens that carry a
224    // delegation chain; root-issued tokens have nothing to split.
225    //
226    // The parent must already be registered from verifier-owned lineage
227    // or a parent snapshot. Unknown parents fail closed; the verifier must
228    // not fabricate a missing parent share at MAX_BUDGET_SHARE_BPS.
229    //
230    // This runs on the shared VERIFY surface (portable/preflight verdicts and
231    // adapter one-shot evaluations produced by the pure `evaluate_*` entry
232    // points). None of those callers hold a `PostAdmissionDropGuard` or reach
233    // `release_admitted_capability_budget`, so this MUST NOT take a holder
234    // lease: a lease acquired here would never be released and would pin the
235    // child edge upward forever. `verify_child_admission` runs the same
236    // fail-closed oversubscription checks and still commits a fresh child's
237    // share for sibling-sum accounting, but records no releasable holder. The
238    // authoritative lease is taken separately by the hosted dispatch path
239    // (`ChioKernel::admit_capability_budget`), which owns the matching release.
240    if let Some(parent_link) = token.delegation_chain.last() {
241        let proposed_share = token
242            .budget_share_bps
243            .unwrap_or(crate::budget_split::MAX_BUDGET_SHARE_BPS);
244        budgets.verify_child_admission(
245            parent_link.capability_id.as_str(),
246            token.id.clone(),
247            proposed_share,
248        )?;
249    }
250
251    Ok(())
252}
253
254/// Verify a capability token while enforcing the configured crypto floor.
255/// The peer profile is still accepted so federation callers can keep one
256/// call surface, but Chio-owned capability schemas are single-version until
257/// first release.
258pub fn verify_capability_with_negotiated_floor(
259    token: &CapabilityToken,
260    trusted_issuers: &[PublicKey],
261    clock: &dyn Clock,
262    crypto_floor: CapabilityCryptoFloor,
263    peer: &CapabilityNegotiation,
264) -> Result<VerifiedCapability, CapabilityError> {
265    validate_peer_capabilities(peer)?;
266    let aggregate_budget_enabled = peer.supports(AGGREGATE_INVOCATION_BUDGET);
267    let cumulative_approval_enabled = peer.supports(CUMULATIVE_APPROVAL_BUDGET);
268    let verified = verify_capability_base(
269        token,
270        trusted_issuers,
271        clock,
272        crypto_floor,
273        aggregate_budget_enabled,
274        cumulative_approval_enabled,
275    )?;
276    verify_negotiated_aggregate_budget(token, trusted_issuers, aggregate_budget_enabled, None)?;
277    verify_negotiated_cumulative_approval(
278        token,
279        trusted_issuers,
280        cumulative_approval_enabled,
281        None,
282    )?;
283    let mut budgets = NoopBudgetRegistry;
284    admit_delegated_budget(token, &mut budgets)?;
285    Ok(verified)
286}
287
288/// Convenience wrapper around [`verify_capability`] that returns the
289/// trusted-issuer list as a `Vec` so adapters can build it lazily.
290pub fn verify_capability_with_trusted<I>(
291    token: &CapabilityToken,
292    trusted_issuers: I,
293    clock: &dyn Clock,
294) -> Result<VerifiedCapability, CapabilityError>
295where
296    I: IntoIterator<Item = PublicKey>,
297{
298    let trusted: Vec<PublicKey> = trusted_issuers.into_iter().collect();
299    verify_capability(token, &trusted, clock)
300}
301
302/// Convenience wrapper around [`verify_capability_with_floor`] for callers that
303/// build the trusted issuer set lazily. Uses a [`NoopBudgetRegistry`]; new
304/// callers that care about sibling-sum enforcement should call
305/// [`verify_capability_with_floor`] with their own registry.
306pub fn verify_capability_with_trusted_and_floor<I>(
307    token: &CapabilityToken,
308    trusted_issuers: I,
309    clock: &dyn Clock,
310    crypto_floor: CapabilityCryptoFloor,
311) -> Result<VerifiedCapability, CapabilityError>
312where
313    I: IntoIterator<Item = PublicKey>,
314{
315    let trusted: Vec<PublicKey> = trusted_issuers.into_iter().collect();
316    let mut budgets = NoopBudgetRegistry;
317    verify_capability_with_floor(token, &trusted, clock, crypto_floor, &mut budgets)
318}
319
320/// Resolver returning the trust-root scope hash bound to a given issuer
321/// public key. Kernels supply this so the verifier can bind
322/// `attenuation_proof.parent_scope_hash` to the issuing CA's authority
323/// hash on direct-issue attenuated tokens.
324pub trait TrustRootResolver {
325    /// Resolve the trust-root scope hash for `issuer`, returning `None`
326    /// when the issuer has no registered authority hash. The verifier
327    /// treats `None` as a fail-closed deny for attenuated tokens that require
328    /// chain binding.
329    fn trust_root_scope_hash(&self, issuer: &PublicKey) -> Option<ScopeHash>;
330}
331
332impl<F> TrustRootResolver for F
333where
334    F: Fn(&PublicKey) -> Option<ScopeHash>,
335{
336    fn trust_root_scope_hash(&self, issuer: &PublicKey) -> Option<ScopeHash> {
337        (self)(issuer)
338    }
339}
340
341/// Negotiated optional-feature profile and authenticated family-root evidence.
342#[derive(Debug, Clone, Copy)]
343pub struct CapabilityFeatureContext<'a> {
344    pub peer: &'a CapabilityNegotiation,
345    pub direct_root: Option<&'a CapabilityToken>,
346}
347
348/// Chain-binding entry point. Verify a capability token while also
349/// enforcing the chain-binding rule required for delegation soundness.
350///
351/// In addition to the checks in [`verify_capability_with_floor`], this
352/// entry point checks tokens that carry an `attenuation_proof`
353/// whose `parent_scope_hash` matches either:
354///
355/// - `trust_root_scope_hash` (when the delegation chain is empty: a
356///   direct issue from the trust-root authority binds the witness to the
357///   verifier-known authority hash); or
358/// - `delegation_chain.last().scope_hash` (when delegation has occurred:
359///   the witness binds to the predecessor's signed scope_hash).
360///
361/// Non-attenuated tokens are accepted unchanged.
362pub fn verify_capability_with_floor_and_trust_root(
363    token: &CapabilityToken,
364    trusted_issuers: &[PublicKey],
365    clock: &dyn Clock,
366    crypto_floor: CapabilityCryptoFloor,
367    trust_root_scope_hash: &ScopeHash,
368) -> Result<VerifiedCapability, CapabilityError> {
369    let verified =
370        verify_capability_base(token, trusted_issuers, clock, crypto_floor, false, false)?;
371    verify_delegation_chain_shape(token)?;
372    verify_chain_binding_with_trust_root(token, trust_root_scope_hash)?;
373
374    Ok(verified)
375}
376
377/// Resolver-driven variant of [`verify_capability_with_floor_and_trust_root`].
378///
379/// Kernels that maintain a per-issuer trust-root registry pass a
380/// [`TrustRootResolver`] so the verifier can pick the correct authority
381/// hash without leaking the registry shape into the verifier surface.
382pub fn verify_capability_with_floor_and_resolver(
383    token: &CapabilityToken,
384    trusted_issuers: &[PublicKey],
385    clock: &dyn Clock,
386    crypto_floor: CapabilityCryptoFloor,
387    trust_root: &dyn TrustRootResolver,
388) -> Result<VerifiedCapability, CapabilityError> {
389    let verified =
390        verify_capability_base(token, trusted_issuers, clock, crypto_floor, false, false)?;
391    verify_delegation_chain_shape(token)?;
392    verify_chain_binding_with_resolver(token, trust_root)?;
393
394    Ok(verified)
395}
396
397/// Full verifier entry point for current capability semantics without signed
398/// optional-family root evidence.
399pub fn verify_capability_full(
400    token: &CapabilityToken,
401    trusted_issuers: &[PublicKey],
402    clock: &dyn Clock,
403    crypto_floor: CapabilityCryptoFloor,
404    peer: &CapabilityNegotiation,
405    trust_root: &dyn TrustRootResolver,
406    budgets: &mut dyn BudgetRegistry,
407) -> Result<VerifiedCapability, CapabilityError> {
408    verify_capability_full_with_root(
409        token,
410        trusted_issuers,
411        clock,
412        crypto_floor,
413        CapabilityFeatureContext {
414            peer,
415            direct_root: None,
416        },
417        trust_root,
418        budgets,
419    )
420}
421
422/// Full verifier entry point with authenticated optional-family root evidence
423/// for delegated tokens.
424pub fn verify_capability_full_with_root(
425    token: &CapabilityToken,
426    trusted_issuers: &[PublicKey],
427    clock: &dyn Clock,
428    crypto_floor: CapabilityCryptoFloor,
429    features: CapabilityFeatureContext<'_>,
430    trust_root: &dyn TrustRootResolver,
431    budgets: &mut dyn BudgetRegistry,
432) -> Result<VerifiedCapability, CapabilityError> {
433    let CapabilityFeatureContext { peer, direct_root } = features;
434    validate_peer_capabilities(peer)?;
435    let aggregate_budget_enabled = peer.supports(AGGREGATE_INVOCATION_BUDGET);
436    let cumulative_approval_enabled = peer.supports(CUMULATIVE_APPROVAL_BUDGET);
437    let verified = verify_capability_base(
438        token,
439        trusted_issuers,
440        clock,
441        crypto_floor,
442        aggregate_budget_enabled,
443        cumulative_approval_enabled,
444    )?;
445    if let Some(root) = direct_root {
446        verify_capability_base(
447            root,
448            trusted_issuers,
449            clock,
450            crypto_floor,
451            aggregate_budget_enabled,
452            cumulative_approval_enabled,
453        )?;
454    }
455    verify_negotiated_aggregate_budget(
456        token,
457        trusted_issuers,
458        aggregate_budget_enabled,
459        direct_root,
460    )?;
461    verify_negotiated_cumulative_approval(
462        token,
463        trusted_issuers,
464        cumulative_approval_enabled,
465        direct_root,
466    )?;
467    verify_delegation_chain_shape(token)?;
468    verify_chain_binding_with_negotiation(token, peer, trust_root)?;
469    admit_delegated_budget(token, budgets)?;
470    Ok(verified)
471}
472
473fn validate_peer_capabilities(peer: &CapabilityNegotiation) -> Result<(), CapabilityError> {
474    peer.validate().map_err(|error| {
475        CapabilityError::AttenuationViolation(format!(
476            "invalid capability negotiation profile: {error}"
477        ))
478    })
479}
480
481fn token_uses_aggregate_budget(token: &CapabilityToken) -> bool {
482    token.aggregate_invocation_budget.is_some()
483        || token
484            .delegation_chain
485            .iter()
486            .any(|link| link.aggregate_budget.is_some())
487        || token
488            .attenuation_proof
489            .as_ref()
490            .is_some_and(|proof| proof.normalized_subset_proof.aggregate_budget.is_some())
491}
492
493fn verify_negotiated_aggregate_budget(
494    token: &CapabilityToken,
495    trusted_issuers: &[PublicKey],
496    enabled: bool,
497    direct_root: Option<&CapabilityToken>,
498) -> Result<(), CapabilityError> {
499    if !enabled {
500        if token_uses_aggregate_budget(token)
501            || direct_root.is_some_and(token_uses_aggregate_budget)
502        {
503            return Err(CapabilityError::AttenuationViolation(
504                "aggregate_invocation_budget was not negotiated".to_string(),
505            ));
506        }
507        return Ok(());
508    }
509
510    verify_aggregate_invocation_budget(token, trusted_issuers, direct_root)
511        .map(|_| ())
512        .map_err(map_optional_feature_error)
513}
514
515fn verify_negotiated_cumulative_approval(
516    token: &CapabilityToken,
517    trusted_issuers: &[PublicKey],
518    enabled: bool,
519    direct_root: Option<&CapabilityToken>,
520) -> Result<(), CapabilityError> {
521    if !enabled {
522        if token.scope.has_cumulative_approval()
523            || direct_root.is_some_and(|root| root.scope.has_cumulative_approval())
524        {
525            return Err(CapabilityError::AttenuationViolation(
526                "cumulative_approval_budget was not negotiated".to_string(),
527            ));
528        }
529        return Ok(());
530    }
531
532    verify_cumulative_approval_constraints(token, trusted_issuers, direct_root)
533        .map(|_| ())
534        .map_err(map_optional_feature_error)
535}
536
537fn map_optional_feature_error(error: CoreError) -> CapabilityError {
538    match error {
539        CoreError::SignatureVerificationFailed | CoreError::InvalidSignature(_) => {
540            CapabilityError::InvalidSignature
541        }
542        CoreError::AttenuationViolation { reason }
543        | CoreError::DelegationChainBroken { reason }
544        | CoreError::ScopeMismatch { reason } => CapabilityError::AttenuationViolation(reason),
545        other => CapabilityError::Internal(other.to_string()),
546    }
547}
548
549fn verify_delegation_chain_shape(token: &CapabilityToken) -> Result<(), CapabilityError> {
550    if token.delegation_chain.is_empty() {
551        return Ok(());
552    }
553    validate_delegation_chain(&token.delegation_chain, None)
554        .map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
555    let Some(final_link) = token.delegation_chain.last() else {
556        return Ok(());
557    };
558    let final_delegatee = &final_link.delegatee;
559    if final_delegatee != &token.subject {
560        return Err(CapabilityError::AttenuationViolation(
561            "delegation chain final delegatee does not match capability subject".to_string(),
562        ));
563    }
564    Ok(())
565}
566
567fn verify_chain_binding_with_trust_root(
568    token: &CapabilityToken,
569    trust_root_scope_hash: &ScopeHash,
570) -> Result<(), CapabilityError> {
571    if token.requires_chain_binding() {
572        token
573            .validate_chain_binding(trust_root_scope_hash)
574            .map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
575    }
576    Ok(())
577}
578
579fn verify_chain_binding_with_resolver(
580    token: &CapabilityToken,
581    trust_root: &dyn TrustRootResolver,
582) -> Result<(), CapabilityError> {
583    if token.requires_chain_binding() {
584        let issuer_root = trust_root
585            .trust_root_scope_hash(&token.issuer)
586            .ok_or_else(|| {
587                CapabilityError::AttenuationViolation(
588                    "chain-binding: no trust-root scope hash registered for issuer".to_string(),
589                )
590            })?;
591        validate_delegation_chain_with_trust_root(&token.delegation_chain, None, &issuer_root)
592            .map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
593        token
594            .validate_chain_binding(&issuer_root)
595            .map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
596    }
597    Ok(())
598}
599
600fn verify_chain_binding_with_negotiation(
601    token: &CapabilityToken,
602    peer: &CapabilityNegotiation,
603    trust_root: &dyn TrustRootResolver,
604) -> Result<(), CapabilityError> {
605    if token.requires_chain_binding() {
606        let chain_binding_enabled = peer
607            .features
608            .get(chio_core_types::capability::features::DELEGATION_CHAIN_BINDING)
609            .copied()
610            .unwrap_or(true);
611        if !chain_binding_enabled {
612            return Err(CapabilityError::AttenuationViolation(
613                "chain-binding: peer disabled delegation_chain_binding; attenuated tokens are rejected".to_string(),
614            ));
615        }
616        verify_chain_binding_with_resolver(token, trust_root)?;
617    }
618    Ok(())
619}
620
621#[cfg(test)]
622#[path = "capability_verify_tests.rs"]
623mod tests;