Skip to main content

dsfb_rf/
dna.rs

1//! Hardware DNA authentication via Allan variance fingerprinting.
2//!
3//! ## Theoretical Basis
4//!
5//! Every hardware oscillator (OCXO, TCXO, VCXO, MEMS) produces a unique
6//! combination of frequency-stability noise coefficients that can be estimated
7//! from short-term measurements of the RF residual's carrier phase.  The
8//! Allan deviation σ_y(τ) evaluated at a set of averaging times
9//! {τ₁, τ₂, τ₄, τ₈, τ₁₆, τ₃₂, τ₆₄, τ₁₂₈} forms a 8-dimensional fingerprint
10//! vector that is unique to each physical oscillator at the manufacturing-
11//! process level (thermal-mechanical history, crystal cut variations, ageing
12//! state).
13//!
14//! **Authentication Protocol:**
15//! 1. During commissioning, compute the hardware DNA fingerprint and register it.
16//! 2. At each calibration epoch, compute a fresh fingerprint from the live residual.
17//! 3. Compute the cosine similarity between the fresh and registered fingerprints.
18//! 4. Authenticate if similarity > AUTHENTICATION_THRESHOLD (default 0.95).
19//!
20//! **Security Note:** This is a *physical layer* authentication signal.
21//! It detects hardware substitution (swap attack) and clock-injection spoofing
22//! by comparing the intrinsic noise signature of the oscillator, not a
23//! transmitted authentication code.  It does **not** provide cryptographic
24//! guarantees and is not a replacement for link-layer authentication.
25//!
26//! ## Design
27//!
28//! - `no_std`, `no_alloc`, zero `unsafe`
29//! - Rolling circular buffer of N residual norms
30//! - Allan deviation computed at 8 averaging times via overlapping samples
31//! - Cosine similarity matching with configurable threshold
32//!
33//! ## References
34//!
35//! Allan, D.W. (1966) "Statistics of atomic frequency standards,"
36//!   *Proc. IEEE* 54(2):221–230. doi:10.1109/PROC.1966.4634.
37//!
38//! IEEE Std 1139-2008, "Standard Definitions of Physical Quantities for
39//!   Fundamental Frequency and Time Metrology—Random Jitter and Phase Noise."
40//!
41//! Danev, B., Zanetti, D. and Capkun, S. (2010) "On physical-layer identification
42//!   of wireless devices," *IEEE TNET* 20(3):1157–1270.
43//!   doi:10.1109/TNET.2012.2191619.
44
45use crate::math::sqrt_f32;
46
47// ── Constants ──────────────────────────────────────────────────────────────
48
49/// Cosine similarity threshold for authentic match (> 0.95).
50pub const AUTHENTICATION_THRESHOLD: f32 = 0.95;
51
52/// Allan deviation averaging times in samples (powers of 2: 1, 2, 4, …, 128).
53pub const ALLAN_TAUS: [u32; 8] = [1, 2, 4, 8, 16, 32, 64, 128];
54
55// ── Hardware DNA Fingerprint ───────────────────────────────────────────────
56
57/// Authentication verdict from DNA comparison.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum DnaVerdict {
60    /// Similarity ≥ AUTHENTICATION_THRESHOLD: fingerprint matches registration.
61    Authentic,
62    /// Similarity ∈ [0.85, threshold): close but not confident.  Flag for review.
63    Suspicious,
64    /// Similarity < 0.85: hardware substitution or spoofed clock likely.
65    Spoofed,
66}
67
68/// Result of comparing an incoming oscillator fingerprint to a registered DNA.
69#[derive(Debug, Clone, Copy)]
70pub struct DnaMatchResult {
71    /// Cosine similarity between incoming and registered fingerprints ∈ [−1, 1].
72    pub similarity: f32,
73    /// True if similarity ≥ AUTHENTICATION_THRESHOLD.
74    pub is_authentic: bool,
75    /// Verdict classification.
76    pub verdict: DnaVerdict,
77}
78
79/// Registered hardware DNA fingerprint.
80///
81/// The 8 Allan deviation values at tau = 1, 2, 4, 8, 16, 32, 64, 128.
82#[derive(Debug, Clone, Copy)]
83pub struct HardwareDna {
84    /// Allan deviation fingerprint: σ_y(τ) at the 8 standard averaging times.
85    pub signature: [f32; 8],
86    /// Human-readable hardware label.
87    pub label: &'static str,
88}
89
90impl HardwareDna {
91    /// Create a DNA record from a computed Allan deviation fingerprint.
92    pub const fn new(signature: [f32; 8], label: &'static str) -> Self {
93        Self { signature, label }
94    }
95}
96
97// ── Allan Variance Estimator ───────────────────────────────────────────────
98
99/// Rolling Allan variance estimator.
100///
101/// Maintains a circular buffer of N phase/norm samples and computes σ_y(τ)
102/// for each of the ALLAN_TAUS averaging intervals using the overlapping
103/// Allan variance formula:
104///
105/// ```text
106/// σ²_y(τ) = 1 / (2τ²(N-2τ)) · Σ_{k=1}^{N-2τ} [x(k+2τ) - 2x(k+τ) + x(k)]²
107/// ```
108///
109/// ## Type Parameters
110/// - `N`: Buffer capacity (≥ 256 recommended for τ=128 support; 512 is ideal).
111pub struct AllanVarianceEstimator<const N: usize> {
112    buf: [f32; N],
113    head: usize,
114    count: usize,
115}
116
117impl<const N: usize> AllanVarianceEstimator<N> {
118    /// Create a new estimator with empty buffer.
119    pub const fn new() -> Self {
120        Self { buf: [0.0; N], head: 0, count: 0 }
121    }
122
123    /// Absorb one sample (residual norm or phase increment).
124    pub fn push(&mut self, sample: f32) {
125        self.buf[self.head] = sample;
126        self.head = (self.head + 1) % N;
127        if self.count < N { self.count += 1; }
128    }
129
130    /// Number of valid samples.
131    pub fn len(&self) -> usize { self.count }
132
133    /// Whether enough samples are available for fingerprint computation.
134    /// Requires count ≥ 2 * max_tau + 1 = 257 for τ_max = 128.
135    pub fn is_ready(&self) -> bool { self.count >= 2 * ALLAN_TAUS[7] as usize + 1 }
136
137    /// Access sample at absolute position `i` in the ring buffer.
138    fn sample(&self, i: usize) -> f32 {
139        // Most-recent sample is at index (head-1+N)%N
140        // sample(0) = most recent, sample(count-1) = oldest
141        let idx = (self.head + N - 1 - i) % N;
142        self.buf[idx]
143    }
144
145    /// Compute Allan deviation σ_y(τ) for averaging time τ (in samples).
146    ///
147    /// Uses overlapping samples for N_eff = count − 2τ averages.
148    /// Returns 0.0 if count < 2τ + 1.
149    pub fn allan_deviation(&self, tau: u32) -> f32 {
150        let t = tau as usize;
151        if self.count < 2 * t + 1 { return 0.0; }
152        let n = self.count.min(N);
153        let max_k = n.saturating_sub(2 * t);
154        if max_k == 0 { return 0.0; }
155        let tau_sq = (t * t) as f32;
156        let mut sum = 0.0_f32;
157        for k in 0..max_k {
158            // x(k), x(k+τ), x(k+2τ) — oldest-first ordering
159            // In ring buf with sample(0)=most recent: oldest is sample(count-1)
160            // k=0 is the oldest triple
161            let offset = n.saturating_sub(1).saturating_sub(k);
162            let x0 = if offset >= 2 * t { self.sample(offset) } else { 0.0 };
163            let x1 = if offset >= t { self.sample(offset - t) } else { 0.0 };
164            let x2 = self.sample(offset);
165            // Second difference: Δ = x(k+2τ) - 2x(k+τ) + x(k)
166            let diff = x2 - 2.0 * x1 + x0;
167            sum += diff * diff;
168        }
169        let avar = sum / (2.0 * tau_sq * max_k as f32);
170        sqrt_f32(avar.max(0.0))
171    }
172
173    /// Compute the 8-element Allan deviation fingerprint.
174    ///
175    /// Returns `None` if `is_ready()` is false.
176    pub fn fingerprint(&self) -> Option<[f32; 8]> {
177        if !self.is_ready() { return None; }
178        let mut sig = [0.0_f32; 8];
179        for (i, &tau) in ALLAN_TAUS.iter().enumerate() {
180            sig[i] = self.allan_deviation(tau);
181        }
182        Some(sig)
183    }
184
185    /// Reset the buffer.
186    pub fn reset(&mut self) {
187        self.buf = [0.0; N];
188        self.head = 0;
189        self.count = 0;
190    }
191}
192
193impl<const N: usize> Default for AllanVarianceEstimator<N> {
194    fn default() -> Self { Self::new() }
195}
196
197// ── Authentication ─────────────────────────────────────────────────────────
198
199/// Authenticate an incoming fingerprint against a registered hardware DNA.
200///
201/// Uses cosine similarity as the distance metric.  Cosine similarity is
202/// invariant to overall scale amplitude changes (gain variations), making
203/// it robust to received-power differences while sensitive to the *shape*
204/// of the σ_y(τ) curve which is intrinsic to the oscillator hardware.
205pub fn verify_dna(incoming: &[f32; 8], registered: &HardwareDna) -> DnaMatchResult {
206    let sim = cosine_similarity(incoming, &registered.signature);
207    let is_authentic = sim >= AUTHENTICATION_THRESHOLD;
208    let verdict = if sim >= AUTHENTICATION_THRESHOLD {
209        DnaVerdict::Authentic
210    } else if sim >= 0.85 {
211        DnaVerdict::Suspicious
212    } else {
213        DnaVerdict::Spoofed
214    };
215    DnaMatchResult { similarity: sim, is_authentic, verdict }
216}
217
218/// Cosine similarity between two 8-element vectors.  
219/// Returns 0.0 if either vector is zero-magnitude.
220pub fn cosine_similarity(a: &[f32; 8], b: &[f32; 8]) -> f32 {
221    let dot: f32    = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
222    let mag_a: f32  = sqrt_f32(a.iter().map(|&x| x * x).sum::<f32>());
223    let mag_b: f32  = sqrt_f32(b.iter().map(|&x| x * x).sum::<f32>());
224    if mag_a < 1e-20 || mag_b < 1e-20 { return 0.0; }
225    (dot / (mag_a * mag_b)).max(-1.0).min(1.0)
226}
227
228// ── Tests ──────────────────────────────────────────────────────────────────
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn cosine_identical_vectors_is_one() {
236        let a = [0.1_f32, 0.2, 0.3, 0.1, 0.05, 0.02, 0.01, 0.005];
237        let sim = cosine_similarity(&a, &a);
238        assert!((sim - 1.0).abs() < 1e-4, "identical vectors: cosine={}", sim);
239    }
240
241    #[test]
242    fn cosine_orthogonal_vectors_is_zero() {
243        let a = [1.0_f32, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
244        let b = [0.0_f32, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
245        let sim = cosine_similarity(&a, &b);
246        assert!(sim.abs() < 1e-4, "orthogonal: cosine={}", sim);
247    }
248
249    #[test]
250    fn authentic_match_passes() {
251        let sig = [0.1_f32, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01];
252        let dna = HardwareDna::new(sig, "OCXO_test");
253        // Identical fingerprint: similarity = 1.0 → Authentic
254        let result = verify_dna(&sig, &dna);
255        assert_eq!(result.verdict, DnaVerdict::Authentic);
256        assert!(result.is_authentic);
257    }
258
259    #[test]
260    fn spoofed_fingerprint_detected() {
261        let registered = [0.1_f32, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01];
262        let dna = HardwareDna::new(registered, "reference");
263        // Very different shape: opposite slope
264        let incoming = [0.01_f32, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.10];
265        let result = verify_dna(&incoming, &dna);
266        assert_ne!(result.verdict, DnaVerdict::Authentic,
267            "opposite-slope fingerprint should not authenticate: sim={:.3}", result.similarity);
268    }
269
270    #[test]
271    fn allan_estimator_ready_after_sufficient_samples() {
272        let mut est = AllanVarianceEstimator::<512>::new();
273        assert!(!est.is_ready());
274        for i in 0..257 {
275            est.push(0.01 + i as f32 * 0.0001);
276        }
277        assert!(est.is_ready(), "estimator must be ready after 257 samples");
278    }
279
280    #[test]
281    fn allan_estimator_returns_none_when_not_ready() {
282        let mut est = AllanVarianceEstimator::<512>::new();
283        for _ in 0..10 { est.push(0.01); }
284        assert!(est.fingerprint().is_none());
285    }
286
287    #[test]
288    fn fingerprint_returns_some_when_ready() {
289        let mut est = AllanVarianceEstimator::<512>::new();
290        for i in 0..512 { est.push(0.01 + (i as f32 * 0.0001).sin() * 0.001); }
291        let fp = est.fingerprint();
292        assert!(fp.is_some(), "must return Some after 512 samples");
293        let sig = fp.unwrap();
294        // All values must be non-negative
295        for (i, &v) in sig.iter().enumerate() {
296            assert!(v >= 0.0, "sigma[{}] = {} must be non-negative", i, v);
297        }
298    }
299
300    #[test]
301    fn verify_dna_with_small_perturbation_authentic() {
302        // Build an exact fingerprint via cosine similarity check
303        let base = [0.10_f32, 0.07, 0.05, 0.04, 0.03, 0.02, 0.015, 0.01];
304        let dna = HardwareDna::new(base, "reference");
305        // Tiny perturbation: < 1% change per element
306        let perturbed: [f32; 8] = core::array::from_fn(|i| base[i] * (1.0 + 0.002 * (i as f32)));
307        let result = verify_dna(&perturbed, &dna);
308        assert_eq!(result.verdict, DnaVerdict::Authentic,
309            "tiny perturbation should still authenticate: sim={:.4}", result.similarity);
310    }
311}