edgesentry_rs/ingest/
verify.rs1use 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}