Skip to main content

immutable_trace/ingest/
verify.rs

1use 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}