use crate::CryptoError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignatureStatus {
Verified,
Unverified,
Unsigned,
Invalid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignerRole {
Author,
Evaluator,
Orchestrator,
Operator,
Witness,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvelopeSignature {
pub algorithm: String,
pub public_key: String,
pub signature: String,
pub role: SignerRole,
pub signer_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEnvelope<T: Serialize> {
payload: T,
subject: String,
timestamp: u64,
agent_id: String,
#[serde(default)]
signatures: Vec<EnvelopeSignature>,
status: SignatureStatus,
#[serde(default, skip_serializing_if = "String::is_empty")]
algorithm: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
public_key: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
signature: String,
}
impl<T: Serialize> AuditEnvelope<T> {
pub fn unsigned(payload: T, subject: &str, agent_id: &str) -> Self {
Self {
payload,
subject: subject.to_string(),
timestamp: now_secs(),
agent_id: agent_id.to_string(),
signatures: Vec::new(),
status: SignatureStatus::Unsigned,
algorithm: String::new(),
public_key: String::new(),
signature: String::new(),
}
}
pub async fn signed(
payload: T,
subject: &str,
agent_id: &str,
signer: &dyn crate::AuditSigner,
) -> Result<Self, CryptoError> {
let timestamp = now_secs();
let payload_json =
serde_json::to_vec(&payload).map_err(|e| CryptoError::Serialization(e.to_string()))?;
let canonical = crate::canonical_bytes(subject, &payload_json, timestamp, agent_id);
let sig_bytes = signer.sign(&canonical).await?;
let sig_b64 = b64_encode(&sig_bytes);
let envelope_sig = EnvelopeSignature {
algorithm: signer.algorithm().to_string(),
public_key: signer.public_key_display(),
signature: sig_b64.clone(),
role: SignerRole::Author,
signer_id: agent_id.to_string(),
};
Ok(Self {
payload,
subject: subject.to_string(),
timestamp,
agent_id: agent_id.to_string(),
signatures: vec![envelope_sig],
status: SignatureStatus::Unverified,
algorithm: signer.algorithm().to_string(),
public_key: signer.public_key_display(),
signature: sig_b64,
})
}
pub async fn co_sign(
&mut self,
signer: &dyn crate::AuditSigner,
role: SignerRole,
signer_id: &str,
) -> Result<(), CryptoError> {
let payload_json = serde_json::to_vec(&self.payload)
.map_err(|e| CryptoError::Serialization(e.to_string()))?;
let chained = canonical_bytes_chained(
&self.subject,
&payload_json,
self.timestamp,
&self.agent_id,
&self.signatures,
);
let sig_bytes = signer.sign(&chained).await?;
self.signatures.push(EnvelopeSignature {
algorithm: signer.algorithm().to_string(),
public_key: signer.public_key_display(),
signature: b64_encode(&sig_bytes),
role,
signer_id: signer_id.to_string(),
});
self.status = SignatureStatus::Unverified;
Ok(())
}
pub fn verify_chain(
&mut self,
registry: &crate::VerifierRegistry,
) -> Result<bool, CryptoError> {
if self.signatures.is_empty() && !self.signature.is_empty() {
self.signatures.push(EnvelopeSignature {
algorithm: self.algorithm.clone(),
public_key: self.public_key.clone(),
signature: self.signature.clone(),
role: SignerRole::Author,
signer_id: self.agent_id.clone(),
});
}
if self.signatures.is_empty() {
self.status = SignatureStatus::Unsigned;
return Ok(true);
}
let payload_json = serde_json::to_vec(&self.payload)
.map_err(|e| CryptoError::Serialization(e.to_string()))?;
for (i, sig) in self.signatures.iter().enumerate() {
let canonical = canonical_bytes_chained(
&self.subject,
&payload_json,
self.timestamp,
&self.agent_id,
&self.signatures[..i], );
let sig_bytes = b64_decode(&sig.signature).map_err(|e| {
CryptoError::VerificationFailed(format!(
"Invalid base64 in signature {i} ({}): {e}",
sig.signer_id
))
})?;
let pubkey_bytes =
hex::decode(sig.public_key.trim_start_matches("0x")).map_err(|e| {
CryptoError::InvalidKey(format!("Invalid hex public key in signature {i}: {e}"))
})?;
if !registry.verify(&sig.algorithm, &canonical, &sig_bytes, &pubkey_bytes)? {
self.status = SignatureStatus::Invalid;
return Ok(false);
}
}
self.status = SignatureStatus::Verified;
Ok(true)
}
pub fn verify(&mut self, registry: &crate::VerifierRegistry) -> Result<bool, CryptoError> {
self.verify_chain(registry)
}
pub fn signature_count(&self) -> usize {
self.signatures.len()
}
pub fn has_role(&self, role: &SignerRole) -> bool {
self.signatures.iter().any(|s| &s.role == role)
}
pub fn payload(&self) -> &T {
&self.payload
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn timestamp(&self) -> u64 {
self.timestamp
}
pub fn status(&self) -> &SignatureStatus {
&self.status
}
pub fn signatures(&self) -> &[EnvelopeSignature] {
&self.signatures
}
pub fn invalidate(&mut self) {
if self.signatures.is_empty() {
self.status = SignatureStatus::Unsigned;
} else {
self.status = SignatureStatus::Unverified;
}
}
#[doc(hidden)]
pub fn signatures_mut(&mut self) -> &mut Vec<EnvelopeSignature> {
self.status = SignatureStatus::Unverified;
&mut self.signatures
}
#[doc(hidden)]
pub fn payload_mut(&mut self) -> &mut T {
self.status = SignatureStatus::Unverified;
&mut self.payload
}
}
fn canonical_bytes_chained(
subject: &str,
payload_json: &[u8],
timestamp: u64,
agent_id: &str,
prior_signatures: &[EnvelopeSignature],
) -> Vec<u8> {
let mut buf = crate::canonical_bytes(subject, payload_json, timestamp, agent_id);
for sig in prior_signatures {
buf.push(0x00); buf.extend_from_slice(sig.algorithm.as_bytes());
buf.push(0x00);
buf.extend_from_slice(sig.public_key.as_bytes());
buf.push(0x00);
buf.extend_from_slice(sig.signature.as_bytes());
buf.push(0x00);
buf.extend_from_slice(format!("{:?}", sig.role).as_bytes());
buf.push(0x00);
buf.extend_from_slice(sig.signer_id.as_bytes());
}
buf
}
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn b64_encode(data: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(data)
}
fn b64_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
use base64::Engine;
base64::engine::general_purpose::STANDARD.decode(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signer::{Ed25519Signer, Secp256k1Signer};
use crate::verifier::VerifierRegistry;
#[tokio::test]
async fn envelope_ed25519_sign_verify() {
let signer = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "hello"}),
"proposal",
"agent-1",
&signer,
)
.await
.unwrap();
assert_eq!((*envelope.status()), SignatureStatus::Unverified);
assert_eq!(envelope.signature_count(), 1);
let registry = VerifierRegistry::with_defaults();
assert!(envelope.verify(®istry).unwrap());
assert_eq!((*envelope.status()), SignatureStatus::Verified);
}
#[tokio::test]
async fn envelope_secp256k1_sign_verify() {
let signer = Secp256k1Signer::generate();
let mut envelope =
AuditEnvelope::signed("evaluation result", "evaluation", "agent-2", &signer)
.await
.unwrap();
let registry = VerifierRegistry::with_defaults();
assert!(envelope.verify(®istry).unwrap());
}
#[tokio::test]
async fn envelope_tampered_payload_fails() {
let signer = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"score": 8.5}),
"evaluation",
"agent-1",
&signer,
)
.await
.unwrap();
*envelope.payload_mut() = serde_json::json!({"score": 10.0});
let registry = VerifierRegistry::with_defaults();
assert!(!envelope.verify(®istry).unwrap());
assert_eq!((*envelope.status()), SignatureStatus::Invalid);
}
#[test]
fn unsigned_envelope_verifies_trivially() {
let mut envelope = AuditEnvelope::<String>::unsigned(
"unsigned content".to_string(),
"proposal",
"agent-dev",
);
let registry = VerifierRegistry::with_defaults();
assert!(envelope.verify(®istry).unwrap());
assert_eq!((*envelope.status()), SignatureStatus::Unsigned);
}
#[tokio::test]
async fn multi_sig_chain_verify() {
let author = Ed25519Signer::generate();
let evaluator = Secp256k1Signer::generate();
let orchestrator = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "proposal text"}),
"proposal",
"agent-author",
&author,
)
.await
.unwrap();
envelope
.co_sign(&evaluator, SignerRole::Evaluator, "agent-evaluator")
.await
.unwrap();
envelope
.co_sign(&orchestrator, SignerRole::Orchestrator, "orchestrator-1")
.await
.unwrap();
assert_eq!(envelope.signature_count(), 3);
assert!(envelope.has_role(&SignerRole::Author));
assert!(envelope.has_role(&SignerRole::Evaluator));
assert!(envelope.has_role(&SignerRole::Orchestrator));
let registry = VerifierRegistry::with_defaults();
assert!(envelope.verify_chain(®istry).unwrap());
assert_eq!((*envelope.status()), SignatureStatus::Verified);
}
#[tokio::test]
async fn chain_detects_removed_signature() {
let author = Ed25519Signer::generate();
let evaluator = Ed25519Signer::generate();
let orchestrator = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "test"}),
"proposal",
"agent-1",
&author,
)
.await
.unwrap();
envelope
.co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
.await
.unwrap();
envelope
.co_sign(&orchestrator, SignerRole::Orchestrator, "orch-1")
.await
.unwrap();
envelope.signatures_mut().remove(1);
let registry = VerifierRegistry::with_defaults();
assert!(!envelope.verify_chain(®istry).unwrap());
assert_eq!((*envelope.status()), SignatureStatus::Invalid);
}
#[tokio::test]
async fn chain_detects_reordered_signatures() {
let author = Ed25519Signer::generate();
let evaluator = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "test"}),
"proposal",
"agent-1",
&author,
)
.await
.unwrap();
envelope
.co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
.await
.unwrap();
envelope.signatures_mut().swap(0, 1);
let registry = VerifierRegistry::with_defaults();
assert!(!envelope.verify_chain(®istry).unwrap());
}
#[tokio::test]
async fn chain_detects_tampered_payload_with_multi_sig() {
let author = Ed25519Signer::generate();
let evaluator = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"score": 8.0}),
"evaluation",
"agent-1",
&author,
)
.await
.unwrap();
envelope
.co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
.await
.unwrap();
*envelope.payload_mut() = serde_json::json!({"score": 10.0});
let registry = VerifierRegistry::with_defaults();
assert!(!envelope.verify_chain(®istry).unwrap());
}
#[tokio::test]
async fn legacy_single_sig_migrates_to_chain() {
let signer = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "legacy"}),
"proposal",
"agent-1",
&signer,
)
.await
.unwrap();
let legacy_sig = envelope.signatures()[0].signature.clone();
envelope.signatures_mut().clear();
assert_eq!(envelope.signature_count(), 0);
assert!(!envelope.signature.is_empty());
let registry = VerifierRegistry::with_defaults();
assert!(envelope.verify_chain(®istry).unwrap());
assert_eq!(envelope.signature_count(), 1); assert_eq!(envelope.signatures()[0].signature, legacy_sig);
}
#[tokio::test]
async fn has_role_queries() {
let signer = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(serde_json::json!({}), "test", "agent-1", &signer)
.await
.unwrap();
assert!(envelope.has_role(&SignerRole::Author));
assert!(!envelope.has_role(&SignerRole::Evaluator));
assert!(!envelope.has_role(&SignerRole::Operator));
let eval_signer = Ed25519Signer::generate();
envelope
.co_sign(&eval_signer, SignerRole::Operator, "human-1")
.await
.unwrap();
assert!(envelope.has_role(&SignerRole::Operator));
}
#[tokio::test]
async fn chain_detects_tampered_role() {
let author = Ed25519Signer::generate();
let evaluator = Ed25519Signer::generate();
let orchestrator = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "test"}),
"proposal",
"agent-1",
&author,
)
.await
.unwrap();
envelope
.co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
.await
.unwrap();
envelope
.co_sign(&orchestrator, SignerRole::Orchestrator, "orch-1")
.await
.unwrap();
envelope.signatures_mut()[1].role = SignerRole::Operator;
let registry = VerifierRegistry::with_defaults();
assert!(!envelope.verify_chain(®istry).unwrap());
}
#[tokio::test]
async fn chain_detects_tampered_signer_id() {
let author = Ed25519Signer::generate();
let evaluator = Ed25519Signer::generate();
let orchestrator = Ed25519Signer::generate();
let mut envelope = AuditEnvelope::signed(
serde_json::json!({"content": "test"}),
"proposal",
"agent-1",
&author,
)
.await
.unwrap();
envelope
.co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
.await
.unwrap();
envelope
.co_sign(&orchestrator, SignerRole::Orchestrator, "orch-1")
.await
.unwrap();
envelope.signatures_mut()[1].signer_id = "impersonator".to_string();
let registry = VerifierRegistry::with_defaults();
assert!(!envelope.verify_chain(®istry).unwrap());
}
}