Skip to main content

edgesentry_rs/ingest/
verify.rs

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