use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::protocol::AgentCard;
use super::A2AError;
pub fn sign_agent_card(card: &mut AgentCard, secret: &[u8]) -> Result<(), A2AError> {
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
.map_err(|_| A2AError::Signature("invalid signing secret length".to_string()))?;
mac.update(&canonical_card_bytes(card)?);
let tag = mac.finalize().into_bytes();
card.signature = Some(hex_encode(&tag));
Ok(())
}
pub fn verify_card_signature(card: &AgentCard, secret: &[u8]) -> Result<(), A2AError> {
let sig = match card.signature.as_deref() {
Some(s) if !s.is_empty() => s,
_ => return Ok(()),
};
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
.map_err(|_| A2AError::Signature("invalid verification secret length".to_string()))?;
mac.update(&canonical_card_bytes(card)?);
let expected = hex_encode(&mac.finalize().into_bytes());
if constant_time_eq(sig, &expected) {
Ok(())
} else {
Err(A2AError::Signature(
"agent card signature verification failed".to_string(),
))
}
}
fn canonical_card_bytes(card: &AgentCard) -> Result<Vec<u8>, A2AError> {
let mut value = serde_json::to_value(card)
.map_err(|e| A2AError::Parse(format!("Failed to serialize agent card: {}", e)))?;
if let Some(obj) = value.as_object_mut() {
obj.remove("signature");
}
serde_json::to_vec(&value)
.map_err(|e| A2AError::Parse(format!("Failed to serialize agent card: {}", e)))
}
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
write!(s, "{:02x}", b).expect("writing to a String cannot fail");
}
s
}
pub(crate) fn constant_time_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let a = a.as_bytes();
let b = b.as_bytes();
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
pub fn canonical_json(value: &serde_json::Value) -> serde_json::Value {
use serde_json::Value as V;
match value {
V::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let mut out = serde_json::Map::new();
for k in keys {
out.insert(k.clone(), canonical_json(&map[k]));
}
V::Object(out)
}
V::Array(items) => V::Array(items.iter().map(canonical_json).collect()),
other => other.clone(),
}
}
fn base64url_encode(bytes: &[u8]) -> String {
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::new();
let mut acc: u32 = 0;
let mut bits = 0u32;
for b in bytes {
acc = (acc << 8) | *b as u32;
bits += 8;
while bits >= 6 {
bits -= 6;
let idx = ((acc >> bits) & 0x3F) as usize;
out.push(TABLE[idx] as char);
}
}
if bits > 0 {
let idx = ((acc << (6 - bits)) & 0x3F) as usize;
out.push(TABLE[idx] as char);
}
out
}
fn base64url_decode(input: &str) -> Option<Vec<u8>> {
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut buf = Vec::with_capacity(input.len() * 3 / 4);
let mut acc: u32 = 0;
let mut bits = 0u32;
for b in input.bytes() {
let v = TABLE.iter().position(|t| *t == b)? as u32;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
buf.push(((acc >> bits) & 0xFF) as u8);
}
}
Some(buf)
}
pub fn sign_card_jws(card: &AgentCard, secret: &[u8]) -> Result<String, A2AError> {
let value = canonical_json(
&serde_json::to_value(card)
.map_err(|e| A2AError::Parse(format!("Failed to serialize agent card: {e}")))?,
);
let payload = serde_json::to_vec(&value)
.map_err(|e| A2AError::Parse(format!("Failed to canonicalize agent card: {e}")))?;
let header = br#"{"alg":"HS256","typ":"JWS"}"#;
let signing_input = format!(
"{}.{}",
base64url_encode(header),
base64url_encode(&payload)
);
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
.map_err(|_| A2AError::Signature("invalid JWS secret length".to_string()))?;
mac.update(signing_input.as_bytes());
let sig = base64url_encode(&mac.finalize().into_bytes());
Ok(format!("{signing_input}.{sig}"))
}
pub fn verify_card_jws(card: &AgentCard, jws: &str, secret: &[u8]) -> Result<(), A2AError> {
let value = canonical_json(
&serde_json::to_value(card)
.map_err(|e| A2AError::Parse(format!("Failed to serialize agent card: {e}")))?,
);
let payload = serde_json::to_vec(&value)
.map_err(|e| A2AError::Parse(format!("Failed to canonicalize agent card: {e}")))?;
let header = br#"{"alg":"HS256","typ":"JWS"}"#;
let signing_input = format!(
"{}.{}",
base64url_encode(header),
base64url_encode(&payload)
);
let mut parts = jws.split('.');
let h = parts
.next()
.ok_or_else(|| A2AError::Signature("malformed JWS: no header".into()))?;
let p = parts
.next()
.ok_or_else(|| A2AError::Signature("malformed JWS: no payload".into()))?;
let s = parts
.next()
.ok_or_else(|| A2AError::Signature("malformed JWS: no signature".into()))?;
if parts.next().is_some() {
return Err(A2AError::Signature("malformed JWS: extra segments".into()));
}
let header_json: serde_json::Value = serde_json::from_slice(
&base64url_decode(h)
.ok_or_else(|| A2AError::Signature("JWS header not base64url".into()))?,
)
.map_err(|e| A2AError::Signature(format!("JWS header not JSON: {e}")))?;
if header_json.get("alg").and_then(|a| a.as_str()) != Some("HS256") {
return Err(A2AError::Signature(
"JWS alg mismatch: expected HS256".into(),
));
}
let expected_payload = base64url_encode(&payload);
if p != expected_payload {
return Err(A2AError::Signature(
"JWS payload does not match the card".into(),
));
}
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
.map_err(|_| A2AError::Signature("invalid JWS secret length".to_string()))?;
mac.update(signing_input.as_bytes());
let expected_sig = base64url_encode(&mac.finalize().into_bytes());
if constant_time_eq(s, &expected_sig) {
Ok(())
} else {
Err(A2AError::Signature(
"JWS signature verification failed".into(),
))
}
}
#[cfg(test)]
mod jws_tests {
use super::*;
use crate::protocol::{A2ATransport, AgentInterface, A2A_VERSION_V101};
fn card() -> AgentCard {
AgentCard::new("agent-a", "test agent", "https://a.example").with_supported_interface(
AgentInterface::new(
A2A_VERSION_V101,
A2ATransport::HttpJson,
"https://a.example/a2a",
),
)
}
#[test]
fn jws_roundtrip() {
let card = card();
let jws = sign_card_jws(&card, b"secret-1").unwrap();
assert_eq!(jws.split('.').count(), 3);
verify_card_jws(&card, &jws, b"secret-1").unwrap();
}
#[test]
fn jws_detects_card_tamper() {
let jws = sign_card_jws(&card(), b"secret-1").unwrap();
let tampered = AgentCard::new("agent-a", "HACKED", "https://a.example");
assert!(verify_card_jws(&tampered, &jws, b"secret-1").is_err());
}
#[test]
fn jws_detects_signature_tamper() {
let card = card();
let jws = sign_card_jws(&card, b"secret-1").unwrap();
let mut bad = jws.clone();
bad.pop();
bad.push('A');
assert!(verify_card_jws(&card, &bad, b"secret-1").is_err());
}
#[test]
fn jws_rejects_wrong_secret_and_alg() {
let card = card();
let jws = sign_card_jws(&card, b"secret-1").unwrap();
assert!(verify_card_jws(&card, &jws, b"secret-2").is_err());
let parts: Vec<&str> = jws.split('.').collect();
let forged_header = base64url_encode(br#"{"alg":"none","typ":"JWS"}"#);
let forged = format!("{}.{}.{}", forged_header, parts[1], parts[2]);
assert!(verify_card_jws(&card, &forged, b"secret-1").is_err());
}
#[test]
fn canonical_json_sorts_keys() {
let v: serde_json::Value = serde_json::json!({ "b": 1, "a": { "y": 2, "x": 3 } });
let c = canonical_json(&v);
let s = serde_json::to_string(&c).unwrap();
assert_eq!(s, r#"{"a":{"x":3,"y":2},"b":1}"#);
}
#[test]
fn v101_negotiation() {
let card = card();
let iface = card
.negotiate(A2ATransport::HttpJson, &[A2A_VERSION_V101])
.unwrap();
assert_eq!(iface.protocol_version, A2A_VERSION_V101);
assert!(card
.negotiate(A2ATransport::Grpc, &[A2A_VERSION_V101])
.is_err());
assert!(card.negotiate(A2ATransport::HttpJson, &["0.3.0"]).is_err());
}
#[test]
fn interface_tenant() {
let iface =
AgentInterface::new("1.0.1", A2ATransport::HttpJson, "https://a").with_tenant("acme");
assert_eq!(iface.tenant.as_deref(), Some("acme"));
let json = serde_json::to_string(&iface).unwrap();
assert!(json.contains("\"tenant\":\"acme\""), "{json}");
}
}