use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
use sha2::{Digest, Sha256};
use aa_proto::assembly::ipc::v1::HandshakeProof;
use aa_security::sdk_identity::VerifiedSdkIdentity;
pub const NONCE_LEN: usize = 32;
pub fn expected_verifying_key(agent_id: &str) -> VerifyingKey {
let seed: [u8; 32] = Sha256::digest(agent_id.as_bytes()).into();
SigningKey::from_bytes(&seed).verifying_key()
}
pub fn generate_nonce() -> [u8; NONCE_LEN] {
rand::random::<[u8; NONCE_LEN]>()
}
fn signed_payload(nonce: &[u8], sdk_version: &str) -> Vec<u8> {
let mut payload = Vec::with_capacity(nonce.len() + sdk_version.len());
payload.extend_from_slice(nonce);
payload.extend_from_slice(sdk_version.as_bytes());
payload
}
pub fn verify_proof(nonce: &[u8], proof: &HandshakeProof, expected: &VerifyingKey) -> Option<VerifiedSdkIdentity> {
let expected_hex = hex::encode(expected.to_bytes());
if proof.public_key != expected_hex {
return None;
}
let sig_bytes: [u8; 64] = match proof.signature.as_slice().try_into() {
Ok(b) => b,
Err(_) => return None,
};
let signature = Signature::from_bytes(&sig_bytes);
let payload = signed_payload(nonce, &proof.sdk_version);
if expected.verify_strict(&payload, &signature).is_err() {
return None;
}
Some(if proof.sdk_version.is_empty() {
VerifiedSdkIdentity::none()
} else {
VerifiedSdkIdentity::with_version(proof.sdk_version.clone())
})
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::Signer;
fn signing_key(agent_id: &str) -> SigningKey {
let seed: [u8; 32] = Sha256::digest(agent_id.as_bytes()).into();
SigningKey::from_bytes(&seed)
}
fn valid_proof(agent_id: &str, nonce: &[u8]) -> HandshakeProof {
valid_proof_with_version(agent_id, nonce, "")
}
fn valid_proof_with_version(agent_id: &str, nonce: &[u8], sdk_version: &str) -> HandshakeProof {
let sk = signing_key(agent_id);
let sig = sk.sign(&signed_payload(nonce, sdk_version));
HandshakeProof {
agent_did: format!("did:key:{agent_id}"),
public_key: hex::encode(sk.verifying_key().to_bytes()),
signature: sig.to_bytes().to_vec(),
sdk_version: sdk_version.to_string(),
}
}
#[test]
fn derivation_matches_sdk_keypair_for_same_agent_id() {
let vk = expected_verifying_key("agent-x");
let sk = signing_key("agent-x");
assert_eq!(vk.to_bytes(), sk.verifying_key().to_bytes());
}
#[test]
fn nonce_is_32_bytes_and_varies() {
let a = generate_nonce();
let b = generate_nonce();
assert_eq!(a.len(), NONCE_LEN);
assert_ne!(a, b, "two nonces must differ (random)");
}
#[test]
fn valid_proof_verifies() {
let nonce = generate_nonce();
let proof = valid_proof("agent-x", &nonce);
let verified = verify_proof(&nonce, &proof, &expected_verifying_key("agent-x"));
assert_eq!(verified, Some(VerifiedSdkIdentity::none()));
}
#[test]
fn valid_proof_with_version_carries_the_verified_version() {
let nonce = generate_nonce();
let proof = valid_proof_with_version("agent-x", &nonce, "1.4.0");
let verified = verify_proof(&nonce, &proof, &expected_verifying_key("agent-x"));
assert_eq!(verified, Some(VerifiedSdkIdentity::with_version("1.4.0")));
}
#[test]
fn tampered_version_is_rejected() {
let nonce = generate_nonce();
let mut proof = valid_proof_with_version("agent-x", &nonce, "0.1.0");
proof.sdk_version = "9.9.9".to_string(); assert!(verify_proof(&nonce, &proof, &expected_verifying_key("agent-x")).is_none());
}
#[test]
fn forged_signature_is_rejected() {
let nonce = generate_nonce();
let mut proof = valid_proof("agent-x", &nonce);
proof.signature[0] ^= 0xFF;
assert!(verify_proof(&nonce, &proof, &expected_verifying_key("agent-x")).is_none());
}
#[test]
fn signature_over_a_different_nonce_is_rejected() {
let nonce = generate_nonce();
let other = generate_nonce();
let proof = valid_proof("agent-x", &other);
assert!(verify_proof(&nonce, &proof, &expected_verifying_key("agent-x")).is_none());
}
#[test]
fn proof_from_a_different_agent_key_is_rejected() {
let nonce = generate_nonce();
let proof = valid_proof("attacker-agent", &nonce);
assert!(verify_proof(&nonce, &proof, &expected_verifying_key("agent-x")).is_none());
}
#[test]
fn wrong_length_signature_is_rejected() {
let nonce = generate_nonce();
let mut proof = valid_proof("agent-x", &nonce);
proof.signature.truncate(10);
assert!(verify_proof(&nonce, &proof, &expected_verifying_key("agent-x")).is_none());
}
}