use super::quic_transport::{
FramedMessage, MessageType, QuicTransportConfig, QuicTransportError, QuicTransportHandle,
SecurityMode,
};
use super::tdm::{SyncBeacon, TdmCoordinator, TdmSchedule, TdmSlotCompleted};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::collections::VecDeque;
use std::fmt;
type HmacSha256 = Hmac<Sha256>;
const HMAC_TAG_SIZE: usize = 8;
#[inline(never)]
fn constant_time_tag_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
core::hint::black_box(diff) == 0
}
const NONCE_SIZE: usize = 4;
const REPLAY_WINDOW: u32 = 16;
pub const AUTHENTICATED_BEACON_SIZE: usize = 16 + NONCE_SIZE + HMAC_TAG_SIZE;
const DEFAULT_TEST_KEY: [u8; 16] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
];
#[derive(Debug, Clone, PartialEq)]
pub enum SecureTdmError {
BeaconAuthFailed,
BeaconReplay { nonce: u32, last_accepted: u32 },
BeaconTooShort { expected: usize, got: usize },
Transport(QuicTransportError),
ModeMismatch {
expected: SecurityMode,
got: SecurityMode,
},
NoMeshKey,
}
impl fmt::Display for SecureTdmError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecureTdmError::BeaconAuthFailed => write!(f, "Beacon HMAC verification failed"),
SecureTdmError::BeaconReplay {
nonce,
last_accepted,
} => {
write!(
f,
"Beacon replay: nonce {} <= last_accepted {} - REPLAY_WINDOW",
nonce, last_accepted
)
}
SecureTdmError::BeaconTooShort { expected, got } => {
write!(
f,
"Beacon too short: expected {} bytes, got {}",
expected, got
)
}
SecureTdmError::Transport(e) => write!(f, "Transport error: {}", e),
SecureTdmError::ModeMismatch { expected, got } => {
write!(
f,
"Security mode mismatch: expected {}, got {}",
expected, got
)
}
SecureTdmError::NoMeshKey => write!(f, "Mesh key not provisioned"),
}
}
}
impl std::error::Error for SecureTdmError {}
impl From<QuicTransportError> for SecureTdmError {
fn from(e: QuicTransportError) -> Self {
SecureTdmError::Transport(e)
}
}
#[derive(Debug, Clone)]
pub struct ReplayWindow {
last_accepted: u32,
window_size: u32,
seen: VecDeque<u32>,
}
impl ReplayWindow {
pub fn new(window_size: u32) -> Self {
Self {
last_accepted: 0,
window_size,
seen: VecDeque::with_capacity(window_size as usize),
}
}
pub fn check(&self, nonce: u32) -> bool {
if nonce == 0 && self.last_accepted == 0 && self.seen.is_empty() {
return true;
}
if self.last_accepted >= self.window_size
&& nonce < self.last_accepted.saturating_sub(self.window_size)
{
return false;
}
!self.seen.contains(&nonce)
}
pub fn accept(&mut self, nonce: u32) -> bool {
if !self.check(nonce) {
return false;
}
self.seen.push_back(nonce);
if self.seen.len() > self.window_size as usize {
self.seen.pop_front();
}
if nonce > self.last_accepted {
self.last_accepted = nonce;
}
true
}
pub fn last_accepted(&self) -> u32 {
self.last_accepted
}
pub fn window_count(&self) -> usize {
self.seen.len()
}
}
#[derive(Debug, Clone)]
pub struct AuthenticatedBeacon {
pub beacon: SyncBeacon,
pub nonce: u32,
pub hmac_tag: [u8; HMAC_TAG_SIZE],
}
impl AuthenticatedBeacon {
pub fn to_bytes(&self) -> [u8; AUTHENTICATED_BEACON_SIZE] {
let mut buf = [0u8; AUTHENTICATED_BEACON_SIZE];
let beacon_bytes = self.beacon.to_bytes();
buf[..16].copy_from_slice(&beacon_bytes);
buf[16..20].copy_from_slice(&self.nonce.to_le_bytes());
buf[20..28].copy_from_slice(&self.hmac_tag);
buf
}
pub fn from_bytes(buf: &[u8]) -> Result<Self, SecureTdmError> {
if buf.len() < AUTHENTICATED_BEACON_SIZE {
return Err(SecureTdmError::BeaconTooShort {
expected: AUTHENTICATED_BEACON_SIZE,
got: buf.len(),
});
}
let beacon = SyncBeacon::from_bytes(&buf[..16]).ok_or(SecureTdmError::BeaconTooShort {
expected: 16,
got: buf.len(),
})?;
let nonce = u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]);
let mut hmac_tag = [0u8; HMAC_TAG_SIZE];
hmac_tag.copy_from_slice(&buf[20..28]);
Ok(Self {
beacon,
nonce,
hmac_tag,
})
}
pub fn compute_tag(payload_and_nonce: &[u8], key: &[u8; 16]) -> [u8; HMAC_TAG_SIZE] {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC-SHA256 accepts any key length");
mac.update(payload_and_nonce);
let result = mac.finalize().into_bytes();
let mut tag = [0u8; HMAC_TAG_SIZE];
tag.copy_from_slice(&result[..HMAC_TAG_SIZE]);
tag
}
pub fn verify(&self, key: &[u8; 16]) -> Result<(), SecureTdmError> {
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&self.beacon.to_bytes());
msg[16..20].copy_from_slice(&self.nonce.to_le_bytes());
let expected = Self::compute_tag(&msg, key);
if constant_time_tag_eq(&self.hmac_tag, &expected) {
Ok(())
} else {
Err(SecureTdmError::BeaconAuthFailed)
}
}
}
#[derive(Debug, Clone)]
pub struct SecureTdmConfig {
pub security_mode: SecurityMode,
pub mesh_key: Option<[u8; 16]>,
pub quic_config: QuicTransportConfig,
pub sec_level: SecLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecLevel {
Permissive = 0,
Transitional = 1,
Enforcing = 2,
}
impl Default for SecureTdmConfig {
fn default() -> Self {
Self {
security_mode: SecurityMode::QuicTransport,
mesh_key: Some(DEFAULT_TEST_KEY),
quic_config: QuicTransportConfig::default(),
sec_level: SecLevel::Transitional,
}
}
}
#[derive(Debug)]
pub struct SecureTdmCoordinator {
inner: TdmCoordinator,
config: SecureTdmConfig,
nonce_counter: u32,
transport: Option<QuicTransportHandle>,
replay_window: ReplayWindow,
beacons_produced: u64,
beacons_verified: u64,
verification_failures: u64,
}
impl SecureTdmCoordinator {
pub fn new(schedule: TdmSchedule, config: SecureTdmConfig) -> Result<Self, SecureTdmError> {
let transport = if config.security_mode == SecurityMode::QuicTransport {
Some(QuicTransportHandle::new(config.quic_config.clone())?)
} else {
None
};
Ok(Self {
inner: TdmCoordinator::new(schedule),
config,
nonce_counter: 0,
transport,
replay_window: ReplayWindow::new(REPLAY_WINDOW),
beacons_produced: 0,
beacons_verified: 0,
verification_failures: 0,
})
}
pub fn begin_secure_cycle(&mut self) -> Result<SecureCycleOutput, SecureTdmError> {
let beacon = self.inner.begin_cycle();
self.beacons_produced += 1;
match self.config.security_mode {
SecurityMode::ManualCrypto => {
let key = self.config.mesh_key.ok_or(SecureTdmError::NoMeshKey)?;
self.nonce_counter = self.nonce_counter.wrapping_add(1);
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&self.nonce_counter.to_le_bytes());
let tag = AuthenticatedBeacon::compute_tag(&msg, &key);
let auth_beacon = AuthenticatedBeacon {
beacon: beacon.clone(),
nonce: self.nonce_counter,
hmac_tag: tag,
};
Ok(SecureCycleOutput {
beacon,
authenticated_bytes: auth_beacon.to_bytes().to_vec(),
mode: SecurityMode::ManualCrypto,
})
}
SecurityMode::QuicTransport => {
let beacon_bytes = beacon.to_bytes();
let framed = FramedMessage::new(MessageType::Beacon, beacon_bytes.to_vec());
let wire = framed.to_bytes();
if let Some(ref mut transport) = self.transport {
transport.record_beacon_sent(wire.len());
}
Ok(SecureCycleOutput {
beacon,
authenticated_bytes: wire,
mode: SecurityMode::QuicTransport,
})
}
}
}
pub fn verify_beacon(&mut self, buf: &[u8]) -> Result<SyncBeacon, SecureTdmError> {
match self.config.security_mode {
SecurityMode::ManualCrypto => {
if buf.len() >= AUTHENTICATED_BEACON_SIZE {
let auth = AuthenticatedBeacon::from_bytes(buf)?;
let key = self.config.mesh_key.ok_or(SecureTdmError::NoMeshKey)?;
match auth.verify(&key) {
Ok(()) => {
if !self.replay_window.accept(auth.nonce) {
self.verification_failures += 1;
return Err(SecureTdmError::BeaconReplay {
nonce: auth.nonce,
last_accepted: self.replay_window.last_accepted(),
});
}
self.beacons_verified += 1;
Ok(auth.beacon)
}
Err(e) => {
self.verification_failures += 1;
Err(e)
}
}
} else if buf.len() >= 16 && self.config.sec_level != SecLevel::Enforcing {
let beacon =
SyncBeacon::from_bytes(buf).ok_or(SecureTdmError::BeaconTooShort {
expected: 16,
got: buf.len(),
})?;
self.beacons_verified += 1;
Ok(beacon)
} else {
Err(SecureTdmError::BeaconTooShort {
expected: AUTHENTICATED_BEACON_SIZE,
got: buf.len(),
})
}
}
SecurityMode::QuicTransport => {
let (framed, _) =
FramedMessage::from_bytes(buf).ok_or(SecureTdmError::BeaconTooShort {
expected: 5 + 16,
got: buf.len(),
})?;
if framed.message_type != MessageType::Beacon {
return Err(SecureTdmError::ModeMismatch {
expected: SecurityMode::QuicTransport,
got: SecurityMode::ManualCrypto,
});
}
let beacon = SyncBeacon::from_bytes(&framed.payload).ok_or(
SecureTdmError::BeaconTooShort {
expected: 16,
got: framed.payload.len(),
},
)?;
self.beacons_verified += 1;
if let Some(ref mut transport) = self.transport {
transport.record_beacon_received(buf.len());
}
Ok(beacon)
}
}
}
pub fn complete_slot(&mut self, slot_index: usize, capture_quality: f32) -> TdmSlotCompleted {
self.inner.complete_slot(slot_index, capture_quality)
}
pub fn is_cycle_complete(&self) -> bool {
self.inner.is_cycle_complete()
}
pub fn cycle_id(&self) -> u64 {
self.inner.cycle_id()
}
pub fn security_mode(&self) -> SecurityMode {
self.config.security_mode
}
pub fn inner(&self) -> &TdmCoordinator {
&self.inner
}
pub fn beacons_produced(&self) -> u64 {
self.beacons_produced
}
pub fn beacons_verified(&self) -> u64 {
self.beacons_verified
}
pub fn verification_failures(&self) -> u64 {
self.verification_failures
}
pub fn transport(&self) -> Option<&QuicTransportHandle> {
self.transport.as_ref()
}
pub fn transport_mut(&mut self) -> Option<&mut QuicTransportHandle> {
self.transport.as_mut()
}
pub fn nonce_counter(&self) -> u32 {
self.nonce_counter
}
pub fn replay_window(&self) -> &ReplayWindow {
&self.replay_window
}
pub fn sec_level(&self) -> SecLevel {
self.config.sec_level
}
}
#[derive(Debug, Clone)]
pub struct SecureCycleOutput {
pub beacon: SyncBeacon,
pub authenticated_bytes: Vec<u8>,
pub mode: SecurityMode,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::esp32::tdm::TdmSchedule;
use std::time::Duration;
fn test_schedule() -> TdmSchedule {
TdmSchedule::default_4node()
}
fn manual_config() -> SecureTdmConfig {
SecureTdmConfig {
security_mode: SecurityMode::ManualCrypto,
mesh_key: Some(DEFAULT_TEST_KEY),
quic_config: QuicTransportConfig::default(),
sec_level: SecLevel::Transitional,
}
}
fn quic_config() -> SecureTdmConfig {
SecureTdmConfig {
security_mode: SecurityMode::QuicTransport,
mesh_key: Some(DEFAULT_TEST_KEY),
quic_config: QuicTransportConfig::default(),
sec_level: SecLevel::Transitional,
}
}
#[test]
fn test_replay_window_new() {
let rw = ReplayWindow::new(16);
assert_eq!(rw.last_accepted(), 0);
assert_eq!(rw.window_count(), 0);
}
#[test]
fn test_replay_window_accept_first() {
let mut rw = ReplayWindow::new(16);
assert!(rw.accept(0)); assert_eq!(rw.window_count(), 1);
}
#[test]
fn test_replay_window_monotonic() {
let mut rw = ReplayWindow::new(16);
assert!(rw.accept(1));
assert!(rw.accept(2));
assert!(rw.accept(3));
assert_eq!(rw.last_accepted(), 3);
}
#[test]
fn test_replay_window_reject_duplicate() {
let mut rw = ReplayWindow::new(16);
assert!(rw.accept(1));
assert!(!rw.accept(1)); }
#[test]
fn test_replay_window_accept_within_window() {
let mut rw = ReplayWindow::new(16);
assert!(rw.accept(5));
assert!(rw.accept(3)); assert_eq!(rw.last_accepted(), 5);
}
#[test]
fn test_replay_window_reject_too_old() {
let mut rw = ReplayWindow::new(4);
for i in 0..20 {
rw.accept(i);
}
assert!(!rw.accept(0));
}
#[test]
fn test_replay_window_evicts_old() {
let mut rw = ReplayWindow::new(4);
for i in 0..10 {
rw.accept(i);
}
assert!(rw.window_count() <= 4);
}
#[test]
fn test_auth_beacon_roundtrip() {
let beacon = SyncBeacon {
cycle_id: 42,
cycle_period: Duration::from_millis(50),
drift_correction_us: -3,
generated_at: std::time::Instant::now(),
};
let key = DEFAULT_TEST_KEY;
let nonce = 7u32;
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&nonce.to_le_bytes());
let tag = AuthenticatedBeacon::compute_tag(&msg, &key);
let auth = AuthenticatedBeacon {
beacon,
nonce,
hmac_tag: tag,
};
let bytes = auth.to_bytes();
assert_eq!(bytes.len(), AUTHENTICATED_BEACON_SIZE);
let decoded = AuthenticatedBeacon::from_bytes(&bytes).unwrap();
assert_eq!(decoded.beacon.cycle_id, 42);
assert_eq!(decoded.nonce, 7);
assert_eq!(decoded.hmac_tag, tag);
}
#[test]
fn test_auth_beacon_verify_ok() {
let beacon = SyncBeacon {
cycle_id: 100,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let key = DEFAULT_TEST_KEY;
let nonce = 1u32;
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&nonce.to_le_bytes());
let tag = AuthenticatedBeacon::compute_tag(&msg, &key);
let auth = AuthenticatedBeacon {
beacon,
nonce,
hmac_tag: tag,
};
assert!(auth.verify(&key).is_ok());
}
#[test]
fn test_auth_beacon_verify_tampered() {
let beacon = SyncBeacon {
cycle_id: 100,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let key = DEFAULT_TEST_KEY;
let nonce = 1u32;
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&nonce.to_le_bytes());
let mut tag = AuthenticatedBeacon::compute_tag(&msg, &key);
tag[0] ^= 0xFF;
let auth = AuthenticatedBeacon {
beacon,
nonce,
hmac_tag: tag,
};
assert!(matches!(
auth.verify(&key),
Err(SecureTdmError::BeaconAuthFailed)
));
}
#[test]
fn tag_compare_is_constant_time_shape() {
let base = [0xA5u8; HMAC_TAG_SIZE];
assert!(constant_time_tag_eq(&base, &base), "equal tags must accept");
let mut first = base;
first[0] ^= 0xFF;
assert!(
!constant_time_tag_eq(&base, &first),
"first-byte-differ must reject"
);
let mut last = base;
last[HMAC_TAG_SIZE - 1] ^= 0x01;
assert!(
!constant_time_tag_eq(&base, &last),
"last-byte-differ must reject"
);
let all = [0x5Au8; HMAC_TAG_SIZE]; assert!(
!constant_time_tag_eq(&base, &all),
"all-bytes-differ must reject"
);
assert!(
!constant_time_tag_eq(&base, &base[..HMAC_TAG_SIZE - 1]),
"length mismatch must reject"
);
let beacon = SyncBeacon {
cycle_id: 7,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let key = DEFAULT_TEST_KEY;
let nonce = 1u32;
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&nonce.to_le_bytes());
let mut tag = AuthenticatedBeacon::compute_tag(&msg, &key);
tag[HMAC_TAG_SIZE - 1] ^= 0x01; let auth = AuthenticatedBeacon {
beacon,
nonce,
hmac_tag: tag,
};
assert!(
matches!(auth.verify(&key), Err(SecureTdmError::BeaconAuthFailed)),
"last-byte tamper must fail verify()"
);
}
#[test]
#[ignore = "timing smoke check — noisy host, run manually with --ignored"]
fn tag_compare_timing_invariance_smoke() {
use std::time::Instant;
const ITERS: u32 = 2_000_000;
let base = [0xA5u8; HMAC_TAG_SIZE];
let mut first = base;
first[0] ^= 0xFF;
let mut last = base;
last[HMAC_TAG_SIZE - 1] ^= 0x01;
for _ in 0..ITERS / 10 {
core::hint::black_box(constant_time_tag_eq(&base, &first));
}
let t0 = Instant::now();
let mut acc = false;
for _ in 0..ITERS {
acc ^= constant_time_tag_eq(&base, &first);
}
core::hint::black_box(acc);
let dt_first = t0.elapsed().as_nanos() as f64;
let t1 = Instant::now();
let mut acc2 = false;
for _ in 0..ITERS {
acc2 ^= constant_time_tag_eq(&base, &last);
}
core::hint::black_box(acc2);
let dt_last = t1.elapsed().as_nanos() as f64;
let ratio = dt_last.max(dt_first) / dt_last.min(dt_first).max(1.0);
println!(
"first-differ {dt_first:.0}ns, last-differ {dt_last:.0}ns, ratio {ratio:.3}"
);
assert!(
ratio < 4.0,
"timing ratio {ratio:.3} too large — possible short-circuit leak"
);
}
#[test]
fn test_auth_beacon_too_short() {
let result = AuthenticatedBeacon::from_bytes(&[0u8; 10]);
assert!(matches!(result, Err(SecureTdmError::BeaconTooShort { .. })));
}
#[test]
fn test_auth_beacon_size_constant() {
assert_eq!(AUTHENTICATED_BEACON_SIZE, 28);
}
#[test]
fn test_secure_coordinator_manual_create() {
let coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
assert_eq!(coord.security_mode(), SecurityMode::ManualCrypto);
assert_eq!(coord.beacons_produced(), 0);
assert!(coord.transport().is_none());
}
#[test]
fn test_secure_coordinator_manual_begin_cycle() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
let output = coord.begin_secure_cycle().unwrap();
assert_eq!(output.mode, SecurityMode::ManualCrypto);
assert_eq!(output.authenticated_bytes.len(), AUTHENTICATED_BEACON_SIZE);
assert_eq!(output.beacon.cycle_id, 0);
assert_eq!(coord.beacons_produced(), 1);
assert_eq!(coord.nonce_counter(), 1);
}
#[test]
fn test_secure_coordinator_manual_nonce_increments() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
for expected_nonce in 1..=5u32 {
let _output = coord.begin_secure_cycle().unwrap();
for i in 0..4 {
coord.complete_slot(i, 1.0);
}
assert_eq!(coord.nonce_counter(), expected_nonce);
}
}
#[test]
fn test_secure_coordinator_manual_verify_own_beacon() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
let output = coord.begin_secure_cycle().unwrap();
let mut verifier = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
let beacon = verifier.verify_beacon(&output.authenticated_bytes).unwrap();
assert_eq!(beacon.cycle_id, 0);
}
#[test]
fn test_secure_coordinator_manual_reject_tampered() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
let output = coord.begin_secure_cycle().unwrap();
let mut tampered = output.authenticated_bytes.clone();
tampered[25] ^= 0xFF;
let mut verifier = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
assert!(verifier.verify_beacon(&tampered).is_err());
assert_eq!(verifier.verification_failures(), 1);
}
#[test]
fn test_secure_coordinator_manual_reject_replay() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
let output = coord.begin_secure_cycle().unwrap();
let mut verifier = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
verifier.verify_beacon(&output.authenticated_bytes).unwrap();
let result = verifier.verify_beacon(&output.authenticated_bytes);
assert!(result.is_err());
}
#[test]
fn test_secure_coordinator_manual_backward_compat_permissive() {
let mut cfg = manual_config();
cfg.sec_level = SecLevel::Permissive;
let mut coord = SecureTdmCoordinator::new(test_schedule(), cfg).unwrap();
let beacon = SyncBeacon {
cycle_id: 99,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let bytes = beacon.to_bytes();
let verified = coord.verify_beacon(&bytes).unwrap();
assert_eq!(verified.cycle_id, 99);
}
#[test]
fn test_secure_coordinator_manual_reject_unauthenticated_enforcing() {
let mut cfg = manual_config();
cfg.sec_level = SecLevel::Enforcing;
let mut coord = SecureTdmCoordinator::new(test_schedule(), cfg).unwrap();
let beacon = SyncBeacon {
cycle_id: 99,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let bytes = beacon.to_bytes();
let result = coord.verify_beacon(&bytes);
assert!(result.is_err());
}
#[test]
fn test_secure_coordinator_no_mesh_key() {
let cfg = SecureTdmConfig {
security_mode: SecurityMode::ManualCrypto,
mesh_key: None,
..Default::default()
};
let mut coord = SecureTdmCoordinator::new(test_schedule(), cfg).unwrap();
let result = coord.begin_secure_cycle();
assert!(matches!(result, Err(SecureTdmError::NoMeshKey)));
}
#[test]
fn test_secure_coordinator_quic_create() {
let coord = SecureTdmCoordinator::new(test_schedule(), quic_config()).unwrap();
assert_eq!(coord.security_mode(), SecurityMode::QuicTransport);
assert!(coord.transport().is_some());
}
#[test]
fn test_secure_coordinator_quic_begin_cycle() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), quic_config()).unwrap();
let output = coord.begin_secure_cycle().unwrap();
assert_eq!(output.mode, SecurityMode::QuicTransport);
assert_eq!(output.authenticated_bytes.len(), 5 + 16);
assert_eq!(coord.beacons_produced(), 1);
}
#[test]
fn test_secure_coordinator_quic_verify_own_beacon() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), quic_config()).unwrap();
let output = coord.begin_secure_cycle().unwrap();
let mut verifier = SecureTdmCoordinator::new(test_schedule(), quic_config()).unwrap();
let beacon = verifier.verify_beacon(&output.authenticated_bytes).unwrap();
assert_eq!(beacon.cycle_id, 0);
}
#[test]
fn test_secure_coordinator_complete_cycle() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
coord.begin_secure_cycle().unwrap();
for i in 0..4 {
let event = coord.complete_slot(i, 0.95);
assert_eq!(event.slot_index, i);
}
assert!(coord.is_cycle_complete());
}
#[test]
fn test_secure_coordinator_cycle_id_increments() {
let mut coord = SecureTdmCoordinator::new(test_schedule(), manual_config()).unwrap();
let out0 = coord.begin_secure_cycle().unwrap();
assert_eq!(out0.beacon.cycle_id, 0);
for i in 0..4 {
coord.complete_slot(i, 1.0);
}
let out1 = coord.begin_secure_cycle().unwrap();
assert_eq!(out1.beacon.cycle_id, 1);
}
#[test]
fn test_sec_level_values() {
assert_eq!(SecLevel::Permissive as u8, 0);
assert_eq!(SecLevel::Transitional as u8, 1);
assert_eq!(SecLevel::Enforcing as u8, 2);
}
#[test]
fn test_hmac_different_keys_produce_different_tags() {
let msg = b"test payload with nonce";
let key1: [u8; 16] = [0x01; 16];
let key2: [u8; 16] = [0x02; 16];
let tag1 = AuthenticatedBeacon::compute_tag(msg, &key1);
let tag2 = AuthenticatedBeacon::compute_tag(msg, &key2);
assert_ne!(
tag1, tag2,
"Different keys must produce different HMAC tags"
);
}
#[test]
fn test_hmac_different_messages_produce_different_tags() {
let key: [u8; 16] = DEFAULT_TEST_KEY;
let tag1 = AuthenticatedBeacon::compute_tag(b"message one", &key);
let tag2 = AuthenticatedBeacon::compute_tag(b"message two", &key);
assert_ne!(
tag1, tag2,
"Different messages must produce different HMAC tags"
);
}
#[test]
fn test_hmac_is_deterministic() {
let key: [u8; 16] = DEFAULT_TEST_KEY;
let msg = b"determinism test";
let tag1 = AuthenticatedBeacon::compute_tag(msg, &key);
let tag2 = AuthenticatedBeacon::compute_tag(msg, &key);
assert_eq!(tag1, tag2, "Same key + message must produce identical tags");
}
#[test]
fn test_wrong_key_fails_verification() {
let beacon = SyncBeacon {
cycle_id: 42,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let correct_key: [u8; 16] = DEFAULT_TEST_KEY;
let wrong_key: [u8; 16] = [0xFF; 16];
let nonce = 1u32;
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&nonce.to_le_bytes());
let tag = AuthenticatedBeacon::compute_tag(&msg, &correct_key);
let auth = AuthenticatedBeacon {
beacon,
nonce,
hmac_tag: tag,
};
assert!(
auth.verify(&wrong_key).is_err(),
"Wrong key must fail verification"
);
}
#[test]
fn test_single_bit_flip_in_payload_fails_verification() {
let beacon = SyncBeacon {
cycle_id: 42,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
};
let key: [u8; 16] = DEFAULT_TEST_KEY;
let nonce = 1u32;
let mut msg = [0u8; 20];
msg[..16].copy_from_slice(&beacon.to_bytes());
msg[16..20].copy_from_slice(&nonce.to_le_bytes());
let tag = AuthenticatedBeacon::compute_tag(&msg, &key);
let auth = AuthenticatedBeacon {
beacon,
nonce,
hmac_tag: tag,
};
let mut wire = auth.to_bytes();
wire[0] ^= 0x01;
let tampered = AuthenticatedBeacon::from_bytes(&wire).unwrap();
assert!(
tampered.verify(&key).is_err(),
"Single bit flip must fail verification"
);
}
#[test]
fn test_enforcing_mode_rejects_unauthenticated() {
let mut cfg = manual_config();
cfg.sec_level = SecLevel::Enforcing;
let mut coord = SecureTdmCoordinator::new(test_schedule(), cfg).unwrap();
let raw = SyncBeacon {
cycle_id: 1,
cycle_period: Duration::from_millis(50),
drift_correction_us: 0,
generated_at: std::time::Instant::now(),
}
.to_bytes();
assert!(coord.verify_beacon(&raw).is_err());
}
#[test]
fn test_secure_tdm_error_display() {
let err = SecureTdmError::BeaconAuthFailed;
assert!(format!("{}", err).contains("HMAC"));
let err = SecureTdmError::BeaconReplay {
nonce: 5,
last_accepted: 10,
};
assert!(format!("{}", err).contains("replay"));
let err = SecureTdmError::NoMeshKey;
assert!(format!("{}", err).contains("Mesh key"));
}
}