use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AMessage {
pub from: String,
pub to: String,
pub nonce: String,
pub timestamp_ms: u64,
pub signature: String,
pub content_type: String,
#[serde(with = "serde_bytes_as_base64")]
pub payload: Vec<u8>,
}
mod serde_bytes_as_base64 {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let b64 = encode_base64(bytes);
b64.serialize(s)
}
pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(d)?;
decode_base64(&s).map_err(serde::de::Error::custom)
}
fn encode_base64(input: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let cap = input.len().div_ceil(3) * 4;
let mut out = String::with_capacity(cap);
for chunk in input.chunks(3) {
let b0 = chunk[0] as usize;
let b1 = if chunk.len() > 1 {
chunk[1] as usize
} else {
0
};
let b2 = if chunk.len() > 2 {
chunk[2] as usize
} else {
0
};
out.push(CHARS[b0 >> 2] as char);
out.push(CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
if chunk.len() > 1 {
out.push(CHARS[((b1 & 0xF) << 2) | (b2 >> 6)] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(CHARS[b2 & 0x3F] as char);
} else {
out.push('=');
}
}
out
}
fn decode_base64(input: &str) -> Result<Vec<u8>, String> {
let input = input.trim_end_matches('=');
let mut out = Vec::with_capacity(input.len() * 3 / 4);
let mut buf = 0u32;
let mut bits = 0u8;
for ch in input.bytes() {
let v = match ch {
b'A'..=b'Z' => ch - b'A',
b'a'..=b'z' => ch - b'a' + 26,
b'0'..=b'9' => ch - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => return Err(format!("invalid base64 char: {ch}")),
} as u32;
buf = (buf << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
Ok(out)
}
}
impl A2AMessage {
pub fn canonical_bytes(&self) -> Vec<u8> {
let payload_hash = {
let mut h = Sha256::new();
h.update(&self.payload);
hex::encode(h.finalize())
};
format!(
"{}\n{}\n{}\n{}\n{}\n{}",
self.from, self.to, self.nonce, self.timestamp_ms, self.content_type, payload_hash
)
.into_bytes()
}
pub fn sign(&mut self, sk: &[u8; 32]) -> Result<(), String> {
let payload = self.canonical_bytes();
let signing_key = SigningKey::from_bytes(sk);
let sig: Signature = signing_key.sign(&payload);
self.signature = hex::encode(sig.to_bytes());
Ok(())
}
pub fn verify(&self, pk: &[u8; 32]) -> bool {
let Ok(verifying_key) = VerifyingKey::from_bytes(pk) else {
return false;
};
let Ok(sig_bytes) = hex::decode(&self.signature) else {
return false;
};
let Ok(sig_arr): Result<[u8; 64], _> = sig_bytes.try_into() else {
return false;
};
let signature = Signature::from_bytes(&sig_arr);
let payload = self.canonical_bytes();
verifying_key.verify(&payload, &signature).is_ok()
}
pub fn build(
from: impl Into<String>,
to: impl Into<String>,
nonce: impl Into<String>,
timestamp_ms: u64,
content_type: impl Into<String>,
payload: Vec<u8>,
) -> Self {
A2AMessage {
from: from.into(),
to: to.into(),
nonce: nonce.into(),
timestamp_ms,
signature: String::new(),
content_type: content_type.into(),
payload,
}
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn a2a_build_and_sign(
from_id: &str,
to_id: &str,
nonce_hex: &str,
timestamp_ms: f64,
content_type: &str,
payload_b64: &str,
sk_hex: &str,
) -> String {
let Ok(sk_bytes) = hex::decode(sk_hex) else {
return r#"{"error":"invalid sk_hex"}"#.to_string();
};
let Ok(sk_arr): Result<[u8; 32], _> = sk_bytes.try_into() else {
return r#"{"error":"sk must be 32 bytes"}"#.to_string();
};
let payload = match decode_b64_simple(payload_b64) {
Ok(b) => b,
Err(e) => return format!(r#"{{"error":"invalid payload_b64: {e}"}}"#),
};
let mut msg = A2AMessage::build(
from_id,
to_id,
nonce_hex,
timestamp_ms as u64,
content_type,
payload,
);
if let Err(e) = msg.sign(&sk_arr) {
return format!(r#"{{"error":"sign failed: {e}"}}"#);
}
serde_json::to_string(&msg)
.unwrap_or_else(|e| format!(r#"{{"error":"serialize failed: {e}"}}"#))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn a2a_verify(msg_json: &str, pk_hex: &str) -> bool {
let Ok(msg): Result<A2AMessage, _> = serde_json::from_str(msg_json) else {
return false;
};
let Ok(pk_bytes) = hex::decode(pk_hex) else {
return false;
};
let Ok(pk_arr): Result<[u8; 32], _> = pk_bytes.try_into() else {
return false;
};
msg.verify(&pk_arr)
}
#[cfg(feature = "wasm")]
fn decode_b64_simple(input: &str) -> Result<Vec<u8>, String> {
let input = input.trim_end_matches('=');
let mut out = Vec::with_capacity(input.len() * 3 / 4);
let mut buf = 0u32;
let mut bits = 0u8;
for ch in input.bytes() {
let v = match ch {
b'A'..=b'Z' => ch - b'A',
b'a'..=b'z' => ch - b'a' + 26,
b'0'..=b'9' => ch - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => return Err(format!("invalid base64 char: {ch}")),
} as u32;
buf = (buf << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::keygen;
#[test]
fn test_a2a_build_sign_verify_roundtrip() {
let (sk, pk) = keygen();
let mut msg = A2AMessage::build(
"agent-alice",
"agent-bob",
"aabbccdd00112233aabbccdd00112233",
1_700_000_000_000,
"application/json",
br#"{"action":"ping"}"#.to_vec(),
);
msg.sign(&sk).expect("sign must succeed");
assert!(!msg.signature.is_empty(), "signature must be set");
assert!(msg.verify(&pk), "valid signature must verify");
}
#[test]
fn test_a2a_tampered_payload_fails() {
let (sk, pk) = keygen();
let mut msg = A2AMessage::build(
"agent-alice",
"agent-bob",
"aabbccdd00112233aabbccdd00112233",
1_700_000_000_000,
"application/json",
b"original payload".to_vec(),
);
msg.sign(&sk).expect("sign must succeed");
msg.payload = b"tampered payload".to_vec();
assert!(!msg.verify(&pk), "tampered payload must not verify");
}
#[test]
fn test_a2a_wrong_key_fails() {
let (sk_a, _pk_a) = keygen();
let (_sk_b, pk_b) = keygen();
let mut msg = A2AMessage::build(
"agent-alice",
"agent-bob",
"deadbeef00112233deadbeef00112233",
1_700_000_000_001,
"text/plain",
b"hello".to_vec(),
);
msg.sign(&sk_a).expect("sign with key A");
assert!(
!msg.verify(&pk_b),
"signature from key-A must not verify under key-B"
);
}
#[test]
fn test_a2a_canonical_bytes_deterministic() {
let msg = A2AMessage {
from: "alice".to_string(),
to: "bob".to_string(),
nonce: "nonce123".to_string(),
timestamp_ms: 1_700_000_000,
signature: "".to_string(),
content_type: "application/json".to_string(),
payload: b"{}".to_vec(),
};
let b1 = msg.canonical_bytes();
let b2 = msg.canonical_bytes();
assert_eq!(b1, b2, "canonical_bytes must be deterministic");
}
#[test]
fn test_a2a_canonical_bytes_format() {
let msg = A2AMessage {
from: "alice".to_string(),
to: "bob".to_string(),
nonce: "nonce123".to_string(),
timestamp_ms: 1_700_000_000,
signature: "".to_string(),
content_type: "text/plain".to_string(),
payload: b"hello".to_vec(),
};
let bytes = msg.canonical_bytes();
let s = String::from_utf8(bytes).unwrap();
let parts: Vec<&str> = s.splitn(6, '\n').collect();
assert_eq!(parts[0], "alice");
assert_eq!(parts[1], "bob");
assert_eq!(parts[2], "nonce123");
assert_eq!(parts[3], "1700000000");
assert_eq!(parts[4], "text/plain");
assert_eq!(
parts[5],
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
}
#[test]
fn test_a2a_serde_roundtrip() {
let (sk, pk) = keygen();
let mut msg = A2AMessage::build(
"agent-1",
"agent-2",
"0011223344556677",
42_000,
"application/json",
br#"{"key":"value"}"#.to_vec(),
);
msg.sign(&sk).expect("sign must succeed");
let json = serde_json::to_string(&msg).expect("serialize");
let parsed: A2AMessage = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed.from, "agent-1");
assert_eq!(parsed.to, "agent-2");
assert!(parsed.verify(&pk), "deserialized message must verify");
}
}