use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use sha2::{Digest, Sha256};
use crate::canonicalize::canonicalize;
use crate::envelope::SignatureEnvelope;
use crate::error::Error;
use crate::signers::Signer;
pub fn sign_envelope<S: Signer + ?Sized>(envelope: &SignatureEnvelope, signer: &S) -> Result<String, Error> {
let envelope_value = envelope.to_value();
let canonical_json = canonicalize(&envelope_value)?;
let message_hash = compute_sha256(canonical_json.as_bytes());
let signature_bytes = signer.sign(&message_hash)?;
Ok(BASE64.encode(signature_bytes))
}
#[must_use]
pub fn compute_nonce_hash(agent_id: &str, nonce: &str) -> String {
let data = format!("{agent_id}:{nonce}");
let hash = compute_sha256(data.as_bytes());
hex::encode(hash)
}
pub fn get_canonical_json(envelope: &SignatureEnvelope) -> Result<String, Error> {
canonicalize(&envelope.to_value())
}
pub fn get_message_hash(envelope: &SignatureEnvelope) -> Result<[u8; 32], Error> {
let canonical_json = canonicalize(&envelope.to_value())?;
Ok(compute_sha256(canonical_json.as_bytes()))
}
fn compute_sha256(data: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().into()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signers::Ed25519Signer;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
#[test]
fn test_sign_envelope() {
let (signer, _) = Ed25519Signer::generate();
let envelope = SignatureEnvelope::new(
"agent-123".to_string(),
"star".to_string(),
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.expect("valid timestamp")
.with_timezone(&Utc),
"nonce-456".to_string(),
HashMap::new(),
);
let signature = sign_envelope(&envelope, &signer).expect("signing should succeed");
assert!(BASE64.decode(&signature).is_ok());
let decoded = BASE64.decode(&signature).expect("valid base64");
assert_eq!(decoded.len(), 64);
}
#[test]
fn test_sign_envelope_is_deterministic() {
let (signer, _) = Ed25519Signer::generate();
let envelope = SignatureEnvelope::new(
"agent-123".to_string(),
"star".to_string(),
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.expect("valid timestamp")
.with_timezone(&Utc),
"nonce-456".to_string(),
HashMap::new(),
);
let sig1 = sign_envelope(&envelope, &signer).expect("signing should succeed");
let sig2 = sign_envelope(&envelope, &signer).expect("signing should succeed");
assert_eq!(sig1, sig2, "Signatures should be deterministic");
}
#[test]
fn test_compute_nonce_hash() {
let hash = compute_nonce_hash("agent-123", "nonce-456");
assert_eq!(hash.len(), 64); assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_nonce_hash_is_deterministic() {
let hash1 = compute_nonce_hash("agent-123", "nonce-456");
let hash2 = compute_nonce_hash("agent-123", "nonce-456");
assert_eq!(hash1, hash2);
}
#[test]
fn test_nonce_hash_changes_with_input() {
let hash1 = compute_nonce_hash("agent-123", "nonce-456");
let hash2 = compute_nonce_hash("agent-123", "nonce-789");
let hash3 = compute_nonce_hash("agent-456", "nonce-456");
assert_ne!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_get_canonical_json() {
let envelope = SignatureEnvelope::new(
"agent-123".to_string(),
"star".to_string(),
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.expect("valid timestamp")
.with_timezone(&Utc),
"nonce-456".to_string(),
HashMap::new(),
);
let canonical = get_canonical_json(&envelope).expect("canonicalization should succeed");
let parsed: serde_json::Value =
serde_json::from_str(&canonical).expect("should be valid JSON");
assert_eq!(parsed["agentId"], "agent-123");
assert_eq!(parsed["action"], "star");
}
#[test]
fn test_get_message_hash() {
let envelope = SignatureEnvelope::new(
"agent-123".to_string(),
"star".to_string(),
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.expect("valid timestamp")
.with_timezone(&Utc),
"nonce-456".to_string(),
HashMap::new(),
);
let hash = get_message_hash(&envelope).expect("hashing should succeed");
assert_eq!(hash.len(), 32);
}
}