use serde::{Deserialize, Serialize};
use crate::identity::{AgentIdentity, AgentKeyPair};
use crate::signing::SignedMessage;
use crate::CryptoError;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "value")]
pub enum Caveat {
#[serde(rename = "action_scope")]
ActionScope(Vec<String>),
#[serde(rename = "expires_at")]
ExpiresAt(String),
#[serde(rename = "max_cost")]
MaxCost(f64),
#[serde(rename = "resource")]
Resource(String),
#[serde(rename = "context")]
Context { key: String, value: String },
#[serde(rename = "custom")]
Custom {
key: String,
value: serde_json::Value,
},
}
pub const MAX_CHAIN_DEPTH: usize = 32;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delegation {
pub issuer_did: String,
pub delegate_did: String,
pub issuer_public_key: Vec<u8>,
pub caveats: Vec<Caveat>,
pub parent_proof: Option<Box<Delegation>>,
pub proof: SignedMessage,
}
impl Delegation {
pub fn create_root(
issuer_keypair: &AgentKeyPair,
delegate_did: &str,
caveats: Vec<Caveat>,
) -> Result<Self, CryptoError> {
let issuer_identity = issuer_keypair.identity();
Self::create_inner(
issuer_keypair,
&issuer_identity.did,
delegate_did,
caveats,
None,
)
}
pub fn delegate(
issuer_keypair: &AgentKeyPair,
delegate_did: &str,
additional_caveats: Vec<Caveat>,
parent: Delegation,
) -> Result<Self, CryptoError> {
let issuer_identity = issuer_keypair.identity();
if parent.delegate_did != issuer_identity.did {
return Err(CryptoError::DelegationChainBroken(
"issuer is not the delegate of parent delegation".into(),
));
}
let mut all_caveats = parent.caveats.clone();
all_caveats.extend(additional_caveats);
Self::create_inner(
issuer_keypair,
&issuer_identity.did,
delegate_did,
all_caveats,
Some(Box::new(parent)),
)
}
fn create_inner(
issuer_keypair: &AgentKeyPair,
issuer_did: &str,
delegate_did: &str,
caveats: Vec<Caveat>,
parent: Option<Box<Delegation>>,
) -> Result<Self, CryptoError> {
if let Some(ref p) = parent {
if p.depth() >= MAX_CHAIN_DEPTH {
return Err(CryptoError::DelegationChainBroken(format!(
"chain depth exceeds maximum of {}",
MAX_CHAIN_DEPTH
)));
}
}
let issuer_identity = issuer_keypair.identity();
let parent_hash = parent.as_ref().map(|p| p.proof.content_hash());
let payload = serde_json::json!({
"issuer_did": issuer_did,
"delegate_did": delegate_did,
"caveats": caveats,
"parent_hash": parent_hash,
});
let proof = SignedMessage::sign(issuer_keypair, payload)?;
Ok(Self {
issuer_did: issuer_did.to_string(),
delegate_did: delegate_did.to_string(),
issuer_public_key: issuer_identity.public_key_bytes.clone(),
caveats,
parent_proof: parent,
proof,
})
}
pub fn depth(&self) -> usize {
let mut depth = 0;
let mut current = self;
while let Some(ref parent) = current.parent_proof {
depth += 1;
current = parent;
}
depth
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invocation {
pub invoker_did: String,
pub action: String,
pub args: serde_json::Value,
pub delegation: Delegation,
pub proof: SignedMessage,
}
impl Invocation {
pub fn create(
invoker_keypair: &AgentKeyPair,
action: &str,
args: serde_json::Value,
delegation: Delegation,
) -> Result<Self, CryptoError> {
let invoker_identity = invoker_keypair.identity();
if delegation.delegate_did != invoker_identity.did {
return Err(CryptoError::DelegationChainBroken(
"invoker is not the delegate of the delegation".into(),
));
}
let payload = serde_json::json!({
"invoker_did": invoker_identity.did,
"action": action,
"args": args,
"delegation_hash": delegation.proof.content_hash(),
});
let proof = SignedMessage::sign(invoker_keypair, payload)?;
Ok(Self {
invoker_did: invoker_identity.did,
action: action.to_string(),
args,
delegation,
proof,
})
}
}
#[derive(Debug)]
pub struct VerificationResult {
pub invoker_did: String,
pub root_did: String,
pub chain: Vec<String>,
pub depth: usize,
}
pub fn verify_invocation(
invocation: &Invocation,
invoker_identity: &AgentIdentity,
root_identity: &AgentIdentity,
) -> Result<VerificationResult, CryptoError> {
verify_invocation_with_revocation(invocation, invoker_identity, root_identity, |_| false)
}
pub fn verify_invocation_with_revocation(
invocation: &Invocation,
invoker_identity: &AgentIdentity,
root_identity: &AgentIdentity,
is_revoked: impl Fn(&str) -> bool,
) -> Result<VerificationResult, CryptoError> {
invocation.proof.verify(invoker_identity)?;
if invocation.invoker_did != invocation.delegation.delegate_did {
return Err(CryptoError::DelegationChainBroken(
"invoker is not the delegate of the delegation".into(),
));
}
let mut chain = vec![invocation.invoker_did.clone()];
let mut current = &invocation.delegation;
let mut all_caveats: Vec<Caveat> = Vec::new();
let mut steps = 0usize;
loop {
steps += 1;
if steps > MAX_CHAIN_DEPTH {
return Err(CryptoError::DelegationChainBroken(format!(
"chain depth exceeds maximum of {}",
MAX_CHAIN_DEPTH
)));
}
chain.push(current.issuer_did.clone());
let issuer_identity =
AgentIdentity::from_bytes(¤t.issuer_public_key).map_err(|_| {
CryptoError::DelegationChainBroken(format!(
"invalid embedded public key for '{}'",
current.issuer_did
))
})?;
if issuer_identity.did != current.issuer_did {
return Err(CryptoError::DelegationChainBroken(format!(
"embedded public key produces DID '{}' but delegation claims '{}'",
issuer_identity.did, current.issuer_did
)));
}
current.proof.verify(&issuer_identity)?;
let delegation_hash = current.proof.content_hash();
if is_revoked(&delegation_hash) {
return Err(CryptoError::DelegationRevoked(delegation_hash));
}
if let Some(signed_caveats) = current.proof.payload.get("caveats") {
if let Ok(caveats) = serde_json::from_value::<Vec<Caveat>>(signed_caveats.clone()) {
all_caveats.extend(caveats);
}
}
if current.issuer_did == root_identity.did {
if issuer_identity.public_key_bytes != root_identity.public_key_bytes {
return Err(CryptoError::DelegationChainBroken(
"root public key mismatch".into(),
));
}
break;
}
match ¤t.parent_proof {
Some(parent) => {
if parent.delegate_did != current.issuer_did {
return Err(CryptoError::DelegationChainBroken(format!(
"delegation issuer '{}' is not the delegate of parent delegation '{}'",
current.issuer_did, parent.delegate_did
)));
}
current = parent;
}
None => {
return Err(CryptoError::DelegationChainBroken(format!(
"chain terminates at '{}', expected root '{}'",
current.issuer_did, root_identity.did
)));
}
}
}
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
for caveat in &all_caveats {
check_caveat(caveat, &invocation.action, &invocation.args, &now)?;
}
let depth = chain.len() - 1;
Ok(VerificationResult {
invoker_did: invocation.invoker_did.clone(),
root_did: root_identity.did.clone(),
chain,
depth,
})
}
pub fn verify_delegation_chain(
delegation: &Delegation,
root_identity: &AgentIdentity,
) -> Result<Vec<String>, CryptoError> {
verify_delegation_chain_with_revocation(delegation, root_identity, |_| false)
}
pub fn verify_delegation_chain_with_revocation(
delegation: &Delegation,
root_identity: &AgentIdentity,
is_revoked: impl Fn(&str) -> bool,
) -> Result<Vec<String>, CryptoError> {
let mut chain = Vec::new();
let mut current = delegation;
let mut steps = 0usize;
loop {
steps += 1;
if steps > MAX_CHAIN_DEPTH {
return Err(CryptoError::DelegationChainBroken(format!(
"chain depth exceeds maximum of {}",
MAX_CHAIN_DEPTH
)));
}
chain.push(current.delegate_did.clone());
chain.push(current.issuer_did.clone());
let issuer_identity =
AgentIdentity::from_bytes(¤t.issuer_public_key).map_err(|_| {
CryptoError::DelegationChainBroken(format!(
"invalid embedded public key for '{}'",
current.issuer_did
))
})?;
if issuer_identity.did != current.issuer_did {
return Err(CryptoError::DelegationChainBroken(format!(
"embedded public key produces DID '{}' but delegation claims '{}'",
issuer_identity.did, current.issuer_did
)));
}
current.proof.verify(&issuer_identity)?;
let delegation_hash = current.proof.content_hash();
if is_revoked(&delegation_hash) {
return Err(CryptoError::DelegationRevoked(delegation_hash));
}
if current.issuer_did == root_identity.did {
if issuer_identity.public_key_bytes != root_identity.public_key_bytes {
return Err(CryptoError::DelegationChainBroken(
"root public key mismatch".into(),
));
}
break;
}
match ¤t.parent_proof {
Some(parent) => {
if parent.delegate_did != current.issuer_did {
return Err(CryptoError::DelegationChainBroken(
"chain linkage broken: issuer not delegate of parent".into(),
));
}
current = parent;
}
None => {
return Err(CryptoError::DelegationChainBroken(format!(
"chain terminates at '{}', expected root '{}'",
current.issuer_did, root_identity.did
)));
}
}
}
chain.dedup();
Ok(chain)
}
fn check_caveat(
caveat: &Caveat,
action: &str,
args: &serde_json::Value,
now: &str,
) -> Result<(), CryptoError> {
match caveat {
Caveat::ActionScope(allowed) => {
if !allowed.iter().any(|a| a == action) {
return Err(CryptoError::CaveatViolation(format!(
"action '{}' not in allowed scope {:?}",
action, allowed
)));
}
}
Caveat::ExpiresAt(expiry) => {
if now > expiry.as_str() {
return Err(CryptoError::CaveatViolation(format!(
"delegation expired at {}",
expiry
)));
}
}
Caveat::MaxCost(max) => match args.get("cost").and_then(|v| v.as_f64()) {
Some(cost) if cost > *max => {
return Err(CryptoError::CaveatViolation(format!(
"cost {} exceeds max {}",
cost, max
)));
}
None => {
return Err(CryptoError::CaveatViolation(
"max_cost caveat requires 'cost' field in args".into(),
));
}
_ => {}
},
Caveat::Resource(pattern) => match args.get("resource").and_then(|v| v.as_str()) {
Some(resource) if !matches_glob(pattern, resource) => {
return Err(CryptoError::CaveatViolation(format!(
"resource '{}' does not match pattern '{}'",
resource, pattern
)));
}
None => {
return Err(CryptoError::CaveatViolation(
"resource caveat requires 'resource' field in args".into(),
));
}
_ => {}
},
Caveat::Context { key, value } => {
let actual = args.get(key).and_then(|v| v.as_str());
if actual != Some(value.as_str()) {
return Err(CryptoError::CaveatViolation(format!(
"context '{}' expected '{}', got '{}'",
key,
value,
actual.unwrap_or("<missing>")
)));
}
}
Caveat::Custom { key, value } => {
let actual = args.get(key);
if actual != Some(value) {
return Err(CryptoError::CaveatViolation(format!(
"custom caveat '{}' not satisfied",
key
)));
}
}
}
Ok(())
}
fn matches_glob(pattern: &str, value: &str) -> bool {
if let Some(prefix) = pattern.strip_suffix('*') {
value.starts_with(prefix)
} else {
pattern == value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn keypair() -> AgentKeyPair {
AgentKeyPair::generate()
}
#[test]
fn test_root_delegation() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into(), "search".into()])],
)
.unwrap();
assert_eq!(delegation.issuer_did, root.identity().did);
assert_eq!(delegation.delegate_did, agent_b.identity().did);
assert_eq!(delegation.depth(), 0);
assert!(delegation.parent_proof.is_none());
}
#[test]
fn test_chained_delegation() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let d1 = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into(), "search".into()])],
)
.unwrap();
let d2 = Delegation::delegate(
&agent_b,
&agent_c.identity().did,
vec![], d1,
)
.unwrap();
assert_eq!(d2.issuer_did, agent_b.identity().did);
assert_eq!(d2.delegate_did, agent_c.identity().did);
assert_eq!(d2.depth(), 1);
assert!(d2.parent_proof.is_some());
}
#[test]
fn test_delegate_must_be_parent_delegate() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let unrelated = keypair();
let d1 = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let result = Delegation::delegate(&unrelated, &agent_c.identity().did, vec![], d1);
assert!(result.is_err());
}
#[test]
fn test_invocation_basic() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let invocation = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"entity_id": "123"}),
delegation,
)
.unwrap();
assert_eq!(invocation.invoker_did, agent_b.identity().did);
assert_eq!(invocation.action, "resolve");
}
#[test]
fn test_invocation_must_be_delegation_delegate() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let delegation = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let result = Invocation::create(&agent_c, "resolve", serde_json::json!({}), delegation);
assert!(result.is_err());
}
#[test]
fn test_verify_root_invocation() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity()).unwrap();
assert_eq!(result.invoker_did, agent_b.identity().did);
assert_eq!(result.root_did, root.identity().did);
assert_eq!(result.depth, 1); }
#[test]
fn test_verify_chained_invocation() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let d1 = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into(), "search".into()])],
)
.unwrap();
let d2 = Delegation::delegate(
&agent_b,
&agent_c.identity().did,
vec![], d1,
)
.unwrap();
let invocation =
Invocation::create(&agent_c, "resolve", serde_json::json!({}), d2).unwrap();
let result = verify_invocation(&invocation, &agent_c.identity(), &root.identity()).unwrap();
assert_eq!(result.invoker_did, agent_c.identity().did);
assert_eq!(result.root_did, root.identity().did);
assert_eq!(result.depth, 2); }
#[test]
fn test_verify_wrong_root_fails() {
let root = keypair();
let agent_b = keypair();
let fake_root = keypair();
let delegation = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &fake_root.identity());
assert!(result.is_err());
}
#[test]
fn test_action_scope_caveat_passes() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into(), "search".into()])],
)
.unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
assert!(verify_invocation(&invocation, &agent_b.identity(), &root.identity()).is_ok());
}
#[test]
fn test_action_scope_caveat_blocks() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let invocation = Invocation::create(
&agent_b,
"merge", serde_json::json!({}),
delegation,
)
.unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_expires_at_caveat_blocks() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ExpiresAt("2020-01-01T00:00:00.000Z".into())],
)
.unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_max_cost_caveat_passes() {
let root = keypair();
let agent_b = keypair();
let delegation =
Delegation::create_root(&root, &agent_b.identity().did, vec![Caveat::MaxCost(5.0)])
.unwrap();
let invocation = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"cost": 3.50}),
delegation,
)
.unwrap();
assert!(verify_invocation(&invocation, &agent_b.identity(), &root.identity()).is_ok());
}
#[test]
fn test_max_cost_caveat_blocks() {
let root = keypair();
let agent_b = keypair();
let delegation =
Delegation::create_root(&root, &agent_b.identity().did, vec![Caveat::MaxCost(5.0)])
.unwrap();
let invocation = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"cost": 10.0}),
delegation,
)
.unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_resource_caveat_glob() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::Resource("entity:customer:*".into())],
)
.unwrap();
let inv_ok = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"resource": "entity:customer:123"}),
delegation.clone(),
)
.unwrap();
assert!(verify_invocation(&inv_ok, &agent_b.identity(), &root.identity()).is_ok());
let inv_bad = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"resource": "entity:order:456"}),
delegation,
)
.unwrap();
let result = verify_invocation(&inv_bad, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_context_caveat() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::Context {
key: "task_id".into(),
value: "task-abc".into(),
}],
)
.unwrap();
let inv_ok = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"task_id": "task-abc"}),
delegation.clone(),
)
.unwrap();
assert!(verify_invocation(&inv_ok, &agent_b.identity(), &root.identity()).is_ok());
let inv_bad = Invocation::create(
&agent_b,
"resolve",
serde_json::json!({"task_id": "task-xyz"}),
delegation,
)
.unwrap();
assert!(matches!(
verify_invocation(&inv_bad, &agent_b.identity(), &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
}
#[test]
fn test_attenuation_narrows_not_widens() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let d1 = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into(), "search".into()])],
)
.unwrap();
let d2 = Delegation::delegate(
&agent_b,
&agent_c.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
d1,
)
.unwrap();
let inv = Invocation::create(&agent_c, "search", serde_json::json!({}), d2).unwrap();
let result = verify_invocation(&inv, &agent_c.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_three_level_chain() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let agent_d = keypair();
let d1 = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let d2 = Delegation::delegate(&agent_b, &agent_c.identity().did, vec![], d1).unwrap();
let d3 = Delegation::delegate(&agent_c, &agent_d.identity().did, vec![], d2).unwrap();
let inv = Invocation::create(&agent_d, "resolve", serde_json::json!({}), d3).unwrap();
let result = verify_invocation(&inv, &agent_d.identity(), &root.identity()).unwrap();
assert_eq!(result.depth, 3); }
#[test]
fn test_verify_delegation_chain() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let d1 = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let d2 = Delegation::delegate(&agent_b, &agent_c.identity().did, vec![], d1).unwrap();
let chain = verify_delegation_chain(&d2, &root.identity()).unwrap();
assert!(chain.contains(&root.identity().did));
assert!(chain.contains(&agent_b.identity().did));
assert!(chain.contains(&agent_c.identity().did));
}
#[test]
fn test_delegation_serialization_roundtrip() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![
Caveat::ActionScope(vec!["resolve".into()]),
Caveat::ExpiresAt("2030-01-01T00:00:00.000Z".into()),
Caveat::MaxCost(10.0),
],
)
.unwrap();
let json = serde_json::to_string(&delegation).unwrap();
let restored: Delegation = serde_json::from_str(&json).unwrap();
assert_eq!(restored.issuer_did, delegation.issuer_did);
assert_eq!(restored.delegate_did, delegation.delegate_did);
assert_eq!(restored.caveats.len(), 3);
}
#[test]
fn test_caveat_serialization_roundtrip() {
let caveats = vec![
Caveat::ActionScope(vec!["resolve".into(), "search".into()]),
Caveat::ExpiresAt("2030-01-01T00:00:00.000Z".into()),
Caveat::MaxCost(5.0),
Caveat::Resource("entity:*".into()),
Caveat::Context {
key: "task_id".into(),
value: "t1".into(),
},
Caveat::Custom {
key: "org".into(),
value: serde_json::json!("acme"),
},
];
for caveat in &caveats {
let json = serde_json::to_string(caveat).unwrap();
let restored: Caveat = serde_json::from_str(&json).unwrap();
assert_eq!(&restored, caveat, "Roundtrip failed for {:?}", caveat);
}
}
#[test]
fn test_glob_matching() {
assert!(matches_glob("entity:*", "entity:customer:123"));
assert!(matches_glob("entity:customer:*", "entity:customer:123"));
assert!(!matches_glob("entity:customer:*", "entity:order:456"));
assert!(matches_glob("exact", "exact"));
assert!(!matches_glob("exact", "other"));
assert!(matches_glob("*", "anything"));
}
#[test]
fn test_max_cost_missing_field_fails() {
let root = keypair();
let agent_b = keypair();
let delegation =
Delegation::create_root(&root, &agent_b.identity().did, vec![Caveat::MaxCost(5.0)])
.unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_resource_missing_field_fails() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::Resource("entity:*".into())],
)
.unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_embedded_public_key_present() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
assert_eq!(
delegation.issuer_public_key,
root.identity().public_key_bytes
);
}
#[test]
fn test_tampered_delegation_caveats_detected() {
let root = keypair();
let agent_b = keypair();
let mut delegation = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
delegation.caveats = vec![Caveat::ActionScope(vec!["resolve".into(), "merge".into()])];
let invocation =
Invocation::create(&agent_b, "merge", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation(&invocation, &agent_b.identity(), &root.identity());
assert!(matches!(result, Err(CryptoError::CaveatViolation(_))));
}
#[test]
fn test_intermediate_signature_verified() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let d1 = Delegation::create_root(
&root,
&agent_b.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let mut d2 = Delegation::delegate(&agent_b, &agent_c.identity().did, vec![], d1).unwrap();
d2.proof.signature = "00".repeat(64);
let invocation =
Invocation::create(&agent_c, "resolve", serde_json::json!({}), d2).unwrap();
let result = verify_invocation(&invocation, &agent_c.identity(), &root.identity());
assert!(result.is_err());
}
#[test]
fn test_revocation_blocks_invocation() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let revoked_hash = delegation.proof.content_hash();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
assert!(verify_invocation(&invocation, &agent_b.identity(), &root.identity()).is_ok());
let result = verify_invocation_with_revocation(
&invocation,
&agent_b.identity(),
&root.identity(),
|hash| hash == revoked_hash,
);
assert!(matches!(result, Err(CryptoError::DelegationRevoked(_))));
}
#[test]
fn test_revocation_in_chain_blocks() {
let root = keypair();
let agent_b = keypair();
let agent_c = keypair();
let d1 = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let revoked_hash = d1.proof.content_hash();
let d2 = Delegation::delegate(&agent_b, &agent_c.identity().did, vec![], d1).unwrap();
let invocation =
Invocation::create(&agent_c, "resolve", serde_json::json!({}), d2).unwrap();
let result = verify_invocation_with_revocation(
&invocation,
&agent_c.identity(),
&root.identity(),
|hash| hash == revoked_hash,
);
assert!(matches!(result, Err(CryptoError::DelegationRevoked(_))));
}
#[test]
fn test_no_revocation_callback_passes() {
let root = keypair();
let agent_b = keypair();
let delegation = Delegation::create_root(&root, &agent_b.identity().did, vec![]).unwrap();
let invocation =
Invocation::create(&agent_b, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_invocation_with_revocation(
&invocation,
&agent_b.identity(),
&root.identity(),
|_| false,
);
assert!(result.is_ok());
}
}