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