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::string::{String, ToString};
24use alloc::vec::Vec;
25
26use chio_core_types::capability::{CapabilityToken, ChioScope};
27use chio_core_types::crypto::PublicKey;
28
29use crate::clock::Clock;
30use crate::normalized::{NormalizationError, NormalizedVerifiedCapability};
31
32/// The subset of a verified capability that portable callers actually need.
33///
34/// This deliberately excludes mutable kernel state (budget counters,
35/// revocation membership) and avoids returning a reference into the token
36/// so adapters that drop the token after verification can still act on
37/// the captured scope.
38#[derive(Debug, Clone)]
39pub struct VerifiedCapability {
40    /// The capability ID.
41    pub id: String,
42    /// The subject hex-encoded public key.
43    pub subject_hex: String,
44    /// The issuer hex-encoded public key.
45    pub issuer_hex: String,
46    /// The authorized scope.
47    pub scope: ChioScope,
48    /// `issued_at` timestamp (Unix seconds).
49    pub issued_at: u64,
50    /// `expires_at` timestamp (Unix seconds).
51    pub expires_at: u64,
52    /// The clock value used for time-bound enforcement.
53    pub evaluated_at: u64,
54}
55
56impl VerifiedCapability {
57    /// Project this verification result into the proof-facing normalized AST.
58    pub fn normalized(&self) -> Result<NormalizedVerifiedCapability, NormalizationError> {
59        NormalizedVerifiedCapability::try_from(self)
60    }
61}
62
63/// Errors raised by [`verify_capability`].
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum CapabilityError {
66    /// Issuer public key is not in the trusted set.
67    UntrustedIssuer,
68    /// Canonical-JSON signature did not verify against the issuer key.
69    InvalidSignature,
70    /// Token is not yet valid (clock is before `issued_at`).
71    NotYetValid,
72    /// Token has expired.
73    Expired,
74    /// An internal invariant was violated (e.g. canonical-JSON failure).
75    Internal(String),
76}
77
78/// Verify the signature, issuer trust, and time-bounds of a capability token.
79///
80/// Returns a [`VerifiedCapability`] when all three checks succeed. Delegation
81/// chain validation, revocation lookup, and subject-binding checks are the
82/// caller's responsibility (see module docs).
83pub fn verify_capability(
84    token: &CapabilityToken,
85    trusted_issuers: &[PublicKey],
86    clock: &dyn Clock,
87) -> Result<VerifiedCapability, CapabilityError> {
88    // Issuer trust check. The legacy kernel also trusts its own public key
89    // and the set returned by the capability authority; callers must
90    // provide the full trust set they care about.
91    if !trusted_issuers.contains(&token.issuer) {
92        return Err(CapabilityError::UntrustedIssuer);
93    }
94
95    // Signature check.
96    match token.verify_signature() {
97        Ok(true) => {}
98        Ok(false) => return Err(CapabilityError::InvalidSignature),
99        Err(error) => {
100            return Err(CapabilityError::Internal(error.to_string()));
101        }
102    }
103
104    // Time-bound check.
105    let now = clock.now_unix_secs();
106    if now < token.issued_at {
107        return Err(CapabilityError::NotYetValid);
108    }
109    if now >= token.expires_at {
110        return Err(CapabilityError::Expired);
111    }
112
113    Ok(VerifiedCapability {
114        id: token.id.clone(),
115        subject_hex: token.subject.to_hex(),
116        issuer_hex: token.issuer.to_hex(),
117        scope: token.scope.clone(),
118        issued_at: token.issued_at,
119        expires_at: token.expires_at,
120        evaluated_at: now,
121    })
122}
123
124/// Convenience wrapper around [`verify_capability`] that returns the
125/// trusted-issuer list as a `Vec` so adapters can build it lazily.
126pub fn verify_capability_with_trusted<I>(
127    token: &CapabilityToken,
128    trusted_issuers: I,
129    clock: &dyn Clock,
130) -> Result<VerifiedCapability, CapabilityError>
131where
132    I: IntoIterator<Item = PublicKey>,
133{
134    let trusted: Vec<PublicKey> = trusted_issuers.into_iter().collect();
135    verify_capability(token, &trusted, clock)
136}