1use crate::CryptoError;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum SignatureStatus {
13 Verified,
15 Unverified,
17 Unsigned,
19 Invalid,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum SignerRole {
27 Author,
29 Evaluator,
31 Orchestrator,
33 Operator,
35 Witness,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct EnvelopeSignature {
42 pub algorithm: String,
44 pub public_key: String,
46 pub signature: String,
48 pub role: SignerRole,
50 pub signer_id: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct AuditEnvelope<T: Serialize> {
78 payload: T,
79 subject: String,
80 timestamp: u64,
81 agent_id: String,
82 #[serde(default)]
83 signatures: Vec<EnvelopeSignature>,
84 status: SignatureStatus,
85
86 #[serde(default, skip_serializing_if = "String::is_empty")]
88 algorithm: String,
89 #[serde(default, skip_serializing_if = "String::is_empty")]
90 public_key: String,
91 #[serde(default, skip_serializing_if = "String::is_empty")]
92 signature: String,
93}
94
95impl<T: Serialize> AuditEnvelope<T> {
96 pub fn unsigned(payload: T, subject: &str, agent_id: &str) -> Self {
98 Self {
99 payload,
100 subject: subject.to_string(),
101 timestamp: now_secs(),
102 agent_id: agent_id.to_string(),
103 signatures: Vec::new(),
104 status: SignatureStatus::Unsigned,
105 algorithm: String::new(),
106 public_key: String::new(),
107 signature: String::new(),
108 }
109 }
110
111 pub async fn signed(
113 payload: T,
114 subject: &str,
115 agent_id: &str,
116 signer: &dyn crate::AuditSigner,
117 ) -> Result<Self, CryptoError> {
118 let timestamp = now_secs();
119 let payload_json =
120 serde_json::to_vec(&payload).map_err(|e| CryptoError::Serialization(e.to_string()))?;
121 let canonical = crate::canonical_bytes(subject, &payload_json, timestamp, agent_id);
122
123 let sig_bytes = signer.sign(&canonical).await?;
124 let sig_b64 = b64_encode(&sig_bytes);
125
126 let envelope_sig = EnvelopeSignature {
127 algorithm: signer.algorithm().to_string(),
128 public_key: signer.public_key_display(),
129 signature: sig_b64.clone(),
130 role: SignerRole::Author,
131 signer_id: agent_id.to_string(),
132 };
133
134 Ok(Self {
135 payload,
136 subject: subject.to_string(),
137 timestamp,
138 agent_id: agent_id.to_string(),
139 signatures: vec![envelope_sig],
140 status: SignatureStatus::Unverified,
141 algorithm: signer.algorithm().to_string(),
143 public_key: signer.public_key_display(),
144 signature: sig_b64,
145 })
146 }
147
148 pub async fn co_sign(
151 &mut self,
152 signer: &dyn crate::AuditSigner,
153 role: SignerRole,
154 signer_id: &str,
155 ) -> Result<(), CryptoError> {
156 let payload_json = serde_json::to_vec(&self.payload)
157 .map_err(|e| CryptoError::Serialization(e.to_string()))?;
158
159 let chained = canonical_bytes_chained(
160 &self.subject,
161 &payload_json,
162 self.timestamp,
163 &self.agent_id,
164 &self.signatures,
165 );
166
167 let sig_bytes = signer.sign(&chained).await?;
168
169 self.signatures.push(EnvelopeSignature {
170 algorithm: signer.algorithm().to_string(),
171 public_key: signer.public_key_display(),
172 signature: b64_encode(&sig_bytes),
173 role,
174 signer_id: signer_id.to_string(),
175 });
176
177 self.status = SignatureStatus::Unverified;
179 Ok(())
180 }
181
182 pub fn verify_chain(
187 &mut self,
188 registry: &crate::VerifierRegistry,
189 ) -> Result<bool, CryptoError> {
190 if self.signatures.is_empty() && !self.signature.is_empty() {
192 self.signatures.push(EnvelopeSignature {
193 algorithm: self.algorithm.clone(),
194 public_key: self.public_key.clone(),
195 signature: self.signature.clone(),
196 role: SignerRole::Author,
197 signer_id: self.agent_id.clone(),
198 });
199 }
200
201 if self.signatures.is_empty() {
202 self.status = SignatureStatus::Unsigned;
203 return Ok(true);
204 }
205
206 let payload_json = serde_json::to_vec(&self.payload)
207 .map_err(|e| CryptoError::Serialization(e.to_string()))?;
208
209 for (i, sig) in self.signatures.iter().enumerate() {
210 let canonical = canonical_bytes_chained(
212 &self.subject,
213 &payload_json,
214 self.timestamp,
215 &self.agent_id,
216 &self.signatures[..i], );
218
219 let sig_bytes = b64_decode(&sig.signature).map_err(|e| {
220 CryptoError::VerificationFailed(format!(
221 "Invalid base64 in signature {i} ({}): {e}",
222 sig.signer_id
223 ))
224 })?;
225
226 let pubkey_bytes =
227 hex::decode(sig.public_key.trim_start_matches("0x")).map_err(|e| {
228 CryptoError::InvalidKey(format!("Invalid hex public key in signature {i}: {e}"))
229 })?;
230
231 if !registry.verify(&sig.algorithm, &canonical, &sig_bytes, &pubkey_bytes)? {
232 self.status = SignatureStatus::Invalid;
233 return Ok(false);
234 }
235 }
236
237 self.status = SignatureStatus::Verified;
238 Ok(true)
239 }
240
241 pub fn verify(&mut self, registry: &crate::VerifierRegistry) -> Result<bool, CryptoError> {
244 self.verify_chain(registry)
245 }
246
247 pub fn signature_count(&self) -> usize {
249 self.signatures.len()
250 }
251
252 pub fn has_role(&self, role: &SignerRole) -> bool {
254 self.signatures.iter().any(|s| &s.role == role)
255 }
256
257 pub fn payload(&self) -> &T {
259 &self.payload
260 }
261
262 pub fn subject(&self) -> &str {
264 &self.subject
265 }
266
267 pub fn agent_id(&self) -> &str {
269 &self.agent_id
270 }
271
272 pub fn timestamp(&self) -> u64 {
274 self.timestamp
275 }
276
277 pub fn status(&self) -> &SignatureStatus {
281 &self.status
282 }
283
284 pub fn signatures(&self) -> &[EnvelopeSignature] {
286 &self.signatures
287 }
288
289 pub fn invalidate(&mut self) {
292 if self.signatures.is_empty() {
293 self.status = SignatureStatus::Unsigned;
294 } else {
295 self.status = SignatureStatus::Unverified;
296 }
297 }
298
299 #[doc(hidden)]
302 pub fn signatures_mut(&mut self) -> &mut Vec<EnvelopeSignature> {
303 self.status = SignatureStatus::Unverified;
304 &mut self.signatures
305 }
306
307 #[doc(hidden)]
310 pub fn payload_mut(&mut self) -> &mut T {
311 self.status = SignatureStatus::Unverified;
312 &mut self.payload
313 }
314}
315
316fn canonical_bytes_chained(
328 subject: &str,
329 payload_json: &[u8],
330 timestamp: u64,
331 agent_id: &str,
332 prior_signatures: &[EnvelopeSignature],
333) -> Vec<u8> {
334 let mut buf = crate::canonical_bytes(subject, payload_json, timestamp, agent_id);
336
337 for sig in prior_signatures {
339 buf.push(0x00); buf.extend_from_slice(sig.algorithm.as_bytes());
341 buf.push(0x00);
342 buf.extend_from_slice(sig.public_key.as_bytes());
343 buf.push(0x00);
344 buf.extend_from_slice(sig.signature.as_bytes());
345 buf.push(0x00);
346 buf.extend_from_slice(format!("{:?}", sig.role).as_bytes());
348 buf.push(0x00);
349 buf.extend_from_slice(sig.signer_id.as_bytes());
350 }
351
352 buf
353}
354
355fn now_secs() -> u64 {
360 std::time::SystemTime::now()
361 .duration_since(std::time::UNIX_EPOCH)
362 .unwrap_or_default()
363 .as_secs()
364}
365
366fn b64_encode(data: &[u8]) -> String {
367 use base64::Engine;
368 base64::engine::general_purpose::STANDARD.encode(data)
369}
370
371fn b64_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
372 use base64::Engine;
373 base64::engine::general_purpose::STANDARD.decode(s)
374}
375
376#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::signer::{Ed25519Signer, Secp256k1Signer};
384 use crate::verifier::VerifierRegistry;
385
386 #[tokio::test]
387 async fn envelope_ed25519_sign_verify() {
388 let signer = Ed25519Signer::generate();
389 let mut envelope = AuditEnvelope::signed(
390 serde_json::json!({"content": "hello"}),
391 "proposal",
392 "agent-1",
393 &signer,
394 )
395 .await
396 .unwrap();
397
398 assert_eq!((*envelope.status()), SignatureStatus::Unverified);
399 assert_eq!(envelope.signature_count(), 1);
400
401 let registry = VerifierRegistry::with_defaults();
402 assert!(envelope.verify(®istry).unwrap());
403 assert_eq!((*envelope.status()), SignatureStatus::Verified);
404 }
405
406 #[tokio::test]
407 async fn envelope_secp256k1_sign_verify() {
408 let signer = Secp256k1Signer::generate();
409 let mut envelope =
410 AuditEnvelope::signed("evaluation result", "evaluation", "agent-2", &signer)
411 .await
412 .unwrap();
413
414 let registry = VerifierRegistry::with_defaults();
415 assert!(envelope.verify(®istry).unwrap());
416 }
417
418 #[tokio::test]
419 async fn envelope_tampered_payload_fails() {
420 let signer = Ed25519Signer::generate();
421 let mut envelope = AuditEnvelope::signed(
422 serde_json::json!({"score": 8.5}),
423 "evaluation",
424 "agent-1",
425 &signer,
426 )
427 .await
428 .unwrap();
429
430 *envelope.payload_mut() = serde_json::json!({"score": 10.0});
432
433 let registry = VerifierRegistry::with_defaults();
434 assert!(!envelope.verify(®istry).unwrap());
435 assert_eq!((*envelope.status()), SignatureStatus::Invalid);
436 }
437
438 #[test]
439 fn unsigned_envelope_verifies_trivially() {
440 let mut envelope = AuditEnvelope::<String>::unsigned(
441 "unsigned content".to_string(),
442 "proposal",
443 "agent-dev",
444 );
445
446 let registry = VerifierRegistry::with_defaults();
447 assert!(envelope.verify(®istry).unwrap());
448 assert_eq!((*envelope.status()), SignatureStatus::Unsigned);
449 }
450
451 #[tokio::test]
454 async fn multi_sig_chain_verify() {
455 let author = Ed25519Signer::generate();
456 let evaluator = Secp256k1Signer::generate();
457 let orchestrator = Ed25519Signer::generate();
458
459 let mut envelope = AuditEnvelope::signed(
460 serde_json::json!({"content": "proposal text"}),
461 "proposal",
462 "agent-author",
463 &author,
464 )
465 .await
466 .unwrap();
467
468 envelope
470 .co_sign(&evaluator, SignerRole::Evaluator, "agent-evaluator")
471 .await
472 .unwrap();
473
474 envelope
476 .co_sign(&orchestrator, SignerRole::Orchestrator, "orchestrator-1")
477 .await
478 .unwrap();
479
480 assert_eq!(envelope.signature_count(), 3);
481 assert!(envelope.has_role(&SignerRole::Author));
482 assert!(envelope.has_role(&SignerRole::Evaluator));
483 assert!(envelope.has_role(&SignerRole::Orchestrator));
484
485 let registry = VerifierRegistry::with_defaults();
486 assert!(envelope.verify_chain(®istry).unwrap());
487 assert_eq!((*envelope.status()), SignatureStatus::Verified);
488 }
489
490 #[tokio::test]
491 async fn chain_detects_removed_signature() {
492 let author = Ed25519Signer::generate();
493 let evaluator = Ed25519Signer::generate();
494 let orchestrator = Ed25519Signer::generate();
495
496 let mut envelope = AuditEnvelope::signed(
497 serde_json::json!({"content": "test"}),
498 "proposal",
499 "agent-1",
500 &author,
501 )
502 .await
503 .unwrap();
504
505 envelope
506 .co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
507 .await
508 .unwrap();
509 envelope
510 .co_sign(&orchestrator, SignerRole::Orchestrator, "orch-1")
511 .await
512 .unwrap();
513
514 envelope.signatures_mut().remove(1);
516
517 let registry = VerifierRegistry::with_defaults();
519 assert!(!envelope.verify_chain(®istry).unwrap());
520 assert_eq!((*envelope.status()), SignatureStatus::Invalid);
521 }
522
523 #[tokio::test]
524 async fn chain_detects_reordered_signatures() {
525 let author = Ed25519Signer::generate();
526 let evaluator = Ed25519Signer::generate();
527
528 let mut envelope = AuditEnvelope::signed(
529 serde_json::json!({"content": "test"}),
530 "proposal",
531 "agent-1",
532 &author,
533 )
534 .await
535 .unwrap();
536
537 envelope
538 .co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
539 .await
540 .unwrap();
541
542 envelope.signatures_mut().swap(0, 1);
544
545 let registry = VerifierRegistry::with_defaults();
546 assert!(!envelope.verify_chain(®istry).unwrap());
548 }
549
550 #[tokio::test]
551 async fn chain_detects_tampered_payload_with_multi_sig() {
552 let author = Ed25519Signer::generate();
553 let evaluator = Ed25519Signer::generate();
554
555 let mut envelope = AuditEnvelope::signed(
556 serde_json::json!({"score": 8.0}),
557 "evaluation",
558 "agent-1",
559 &author,
560 )
561 .await
562 .unwrap();
563
564 envelope
565 .co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
566 .await
567 .unwrap();
568
569 *envelope.payload_mut() = serde_json::json!({"score": 10.0});
571
572 let registry = VerifierRegistry::with_defaults();
573 assert!(!envelope.verify_chain(®istry).unwrap());
574 }
575
576 #[tokio::test]
577 async fn legacy_single_sig_migrates_to_chain() {
578 let signer = Ed25519Signer::generate();
579 let mut envelope = AuditEnvelope::signed(
580 serde_json::json!({"content": "legacy"}),
581 "proposal",
582 "agent-1",
583 &signer,
584 )
585 .await
586 .unwrap();
587
588 let legacy_sig = envelope.signatures()[0].signature.clone();
590 envelope.signatures_mut().clear();
591 assert_eq!(envelope.signature_count(), 0);
594 assert!(!envelope.signature.is_empty());
595
596 let registry = VerifierRegistry::with_defaults();
598 assert!(envelope.verify_chain(®istry).unwrap());
599 assert_eq!(envelope.signature_count(), 1); assert_eq!(envelope.signatures()[0].signature, legacy_sig);
601 }
602
603 #[tokio::test]
604 async fn has_role_queries() {
605 let signer = Ed25519Signer::generate();
606 let mut envelope = AuditEnvelope::signed(serde_json::json!({}), "test", "agent-1", &signer)
607 .await
608 .unwrap();
609
610 assert!(envelope.has_role(&SignerRole::Author));
611 assert!(!envelope.has_role(&SignerRole::Evaluator));
612 assert!(!envelope.has_role(&SignerRole::Operator));
613
614 let eval_signer = Ed25519Signer::generate();
615 envelope
616 .co_sign(&eval_signer, SignerRole::Operator, "human-1")
617 .await
618 .unwrap();
619
620 assert!(envelope.has_role(&SignerRole::Operator));
621 }
622
623 #[tokio::test]
624 async fn chain_detects_tampered_role() {
625 let author = Ed25519Signer::generate();
626 let evaluator = Ed25519Signer::generate();
627 let orchestrator = Ed25519Signer::generate();
628
629 let mut envelope = AuditEnvelope::signed(
630 serde_json::json!({"content": "test"}),
631 "proposal",
632 "agent-1",
633 &author,
634 )
635 .await
636 .unwrap();
637
638 envelope
639 .co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
640 .await
641 .unwrap();
642 envelope
643 .co_sign(&orchestrator, SignerRole::Orchestrator, "orch-1")
644 .await
645 .unwrap();
646
647 envelope.signatures_mut()[1].role = SignerRole::Operator;
649
650 let registry = VerifierRegistry::with_defaults();
652 assert!(!envelope.verify_chain(®istry).unwrap());
653 }
654
655 #[tokio::test]
656 async fn chain_detects_tampered_signer_id() {
657 let author = Ed25519Signer::generate();
658 let evaluator = Ed25519Signer::generate();
659 let orchestrator = Ed25519Signer::generate();
660
661 let mut envelope = AuditEnvelope::signed(
662 serde_json::json!({"content": "test"}),
663 "proposal",
664 "agent-1",
665 &author,
666 )
667 .await
668 .unwrap();
669
670 envelope
671 .co_sign(&evaluator, SignerRole::Evaluator, "agent-2")
672 .await
673 .unwrap();
674 envelope
675 .co_sign(&orchestrator, SignerRole::Orchestrator, "orch-1")
676 .await
677 .unwrap();
678
679 envelope.signatures_mut()[1].signer_id = "impersonator".to_string();
681
682 let registry = VerifierRegistry::with_defaults();
684 assert!(!envelope.verify_chain(®istry).unwrap());
685 }
686}