1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::error::Result;
10use crate::hashing::{canonical_json, sha256_hex};
11use crate::receipt::EmbeddedSignature as Signature;
12use crate::receipt::SignatureAlgorithm;
13use crate::signer::{verify_hex, SignerBackend, VerifyError};
14
15pub const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct AuditEvent {
20 pub version: u32,
21 pub seq: u64,
22 pub id: String,
23 pub ts: DateTime<Utc>,
24 #[serde(rename = "type")]
25 pub event_type: String,
26 pub actor: String,
27 pub subject_id: String,
28 pub payload_hash: String,
29 pub metadata: serde_json::Map<String, serde_json::Value>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub policy_version: Option<u32>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub policy_hash: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub attestation_ref: Option<String>,
36 pub prev_event_hash: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct SignedAuditEvent {
42 pub event: AuditEvent,
43 pub event_hash: String,
44 pub signature: Signature,
45}
46
47#[derive(Debug, thiserror::Error)]
48pub enum ChainError {
49 #[error("seq out of order at index {index}: expected {expected}, got {got}")]
50 SeqOutOfOrder {
51 index: usize,
52 expected: u64,
53 got: u64,
54 },
55 #[error("prev_event_hash mismatch at seq {seq}")]
56 PrevHashMismatch { seq: u64 },
57 #[error("event_hash mismatch at seq {seq}")]
58 EventHashMismatch { seq: u64 },
59 #[error("signature verification failed at seq {seq}")]
60 SignatureFailed { seq: u64 },
61 #[error(transparent)]
62 Core(#[from] crate::error::CoreError),
63}
64
65impl AuditEvent {
66 pub fn canonical_hash(&self) -> Result<String> {
67 let v = serde_json::to_value(self)?;
68 let bytes = canonical_json(&v)?;
69 Ok(sha256_hex(&bytes))
70 }
71}
72
73impl SignedAuditEvent {
74 pub fn sign<S: SignerBackend + ?Sized>(event: AuditEvent, signer: &S) -> Result<Self> {
81 let v = serde_json::to_value(&event)?;
82 let bytes = canonical_json(&v)?;
83 let event_hash = sha256_hex(&bytes);
84 let sig_hex = signer.sign_hex(&bytes);
85 Ok(Self {
86 event,
87 event_hash,
88 signature: Signature {
89 algorithm: SignatureAlgorithm::Ed25519,
90 key_id: signer.current_key_id().to_string(),
91 signature_hex: sig_hex,
92 },
93 })
94 }
95
96 pub fn verify_signature(
97 &self,
98 verifying_key_hex: &str,
99 ) -> std::result::Result<(), VerifyError> {
100 let v = serde_json::to_value(&self.event).map_err(|_| VerifyError::Invalid)?;
101 let bytes = canonical_json(&v).map_err(|_| VerifyError::Invalid)?;
102 verify_hex(verifying_key_hex, &bytes, &self.signature.signature_hex)
103 }
104}
105
106pub fn verify_chain(
116 events: &[SignedAuditEvent],
117 verify_hashes: bool,
118 verifying_key_hex: Option<&str>,
119) -> std::result::Result<(), ChainError> {
120 let mut prev_hash = ZERO_HASH.to_string();
121 for (i, signed) in events.iter().enumerate() {
122 let expected_seq = (i as u64) + 1;
123 if signed.event.seq != expected_seq {
124 return Err(ChainError::SeqOutOfOrder {
125 index: i,
126 expected: expected_seq,
127 got: signed.event.seq,
128 });
129 }
130 if signed.event.prev_event_hash != prev_hash {
131 return Err(ChainError::PrevHashMismatch {
132 seq: signed.event.seq,
133 });
134 }
135 if verify_hashes {
136 let computed = signed.event.canonical_hash()?;
137 if computed != signed.event_hash {
138 return Err(ChainError::EventHashMismatch {
139 seq: signed.event.seq,
140 });
141 }
142 }
143 if let Some(pk) = verifying_key_hex {
144 if signed.verify_signature(pk).is_err() {
145 return Err(ChainError::SignatureFailed {
146 seq: signed.event.seq,
147 });
148 }
149 }
150 prev_hash = signed.event_hash.clone();
151 }
152 Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::signer::DevSigner;
159 use serde_json::json;
160
161 fn ev(seq: u64, prev: &str, ts: &str) -> AuditEvent {
162 let suffix = match seq {
164 1 => "01HTAWX5K3R8YV9NQB7C6P2DGQ",
165 2 => "01HTAWX5K3R8YV9NQB7C6P2DGR",
166 3 => "01HTAWX5K3R8YV9NQB7C6P2DGS",
167 _ => "01HTAWX5K3R8YV9NQB7C6P2DGZ",
168 };
169 AuditEvent {
170 version: 1,
171 seq,
172 id: format!("evt-{suffix}"),
173 ts: chrono::DateTime::parse_from_rfc3339(ts).unwrap().into(),
174 event_type: "runtime_started".to_string(),
175 actor: "sbo3l-server".to_string(),
176 subject_id: "runtime".to_string(),
177 payload_hash: ZERO_HASH.to_string(),
178 metadata: json!({"mode":"dev"}).as_object().unwrap().clone(),
179 policy_version: None,
180 policy_hash: None,
181 attestation_ref: None,
182 prev_event_hash: prev.to_string(),
183 }
184 }
185
186 #[test]
187 fn sign_and_verify_envelope() {
188 let signer = DevSigner::from_seed("audit-signer-v1", [13u8; 32]);
189 let signed = SignedAuditEvent::sign(ev(1, ZERO_HASH, "2026-04-27T12:00:00Z"), &signer)
190 .expect("sign");
191 signed
192 .verify_signature(&signer.verifying_key_hex())
193 .unwrap();
194 }
195
196 #[test]
197 fn signed_event_validates_against_schema() {
198 let signer = DevSigner::from_seed("audit-signer-v1", [13u8; 32]);
199 let signed = SignedAuditEvent::sign(ev(1, ZERO_HASH, "2026-04-27T12:00:00Z"), &signer)
200 .expect("sign");
201 let v = serde_json::to_value(&signed).unwrap();
202 crate::schema::validate_audit_event(&v).unwrap();
203 }
204
205 #[test]
206 fn chain_verify_round_trip() {
207 let signer = DevSigner::from_seed("audit-signer-v1", [13u8; 32]);
208 let e1 = SignedAuditEvent::sign(ev(1, ZERO_HASH, "2026-04-27T12:00:00Z"), &signer).unwrap();
209 let e2 =
210 SignedAuditEvent::sign(ev(2, &e1.event_hash, "2026-04-27T12:00:01Z"), &signer).unwrap();
211 let e3 =
212 SignedAuditEvent::sign(ev(3, &e2.event_hash, "2026-04-27T12:00:02Z"), &signer).unwrap();
213 verify_chain(&[e1, e2, e3], true, Some(&signer.verifying_key_hex())).unwrap();
214 }
215
216 #[test]
217 fn chain_verify_detects_tamper_in_middle() {
218 let signer = DevSigner::from_seed("audit-signer-v1", [13u8; 32]);
219 let e1 = SignedAuditEvent::sign(ev(1, ZERO_HASH, "2026-04-27T12:00:00Z"), &signer).unwrap();
220 let mut e2 =
221 SignedAuditEvent::sign(ev(2, &e1.event_hash, "2026-04-27T12:00:01Z"), &signer).unwrap();
222 e2.event.actor = "attacker".to_string();
224 let err = verify_chain(&[e1.clone(), e2.clone()], true, None).unwrap_err();
225 assert!(matches!(err, ChainError::EventHashMismatch { .. }));
226 }
227}