1use hmac::{Hmac, Mac};
25use serde::{Deserialize, Serialize};
26use sha2::Sha256;
27use std::fmt;
28use thiserror::Error;
29
30use crate::event::Event;
31
32#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct EventSignature(pub String);
35
36impl EventSignature {
37 pub fn genesis() -> Self {
39 Self(String::new())
40 }
41
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45
46 pub fn is_genesis(&self) -> bool {
47 self.0.is_empty()
48 }
49}
50
51impl fmt::Debug for EventSignature {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 let truncated: String = self.0.chars().take(12).collect();
55 write!(f, "EventSignature({truncated}…)")
56 }
57}
58
59#[derive(Debug, Clone, Error, PartialEq)]
60pub enum IntegrityError {
61 #[error("hmac key must be at least 32 bytes (got {0})")]
62 KeyTooShort(usize),
63
64 #[error("event {sequence} signature mismatch (aggregate_id: {aggregate_id})")]
65 BrokenAt { aggregate_id: String, sequence: i64 },
66
67 #[error("event {sequence} sequence out of order (expected {expected}, got {sequence}; aggregate_id: {aggregate_id})")]
68 OutOfOrder {
69 aggregate_id: String,
70 expected: i64,
71 sequence: i64,
72 },
73}
74
75#[derive(Debug, Clone, PartialEq)]
77pub enum IntegrityResult {
78 Valid,
79 Broken(IntegrityError),
80}
81
82pub trait IntegrityChain: Send + Sync {
84 fn sign_event(
86 &self,
87 prev_signature: &EventSignature,
88 event: &Event,
89 ) -> Result<EventSignature, IntegrityError>;
90
91 fn verify_chain(&self, events: &[(Event, EventSignature)]) -> IntegrityResult;
95}
96
97fn canonical_bytes(event: &Event) -> Vec<u8> {
101 let signable = (
105 event.event_id,
106 event.aggregate_type.as_str(),
107 event.aggregate_id.as_str(),
108 event.sequence,
109 event.event_type.as_str(),
110 &event.payload,
111 event.timestamp,
112 );
113 serde_json::to_vec(&signable).expect("serde always succeeds for tuple of primitive refs")
114}
115
116pub struct HmacSha256Chain {
118 key: Vec<u8>,
119}
120
121impl HmacSha256Chain {
122 pub fn new(key: impl Into<Vec<u8>>) -> Result<Self, IntegrityError> {
124 let key = key.into();
125 if key.len() < 32 {
126 return Err(IntegrityError::KeyTooShort(key.len()));
127 }
128 Ok(Self { key })
129 }
130
131 pub fn from_hex(hex_key: &str) -> Result<Self, IntegrityError> {
133 let bytes = decode_hex(hex_key)
134 .map_err(|e| IntegrityError::KeyTooShort(format!("invalid hex: {e}").len()))?;
135 Self::new(bytes)
136 }
137}
138
139impl IntegrityChain for HmacSha256Chain {
140 fn sign_event(
141 &self,
142 prev_signature: &EventSignature,
143 event: &Event,
144 ) -> Result<EventSignature, IntegrityError> {
145 type HmacSha256 = Hmac<Sha256>;
146 let mut mac = HmacSha256::new_from_slice(&self.key)
147 .map_err(|_| IntegrityError::KeyTooShort(self.key.len()))?;
148 mac.update(prev_signature.as_str().as_bytes());
149 mac.update(&canonical_bytes(event));
150 Ok(EventSignature(encode_hex(&mac.finalize().into_bytes())))
151 }
152
153 fn verify_chain(&self, events: &[(Event, EventSignature)]) -> IntegrityResult {
154 let mut prev = EventSignature::genesis();
155 let mut expected_sequence: Option<i64> = None;
156
157 for (event, claimed) in events {
158 if let Some(expected) = expected_sequence {
161 if event.sequence != expected {
162 return IntegrityResult::Broken(IntegrityError::OutOfOrder {
163 aggregate_id: event.aggregate_id.clone(),
164 expected,
165 sequence: event.sequence,
166 });
167 }
168 }
169
170 let computed = match self.sign_event(&prev, event) {
171 Ok(s) => s,
172 Err(e) => return IntegrityResult::Broken(e),
173 };
174 if &computed != claimed {
175 return IntegrityResult::Broken(IntegrityError::BrokenAt {
176 aggregate_id: event.aggregate_id.clone(),
177 sequence: event.sequence,
178 });
179 }
180 prev = claimed.clone();
181 expected_sequence = Some(event.sequence + 1);
182 }
183
184 IntegrityResult::Valid
185 }
186}
187
188fn encode_hex(bytes: &[u8]) -> String {
191 const HEX: &[u8; 16] = b"0123456789abcdef";
192 let mut s = String::with_capacity(bytes.len() * 2);
193 for &b in bytes {
194 s.push(HEX[(b >> 4) as usize] as char);
195 s.push(HEX[(b & 0x0F) as usize] as char);
196 }
197 s
198}
199
200fn decode_hex(input: &str) -> Result<Vec<u8>, &'static str> {
201 if !input.len().is_multiple_of(2) {
202 return Err("odd length");
203 }
204 let mut out = Vec::with_capacity(input.len() / 2);
205 let bytes = input.as_bytes();
206 for pair in bytes.chunks(2) {
207 let hi = nibble(pair[0])?;
208 let lo = nibble(pair[1])?;
209 out.push((hi << 4) | lo);
210 }
211 Ok(out)
212}
213
214fn nibble(b: u8) -> Result<u8, &'static str> {
215 match b {
216 b'0'..=b'9' => Ok(b - b'0'),
217 b'a'..=b'f' => Ok(b - b'a' + 10),
218 b'A'..=b'F' => Ok(b - b'A' + 10),
219 _ => Err("non-hex char"),
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::audit::AuditMetadata;
227 use serde_json::json;
228
229 fn key() -> Vec<u8> {
230 b"012345678901234567890123456789AB".to_vec()
232 }
233
234 fn event(seq: i64, ty: &str, payload: serde_json::Value) -> Event {
235 Event::new("User", "agg-1", seq, ty, payload).with_audit(AuditMetadata::test_default())
236 }
237
238 #[test]
239 fn test_key_too_short_rejected() {
240 assert!(matches!(
241 HmacSha256Chain::new(b"short".to_vec()),
242 Err(IntegrityError::KeyTooShort(5))
243 ));
244 }
245
246 #[test]
247 fn test_genesis_signature_is_empty() {
248 assert!(EventSignature::genesis().is_genesis());
249 assert_eq!(EventSignature::genesis().as_str(), "");
250 }
251
252 #[test]
253 fn test_signature_deterministic() {
254 let chain = HmacSha256Chain::new(key()).unwrap();
255 let e = event(1, "Created", json!({"x": 1}));
256 let s1 = chain.sign_event(&EventSignature::genesis(), &e).unwrap();
257 let s2 = chain.sign_event(&EventSignature::genesis(), &e).unwrap();
258 assert_eq!(s1, s2);
259 assert_eq!(s1.as_str().len(), 64);
261 }
262
263 #[test]
264 fn test_signature_changes_with_prev() {
265 let chain = HmacSha256Chain::new(key()).unwrap();
266 let e = event(2, "Updated", json!({}));
267 let s1 = chain.sign_event(&EventSignature::genesis(), &e).unwrap();
268 let s2 = chain
269 .sign_event(&EventSignature("deadbeef".into()), &e)
270 .unwrap();
271 assert_ne!(s1, s2);
272 }
273
274 #[test]
275 fn test_verify_chain_valid_for_well_formed_stream() {
276 let chain = HmacSha256Chain::new(key()).unwrap();
277 let e1 = event(1, "Created", json!({}));
278 let e2 = event(2, "Updated", json!({"name": "X"}));
279
280 let sig1 = chain.sign_event(&EventSignature::genesis(), &e1).unwrap();
281 let sig2 = chain.sign_event(&sig1, &e2).unwrap();
282
283 let result = chain.verify_chain(&[(e1, sig1), (e2, sig2)]);
284 assert_eq!(result, IntegrityResult::Valid);
285 }
286
287 #[test]
288 fn test_verify_chain_detects_payload_tamper() {
289 let chain = HmacSha256Chain::new(key()).unwrap();
290 let original = event(1, "Created", json!({"name": "Alice"}));
291 let sig = chain
292 .sign_event(&EventSignature::genesis(), &original)
293 .unwrap();
294
295 let tampered = event(1, "Created", json!({"name": "Bob"}));
297 let result = chain.verify_chain(&[(tampered, sig)]);
298 assert!(matches!(
299 result,
300 IntegrityResult::Broken(IntegrityError::BrokenAt { sequence: 1, .. })
301 ));
302 }
303
304 #[test]
305 fn test_verify_chain_detects_signature_swap() {
306 let chain = HmacSha256Chain::new(key()).unwrap();
307 let e1 = event(1, "Created", json!({}));
308 let e2 = event(2, "Updated", json!({}));
309 let s1 = chain.sign_event(&EventSignature::genesis(), &e1).unwrap();
310 let s2 = chain.sign_event(&s1, &e2).unwrap();
311
312 let result = chain.verify_chain(&[(e1, s2), (e2, s1)]);
314 assert!(matches!(result, IntegrityResult::Broken(_)));
315 }
316
317 #[test]
318 fn test_verify_chain_detects_out_of_order_sequence() {
319 let chain = HmacSha256Chain::new(key()).unwrap();
320 let e1 = event(1, "Created", json!({}));
321 let e3 = event(3, "Updated", json!({})); let s1 = chain.sign_event(&EventSignature::genesis(), &e1).unwrap();
323 let s3 = chain.sign_event(&s1, &e3).unwrap();
324
325 let result = chain.verify_chain(&[(e1, s1), (e3, s3)]);
326 assert!(matches!(
327 result,
328 IntegrityResult::Broken(IntegrityError::OutOfOrder {
329 expected: 2,
330 sequence: 3,
331 ..
332 })
333 ));
334 }
335
336 #[test]
337 fn test_known_test_vector() {
338 let chain = HmacSha256Chain::new(b"thirty-two-byte-known-test-key!!".to_vec()).unwrap();
341
342 let mut e = Event::new("Vector", "vec-1", 1, "VectorEvent", json!({"n": 1}));
347 e.event_id = uuid::Uuid::nil();
348 e.timestamp = 1700000000000; let sig = chain.sign_event(&EventSignature::genesis(), &e).unwrap();
350 assert_eq!(sig.as_str().len(), 64);
355 assert_eq!(
363 sig.as_str(),
364 "7f519ff1222f551b490282cd220dda12f707a3979300b05d6f89f7a564749a9f",
365 "if this test fails after a deliberate change to canonical_bytes, \
366 update the pinned hash above by running this test with --nocapture and \
367 copying the actual signature."
368 );
369 }
370
371 #[test]
372 fn test_hex_helpers_roundtrip() {
373 let bytes = vec![0x00, 0xff, 0xab, 0xcd];
374 let hex = encode_hex(&bytes);
375 assert_eq!(hex, "00ffabcd");
376 assert_eq!(decode_hex(&hex).unwrap(), bytes);
377 }
378
379 #[test]
380 fn test_decode_hex_rejects_odd_length() {
381 assert!(decode_hex("abc").is_err());
382 }
383
384 #[test]
385 fn test_decode_hex_rejects_non_hex() {
386 assert!(decode_hex("zz").is_err());
387 }
388}