phantom_protocol/validation.rs
1//! Standalone input-validation helpers (DoS / malformed-input guards).
2//!
3//! [`InputValidator`] is a stateless bag of bounds checks — message size, a
4//! 16-byte group-id well-formedness check, and an epoch-drift window. They are
5//! generic guards, NOT bound to the live `PacketHeader` codec: the helpers here
6//! are not invoked on the production receive path (the wire `epoch` is a `u8`
7//! rekey selector validated inside `transport::session`, unrelated to the `u64`
8//! application-level epoch checked here). The module is kept as a reusable
9//! validation surface for embedders and for any caller that needs to bound
10//! untrusted input before handing it to the core.
11
12use anyhow::{bail, Result};
13
14pub struct InputValidator;
15
16impl InputValidator {
17 /// Validate message size to prevent DoS (OOM)
18 pub fn validate_message_size(data: &[u8], max_size: usize) -> Result<()> {
19 if data.is_empty() {
20 bail!("Empty message");
21 }
22 if data.len() > max_size {
23 bail!(
24 "Message too large: {} bytes (max: {})",
25 data.len(),
26 max_size
27 );
28 }
29 Ok(())
30 }
31
32 /// Validate Group ID length (must be 16 bytes)
33 pub fn validate_group_id(group_id: &[u8]) -> Result<[u8; 16]> {
34 if group_id.len() != 16 {
35 bail!(
36 "Invalid group_id length: expected 16, got {}",
37 group_id.len()
38 );
39 }
40
41 // Check for zero-ID (reserved)
42 if group_id.iter().all(|&b| b == 0) {
43 bail!("Zero group_id not allowed");
44 }
45
46 let mut gid = [0u8; 16];
47 gid.copy_from_slice(group_id);
48 Ok(gid)
49 }
50
51 /// Validate Epoch to prevent processing very old or future frames
52 pub fn validate_epoch(epoch: u64, current_epoch: u64) -> Result<()> {
53 const MAX_EPOCH_DRIFT: u64 = 50; // Allow some drift for commit implementation
54
55 // Future check
56 if epoch > current_epoch + MAX_EPOCH_DRIFT {
57 bail!(
58 "Epoch too far in future: {} (current: {})",
59 epoch,
60 current_epoch
61 );
62 }
63
64 // Past check (strictness depends on application logic, usually we reject old epochs)
65 // Here we allow some history for reordering
66 if current_epoch > MAX_EPOCH_DRIFT && epoch < current_epoch - MAX_EPOCH_DRIFT {
67 bail!("Epoch too old: {} (current: {})", epoch, current_epoch);
68 }
69
70 Ok(())
71 }
72}