immutable_trace/ingest/
verify.rs1use std::collections::{HashMap, HashSet};
2
3use ed25519_dalek::VerifyingKey;
4use thiserror::Error;
5
6use crate::crypto::verify_payload_signature;
7use crate::record::{AuditRecord, Hash32};
8
9#[derive(Debug, Error, PartialEq, Eq)]
10pub enum IngestError {
11 #[error("unknown device: {0}")]
12 UnknownDevice(String),
13 #[error("duplicate record for device={device_id} sequence={sequence}")]
14 Duplicate { device_id: String, sequence: u64 },
15 #[error("invalid sequence for device={device_id}: expected={expected} actual={actual}")]
16 InvalidSequence {
17 device_id: String,
18 expected: u64,
19 actual: u64,
20 },
21 #[error("invalid previous hash for device={0}")]
22 InvalidPrevHash(String),
23 #[error("invalid signature for device={0}")]
24 InvalidSignature(String),
25}
26
27#[derive(Default)]
28pub struct IngestState {
29 public_keys: HashMap<String, VerifyingKey>,
30 seen: HashSet<(String, u64)>,
31 last_sequence: HashMap<String, u64>,
32 last_hash: HashMap<String, Hash32>,
33}
34
35impl IngestState {
36 pub fn register_device(&mut self, device_id: impl Into<String>, key: VerifyingKey) {
37 self.public_keys.insert(device_id.into(), key);
38 }
39
40 pub fn verify_and_accept(&mut self, record: &AuditRecord) -> Result<(), IngestError> {
41 let device_id = &record.device_id;
42 let key = self
43 .public_keys
44 .get(device_id)
45 .ok_or_else(|| IngestError::UnknownDevice(device_id.clone()))?;
46
47 if !verify_payload_signature(key, &record.payload_hash, &record.signature) {
48 return Err(IngestError::InvalidSignature(device_id.clone()));
49 }
50
51 if self.seen.contains(&(device_id.clone(), record.sequence)) {
52 return Err(IngestError::Duplicate {
53 device_id: device_id.clone(),
54 sequence: record.sequence,
55 });
56 }
57
58 let expected_sequence = self
59 .last_sequence
60 .get(device_id)
61 .map_or(1, |prev| prev.saturating_add(1));
62 if record.sequence != expected_sequence {
63 return Err(IngestError::InvalidSequence {
64 device_id: device_id.clone(),
65 expected: expected_sequence,
66 actual: record.sequence,
67 });
68 }
69
70 let expected_prev_hash = self
71 .last_hash
72 .get(device_id)
73 .copied()
74 .unwrap_or_else(AuditRecord::zero_hash);
75
76 if record.prev_record_hash != expected_prev_hash {
77 return Err(IngestError::InvalidPrevHash(device_id.clone()));
78 }
79
80 self.seen.insert((device_id.clone(), record.sequence));
81 self.last_sequence.insert(device_id.clone(), record.sequence);
82 self.last_hash.insert(device_id.clone(), record.hash());
83
84 Ok(())
85 }
86}