use serde::{Deserialize, Serialize};
use crate::delegation::{
verify_invocation_with_revocation, Delegation, Invocation, VerificationResult,
};
use crate::error::CryptoError;
use crate::identity::{AgentIdentity, AgentKeyPair};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpProof {
pub invocation: Invocation,
pub invoker_public_key: String,
}
impl McpProof {
pub fn create(
invoker_keypair: &AgentKeyPair,
action: &str,
args: serde_json::Value,
delegation: Delegation,
) -> Result<Self, CryptoError> {
let invocation = Invocation::create(invoker_keypair, action, args, delegation)?;
let invoker_identity = invoker_keypair.identity();
Ok(Self {
invocation,
invoker_public_key: hex::encode(&invoker_identity.public_key_bytes),
})
}
pub fn extract(args: &serde_json::Value) -> (Option<Self>, serde_json::Value) {
let proof_value = args.get("_proof");
let proof = proof_value.and_then(|v| serde_json::from_value::<Self>(v.clone()).ok());
let clean_args = if let serde_json::Value::Object(map) = args {
if map.contains_key("_proof") {
let mut clean = map.clone();
clean.remove("_proof");
serde_json::Value::Object(clean)
} else {
args.clone()
}
} else {
args.clone()
};
(proof, clean_args)
}
pub fn inject(&self, args: &mut serde_json::Value) {
if let serde_json::Value::Object(ref mut map) = args {
if let Ok(proof_value) = serde_json::to_value(self) {
map.insert("_proof".to_string(), proof_value);
}
}
}
}
pub fn verify_mcp_call(
proof: &McpProof,
root_identity: &AgentIdentity,
) -> Result<VerificationResult, CryptoError> {
verify_mcp_call_with_revocation(proof, root_identity, |_| false)
}
pub fn verify_mcp_call_with_revocation(
proof: &McpProof,
root_identity: &AgentIdentity,
is_revoked: impl Fn(&str) -> bool,
) -> Result<VerificationResult, CryptoError> {
let pk_bytes = hex::decode(&proof.invoker_public_key).map_err(|_| {
CryptoError::DelegationChainBroken("invalid hex in invoker_public_key".into())
})?;
let invoker_identity = AgentIdentity::from_bytes(&pk_bytes).map_err(|_| {
CryptoError::DelegationChainBroken("invalid Ed25519 public key in proof".into())
})?;
if invoker_identity.did != proof.invocation.invoker_did {
return Err(CryptoError::DelegationChainBroken(format!(
"embedded public key produces DID '{}' but invocation claims '{}'",
invoker_identity.did, proof.invocation.invoker_did
)));
}
verify_invocation_with_revocation(
&proof.invocation,
&invoker_identity,
root_identity,
is_revoked,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpAuthMode {
Required,
Optional,
Disabled,
}
impl McpAuthMode {
pub fn from_str_lossy(s: &str) -> Self {
match s.to_lowercase().as_str() {
"required" | "enforce" | "strict" => Self::Required,
"disabled" | "off" | "none" => Self::Disabled,
_ => Self::Optional,
}
}
}
#[derive(Debug)]
pub struct McpAuthOutcome {
pub verified: Option<VerificationResult>,
pub args: serde_json::Value,
}
pub fn verify_mcp_tool_call(
tool_name: &str,
args: &serde_json::Value,
root_identity: &AgentIdentity,
mode: McpAuthMode,
) -> Result<McpAuthOutcome, CryptoError> {
verify_mcp_tool_call_with_revocation(tool_name, args, root_identity, mode, |_| false)
}
pub fn verify_mcp_tool_call_with_revocation(
_tool_name: &str,
args: &serde_json::Value,
root_identity: &AgentIdentity,
mode: McpAuthMode,
is_revoked: impl Fn(&str) -> bool,
) -> Result<McpAuthOutcome, CryptoError> {
let (proof, clean_args) = McpProof::extract(args);
match mode {
McpAuthMode::Disabled => Ok(McpAuthOutcome {
verified: None,
args: clean_args,
}),
McpAuthMode::Optional => match proof {
Some(p) => {
let result = verify_mcp_call_with_revocation(&p, root_identity, is_revoked)?;
Ok(McpAuthOutcome {
verified: Some(result),
args: clean_args,
})
}
None => Ok(McpAuthOutcome {
verified: None,
args: clean_args,
}),
},
McpAuthMode::Required => match proof {
Some(p) => {
let result = verify_mcp_call_with_revocation(&p, root_identity, is_revoked)?;
Ok(McpAuthOutcome {
verified: Some(result),
args: clean_args,
})
}
None => Err(CryptoError::DelegationChainBroken(
"MCP auth required but no _proof provided in tool arguments".into(),
)),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::delegation::Caveat;
fn keypair() -> AgentKeyPair {
AgentKeyPair::generate()
}
#[test]
fn test_create_and_verify_mcp_proof() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let result = verify_mcp_call(&proof, &root.identity()).unwrap();
assert_eq!(result.invoker_did, agent.identity().did);
assert_eq!(result.root_did, root.identity().did);
}
#[test]
fn test_extract_and_inject() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let mut args = serde_json::json!({"source": "crm", "external_id": "123"});
proof.inject(&mut args);
assert!(args.get("_proof").is_some());
assert!(args.get("source").is_some());
let (extracted, clean_args) = McpProof::extract(&args);
assert!(extracted.is_some());
assert!(clean_args.get("_proof").is_none());
assert_eq!(clean_args.get("source").unwrap(), "crm");
assert_eq!(clean_args.get("external_id").unwrap(), "123");
let result = verify_mcp_call(&extracted.unwrap(), &root.identity()).unwrap();
assert_eq!(result.invoker_did, agent.identity().did);
}
#[test]
fn test_extract_no_proof() {
let args = serde_json::json!({"source": "crm", "external_id": "123"});
let (proof, clean_args) = McpProof::extract(&args);
assert!(proof.is_none());
assert_eq!(clean_args, args);
}
#[test]
fn test_extract_invalid_proof() {
let args = serde_json::json!({"source": "crm", "_proof": "not-valid-json"});
let (proof, clean_args) = McpProof::extract(&args);
assert!(proof.is_none());
assert!(clean_args.get("_proof").is_none());
assert_eq!(clean_args.get("source").unwrap(), "crm");
}
#[test]
fn test_wrong_invoker_key_rejected() {
let root = keypair();
let agent = keypair();
let impersonator = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
let tampered = McpProof {
invoker_public_key: hex::encode(&impersonator.identity().public_key_bytes),
..proof
};
let result = verify_mcp_call(&tampered, &root.identity());
assert!(result.is_err());
}
#[test]
fn test_wrong_root_rejected() {
let root = keypair();
let fake_root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
let result = verify_mcp_call(&proof, &fake_root.identity());
assert!(result.is_err());
}
#[test]
fn test_caveat_enforcement_through_mcp() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![
Caveat::ActionScope(vec!["resolve".into()]),
Caveat::MaxCost(5.0),
],
)
.unwrap();
let proof_ok = McpProof::create(
&agent,
"resolve",
serde_json::json!({"cost": 3.0}),
delegation.clone(),
)
.unwrap();
assert!(verify_mcp_call(&proof_ok, &root.identity()).is_ok());
let proof_bad_action = McpProof::create(
&agent,
"merge",
serde_json::json!({"cost": 1.0}),
delegation.clone(),
)
.unwrap();
assert!(matches!(
verify_mcp_call(&proof_bad_action, &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
let proof_bad_cost = McpProof::create(
&agent,
"resolve",
serde_json::json!({"cost": 10.0}),
delegation,
)
.unwrap();
assert!(matches!(
verify_mcp_call(&proof_bad_cost, &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
}
#[test]
fn test_chained_delegation_through_mcp() {
let root = keypair();
let manager = keypair();
let worker = keypair();
let d1 = Delegation::create_root(
&root,
&manager.identity().did,
vec![Caveat::ActionScope(vec![
"resolve".into(),
"search".into(),
"merge".into(),
])],
)
.unwrap();
let d2 = Delegation::delegate(
&manager,
&worker.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
d1,
)
.unwrap();
let proof = McpProof::create(&worker, "resolve", serde_json::json!({}), d2).unwrap();
let result = verify_mcp_call(&proof, &root.identity()).unwrap();
assert_eq!(result.invoker_did, worker.identity().did);
assert_eq!(result.root_did, root.identity().did);
assert_eq!(result.depth, 2); }
#[test]
fn test_revocation_through_mcp() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let revoked_hash = delegation.proof.content_hash();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
assert!(verify_mcp_call(&proof, &root.identity()).is_ok());
let result =
verify_mcp_call_with_revocation(&proof, &root.identity(), |hash| hash == revoked_hash);
assert!(matches!(result, Err(CryptoError::DelegationRevoked(_))));
}
#[test]
fn test_auth_mode_required_no_proof() {
let root = keypair();
let args = serde_json::json!({"source": "crm"});
let result =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Required);
assert!(matches!(result, Err(CryptoError::DelegationChainBroken(_))));
}
#[test]
fn test_auth_mode_required_with_valid_proof() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let mut args = serde_json::json!({"source": "crm"});
proof.inject(&mut args);
let outcome =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Required)
.unwrap();
assert!(outcome.verified.is_some());
assert!(outcome.args.get("_proof").is_none());
}
#[test]
fn test_auth_mode_optional_no_proof() {
let root = keypair();
let args = serde_json::json!({"source": "crm"});
let outcome =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Optional)
.unwrap();
assert!(outcome.verified.is_none());
}
#[test]
fn test_auth_mode_optional_with_proof() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let mut args = serde_json::json!({"source": "crm"});
proof.inject(&mut args);
let outcome =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Optional)
.unwrap();
assert!(outcome.verified.is_some());
}
#[test]
fn test_auth_mode_disabled() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let mut args = serde_json::json!({"source": "crm"});
proof.inject(&mut args);
let outcome =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Disabled)
.unwrap();
assert!(outcome.verified.is_none());
assert!(outcome.args.get("_proof").is_none());
}
#[test]
fn test_auth_mode_from_str() {
assert_eq!(
McpAuthMode::from_str_lossy("required"),
McpAuthMode::Required
);
assert_eq!(
McpAuthMode::from_str_lossy("enforce"),
McpAuthMode::Required
);
assert_eq!(McpAuthMode::from_str_lossy("strict"), McpAuthMode::Required);
assert_eq!(
McpAuthMode::from_str_lossy("disabled"),
McpAuthMode::Disabled
);
assert_eq!(McpAuthMode::from_str_lossy("off"), McpAuthMode::Disabled);
assert_eq!(
McpAuthMode::from_str_lossy("optional"),
McpAuthMode::Optional
);
assert_eq!(
McpAuthMode::from_str_lossy("anything"),
McpAuthMode::Optional
);
}
#[test]
fn test_create_fails_when_invoker_not_delegate() {
let root = keypair();
let agent = keypair();
let wrong_agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let result = McpProof::create(&wrong_agent, "resolve", serde_json::json!({}), delegation);
assert!(result.is_err());
}
#[test]
fn test_verify_invalid_hex_public_key() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let mut proof =
McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
proof.invoker_public_key = "not-valid-hex!@#$".to_string();
let result = verify_mcp_call(&proof, &root.identity());
assert!(matches!(result, Err(CryptoError::DelegationChainBroken(_))));
if let Err(CryptoError::DelegationChainBroken(msg)) = result {
assert!(msg.contains("invalid hex"), "got: {}", msg);
}
}
#[test]
fn test_verify_wrong_length_public_key() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let mut proof =
McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
proof.invoker_public_key = hex::encode(&[0u8; 16]);
let result = verify_mcp_call(&proof, &root.identity());
assert!(matches!(result, Err(CryptoError::DelegationChainBroken(_))));
if let Err(CryptoError::DelegationChainBroken(msg)) = result {
assert!(msg.contains("invalid Ed25519"), "got: {}", msg);
}
}
#[test]
fn test_verify_tampered_invocation_signature() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let mut proof =
McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
proof.invocation.proof.signature = "00".repeat(64);
let result = verify_mcp_call(&proof, &root.identity());
assert!(
result.is_err(),
"tampered signature should fail verification"
);
}
#[test]
fn test_invoker_public_key_is_64_char_hex() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
assert_eq!(proof.invoker_public_key.len(), 64);
assert!(proof
.invoker_public_key
.chars()
.all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_extract_non_object_args() {
let (proof, clean) = McpProof::extract(&serde_json::json!([1, 2, 3]));
assert!(proof.is_none());
assert_eq!(clean, serde_json::json!([1, 2, 3]));
let (proof, clean) = McpProof::extract(&serde_json::json!("hello"));
assert!(proof.is_none());
assert_eq!(clean, serde_json::json!("hello"));
let (proof, clean) = McpProof::extract(&serde_json::Value::Null);
assert!(proof.is_none());
assert_eq!(clean, serde_json::Value::Null);
let (proof, clean) = McpProof::extract(&serde_json::json!(42));
assert!(proof.is_none());
assert_eq!(clean, serde_json::json!(42));
}
#[test]
fn test_extract_proof_object_wrong_shape() {
let args = serde_json::json!({
"source": "crm",
"_proof": {"wrong": "shape"}
});
let (proof, clean_args) = McpProof::extract(&args);
assert!(proof.is_none());
assert!(clean_args.get("_proof").is_none());
assert_eq!(clean_args.get("source").unwrap(), "crm");
}
#[test]
fn test_extract_proof_null_value() {
let args = serde_json::json!({"source": "crm", "_proof": null});
let (proof, clean_args) = McpProof::extract(&args);
assert!(proof.is_none());
assert!(clean_args.get("_proof").is_none());
}
#[test]
fn test_extract_empty_object() {
let args = serde_json::json!({});
let (proof, clean_args) = McpProof::extract(&args);
assert!(proof.is_none());
assert_eq!(clean_args, serde_json::json!({}));
}
#[test]
fn test_inject_non_object_is_noop() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
let mut arr = serde_json::json!([1, 2, 3]);
proof.inject(&mut arr);
assert_eq!(arr, serde_json::json!([1, 2, 3]));
let mut s = serde_json::json!("hello");
proof.inject(&mut s);
assert_eq!(s, serde_json::json!("hello"));
}
#[test]
fn test_auth_mode_required_with_invalid_proof_fails() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let mut proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
proof.invocation.proof.signature = "ff".repeat(64);
let mut args = serde_json::json!({"source": "crm"});
proof.inject(&mut args);
let result =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Required);
assert!(
result.is_err(),
"invalid proof in required mode should fail"
);
}
#[test]
fn test_auth_mode_optional_with_invalid_proof_fails() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let mut proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
proof.invocation.proof.signature = "ff".repeat(64);
let mut args = serde_json::json!({"source": "crm"});
proof.inject(&mut args);
let result =
verify_mcp_tool_call("resolve", &args, &root.identity(), McpAuthMode::Optional);
assert!(
result.is_err(),
"invalid proof in optional mode should fail (proof was present)"
);
}
#[test]
fn test_expires_at_caveat_through_mcp() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::ExpiresAt("2020-01-01T00:00:00.000Z".into())],
)
.unwrap();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
assert!(matches!(
verify_mcp_call(&proof, &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
}
#[test]
fn test_expires_at_future_passes() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::ExpiresAt("2099-12-31T23:59:59.999Z".into())],
)
.unwrap();
let proof = McpProof::create(&agent, "resolve", serde_json::json!({}), delegation).unwrap();
assert!(verify_mcp_call(&proof, &root.identity()).is_ok());
}
#[test]
fn test_resource_caveat_through_mcp() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::Resource("entity:customer:*".into())],
)
.unwrap();
let proof_ok = McpProof::create(
&agent,
"resolve",
serde_json::json!({"resource": "entity:customer:123"}),
delegation.clone(),
)
.unwrap();
assert!(verify_mcp_call(&proof_ok, &root.identity()).is_ok());
let proof_bad = McpProof::create(
&agent,
"resolve",
serde_json::json!({"resource": "entity:order:456"}),
delegation,
)
.unwrap();
assert!(matches!(
verify_mcp_call(&proof_bad, &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
}
#[test]
fn test_context_caveat_through_mcp() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::Context {
key: "session_id".into(),
value: "sess-abc".into(),
}],
)
.unwrap();
let proof_ok = McpProof::create(
&agent,
"resolve",
serde_json::json!({"session_id": "sess-abc"}),
delegation.clone(),
)
.unwrap();
assert!(verify_mcp_call(&proof_ok, &root.identity()).is_ok());
let proof_bad = McpProof::create(
&agent,
"resolve",
serde_json::json!({"session_id": "sess-xyz"}),
delegation,
)
.unwrap();
assert!(matches!(
verify_mcp_call(&proof_bad, &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
}
#[test]
fn test_multiple_caveats_all_checked() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![
Caveat::ActionScope(vec!["resolve".into()]),
Caveat::MaxCost(10.0),
Caveat::Resource("entity:*".into()),
],
)
.unwrap();
let proof_ok = McpProof::create(
&agent,
"resolve",
serde_json::json!({"cost": 5.0, "resource": "entity:123"}),
delegation.clone(),
)
.unwrap();
assert!(verify_mcp_call(&proof_ok, &root.identity()).is_ok());
let proof_bad = McpProof::create(
&agent,
"merge",
serde_json::json!({"cost": 5.0, "resource": "entity:123"}),
delegation,
)
.unwrap();
assert!(matches!(
verify_mcp_call(&proof_bad, &root.identity()),
Err(CryptoError::CaveatViolation(_))
));
}
#[test]
fn test_proof_json_format_cross_language() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(&root, &agent.identity().did, vec![]).unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let json: serde_json::Value = serde_json::to_value(&proof).unwrap();
assert!(
json["invoker_public_key"].is_string(),
"invoker_public_key must serialize as hex string, got: {}",
json["invoker_public_key"]
);
let pk_str = json["invoker_public_key"].as_str().unwrap();
assert_eq!(pk_str.len(), 64, "hex-encoded 32-byte key = 64 chars");
assert!(
pk_str.chars().all(|c| c.is_ascii_hexdigit()),
"must be valid hex: {}",
pk_str
);
}
#[test]
fn test_serialization_roundtrip() {
let root = keypair();
let agent = keypair();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
)
.unwrap();
let proof = McpProof::create(
&agent,
"resolve",
serde_json::json!({"source": "crm"}),
delegation,
)
.unwrap();
let json = serde_json::to_string(&proof).unwrap();
let restored: McpProof = serde_json::from_str(&json).unwrap();
let result = verify_mcp_call(&restored, &root.identity()).unwrap();
assert_eq!(result.invoker_did, agent.identity().did);
}
}