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}