1use 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#[derive(Debug, Clone)]
53pub struct VerifiedCapability {
54 pub id: String,
56 pub subject_hex: String,
58 pub issuer_hex: String,
60 pub scope: ChioScope,
62 pub issued_at: u64,
64 pub expires_at: u64,
66 pub evaluated_at: u64,
68}
69
70impl VerifiedCapability {
71 pub fn normalized(&self) -> Result<NormalizedVerifiedCapability, NormalizationError> {
73 NormalizedVerifiedCapability::try_from(self)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum CapabilityError {
80 UntrustedIssuer,
82 InvalidSignature,
84 CryptoFloorRejected(String),
86 NotYetValid,
88 Expired,
90 AttenuationViolation(String),
95 BudgetSplitRejected(BudgetSplitError),
97 Internal(String),
99}
100
101impl From<BudgetSplitError> for CapabilityError {
102 fn from(err: BudgetSplitError) -> Self {
103 CapabilityError::BudgetSplitRejected(err)
104 }
105}
106
107pub 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
131pub 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 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 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 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 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
254pub 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
288pub 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
302pub 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
320pub trait TrustRootResolver {
325 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#[derive(Debug, Clone, Copy)]
343pub struct CapabilityFeatureContext<'a> {
344 pub peer: &'a CapabilityNegotiation,
345 pub direct_root: Option<&'a CapabilityToken>,
346}
347
348pub 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
377pub 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
397pub 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
422pub 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;