use ed25519_dalek::Signer;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::identity::{AgentIdentity, AgentKeyPair};
use crate::CryptoError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedMessage {
pub payload: serde_json::Value,
pub signer_did: String,
pub nonce: String,
pub timestamp: String,
pub signature: String,
}
impl SignedMessage {
pub fn sign(keypair: &AgentKeyPair, payload: serde_json::Value) -> Result<Self, CryptoError> {
let identity = keypair.identity();
let nonce = uuid::Uuid::new_v4().to_string();
let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let canonical = canonical_bytes(&payload, &identity.did, &nonce, ×tamp)?;
let signature = keypair.signing_key().sign(&canonical);
let signature_hex = hex::encode(signature.to_bytes());
Ok(Self {
payload,
signer_did: identity.did,
nonce,
timestamp,
signature: signature_hex,
})
}
pub fn verify(&self, identity: &AgentIdentity) -> Result<(), CryptoError> {
if self.signer_did != identity.did {
return Err(CryptoError::SignatureInvalid);
}
let verifying_key = identity.verifying_key()?;
let canonical = canonical_bytes(
&self.payload,
&self.signer_did,
&self.nonce,
&self.timestamp,
)?;
let sig_bytes = hex::decode(&self.signature)
.map_err(|e| CryptoError::InvalidSignatureEncoding(e.to_string()))?;
let sig_array: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| CryptoError::InvalidSignatureEncoding("expected 64 bytes".into()))?;
let signature = ed25519_dalek::Signature::from_bytes(&sig_array);
ed25519_dalek::Verifier::verify(&verifying_key, &canonical, &signature)
.map_err(|_| CryptoError::SignatureInvalid)
}
pub fn content_hash(&self) -> String {
let mut map = std::collections::BTreeMap::new();
map.insert("nonce", serde_json::json!(&self.nonce));
map.insert("payload", self.payload.clone());
map.insert("signature", serde_json::json!(&self.signature));
map.insert("signer_did", serde_json::json!(&self.signer_did));
map.insert("timestamp", serde_json::json!(&self.timestamp));
let serialized = serde_json::to_string(&map).unwrap_or_default();
let hash = Sha256::digest(serialized.as_bytes());
hex::encode(hash)
}
}
pub(crate) fn canonical_bytes(
payload: &serde_json::Value,
signer_did: &str,
nonce: &str,
timestamp: &str,
) -> Result<Vec<u8>, CryptoError> {
let mut map = std::collections::BTreeMap::new();
map.insert("nonce", serde_json::json!(nonce));
map.insert("payload", payload.clone());
map.insert("signer_did", serde_json::json!(signer_did));
map.insert("timestamp", serde_json::json!(timestamp));
serde_json::to_vec(&map).map_err(|e| CryptoError::SerializationError(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_keypair() -> AgentKeyPair {
AgentKeyPair::generate()
}
#[test]
fn test_sign_and_verify() {
let kp = test_keypair();
let payload = serde_json::json!({"action": "merge", "entity_id": "abc123"});
let signed = SignedMessage::sign(&kp, payload).unwrap();
let identity = kp.identity();
assert!(signed.verify(&identity).is_ok());
}
#[test]
fn test_tampered_payload_fails() {
let kp = test_keypair();
let payload = serde_json::json!({"action": "merge"});
let mut signed = SignedMessage::sign(&kp, payload).unwrap();
signed.payload = serde_json::json!({"action": "split"});
let identity = kp.identity();
assert!(signed.verify(&identity).is_err());
}
#[test]
fn test_tampered_nonce_fails() {
let kp = test_keypair();
let payload = serde_json::json!({"data": "test"});
let mut signed = SignedMessage::sign(&kp, payload).unwrap();
signed.nonce = "tampered-nonce".to_string();
let identity = kp.identity();
assert!(signed.verify(&identity).is_err());
}
#[test]
fn test_wrong_identity_fails() {
let kp1 = test_keypair();
let kp2 = test_keypair();
let payload = serde_json::json!({"data": "test"});
let signed = SignedMessage::sign(&kp1, payload).unwrap();
let wrong_identity = kp2.identity();
assert!(signed.verify(&wrong_identity).is_err());
}
#[test]
fn test_nonce_is_unique() {
let kp = test_keypair();
let payload = serde_json::json!({"data": "test"});
let s1 = SignedMessage::sign(&kp, payload.clone()).unwrap();
let s2 = SignedMessage::sign(&kp, payload).unwrap();
assert_ne!(s1.nonce, s2.nonce);
}
#[test]
fn test_signature_is_hex_128_chars() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
assert_eq!(signed.signature.len(), 128); assert!(signed.signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_signer_did_populated() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
assert!(signed.signer_did.starts_with("did:agent:"));
assert_eq!(signed.signer_did, kp.identity().did);
}
#[test]
fn test_timestamp_is_rfc3339() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
assert!(signed.timestamp.ends_with('Z'));
chrono::DateTime::parse_from_rfc3339(&signed.timestamp).unwrap();
}
#[test]
fn test_content_hash_deterministic() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({"x": 1})).unwrap();
let h1 = signed.content_hash();
let h2 = signed.content_hash();
assert_eq!(h1, h2);
}
#[test]
fn test_serialization_roundtrip() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({"key": "value"})).unwrap();
let json = serde_json::to_string(&signed).unwrap();
let deserialized: SignedMessage = serde_json::from_str(&json).unwrap();
assert_eq!(signed.signer_did, deserialized.signer_did);
assert_eq!(signed.nonce, deserialized.nonce);
assert_eq!(signed.signature, deserialized.signature);
let identity = kp.identity();
assert!(deserialized.verify(&identity).is_ok());
}
#[test]
fn test_invalid_signature_hex() {
let kp = test_keypair();
let mut signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
signed.signature = "not-hex!".to_string();
let identity = kp.identity();
assert!(matches!(
signed.verify(&identity),
Err(CryptoError::InvalidSignatureEncoding(_))
));
}
#[test]
fn test_canonical_ordering_deterministic() {
let payload = serde_json::json!({"z": 1, "a": 2, "m": 3});
let b1 = canonical_bytes(&payload, "did:agent:test", "nonce1", "ts1").unwrap();
let b2 = canonical_bytes(&payload, "did:agent:test", "nonce1", "ts1").unwrap();
assert_eq!(b1, b2);
}
#[test]
fn test_tampered_timestamp_fails() {
let kp = test_keypair();
let mut signed = SignedMessage::sign(&kp, serde_json::json!({"x": 1})).unwrap();
signed.timestamp = "2020-01-01T00:00:00.000Z".to_string();
let identity = kp.identity();
assert!(signed.verify(&identity).is_err());
}
#[test]
fn test_tampered_signer_did_fails() {
let kp = test_keypair();
let mut signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
signed.signer_did = "did:agent:0000000000000000000000000000fake".to_string();
let identity = kp.identity();
assert!(matches!(
signed.verify(&identity),
Err(CryptoError::SignatureInvalid)
));
}
#[test]
fn test_empty_payload_sign_verify() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
assert!(signed.verify(&kp.identity()).is_ok());
}
#[test]
fn test_large_payload_sign_verify() {
let kp = test_keypair();
let large_array: Vec<i32> = (0..1000).collect();
let payload = serde_json::json!({
"records": large_array,
"nested": {"deep": {"value": "test"}}
});
let signed = SignedMessage::sign(&kp, payload).unwrap();
assert!(signed.verify(&kp.identity()).is_ok());
}
#[test]
fn test_signature_wrong_length_fails() {
let kp = test_keypair();
let mut signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
signed.signature = "aa".repeat(32);
let identity = kp.identity();
assert!(matches!(
signed.verify(&identity),
Err(CryptoError::InvalidSignatureEncoding(_))
));
}
#[test]
fn test_different_messages_different_content_hashes() {
let kp = test_keypair();
let s1 = SignedMessage::sign(&kp, serde_json::json!({"a": 1})).unwrap();
let s2 = SignedMessage::sign(&kp, serde_json::json!({"a": 2})).unwrap();
assert_ne!(s1.content_hash(), s2.content_hash());
}
#[test]
fn test_content_hash_is_sha256_hex() {
let kp = test_keypair();
let signed = SignedMessage::sign(&kp, serde_json::json!({})).unwrap();
let hash = signed.content_hash();
assert_eq!(hash.len(), 64); assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_canonical_bytes_different_payloads_differ() {
let b1 = canonical_bytes(
&serde_json::json!({"x": 1}),
"did:agent:test",
"nonce1",
"ts1",
)
.unwrap();
let b2 = canonical_bytes(
&serde_json::json!({"x": 2}),
"did:agent:test",
"nonce1",
"ts1",
)
.unwrap();
assert_ne!(b1, b2);
}
#[test]
fn test_canonical_bytes_different_nonces_differ() {
let payload = serde_json::json!({"x": 1});
let b1 = canonical_bytes(&payload, "did:agent:test", "nonce-a", "ts1").unwrap();
let b2 = canonical_bytes(&payload, "did:agent:test", "nonce-b", "ts1").unwrap();
assert_ne!(b1, b2);
}
#[test]
fn test_crypto_error_display() {
let err = CryptoError::InvalidKeyLength(16);
assert!(err.to_string().contains("16"));
assert!(err.to_string().contains("32"));
let err = CryptoError::SignatureInvalid;
assert!(err.to_string().contains("verification failed"));
}
}