use std::time::Duration as StdDuration;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
use crate::identity::{AgentIdentity, PqcCredential, SpiffeId, DOMAIN_TOKEN};
pub const MAX_DELEGATION_DEPTH: u32 = 2;
pub const DEFAULT_CLOCK_SKEW_SECS: u64 = 30;
pub const MAX_TOKEN_BYTES: u64 = 8 * 1024;
pub const MAX_CHAIN_BYTES: u64 = 32 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Capability(String);
impl Capability {
pub fn new(scope: &str) -> Result<Self> {
if scope.is_empty() {
return Err(Error::InvalidScope("scope must not be empty".to_string()));
}
if scope.chars().any(|c| c.is_whitespace()) {
return Err(Error::InvalidScope(format!(
"scope must not contain whitespace: {:?}",
scope
)));
}
Ok(Capability(scope.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for Capability {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Capability::new(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UnsignedToken {
issuer: SpiffeId,
subject: SpiffeId,
scopes: Vec<Capability>,
issued_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
parent_token_hash: Option<[u8; 32]>,
depth: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationToken {
pub issuer: SpiffeId,
pub subject: SpiffeId,
pub scopes: Vec<Capability>,
pub issued_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub parent_token_hash: Option<[u8; 32]>,
pub depth: u32,
pub issuer_credential: PqcCredential,
pub signature: Vec<u8>,
}
impl DelegationToken {
pub fn issue(
issuer: &AgentIdentity,
subject_spiffe_id: SpiffeId,
scopes: Vec<Capability>,
issuer_scopes: &[Capability],
expiry: StdDuration,
parent: Option<&DelegationToken>,
) -> Result<Self> {
let depth = match parent {
None => 0,
Some(p) => p.depth + 1,
};
if depth > MAX_DELEGATION_DEPTH {
return Err(Error::DelegationDepthExceeded);
}
for scope in &scopes {
if !issuer_scopes.contains(scope) {
return Err(Error::ScopeEscalation);
}
}
let parent_token_hash = parent
.map(|p| -> Result<[u8; 32]> {
let bytes = p.to_bytes()?;
Ok(Sha256::digest(&bytes).into())
})
.transpose()?;
let now = Utc::now();
let expiry_secs: i64 = expiry.as_secs().try_into().unwrap_or(i64::MAX);
let expires_at = now + Duration::seconds(expiry_secs);
let unsigned = UnsignedToken {
issuer: issuer.spiffe_id().clone(),
subject: subject_spiffe_id.clone(),
scopes: scopes.clone(),
issued_at: now,
expires_at,
parent_token_hash,
depth,
};
let payload_bytes = bincode::serialize(&unsigned)
.map_err(|e| Error::Serialization(format!("token payload serialize: {e}")))?;
let signature = issuer.sign_with_domain(DOMAIN_TOKEN, &payload_bytes)?;
Ok(DelegationToken {
issuer: issuer.spiffe_id().clone(),
subject: subject_spiffe_id,
scopes,
issued_at: now,
expires_at,
parent_token_hash,
depth,
issuer_credential: issuer.credential(),
signature,
})
}
pub fn verify(&self, clock_skew: Option<StdDuration>) -> Result<()> {
if self.issuer != self.issuer_credential.spiffe_id {
return Err(Error::ChainVerificationFailed(format!(
"issuer {} does not match embedded credential subject {}",
self.issuer, self.issuer_credential.spiffe_id
)));
}
let skew_secs = clock_skew
.unwrap_or(StdDuration::from_secs(DEFAULT_CLOCK_SKEW_SECS))
.as_secs() as i64;
let skew = Duration::seconds(skew_secs);
let now = Utc::now();
if now > self.expires_at + skew {
return Err(Error::TokenExpired);
}
if self.issued_at > now + skew {
return Err(Error::TokenNotYetValid);
}
let unsigned = UnsignedToken {
issuer: self.issuer.clone(),
subject: self.subject.clone(),
scopes: self.scopes.clone(),
issued_at: self.issued_at,
expires_at: self.expires_at,
parent_token_hash: self.parent_token_hash,
depth: self.depth,
};
let payload_bytes = bincode::serialize(&unsigned)
.map_err(|e| Error::Serialization(format!("token payload serialize: {e}")))?;
let valid = AgentIdentity::verify_with_domain(
&self.issuer_credential.verifying_key_bytes,
DOMAIN_TOKEN,
&payload_bytes,
&self.signature,
)?;
if !valid {
return Err(Error::ChainVerificationFailed(format!(
"signature invalid for token from {} to {}",
self.issuer, self.subject
)));
}
Ok(())
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
bincode::serialize(self).map_err(|e| Error::Serialization(format!("token serialize: {e}")))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() as u64 > MAX_TOKEN_BYTES {
return Err(Error::Serialization(format!(
"input exceeds maximum size ({} > {})",
bytes.len(),
MAX_TOKEN_BYTES
)));
}
use bincode::Options as _;
bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(MAX_TOKEN_BYTES)
.deserialize(bytes)
.map_err(|e| Error::Serialization(format!("token deserialize: {e}")))
}
pub fn hash(&self) -> Result<[u8; 32]> {
let bytes = self.to_bytes()?;
Ok(Sha256::digest(&bytes).into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationChain {
pub tokens: Vec<DelegationToken>,
}
impl DelegationChain {
pub fn new(tokens: Vec<DelegationToken>) -> Self {
DelegationChain { tokens }
}
pub fn verify(&self, clock_skew: Option<StdDuration>) -> Result<()> {
if self.tokens.is_empty() {
return Err(Error::ChainVerificationFailed("chain is empty".to_string()));
}
let mut prev_token: Option<&DelegationToken> = None;
for (i, token) in self.tokens.iter().enumerate() {
token.verify(clock_skew)?;
if token.depth as usize != i {
return Err(Error::ChainVerificationFailed(format!(
"token at index {i} has depth {} (expected {i})",
token.depth
)));
}
if let Some(prev) = prev_token {
let expected_hash = prev.hash()?;
match token.parent_token_hash {
None => {
return Err(Error::ChainVerificationFailed(format!(
"token at index {i} missing parent_token_hash"
)));
}
Some(h) if h != expected_hash => {
return Err(Error::ChainVerificationFailed(format!(
"token at index {i} parent_token_hash mismatch"
)));
}
_ => {}
}
for scope in &token.scopes {
if !prev.scopes.contains(scope) {
return Err(Error::ChainVerificationFailed(format!(
"scope escalation at index {i}: {scope} not in parent scopes"
)));
}
}
if token.issuer != prev.subject {
return Err(Error::ChainVerificationFailed(format!(
"chain broken at index {i}: token issuer {} != prev subject {}",
token.issuer, prev.subject
)));
}
} else {
if token.parent_token_hash.is_some() {
return Err(Error::ChainVerificationFailed(
"root token (index 0) must not have a parent_token_hash".to_string(),
));
}
}
prev_token = Some(token);
}
Ok(())
}
pub fn leaf(&self) -> Option<&DelegationToken> {
self.tokens.last()
}
pub fn effective_scopes(&self) -> &[Capability] {
self.leaf().map(|t| t.scopes.as_slice()).unwrap_or(&[])
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
bincode::serialize(self).map_err(|e| Error::Serialization(format!("chain serialize: {e}")))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() as u64 > MAX_CHAIN_BYTES {
return Err(Error::Serialization(format!(
"input exceeds maximum size ({} > {})",
bytes.len(),
MAX_CHAIN_BYTES
)));
}
use bincode::Options as _;
bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(MAX_CHAIN_BYTES)
.deserialize(bytes)
.map_err(|e| Error::Serialization(format!("chain deserialize: {e}")))
}
pub fn ascii_tree(&self) -> String {
let mut out = String::new();
for (i, token) in self.tokens.iter().enumerate() {
let indent = " ".repeat(i);
let connector = if i == 0 { "" } else { "-> " };
let scopes: Vec<&str> = token.scopes.iter().map(|s| s.as_str()).collect();
let scopes_str = if scopes.is_empty() {
"(no scopes)".to_string()
} else {
format!("[{}]", scopes.join(", "))
};
out.push_str(&format!(
"{indent}{connector}[{i}] {} {scopes_str}\n",
token.subject
));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::AgentIdentity;
fn with_large_stack<F: FnOnce() + Send + 'static>(f: F) {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(f)
.expect("thread spawn failed")
.join()
.expect("thread panicked");
}
#[test]
fn capability_valid_scopes() {
assert!(Capability::new("read:db").is_ok());
assert!(Capability::new("write:api").is_ok());
assert!(Capability::new("invoke:llm").is_ok());
assert!(Capability::new("admin").is_ok());
assert!(Capability::new("read:db:table1").is_ok());
}
#[test]
fn capability_empty_scope_rejected() {
assert!(matches!(Capability::new(""), Err(Error::InvalidScope(_))));
}
#[test]
fn capability_whitespace_rejected() {
assert!(matches!(
Capability::new("read db"),
Err(Error::InvalidScope(_))
));
assert!(matches!(
Capability::new("read\tdb"),
Err(Error::InvalidScope(_))
));
assert!(matches!(
Capability::new("read\ndb"),
Err(Error::InvalidScope(_))
));
}
#[test]
fn capability_display() {
let c = Capability::new("read:db").unwrap();
assert_eq!(c.to_string(), "read:db");
}
#[test]
fn capability_from_str() {
let c: Capability = "write:api".parse().unwrap();
assert_eq!(c.as_str(), "write:api");
}
#[test]
fn capability_serialize_roundtrip() {
let c = Capability::new("read:db").unwrap();
let bytes = bincode::serialize(&c).unwrap();
let c2: Capability = bincode::deserialize(&bytes).unwrap();
assert_eq!(c, c2);
}
#[test]
fn delegation_token_issue_and_verify() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![
Capability::new("read:db").unwrap(),
Capability::new("write:api").unwrap(),
];
let token = DelegationToken::issue(
&issuer,
subject_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
token.verify(None).unwrap();
assert_eq!(token.depth, 0);
assert!(token.parent_token_hash.is_none());
});
}
#[test]
fn delegation_token_serialize_roundtrip() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let token = DelegationToken::issue(
&issuer,
subject_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let bytes = token.to_bytes().unwrap();
let token2 = DelegationToken::from_bytes(&bytes).unwrap();
assert_eq!(token.issuer, token2.issuer);
assert_eq!(token.subject, token2.subject);
assert_eq!(token.scopes, token2.scopes);
assert_eq!(token.depth, token2.depth);
token2.verify(None).unwrap();
});
}
#[test]
fn delegation_token_scope_escalation_rejected() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/1").unwrap();
let issuer_scopes = vec![Capability::new("read:db").unwrap()];
let requested = vec![
Capability::new("read:db").unwrap(),
Capability::new("admin").unwrap(),
];
let result = DelegationToken::issue(
&issuer,
subject_id,
requested,
&issuer_scopes,
StdDuration::from_secs(3600),
None,
);
assert!(matches!(result, Err(Error::ScopeEscalation)));
});
}
#[test]
fn delegation_token_verify_rejects_tampered_signature() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let mut token = DelegationToken::issue(
&issuer,
subject_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
token.signature[0] ^= 0xFF;
let result = token.verify(None);
assert!(matches!(
result,
Err(Error::ChainVerificationFailed(_)) | Err(Error::Crypto(_))
));
});
}
#[test]
fn delegation_token_verify_rejects_expired() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let mut token = DelegationToken::issue(
&issuer,
subject_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
token.expires_at = Utc::now() - Duration::seconds(100);
let result = token.verify(Some(StdDuration::from_secs(0)));
assert!(matches!(result, Err(Error::TokenExpired)));
});
}
#[test]
fn delegation_token_empty_scopes_allowed() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/1").unwrap();
let issuer_scopes: Vec<Capability> = vec![];
let token = DelegationToken::issue(
&issuer,
subject_id,
vec![],
&issuer_scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
token.verify(None).unwrap();
});
}
#[test]
fn delegation_chain_two_hops() {
with_large_stack(|| {
let orchestrator = AgentIdentity::new("example.com", "orchestrator").unwrap();
let worker = AgentIdentity::new("example.com", "worker/1").unwrap();
let sub_id = SpiffeId::new("example.com", "sub-worker/1").unwrap();
let root_scopes = vec![
Capability::new("read:db").unwrap(),
Capability::new("write:api").unwrap(),
];
let token1 = DelegationToken::issue(
&orchestrator,
worker.spiffe_id().clone(),
root_scopes.clone(),
&root_scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let worker_scopes = vec![Capability::new("read:db").unwrap()];
let token2 = DelegationToken::issue(
&worker,
sub_id,
worker_scopes,
&token1.scopes,
StdDuration::from_secs(1800),
Some(&token1),
)
.unwrap();
assert_eq!(token2.depth, 1);
assert!(token2.parent_token_hash.is_some());
let chain = DelegationChain::new(vec![token1, token2]);
chain.verify(None).unwrap();
});
}
#[test]
fn delegation_chain_max_depth_enforced() {
with_large_stack(|| {
let a = AgentIdentity::new("example.com", "agent/a").unwrap();
let b = AgentIdentity::new("example.com", "agent/b").unwrap();
let c = AgentIdentity::new("example.com", "agent/c").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let t1 = DelegationToken::issue(
&a,
b.spiffe_id().clone(),
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let t2 = DelegationToken::issue(
&b,
c.spiffe_id().clone(),
scopes.clone(),
&t1.scopes,
StdDuration::from_secs(3600),
Some(&t1),
)
.unwrap();
let d_id = SpiffeId::new("example.com", "agent/d").unwrap();
let t3 = DelegationToken::issue(
&c,
d_id,
scopes.clone(),
&t2.scopes,
StdDuration::from_secs(3600),
Some(&t2),
)
.unwrap();
assert_eq!(t3.depth, 2);
let e_id = SpiffeId::new("example.com", "agent/e").unwrap();
let result = DelegationToken::issue(
&c,
e_id,
scopes.clone(),
&t3.scopes,
StdDuration::from_secs(3600),
Some(&t3),
);
assert!(matches!(result, Err(Error::DelegationDepthExceeded)));
});
}
#[test]
fn delegation_chain_broken_parent_hash_rejected() {
with_large_stack(|| {
let orchestrator = AgentIdentity::new("example.com", "orchestrator").unwrap();
let worker = AgentIdentity::new("example.com", "worker/1").unwrap();
let sub_id = SpiffeId::new("example.com", "sub-worker/1").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let token1 = DelegationToken::issue(
&orchestrator,
worker.spiffe_id().clone(),
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let mut token2 = DelegationToken::issue(
&worker,
sub_id,
scopes.clone(),
&token1.scopes,
StdDuration::from_secs(1800),
Some(&token1),
)
.unwrap();
if let Some(ref mut h) = token2.parent_token_hash {
h[0] ^= 0xFF;
}
let chain = DelegationChain::new(vec![token1, token2]);
assert!(matches!(
chain.verify(None),
Err(Error::ChainVerificationFailed(_))
));
});
}
#[test]
fn delegation_chain_scope_escalation_in_chain_rejected() {
with_large_stack(|| {
let orchestrator = AgentIdentity::new("example.com", "orchestrator").unwrap();
let worker = AgentIdentity::new("example.com", "worker/1").unwrap();
let sub_id = SpiffeId::new("example.com", "sub-worker/1").unwrap();
let root_scopes = vec![Capability::new("read:db").unwrap()];
let token1 = DelegationToken::issue(
&orchestrator,
worker.spiffe_id().clone(),
root_scopes.clone(),
&root_scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let mut token2 = DelegationToken::issue(
&worker,
sub_id,
root_scopes.clone(),
&root_scopes,
StdDuration::from_secs(1800),
Some(&token1),
)
.unwrap();
token2.scopes.push(Capability::new("admin").unwrap());
let chain = DelegationChain::new(vec![token1, token2]);
assert!(matches!(
chain.verify(None),
Err(Error::ChainVerificationFailed(_))
));
});
}
#[test]
fn delegation_chain_ascii_tree_nonempty() {
with_large_stack(|| {
let orchestrator = AgentIdentity::new("example.com", "orchestrator").unwrap();
let worker_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let token = DelegationToken::issue(
&orchestrator,
worker_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let chain = DelegationChain::new(vec![token]);
let tree = chain.ascii_tree();
assert!(tree.contains("read:db"));
assert!(tree.contains("worker/1"));
});
}
#[test]
fn delegation_chain_empty_fails_verify() {
let chain = DelegationChain::new(vec![]);
assert!(matches!(
chain.verify(None),
Err(Error::ChainVerificationFailed(_))
));
}
#[test]
fn delegation_chain_serialize_roundtrip() {
with_large_stack(|| {
let orchestrator = AgentIdentity::new("example.com", "orchestrator").unwrap();
let worker_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let token = DelegationToken::issue(
&orchestrator,
worker_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let chain = DelegationChain::new(vec![token]);
let bytes = chain.to_bytes().unwrap();
let chain2 = DelegationChain::from_bytes(&bytes).unwrap();
chain2.verify(None).unwrap();
});
}
#[test]
fn delegation_chain_effective_scopes() {
with_large_stack(|| {
let orchestrator = AgentIdentity::new("example.com", "orchestrator").unwrap();
let worker_id = SpiffeId::new("example.com", "worker/1").unwrap();
let scopes = vec![
Capability::new("read:db").unwrap(),
Capability::new("write:api").unwrap(),
];
let token = DelegationToken::issue(
&orchestrator,
worker_id,
scopes.clone(),
&scopes,
StdDuration::from_secs(3600),
None,
)
.unwrap();
let chain = DelegationChain::new(vec![token]);
assert_eq!(chain.effective_scopes().len(), 2);
});
}
#[test]
fn delegation_token_verify_rejects_issuer_credential_mismatch() {
with_large_stack(|| {
let attacker = AgentIdentity::new("example.com", "attacker").unwrap();
let victim_id = SpiffeId::new("example.com", "victim").unwrap();
let subject_id = SpiffeId::new("example.com", "subject").unwrap();
let scopes = vec![Capability::new("read:db").unwrap()];
let now = chrono::Utc::now();
let unsigned = UnsignedToken {
issuer: victim_id.clone(),
subject: subject_id.clone(),
scopes: scopes.clone(),
issued_at: now,
expires_at: now + chrono::Duration::seconds(3600),
parent_token_hash: None,
depth: 0,
};
let payload_bytes = bincode::serialize(&unsigned).unwrap();
let signature = attacker.sign(&payload_bytes).unwrap();
let forged = DelegationToken {
issuer: victim_id.clone(),
subject: subject_id,
scopes,
issued_at: now,
expires_at: now + chrono::Duration::seconds(3600),
parent_token_hash: None,
depth: 0,
issuer_credential: attacker.credential(), signature,
};
let result = forged.verify(None);
assert!(
matches!(result, Err(Error::ChainVerificationFailed(ref msg)) if msg.contains("does not match")),
"expected ChainVerificationFailed(\"does not match ...\"), got: {result:?}"
);
});
}
#[test]
fn delegation_chain_rejects_forged_root_with_mismatched_credential() {
with_large_stack(|| {
let attacker = AgentIdentity::new("example.com", "attacker").unwrap();
let legitimate_worker = AgentIdentity::new("example.com", "worker/1").unwrap();
let victim_id = SpiffeId::new("example.com", "victim").unwrap();
let subject_id = legitimate_worker.spiffe_id().clone();
let scopes = vec![Capability::new("read:db").unwrap()];
let now = chrono::Utc::now();
let unsigned_root = UnsignedToken {
issuer: victim_id.clone(),
subject: subject_id.clone(),
scopes: scopes.clone(),
issued_at: now,
expires_at: now + chrono::Duration::seconds(3600),
parent_token_hash: None,
depth: 0,
};
let root_payload = bincode::serialize(&unsigned_root).unwrap();
let root_sig = attacker.sign(&root_payload).unwrap();
let forged_root = DelegationToken {
issuer: victim_id,
subject: subject_id,
scopes: scopes.clone(),
issued_at: now,
expires_at: now + chrono::Duration::seconds(3600),
parent_token_hash: None,
depth: 0,
issuer_credential: attacker.credential(),
signature: root_sig,
};
let leaf_subject = SpiffeId::new("example.com", "leaf").unwrap();
let child = DelegationToken::issue(
&legitimate_worker,
leaf_subject,
scopes.clone(),
&scopes,
StdDuration::from_secs(1800),
Some(&forged_root),
)
.unwrap();
let chain = DelegationChain::new(vec![forged_root, child]);
let result = chain.verify(None);
assert!(
matches!(result, Err(Error::ChainVerificationFailed(_))),
"expected ChainVerificationFailed, got: {result:?}"
);
});
}
#[test]
fn delegation_token_from_bytes_rejects_oversized_length_prefix() {
let mut crafted = vec![0xFFu8; 8];
crafted.extend_from_slice(&[0u8; 16]);
let result = DelegationToken::from_bytes(&crafted);
assert!(
result.is_err(),
"oversized length prefix must be rejected, got Ok"
);
assert!(
matches!(result, Err(Error::Serialization(_))),
"expected Serialization error, got: {:?}",
result
);
}
#[test]
fn delegation_chain_from_bytes_rejects_oversized_length_prefix() {
let mut crafted = vec![0xFFu8; 8];
crafted.extend_from_slice(&[0u8; 16]);
let result = DelegationChain::from_bytes(&crafted);
assert!(
result.is_err(),
"oversized length prefix must be rejected, got Ok"
);
assert!(
matches!(result, Err(Error::Serialization(_))),
"expected Serialization error, got: {:?}",
result
);
}
#[test]
fn delegation_token_from_bytes_rejects_oversized_valid_input() {
with_large_stack(|| {
let issuer = AgentIdentity::new("example.com", "orchestrator").unwrap();
let subject_id = SpiffeId::new("example.com", "worker/oversized").unwrap();
let big_scopes: Vec<Capability> = (0..500)
.map(|i| Capability::new(&format!("scope:aaaaaaaaaaa{i:04}")).unwrap())
.collect();
let big_issuer_scopes = big_scopes.clone();
let big_token = DelegationToken::issue(
&issuer,
subject_id,
big_scopes,
&big_issuer_scopes,
std::time::Duration::from_secs(3600),
None,
)
.expect("issue oversized token failed");
let big_bytes = big_token
.to_bytes()
.expect("to_bytes failed for oversized token");
assert!(
big_bytes.len() as u64 > MAX_TOKEN_BYTES,
"oversized token must exceed MAX_TOKEN_BYTES ({} <= {})",
big_bytes.len(),
MAX_TOKEN_BYTES
);
let result = DelegationToken::from_bytes(&big_bytes);
assert!(
result.is_err(),
"Expected Err for oversized valid token, got Ok"
);
let err_str = format!("{:?}", result);
assert!(
err_str.contains("exceeds maximum"),
"error must mention 'exceeds maximum', got: {err_str}"
);
});
}
}