use crate::a2a::{A2AArtifact, A2AMessage};
#[cfg(not(target_arch = "wasm32"))]
use crate::agent::loaders::fetch_remote_public_key;
use crate::agent::{
AGENT_SIGNATURE_FIELDNAME, Agent, SignatureContentMode, boilerplate::BoilerPlate,
build_signature_content, document::DocumentTraits, extract_signature_fields,
loaders::FileLoader,
};
use crate::config::{KeyResolutionSource, get_key_resolution_order};
use crate::crypt::{KeyManager, hash::hash_public_key};
use crate::error::JacsError;
use crate::schema::utils::ValueExt;
use crate::time_utils;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tracing::{info, warn};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VerificationStatus {
Verified,
SelfSigned,
Unverified { reason: String },
Invalid { reason: String },
}
impl VerificationStatus {
pub fn is_verified(&self) -> bool {
matches!(
self,
VerificationStatus::Verified | VerificationStatus::SelfSigned
)
}
pub fn is_unverified(&self) -> bool {
matches!(self, VerificationStatus::Unverified { .. })
}
pub fn is_invalid(&self) -> bool {
matches!(self, VerificationStatus::Invalid { .. })
}
}
fn resolve_foreign_public_key(
agent: &Agent,
signer_id: &str,
signer_version: &str,
public_key_hash: &str,
) -> Result<(Vec<u8>, String), String> {
if public_key_hash.is_empty() {
return Err("Missing publicKeyHash in signature".to_string());
}
let resolution_order = get_key_resolution_order();
let mut last_error = "No key source attempted".to_string();
for source in &resolution_order {
match source {
KeyResolutionSource::Local => match agent.fs_load_public_key(public_key_hash) {
Ok(public_key) => match agent.fs_load_public_key_type(public_key_hash) {
Ok(enc_type) => {
return Ok((public_key, enc_type.trim().to_string()));
}
Err(e) => {
last_error = format!("Local key type lookup failed: {}", e);
}
},
Err(e) => {
last_error = format!("Local key lookup failed: {}", e);
}
},
KeyResolutionSource::Dns => {
last_error = "DNS source does not provide public key bytes".to_string();
}
KeyResolutionSource::Registry => {
#[cfg(not(target_arch = "wasm32"))]
{
if signer_id.is_empty() || signer_version.is_empty() {
last_error =
"Registry lookup requires signer agent ID and version UUID".to_string();
continue;
}
match fetch_remote_public_key(signer_id, signer_version) {
Ok(key_info) => {
if !key_info.hash.is_empty() && key_info.hash != public_key_hash {
last_error = format!(
"Registry key hash mismatch: expected {}..., got {}...",
&public_key_hash[..public_key_hash.len().min(16)],
&key_info.hash[..key_info.hash.len().min(16)]
);
continue;
}
return Ok((key_info.public_key, key_info.algorithm));
}
Err(e) => {
last_error = format!("Registry key lookup failed: {}", e);
}
}
}
#[cfg(target_arch = "wasm32")]
{
let _ = signer_id;
let _ = signer_version;
last_error = "Registry lookup is not available on wasm32 targets in this build"
.to_string();
}
}
}
}
Err(format!(
"Could not resolve signer key {}... using sources {:?}. Last error: {}",
&public_key_hash[..public_key_hash.len().min(16)],
resolution_order,
last_error
))
}
fn verify_with_resolved_key(
agent: &Agent,
wrapped_artifact: &Value,
signature_info: &Value,
public_key: Vec<u8>,
public_key_enc_type: String,
) -> Result<(), String> {
let signature = signature_info
.get_str("signature")
.ok_or_else(|| "No signature found in jacsSignature".to_string())?;
let signature_hash = signature_info
.get_str("publicKeyHash")
.ok_or_else(|| "No publicKeyHash found in jacsSignature".to_string())?;
let computed_hash = hash_public_key(&public_key);
if computed_hash != signature_hash {
return Err(format!(
"Resolved public key hash mismatch: expected {}..., got {}...",
&signature_hash[..signature_hash.len().min(16)],
&computed_hash[..computed_hash.len().min(16)]
));
}
let signature_fields = extract_signature_fields(wrapped_artifact, AGENT_SIGNATURE_FIELDNAME);
let (signable_data, _) = build_signature_content(
wrapped_artifact,
signature_fields,
AGENT_SIGNATURE_FIELDNAME,
SignatureContentMode::CanonicalV2,
)
.map_err(|e| format!("Could not build signable payload: {}", e))?;
let explicit_alg = if public_key_enc_type.is_empty() {
signature_info
.get_str("signingAlgorithm")
.map(|s| s.to_string())
} else {
Some(public_key_enc_type)
};
agent
.verify_string(&signable_data, &signature, public_key, explicit_alg)
.map_err(|e| format!("Signature verification failed: {}", e))
}
pub fn wrap_artifact_with_provenance(
agent: &mut Agent,
artifact: Value,
artifact_type: &str,
parent_signatures: Option<Vec<Value>>,
) -> Result<Value, JacsError> {
let artifact_id = Uuid::new_v4().to_string();
let artifact_version = Uuid::new_v4().to_string();
let mut wrapped_artifact = json!({
"jacsId": artifact_id,
"jacsVersion": artifact_version,
"jacsType": format!("a2a-{}", artifact_type),
"jacsLevel": "artifact",
"jacsPreviousVersion": null,
"jacsVersionDate": time_utils::now_rfc3339(),
"$schema": "https://jacs.sh/schemas/header/v1/header.schema.json",
"a2aArtifact": artifact,
});
if let Some(parents) = parent_signatures {
wrapped_artifact["jacsParentSignatures"] = json!(parents);
}
let signature = agent.signing_procedure(&wrapped_artifact, None, AGENT_SIGNATURE_FIELDNAME)?;
wrapped_artifact[AGENT_SIGNATURE_FIELDNAME] = signature;
let document_hash = agent.hash_doc(&wrapped_artifact)?;
wrapped_artifact[crate::agent::SHA256_FIELDNAME] = json!(document_hash);
info!("Successfully wrapped A2A artifact with JACS provenance");
Ok(wrapped_artifact)
}
pub fn wrap_a2a_artifact_with_provenance(
agent: &mut Agent,
artifact: &A2AArtifact,
parent_signatures: Option<Vec<Value>>,
) -> Result<Value, JacsError> {
let artifact_value = serde_json::to_value(artifact)?;
wrap_artifact_with_provenance(agent, artifact_value, "artifact", parent_signatures)
}
pub fn wrap_a2a_message_with_provenance(
agent: &mut Agent,
message: &A2AMessage,
parent_signatures: Option<Vec<Value>>,
) -> Result<Value, JacsError> {
let message_value = serde_json::to_value(message)?;
wrap_artifact_with_provenance(agent, message_value, "message", parent_signatures)
}
pub fn verify_wrapped_artifact(
agent: &Agent,
wrapped_artifact: &Value,
) -> Result<VerificationResult, JacsError> {
if let Err(e) = agent.verify_hash(wrapped_artifact) {
return Ok(VerificationResult {
status: VerificationStatus::Invalid {
reason: format!("Hash verification failed: {}", e),
},
valid: false,
signer_id: String::new(),
signer_version: String::new(),
artifact_type: wrapped_artifact.get_str_or("jacsType", "unknown"),
timestamp: wrapped_artifact.get_str_or("jacsVersionDate", ""),
parent_signatures_valid: false,
parent_verification_results: vec![],
original_artifact: wrapped_artifact
.get("a2aArtifact")
.cloned()
.unwrap_or(Value::Null),
trust_level: None,
trust_assessment: None,
});
}
let signature_info = wrapped_artifact
.get(AGENT_SIGNATURE_FIELDNAME)
.ok_or("No JACS signature found")?;
let agent_id = signature_info
.get_str("agentID")
.ok_or("No agent ID in signature")?;
let agent_version = signature_info
.get_str("agentVersion")
.ok_or("No agent version in signature")?;
let public_key_hash = signature_info.get_str_or("publicKeyHash", "");
let current_agent_id = agent.get_id().ok();
let is_self_signed = current_agent_id
.as_ref()
.map(|id| id == &agent_id)
.unwrap_or(false);
let (status, valid) = if is_self_signed {
let public_key = agent.get_public_key()?;
match agent.signature_verification_procedure(
wrapped_artifact,
None,
AGENT_SIGNATURE_FIELDNAME,
public_key,
agent.get_key_algorithm().cloned(),
None,
None,
) {
Ok(_) => (VerificationStatus::SelfSigned, true),
Err(e) => (
VerificationStatus::Invalid {
reason: format!("Signature verification failed: {}", e),
},
false,
),
}
} else {
match resolve_foreign_public_key(agent, &agent_id, &agent_version, &public_key_hash) {
Ok((public_key, public_key_enc_type)) => match verify_with_resolved_key(
agent,
wrapped_artifact,
signature_info,
public_key,
public_key_enc_type,
) {
Ok(_) => (VerificationStatus::Verified, true),
Err(e) => (
VerificationStatus::Invalid {
reason: format!("Foreign signature verification failed: {}", e),
},
false,
),
},
Err(reason) => {
warn!(
"Could not resolve foreign signature key for agent {}: {}",
agent_id, reason
);
(VerificationStatus::Unverified { reason }, false)
}
}
};
let original_artifact = wrapped_artifact
.get("a2aArtifact")
.ok_or("No A2A artifact found in wrapper")?;
let (parent_signatures_valid, parent_verification_results) =
verify_parent_signatures(agent, wrapped_artifact)?;
Ok(VerificationResult {
status,
valid,
signer_id: agent_id.clone(),
signer_version: agent_version.clone(),
artifact_type: wrapped_artifact.get_str_or("jacsType", "unknown"),
timestamp: wrapped_artifact.get_str_or("jacsVersionDate", ""),
parent_signatures_valid,
parent_verification_results,
original_artifact: original_artifact.clone(),
trust_level: None,
trust_assessment: None,
})
}
pub fn verify_wrapped_artifact_with_policy(
agent: &Agent,
wrapped_artifact: &Value,
remote_card: &super::AgentCard,
policy: super::trust::A2ATrustPolicy,
) -> Result<VerificationResult, JacsError> {
use super::trust::assess_a2a_agent;
let assessment = assess_a2a_agent(agent, remote_card, policy);
if !assessment.allowed {
info!(
policy = %policy,
trust_level = %assessment.trust_level,
reason = %assessment.reason,
"A2A trust policy rejected remote agent"
);
return Ok(VerificationResult {
status: VerificationStatus::Invalid {
reason: assessment.reason.clone(),
},
valid: false,
signer_id: assessment.agent_id.clone().unwrap_or_default(),
signer_version: String::new(),
artifact_type: wrapped_artifact.get_str_or("jacsType", "unknown"),
timestamp: wrapped_artifact.get_str_or("jacsVersionDate", ""),
parent_signatures_valid: false,
parent_verification_results: vec![],
original_artifact: wrapped_artifact
.get("a2aArtifact")
.cloned()
.unwrap_or(Value::Null),
trust_level: Some(assessment.trust_level),
trust_assessment: Some(assessment),
});
}
let mut result = verify_wrapped_artifact(agent, wrapped_artifact)?;
result.trust_level = Some(assessment.trust_level);
result.trust_assessment = Some(assessment);
info!(
policy = %policy,
trust_level = ?result.trust_level,
crypto_valid = result.valid,
"A2A artifact verified with trust policy"
);
Ok(result)
}
fn verify_parent_signatures(
agent: &Agent,
wrapped_artifact: &Value,
) -> Result<(bool, Vec<ParentVerificationResult>), JacsError> {
let parents = match wrapped_artifact.get("jacsParentSignatures") {
Some(Value::Array(arr)) => arr,
Some(_) => return Err("Invalid jacsParentSignatures: must be an array".into()),
None => return Ok((true, vec![])), };
if parents.is_empty() {
return Ok((true, vec![]));
}
let mut results = Vec::with_capacity(parents.len());
let mut all_valid = true;
for (index, parent) in parents.iter().enumerate() {
let parent_id = parent.get_str_or("jacsId", "unknown");
let parent_signer =
parent.get_path_str_or(&[AGENT_SIGNATURE_FIELDNAME, "agentID"], "unknown");
let verification = match verify_wrapped_artifact(agent, parent) {
Ok(result) => {
let status = result.status.clone();
let verified = result.valid;
if !verified {
all_valid = false;
}
ParentVerificationResult {
index,
artifact_id: parent_id,
signer_id: parent_signer,
status,
verified,
}
}
Err(e) => {
all_valid = false;
ParentVerificationResult {
index,
artifact_id: parent_id,
signer_id: parent_signer,
status: VerificationStatus::Invalid {
reason: format!("Verification error: {}", e),
},
verified: false,
}
}
};
results.push(verification);
}
Ok((all_valid, results))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParentVerificationResult {
pub index: usize,
pub artifact_id: String,
pub signer_id: String,
pub status: VerificationStatus,
pub verified: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationResult {
pub status: VerificationStatus,
pub valid: bool,
pub signer_id: String,
pub signer_version: String,
pub artifact_type: String,
pub timestamp: String,
pub parent_signatures_valid: bool,
pub parent_verification_results: Vec<ParentVerificationResult>,
pub original_artifact: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_level: Option<super::trust::TrustLevel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_assessment: Option<super::trust::TrustAssessment>,
}
pub fn create_chain_of_custody(artifacts: Vec<Value>) -> Result<Value, JacsError> {
let mut chain = Vec::new();
for artifact in artifacts {
if let Some(sig) = artifact.get(AGENT_SIGNATURE_FIELDNAME) {
let entry = json!({
"artifactId": artifact.get("jacsId"),
"artifactType": artifact.get("jacsType"),
"timestamp": artifact.get("jacsVersionDate"),
"agentId": sig.get("agentID"),
"agentVersion": sig.get("agentVersion"),
"signatureHash": sig.get("publicKeyHash"),
});
chain.push(entry);
}
}
Ok(json!({
"chainOfCustody": chain,
"created": time_utils::now_rfc3339(),
"totalArtifacts": chain.len(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_status_is_verified() {
assert!(VerificationStatus::Verified.is_verified());
assert!(VerificationStatus::SelfSigned.is_verified());
assert!(
!VerificationStatus::Unverified {
reason: "test".to_string()
}
.is_verified()
);
assert!(
!VerificationStatus::Invalid {
reason: "test".to_string()
}
.is_verified()
);
}
#[test]
fn test_verification_status_is_unverified() {
assert!(!VerificationStatus::Verified.is_unverified());
assert!(!VerificationStatus::SelfSigned.is_unverified());
assert!(
VerificationStatus::Unverified {
reason: "test".to_string()
}
.is_unverified()
);
assert!(
!VerificationStatus::Invalid {
reason: "test".to_string()
}
.is_unverified()
);
}
#[test]
fn test_verification_status_is_invalid() {
assert!(!VerificationStatus::Verified.is_invalid());
assert!(!VerificationStatus::SelfSigned.is_invalid());
assert!(
!VerificationStatus::Unverified {
reason: "test".to_string()
}
.is_invalid()
);
assert!(
VerificationStatus::Invalid {
reason: "test".to_string()
}
.is_invalid()
);
}
#[test]
fn test_verification_result_creation() {
let result = VerificationResult {
status: VerificationStatus::SelfSigned,
valid: true,
signer_id: "test-agent".to_string(),
signer_version: "v1".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: time_utils::now_rfc3339(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"test": "data"}),
trust_level: None,
trust_assessment: None,
};
assert!(result.valid);
assert!(result.status.is_verified());
assert_eq!(result.signer_id, "test-agent");
}
#[test]
fn test_create_chain_of_custody_empty() {
let chain = create_chain_of_custody(vec![]).unwrap();
assert_eq!(chain["totalArtifacts"], 0);
}
#[test]
fn test_verification_result_serialization() {
let result = VerificationResult {
status: VerificationStatus::Unverified {
reason: "No public key".to_string(),
},
valid: false,
signer_id: "foreign-agent".to_string(),
signer_version: "v2".to_string(),
artifact_type: "a2a-message".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"message": "hello"}),
trust_level: None,
trust_assessment: None,
};
let json = serde_json::to_string(&result).expect("serialization should succeed");
assert!(json.contains("Unverified"));
assert!(json.contains("foreign-agent"));
}
use crate::a2a::trust::{A2ATrustPolicy, TrustLevel};
use crate::a2a::{
A2A_PROTOCOL_VERSION, AgentCapabilities, AgentCard, AgentExtension, AgentInterface,
JACS_EXTENSION_URI,
};
fn make_test_card(
name: &str,
with_jacs_extension: bool,
agent_id: Option<&str>,
version: Option<&str>,
) -> AgentCard {
let extensions = if with_jacs_extension {
Some(vec![AgentExtension {
uri: JACS_EXTENSION_URI.to_string(),
description: Some("JACS provenance".to_string()),
required: Some(false),
}])
} else {
None
};
let metadata = match (agent_id, version) {
(Some(id), Some(ver)) => Some(json!({
"jacsId": id,
"jacsVersion": ver,
})),
(Some(id), None) => Some(json!({ "jacsId": id })),
_ => None,
};
AgentCard {
name: name.to_string(),
description: format!("Test agent: {}", name),
version: "1.0".to_string(),
protocol_versions: vec![A2A_PROTOCOL_VERSION.to_string()],
supported_interfaces: vec![AgentInterface {
url: "https://test.example.com".to_string(),
protocol_binding: "jsonrpc".to_string(),
tenant: None,
}],
default_input_modes: vec!["text/plain".to_string()],
default_output_modes: vec!["text/plain".to_string()],
capabilities: AgentCapabilities {
streaming: None,
push_notifications: None,
extended_agent_card: None,
extensions,
},
skills: vec![],
provider: None,
documentation_url: None,
icon_url: None,
security_schemes: None,
security: None,
signatures: None,
metadata,
}
}
fn make_dummy_wrapped_artifact(artifact_type: &str, agent_id: &str) -> Value {
json!({
"jacsId": "artifact-test-001",
"jacsVersion": "v1",
"jacsType": format!("a2a-{}", artifact_type),
"jacsLevel": "artifact",
"jacsVersionDate": "2025-01-01T00:00:00Z",
"$schema": "https://jacs.sh/schemas/header/v1/header.schema.json",
"a2aArtifact": { "test": "data" },
"jacsSignature": {
"agentID": agent_id,
"agentVersion": "v1",
"publicKeyHash": "abc123",
"signature": "deadbeef",
}
})
}
#[test]
fn test_policy_open_accepts_non_jacs_agent() {
let agent = crate::get_empty_agent();
let card = make_test_card("plain-agent", false, None, None);
let artifact = make_dummy_wrapped_artifact("task", "foreign-agent");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Open)
.unwrap();
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(result.trust_assessment.is_some());
assert!(result.trust_assessment.as_ref().unwrap().allowed);
}
#[test]
fn test_policy_open_accepts_jacs_agent() {
let agent = crate::get_empty_agent();
let card = make_test_card("jacs-agent", true, Some("agent-1"), Some("v1"));
let artifact = make_dummy_wrapped_artifact("message", "agent-1");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Open)
.unwrap();
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(result.trust_assessment.as_ref().unwrap().allowed);
}
#[test]
fn test_policy_verified_rejects_non_jacs_agent() {
let agent = crate::get_empty_agent();
let card = make_test_card("plain-agent", false, Some("no-jacs"), Some("v1"));
let artifact = make_dummy_wrapped_artifact("task", "no-jacs");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Verified)
.unwrap();
assert!(!result.valid);
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(!result.trust_assessment.as_ref().unwrap().allowed);
assert!(
result
.trust_assessment
.as_ref()
.unwrap()
.reason
.contains("does not declare JACS provenance")
);
}
#[test]
fn test_policy_verified_rejects_unsigned_jacs_agent() {
let agent = crate::get_empty_agent();
let card = make_test_card("jacs-agent", true, Some("agent-2"), Some("v1"));
let artifact = make_dummy_wrapped_artifact("task", "agent-2");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Verified)
.unwrap();
assert!(!result.valid);
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(!result.trust_assessment.as_ref().unwrap().allowed);
}
#[test]
fn test_policy_strict_rejects_jacs_not_trusted_agent() {
let agent = crate::get_empty_agent();
let card = make_test_card(
"jacs-not-trusted",
true,
Some("550e8400-e29b-41d4-a716-446655440077"),
Some("550e8400-e29b-41d4-a716-446655440078"),
);
let artifact = make_dummy_wrapped_artifact("task", "550e8400-e29b-41d4-a716-446655440077");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Strict)
.unwrap();
assert!(!result.valid);
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(!result.trust_assessment.as_ref().unwrap().allowed);
assert!(
result
.trust_assessment
.as_ref()
.unwrap()
.reason
.contains("not in the local trust store")
);
}
#[test]
fn test_policy_strict_rejects_non_jacs_agent() {
let agent = crate::get_empty_agent();
let card = make_test_card("plain-untrusted", false, None, None);
let artifact = make_dummy_wrapped_artifact("task", "unknown");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Strict)
.unwrap();
assert!(!result.valid);
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(!result.trust_assessment.as_ref().unwrap().allowed);
}
#[test]
fn test_policy_rejection_preserves_artifact_type() {
let agent = crate::get_empty_agent();
let card = make_test_card("rejected-agent", false, Some("rej-1"), Some("v1"));
let artifact = make_dummy_wrapped_artifact("message", "rej-1");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Verified)
.unwrap();
assert!(!result.valid);
assert_eq!(result.artifact_type, "a2a-message");
assert_eq!(result.original_artifact, json!({ "test": "data" }));
}
#[test]
fn test_policy_open_proceeds_to_crypto_verification() {
let agent = crate::get_empty_agent();
let card = make_test_card("open-agent", false, Some("agent-open"), Some("v1"));
let artifact = make_dummy_wrapped_artifact("task", "agent-open");
let result =
verify_wrapped_artifact_with_policy(&agent, &artifact, &card, A2ATrustPolicy::Open)
.unwrap();
assert!(result.trust_assessment.as_ref().unwrap().allowed);
assert_eq!(result.trust_level, Some(TrustLevel::Untrusted));
assert!(!result.valid);
}
#[test]
fn test_verification_result_with_trust_serialization() {
use crate::a2a::trust::TrustAssessment;
let result = VerificationResult {
status: VerificationStatus::Verified,
valid: true,
signer_id: "trusted-agent".to_string(),
signer_version: "v1".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"data": "test"}),
trust_level: Some(TrustLevel::JacsVerified),
trust_assessment: Some(TrustAssessment {
allowed: true,
trust_level: TrustLevel::JacsVerified,
reason: "Verified policy: agent has JACS provenance".to_string(),
jacs_registered: true,
agent_id: Some("trusted-agent".to_string()),
policy: A2ATrustPolicy::Verified,
}),
};
let json_str = serde_json::to_string(&result).expect("should serialize");
assert!(json_str.contains("trustLevel"));
assert!(json_str.contains("trustAssessment"));
assert!(json_str.contains("JacsVerified"));
let deserialized: VerificationResult =
serde_json::from_str(&json_str).expect("should deserialize");
assert_eq!(deserialized.trust_level, Some(TrustLevel::JacsVerified));
assert!(deserialized.trust_assessment.unwrap().allowed);
}
#[test]
fn test_verification_result_without_trust_omits_fields() {
let result = VerificationResult {
status: VerificationStatus::Verified,
valid: true,
signer_id: "agent".to_string(),
signer_version: "v1".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({}),
trust_level: None,
trust_assessment: None,
};
let json_str = serde_json::to_string(&result).expect("should serialize");
assert!(!json_str.contains("trustLevel"));
assert!(!json_str.contains("trustAssessment"));
}
#[test]
fn test_verification_result_camel_case_contract() {
let result = VerificationResult {
status: VerificationStatus::Verified,
valid: true,
signer_id: "agent-contract-test".to_string(),
signer_version: "v1-contract".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: "2025-06-01T00:00:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"content": "test"}),
trust_level: Some(TrustLevel::JacsVerified),
trust_assessment: Some(crate::a2a::trust::TrustAssessment {
allowed: true,
trust_level: TrustLevel::JacsVerified,
reason: "Contract test".to_string(),
jacs_registered: true,
agent_id: Some("agent-contract-test".to_string()),
policy: A2ATrustPolicy::Verified,
}),
};
let json_value: Value = serde_json::to_value(&result).expect("should serialize to Value");
assert!(json_value.get("status").is_some(), "missing 'status'");
assert!(json_value.get("valid").is_some(), "missing 'valid'");
assert!(json_value.get("signerId").is_some(), "missing 'signerId'");
assert!(
json_value.get("signerVersion").is_some(),
"missing 'signerVersion'"
);
assert!(
json_value.get("artifactType").is_some(),
"missing 'artifactType'"
);
assert!(json_value.get("timestamp").is_some(), "missing 'timestamp'");
assert!(
json_value.get("parentSignaturesValid").is_some(),
"missing 'parentSignaturesValid'"
);
assert!(
json_value.get("parentVerificationResults").is_some(),
"missing 'parentVerificationResults'"
);
assert!(
json_value.get("originalArtifact").is_some(),
"missing 'originalArtifact'"
);
assert!(
json_value.get("trustLevel").is_some(),
"missing 'trustLevel'"
);
assert!(
json_value.get("trustAssessment").is_some(),
"missing 'trustAssessment'"
);
assert!(
json_value.get("signer_id").is_none(),
"snake_case 'signer_id' should not appear"
);
assert!(
json_value.get("signer_version").is_none(),
"snake_case 'signer_version' should not appear"
);
assert!(
json_value.get("artifact_type").is_none(),
"snake_case 'artifact_type' should not appear"
);
assert!(
json_value.get("parent_signatures_valid").is_none(),
"snake_case 'parent_signatures_valid' should not appear"
);
assert!(
json_value.get("original_artifact").is_none(),
"snake_case 'original_artifact' should not appear"
);
assert!(
json_value.get("trust_level").is_none(),
"snake_case 'trust_level' should not appear"
);
assert!(
json_value.get("trust_assessment").is_none(),
"snake_case 'trust_assessment' should not appear"
);
assert_eq!(json_value["signerId"], "agent-contract-test");
assert_eq!(json_value["signerVersion"], "v1-contract");
assert_eq!(json_value["artifactType"], "a2a-task");
assert_eq!(json_value["valid"], true);
assert_eq!(json_value["parentSignaturesValid"], true);
let trust = json_value.get("trustAssessment").unwrap();
assert!(
trust.get("trustLevel").is_some(),
"missing trust.trustLevel"
);
assert!(
trust.get("jacsRegistered").is_some(),
"missing trust.jacsRegistered"
);
assert!(trust.get("agentId").is_some(), "missing trust.agentId");
assert!(trust.get("policy").is_some(), "missing trust.policy");
assert!(trust.get("allowed").is_some(), "missing trust.allowed");
assert!(trust.get("reason").is_some(), "missing trust.reason");
}
#[test]
fn test_verification_status_variant_json_shapes() {
let verified = serde_json::to_value(&VerificationStatus::Verified).unwrap();
assert_eq!(verified, json!("Verified"));
let self_signed = serde_json::to_value(&VerificationStatus::SelfSigned).unwrap();
assert_eq!(self_signed, json!("SelfSigned"));
let unverified = serde_json::to_value(&VerificationStatus::Unverified {
reason: "No public key available".to_string(),
})
.unwrap();
assert!(unverified.get("Unverified").is_some());
assert_eq!(
unverified["Unverified"]["reason"],
"No public key available"
);
let invalid = serde_json::to_value(&VerificationStatus::Invalid {
reason: "Signature mismatch".to_string(),
})
.unwrap();
assert!(invalid.get("Invalid").is_some());
assert_eq!(invalid["Invalid"]["reason"], "Signature mismatch");
}
#[test]
fn test_verification_result_json_round_trip() {
let original = VerificationResult {
status: VerificationStatus::Unverified {
reason: "Foreign key not found".to_string(),
},
valid: false,
signer_id: "foreign-agent-xyz".to_string(),
signer_version: "v2".to_string(),
artifact_type: "a2a-message".to_string(),
timestamp: "2025-06-01T12:00:00Z".to_string(),
parent_signatures_valid: false,
parent_verification_results: vec![ParentVerificationResult {
index: 0,
artifact_id: "parent-001".to_string(),
signer_id: "parent-signer".to_string(),
status: VerificationStatus::Verified,
verified: true,
}],
original_artifact: json!({"message": "hello from foreign agent"}),
trust_level: None,
trust_assessment: None,
};
let json_str = serde_json::to_string_pretty(&original).unwrap();
let deserialized: VerificationResult = serde_json::from_str(&json_str).unwrap();
assert_eq!(deserialized.signer_id, original.signer_id);
assert_eq!(deserialized.signer_version, original.signer_version);
assert_eq!(deserialized.artifact_type, original.artifact_type);
assert_eq!(deserialized.valid, original.valid);
assert_eq!(
deserialized.parent_signatures_valid,
original.parent_signatures_valid
);
assert_eq!(
deserialized.parent_verification_results.len(),
original.parent_verification_results.len()
);
assert_eq!(
deserialized.parent_verification_results[0].artifact_id,
"parent-001"
);
assert_eq!(
deserialized.parent_verification_results[0].signer_id,
"parent-signer"
);
assert!(deserialized.parent_verification_results[0].verified);
let json_value: Value = serde_json::from_str(&json_str).unwrap();
let parent = &json_value["parentVerificationResults"][0];
assert!(
parent.get("artifactId").is_some(),
"missing parent.artifactId"
);
assert!(parent.get("signerId").is_some(), "missing parent.signerId");
}
#[test]
fn test_verification_result_golden_verified() {
let result = VerificationResult {
status: VerificationStatus::Verified,
valid: true,
signer_id: "agent-golden-001".to_string(),
signer_version: "v1".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: "2025-01-15T10:30:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"task": "golden"}),
trust_level: None,
trust_assessment: None,
};
let actual: Value = serde_json::to_value(&result).unwrap();
let expected = json!({
"status": "Verified",
"valid": true,
"signerId": "agent-golden-001",
"signerVersion": "v1",
"artifactType": "a2a-task",
"timestamp": "2025-01-15T10:30:00Z",
"parentSignaturesValid": true,
"parentVerificationResults": [],
"originalArtifact": {"task": "golden"}
});
assert_eq!(actual, expected, "Golden JSON mismatch for Verified result");
}
#[test]
fn test_verification_result_golden_self_signed() {
let result = VerificationResult {
status: VerificationStatus::SelfSigned,
valid: true,
signer_id: "self-signer-001".to_string(),
signer_version: "v2".to_string(),
artifact_type: "a2a-message".to_string(),
timestamp: "2025-02-20T14:00:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"msg": "self-signed"}),
trust_level: None,
trust_assessment: None,
};
let actual: Value = serde_json::to_value(&result).unwrap();
let expected = json!({
"status": "SelfSigned",
"valid": true,
"signerId": "self-signer-001",
"signerVersion": "v2",
"artifactType": "a2a-message",
"timestamp": "2025-02-20T14:00:00Z",
"parentSignaturesValid": true,
"parentVerificationResults": [],
"originalArtifact": {"msg": "self-signed"}
});
assert_eq!(
actual, expected,
"Golden JSON mismatch for SelfSigned result"
);
}
#[test]
fn test_verification_result_golden_unverified() {
let result = VerificationResult {
status: VerificationStatus::Unverified {
reason: "Public key not available".to_string(),
},
valid: false,
signer_id: "foreign-agent-xyz".to_string(),
signer_version: "v3".to_string(),
artifact_type: "a2a-artifact".to_string(),
timestamp: "2025-03-10T08:45:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"data": "unverified"}),
trust_level: None,
trust_assessment: None,
};
let actual: Value = serde_json::to_value(&result).unwrap();
let expected = json!({
"status": {
"Unverified": {
"reason": "Public key not available"
}
},
"valid": false,
"signerId": "foreign-agent-xyz",
"signerVersion": "v3",
"artifactType": "a2a-artifact",
"timestamp": "2025-03-10T08:45:00Z",
"parentSignaturesValid": true,
"parentVerificationResults": [],
"originalArtifact": {"data": "unverified"}
});
assert_eq!(
actual, expected,
"Golden JSON mismatch for Unverified result"
);
}
#[test]
fn test_verification_result_golden_invalid() {
let result = VerificationResult {
status: VerificationStatus::Invalid {
reason: "Signature mismatch".to_string(),
},
valid: false,
signer_id: "bad-actor-agent".to_string(),
signer_version: "v1".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: "2025-04-01T12:00:00Z".to_string(),
parent_signatures_valid: false,
parent_verification_results: vec![],
original_artifact: json!({"compromised": true}),
trust_level: None,
trust_assessment: None,
};
let actual: Value = serde_json::to_value(&result).unwrap();
let expected = json!({
"status": {
"Invalid": {
"reason": "Signature mismatch"
}
},
"valid": false,
"signerId": "bad-actor-agent",
"signerVersion": "v1",
"artifactType": "a2a-task",
"timestamp": "2025-04-01T12:00:00Z",
"parentSignaturesValid": false,
"parentVerificationResults": [],
"originalArtifact": {"compromised": true}
});
assert_eq!(actual, expected, "Golden JSON mismatch for Invalid result");
}
#[test]
fn test_verification_result_golden_with_trust() {
use crate::a2a::trust::TrustAssessment;
let result = VerificationResult {
status: VerificationStatus::Verified,
valid: true,
signer_id: "trusted-agent-abc".to_string(),
signer_version: "v1".to_string(),
artifact_type: "a2a-task".to_string(),
timestamp: "2025-05-01T09:00:00Z".to_string(),
parent_signatures_valid: true,
parent_verification_results: vec![],
original_artifact: json!({"payload": "trusted"}),
trust_level: Some(TrustLevel::JacsVerified),
trust_assessment: Some(TrustAssessment {
allowed: true,
trust_level: TrustLevel::JacsVerified,
reason: "Verified policy: agent has JACS provenance extension".to_string(),
jacs_registered: true,
agent_id: Some("trusted-agent-abc".to_string()),
policy: A2ATrustPolicy::Verified,
}),
};
let actual: Value = serde_json::to_value(&result).unwrap();
let expected = json!({
"status": "Verified",
"valid": true,
"signerId": "trusted-agent-abc",
"signerVersion": "v1",
"artifactType": "a2a-task",
"timestamp": "2025-05-01T09:00:00Z",
"parentSignaturesValid": true,
"parentVerificationResults": [],
"originalArtifact": {"payload": "trusted"},
"trustLevel": "JacsVerified",
"trustAssessment": {
"allowed": true,
"trustLevel": "JacsVerified",
"reason": "Verified policy: agent has JACS provenance extension",
"jacsRegistered": true,
"agentId": "trusted-agent-abc",
"policy": "Verified"
}
});
assert_eq!(
actual, expected,
"Golden JSON mismatch for result with trust"
);
}
#[test]
fn test_parent_verification_result_golden() {
let parent = ParentVerificationResult {
index: 0,
artifact_id: "parent-artifact-001".to_string(),
signer_id: "parent-signer-agent".to_string(),
status: VerificationStatus::Verified,
verified: true,
};
let actual: Value = serde_json::to_value(&parent).unwrap();
let expected = json!({
"index": 0,
"artifactId": "parent-artifact-001",
"signerId": "parent-signer-agent",
"status": "Verified",
"verified": true
});
assert_eq!(
actual, expected,
"Golden JSON mismatch for ParentVerificationResult"
);
let parent_unverified = ParentVerificationResult {
index: 2,
artifact_id: "parent-artifact-003".to_string(),
signer_id: "unknown-signer".to_string(),
status: VerificationStatus::Unverified {
reason: "Key not found".to_string(),
},
verified: false,
};
let actual2: Value = serde_json::to_value(&parent_unverified).unwrap();
let expected2 = json!({
"index": 2,
"artifactId": "parent-artifact-003",
"signerId": "unknown-signer",
"status": {
"Unverified": {
"reason": "Key not found"
}
},
"verified": false
});
assert_eq!(
actual2, expected2,
"Golden JSON mismatch for ParentVerificationResult (Unverified)"
);
}
}