use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use chio_core_types::capability::{
aggregate_invocation::verify_aggregate_invocation_budget,
attenuation::{
validate_delegation_chain, validate_delegation_chain_with_trust_root, ScopeHash,
},
crypto_floor::{CapabilityCryptoFloor, CapabilityFloorVerifyError},
cumulative_approval::verify_cumulative_approval_constraints,
features::{CapabilityNegotiation, AGGREGATE_INVOCATION_BUDGET, CUMULATIVE_APPROVAL_BUDGET},
scope::ChioScope,
token::CapabilityToken,
};
use chio_core_types::crypto::PublicKey;
use chio_core_types::error::Error as CoreError;
use crate::budget_split::{BudgetRegistry, BudgetSplitError, NoopBudgetRegistry};
use crate::clock::Clock;
use crate::formal_core::{classify_time_window, TimeWindowStatus};
use crate::normalized::{NormalizationError, NormalizedVerifiedCapability};
#[derive(Debug, Clone)]
pub struct VerifiedCapability {
pub id: String,
pub subject_hex: String,
pub issuer_hex: String,
pub scope: ChioScope,
pub issued_at: u64,
pub expires_at: u64,
pub evaluated_at: u64,
}
impl VerifiedCapability {
pub fn normalized(&self) -> Result<NormalizedVerifiedCapability, NormalizationError> {
NormalizedVerifiedCapability::try_from(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CapabilityError {
UntrustedIssuer,
InvalidSignature,
CryptoFloorRejected(String),
NotYetValid,
Expired,
AttenuationViolation(String),
BudgetSplitRejected(BudgetSplitError),
Internal(String),
}
impl From<BudgetSplitError> for CapabilityError {
fn from(err: BudgetSplitError) -> Self {
CapabilityError::BudgetSplitRejected(err)
}
}
pub fn verify_capability(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
) -> Result<VerifiedCapability, CapabilityError> {
let mut budgets = NoopBudgetRegistry;
verify_capability_with_floor(
token,
trusted_issuers,
clock,
CapabilityCryptoFloor::AllowClassical,
&mut budgets,
)
}
pub fn verify_capability_with_floor(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
budgets: &mut dyn BudgetRegistry,
) -> Result<VerifiedCapability, CapabilityError> {
let verified =
verify_capability_base(token, trusted_issuers, clock, crypto_floor, false, false)?;
admit_delegated_budget(token, budgets)?;
Ok(verified)
}
fn verify_capability_base(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
aggregate_budget_enabled: bool,
cumulative_approval_enabled: bool,
) -> Result<VerifiedCapability, CapabilityError> {
if !trusted_issuers.contains(&token.issuer) {
return Err(CapabilityError::UntrustedIssuer);
}
if !aggregate_budget_enabled && token_uses_aggregate_budget(token) {
return Err(CapabilityError::AttenuationViolation(
"aggregate_invocation_budget was not negotiated".to_string(),
));
}
if !cumulative_approval_enabled && token.scope.has_cumulative_approval() {
return Err(CapabilityError::AttenuationViolation(
"cumulative_approval_budget was not negotiated".to_string(),
));
}
match token.verify_signature_with_floor(crypto_floor) {
Ok(true) => {}
Ok(false) => return Err(CapabilityError::InvalidSignature),
Err(error @ CapabilityFloorVerifyError::RejectedByCryptoFloor { .. }) => {
return Err(CapabilityError::CryptoFloorRejected(error.to_string()));
}
Err(error @ CapabilityFloorVerifyError::AlgorithmMismatch { .. }) => {
return Err(CapabilityError::CryptoFloorRejected(error.to_string()));
}
Err(CapabilityFloorVerifyError::Crypto(error)) => {
return Err(CapabilityError::Internal(error.to_string()));
}
}
let now = clock.now_unix_secs();
match classify_time_window(now, token.issued_at, token.expires_at) {
TimeWindowStatus::Valid => {}
TimeWindowStatus::NotYetValid => return Err(CapabilityError::NotYetValid),
TimeWindowStatus::Expired => return Err(CapabilityError::Expired),
}
Ok(VerifiedCapability {
id: token.id.clone(),
subject_hex: token.subject.to_hex(),
issuer_hex: token.issuer.to_hex(),
scope: token.scope.clone(),
issued_at: token.issued_at,
expires_at: token.expires_at,
evaluated_at: now,
})
}
pub(crate) fn admit_delegated_budget(
token: &CapabilityToken,
budgets: &mut dyn BudgetRegistry,
) -> Result<(), CapabilityError> {
if let Some(parent_link) = token.delegation_chain.last() {
let proposed_share = token
.budget_share_bps
.unwrap_or(crate::budget_split::MAX_BUDGET_SHARE_BPS);
budgets.verify_child_admission(
parent_link.capability_id.as_str(),
token.id.clone(),
proposed_share,
)?;
}
Ok(())
}
pub fn verify_capability_with_negotiated_floor(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
peer: &CapabilityNegotiation,
) -> Result<VerifiedCapability, CapabilityError> {
validate_peer_capabilities(peer)?;
let aggregate_budget_enabled = peer.supports(AGGREGATE_INVOCATION_BUDGET);
let cumulative_approval_enabled = peer.supports(CUMULATIVE_APPROVAL_BUDGET);
let verified = verify_capability_base(
token,
trusted_issuers,
clock,
crypto_floor,
aggregate_budget_enabled,
cumulative_approval_enabled,
)?;
verify_negotiated_aggregate_budget(token, trusted_issuers, aggregate_budget_enabled, None)?;
verify_negotiated_cumulative_approval(
token,
trusted_issuers,
cumulative_approval_enabled,
None,
)?;
let mut budgets = NoopBudgetRegistry;
admit_delegated_budget(token, &mut budgets)?;
Ok(verified)
}
pub fn verify_capability_with_trusted<I>(
token: &CapabilityToken,
trusted_issuers: I,
clock: &dyn Clock,
) -> Result<VerifiedCapability, CapabilityError>
where
I: IntoIterator<Item = PublicKey>,
{
let trusted: Vec<PublicKey> = trusted_issuers.into_iter().collect();
verify_capability(token, &trusted, clock)
}
pub fn verify_capability_with_trusted_and_floor<I>(
token: &CapabilityToken,
trusted_issuers: I,
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
) -> Result<VerifiedCapability, CapabilityError>
where
I: IntoIterator<Item = PublicKey>,
{
let trusted: Vec<PublicKey> = trusted_issuers.into_iter().collect();
let mut budgets = NoopBudgetRegistry;
verify_capability_with_floor(token, &trusted, clock, crypto_floor, &mut budgets)
}
pub trait TrustRootResolver {
fn trust_root_scope_hash(&self, issuer: &PublicKey) -> Option<ScopeHash>;
}
impl<F> TrustRootResolver for F
where
F: Fn(&PublicKey) -> Option<ScopeHash>,
{
fn trust_root_scope_hash(&self, issuer: &PublicKey) -> Option<ScopeHash> {
(self)(issuer)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CapabilityFeatureContext<'a> {
pub peer: &'a CapabilityNegotiation,
pub direct_root: Option<&'a CapabilityToken>,
}
pub fn verify_capability_with_floor_and_trust_root(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
trust_root_scope_hash: &ScopeHash,
) -> Result<VerifiedCapability, CapabilityError> {
let verified =
verify_capability_base(token, trusted_issuers, clock, crypto_floor, false, false)?;
verify_delegation_chain_shape(token)?;
verify_chain_binding_with_trust_root(token, trust_root_scope_hash)?;
Ok(verified)
}
pub fn verify_capability_with_floor_and_resolver(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
trust_root: &dyn TrustRootResolver,
) -> Result<VerifiedCapability, CapabilityError> {
let verified =
verify_capability_base(token, trusted_issuers, clock, crypto_floor, false, false)?;
verify_delegation_chain_shape(token)?;
verify_chain_binding_with_resolver(token, trust_root)?;
Ok(verified)
}
pub fn verify_capability_full(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
peer: &CapabilityNegotiation,
trust_root: &dyn TrustRootResolver,
budgets: &mut dyn BudgetRegistry,
) -> Result<VerifiedCapability, CapabilityError> {
verify_capability_full_with_root(
token,
trusted_issuers,
clock,
crypto_floor,
CapabilityFeatureContext {
peer,
direct_root: None,
},
trust_root,
budgets,
)
}
pub fn verify_capability_full_with_root(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
crypto_floor: CapabilityCryptoFloor,
features: CapabilityFeatureContext<'_>,
trust_root: &dyn TrustRootResolver,
budgets: &mut dyn BudgetRegistry,
) -> Result<VerifiedCapability, CapabilityError> {
let CapabilityFeatureContext { peer, direct_root } = features;
validate_peer_capabilities(peer)?;
let aggregate_budget_enabled = peer.supports(AGGREGATE_INVOCATION_BUDGET);
let cumulative_approval_enabled = peer.supports(CUMULATIVE_APPROVAL_BUDGET);
let verified = verify_capability_base(
token,
trusted_issuers,
clock,
crypto_floor,
aggregate_budget_enabled,
cumulative_approval_enabled,
)?;
if let Some(root) = direct_root {
verify_capability_base(
root,
trusted_issuers,
clock,
crypto_floor,
aggregate_budget_enabled,
cumulative_approval_enabled,
)?;
}
verify_negotiated_aggregate_budget(
token,
trusted_issuers,
aggregate_budget_enabled,
direct_root,
)?;
verify_negotiated_cumulative_approval(
token,
trusted_issuers,
cumulative_approval_enabled,
direct_root,
)?;
verify_delegation_chain_shape(token)?;
verify_chain_binding_with_negotiation(token, peer, trust_root)?;
admit_delegated_budget(token, budgets)?;
Ok(verified)
}
fn validate_peer_capabilities(peer: &CapabilityNegotiation) -> Result<(), CapabilityError> {
peer.validate().map_err(|error| {
CapabilityError::AttenuationViolation(format!(
"invalid capability negotiation profile: {error}"
))
})
}
fn token_uses_aggregate_budget(token: &CapabilityToken) -> bool {
token.aggregate_invocation_budget.is_some()
|| token
.delegation_chain
.iter()
.any(|link| link.aggregate_budget.is_some())
|| token
.attenuation_proof
.as_ref()
.is_some_and(|proof| proof.normalized_subset_proof.aggregate_budget.is_some())
}
fn verify_negotiated_aggregate_budget(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
enabled: bool,
direct_root: Option<&CapabilityToken>,
) -> Result<(), CapabilityError> {
if !enabled {
if token_uses_aggregate_budget(token)
|| direct_root.is_some_and(token_uses_aggregate_budget)
{
return Err(CapabilityError::AttenuationViolation(
"aggregate_invocation_budget was not negotiated".to_string(),
));
}
return Ok(());
}
verify_aggregate_invocation_budget(token, trusted_issuers, direct_root)
.map(|_| ())
.map_err(map_optional_feature_error)
}
fn verify_negotiated_cumulative_approval(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
enabled: bool,
direct_root: Option<&CapabilityToken>,
) -> Result<(), CapabilityError> {
if !enabled {
if token.scope.has_cumulative_approval()
|| direct_root.is_some_and(|root| root.scope.has_cumulative_approval())
{
return Err(CapabilityError::AttenuationViolation(
"cumulative_approval_budget was not negotiated".to_string(),
));
}
return Ok(());
}
verify_cumulative_approval_constraints(token, trusted_issuers, direct_root)
.map(|_| ())
.map_err(map_optional_feature_error)
}
fn map_optional_feature_error(error: CoreError) -> CapabilityError {
match error {
CoreError::SignatureVerificationFailed | CoreError::InvalidSignature(_) => {
CapabilityError::InvalidSignature
}
CoreError::AttenuationViolation { reason }
| CoreError::DelegationChainBroken { reason }
| CoreError::ScopeMismatch { reason } => CapabilityError::AttenuationViolation(reason),
other => CapabilityError::Internal(other.to_string()),
}
}
fn verify_delegation_chain_shape(token: &CapabilityToken) -> Result<(), CapabilityError> {
if token.delegation_chain.is_empty() {
return Ok(());
}
validate_delegation_chain(&token.delegation_chain, None)
.map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
let Some(final_link) = token.delegation_chain.last() else {
return Ok(());
};
let final_delegatee = &final_link.delegatee;
if final_delegatee != &token.subject {
return Err(CapabilityError::AttenuationViolation(
"delegation chain final delegatee does not match capability subject".to_string(),
));
}
Ok(())
}
fn verify_chain_binding_with_trust_root(
token: &CapabilityToken,
trust_root_scope_hash: &ScopeHash,
) -> Result<(), CapabilityError> {
if token.requires_chain_binding() {
token
.validate_chain_binding(trust_root_scope_hash)
.map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
}
Ok(())
}
fn verify_chain_binding_with_resolver(
token: &CapabilityToken,
trust_root: &dyn TrustRootResolver,
) -> Result<(), CapabilityError> {
if token.requires_chain_binding() {
let issuer_root = trust_root
.trust_root_scope_hash(&token.issuer)
.ok_or_else(|| {
CapabilityError::AttenuationViolation(
"chain-binding: no trust-root scope hash registered for issuer".to_string(),
)
})?;
validate_delegation_chain_with_trust_root(&token.delegation_chain, None, &issuer_root)
.map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
token
.validate_chain_binding(&issuer_root)
.map_err(|err| CapabilityError::AttenuationViolation(err.to_string()))?;
}
Ok(())
}
fn verify_chain_binding_with_negotiation(
token: &CapabilityToken,
peer: &CapabilityNegotiation,
trust_root: &dyn TrustRootResolver,
) -> Result<(), CapabilityError> {
if token.requires_chain_binding() {
let chain_binding_enabled = peer
.features
.get(chio_core_types::capability::features::DELEGATION_CHAIN_BINDING)
.copied()
.unwrap_or(true);
if !chain_binding_enabled {
return Err(CapabilityError::AttenuationViolation(
"chain-binding: peer disabled delegation_chain_binding; attenuated tokens are rejected".to_string(),
));
}
verify_chain_binding_with_resolver(token, trust_root)?;
}
Ok(())
}
#[cfg(test)]
#[path = "capability_verify_tests.rs"]
mod tests;