use crate::math::sqrt_f32;
pub const AUTHENTICATION_THRESHOLD: f32 = 0.95;
pub const ALLAN_TAUS: [u32; 8] = [1, 2, 4, 8, 16, 32, 64, 128];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnaVerdict {
Authentic,
Suspicious,
Spoofed,
}
#[derive(Debug, Clone, Copy)]
pub struct DnaMatchResult {
pub similarity: f32,
pub is_authentic: bool,
pub verdict: DnaVerdict,
}
#[derive(Debug, Clone, Copy)]
pub struct HardwareDna {
pub signature: [f32; 8],
pub label: &'static str,
}
impl HardwareDna {
pub const fn new(signature: [f32; 8], label: &'static str) -> Self {
Self { signature, label }
}
}
pub struct AllanVarianceEstimator<const N: usize> {
buf: [f32; N],
head: usize,
count: usize,
}
impl<const N: usize> AllanVarianceEstimator<N> {
pub const fn new() -> Self {
Self { buf: [0.0; N], head: 0, count: 0 }
}
pub fn push(&mut self, sample: f32) {
self.buf[self.head] = sample;
self.head = (self.head + 1) % N;
if self.count < N { self.count += 1; }
}
pub fn len(&self) -> usize { self.count }
pub fn is_ready(&self) -> bool { self.count >= 2 * ALLAN_TAUS[7] as usize + 1 }
fn sample(&self, i: usize) -> f32 {
let idx = (self.head + N - 1 - i) % N;
self.buf[idx]
}
pub fn allan_deviation(&self, tau: u32) -> f32 {
let t = tau as usize;
if self.count < 2 * t + 1 { return 0.0; }
let n = self.count.min(N);
let max_k = n.saturating_sub(2 * t);
if max_k == 0 { return 0.0; }
let tau_sq = (t * t) as f32;
let mut sum = 0.0_f32;
for k in 0..max_k {
let offset = n.saturating_sub(1).saturating_sub(k);
let x0 = if offset >= 2 * t { self.sample(offset) } else { 0.0 };
let x1 = if offset >= t { self.sample(offset - t) } else { 0.0 };
let x2 = self.sample(offset);
let diff = x2 - 2.0 * x1 + x0;
sum += diff * diff;
}
let avar = sum / (2.0 * tau_sq * max_k as f32);
sqrt_f32(avar.max(0.0))
}
pub fn fingerprint(&self) -> Option<[f32; 8]> {
if !self.is_ready() { return None; }
let mut sig = [0.0_f32; 8];
for (i, &tau) in ALLAN_TAUS.iter().enumerate() {
sig[i] = self.allan_deviation(tau);
}
Some(sig)
}
pub fn reset(&mut self) {
self.buf = [0.0; N];
self.head = 0;
self.count = 0;
}
}
impl<const N: usize> Default for AllanVarianceEstimator<N> {
fn default() -> Self { Self::new() }
}
pub fn verify_dna(incoming: &[f32; 8], registered: &HardwareDna) -> DnaMatchResult {
let sim = cosine_similarity(incoming, ®istered.signature);
let is_authentic = sim >= AUTHENTICATION_THRESHOLD;
let verdict = if sim >= AUTHENTICATION_THRESHOLD {
DnaVerdict::Authentic
} else if sim >= 0.85 {
DnaVerdict::Suspicious
} else {
DnaVerdict::Spoofed
};
DnaMatchResult { similarity: sim, is_authentic, verdict }
}
pub fn cosine_similarity(a: &[f32; 8], b: &[f32; 8]) -> f32 {
let dot: f32 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
let mag_a: f32 = sqrt_f32(a.iter().map(|&x| x * x).sum::<f32>());
let mag_b: f32 = sqrt_f32(b.iter().map(|&x| x * x).sum::<f32>());
if mag_a < 1e-20 || mag_b < 1e-20 { return 0.0; }
(dot / (mag_a * mag_b)).max(-1.0).min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cosine_identical_vectors_is_one() {
let a = [0.1_f32, 0.2, 0.3, 0.1, 0.05, 0.02, 0.01, 0.005];
let sim = cosine_similarity(&a, &a);
assert!((sim - 1.0).abs() < 1e-4, "identical vectors: cosine={}", sim);
}
#[test]
fn cosine_orthogonal_vectors_is_zero() {
let a = [1.0_f32, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
let b = [0.0_f32, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
let sim = cosine_similarity(&a, &b);
assert!(sim.abs() < 1e-4, "orthogonal: cosine={}", sim);
}
#[test]
fn authentic_match_passes() {
let sig = [0.1_f32, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01];
let dna = HardwareDna::new(sig, "OCXO_test");
let result = verify_dna(&sig, &dna);
assert_eq!(result.verdict, DnaVerdict::Authentic);
assert!(result.is_authentic);
}
#[test]
fn spoofed_fingerprint_detected() {
let registered = [0.1_f32, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01];
let dna = HardwareDna::new(registered, "reference");
let incoming = [0.01_f32, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.10];
let result = verify_dna(&incoming, &dna);
assert_ne!(result.verdict, DnaVerdict::Authentic,
"opposite-slope fingerprint should not authenticate: sim={:.3}", result.similarity);
}
#[test]
fn allan_estimator_ready_after_sufficient_samples() {
let mut est = AllanVarianceEstimator::<512>::new();
assert!(!est.is_ready());
for i in 0..257 {
est.push(0.01 + i as f32 * 0.0001);
}
assert!(est.is_ready(), "estimator must be ready after 257 samples");
}
#[test]
fn allan_estimator_returns_none_when_not_ready() {
let mut est = AllanVarianceEstimator::<512>::new();
for _ in 0..10 { est.push(0.01); }
assert!(est.fingerprint().is_none());
}
#[test]
fn fingerprint_returns_some_when_ready() {
let mut est = AllanVarianceEstimator::<512>::new();
for i in 0..512 { est.push(0.01 + (i as f32 * 0.0001).sin() * 0.001); }
let fp = est.fingerprint();
assert!(fp.is_some(), "must return Some after 512 samples");
let sig = fp.unwrap();
for (i, &v) in sig.iter().enumerate() {
assert!(v >= 0.0, "sigma[{}] = {} must be non-negative", i, v);
}
}
#[test]
fn verify_dna_with_small_perturbation_authentic() {
let base = [0.10_f32, 0.07, 0.05, 0.04, 0.03, 0.02, 0.015, 0.01];
let dna = HardwareDna::new(base, "reference");
let perturbed: [f32; 8] = core::array::from_fn(|i| base[i] * (1.0 + 0.002 * (i as f32)));
let result = verify_dna(&perturbed, &dna);
assert_eq!(result.verdict, DnaVerdict::Authentic,
"tiny perturbation should still authenticate: sim={:.4}", result.similarity);
}
}