1use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct JitterBinding {
29 #[serde(rename = "1")]
30 pub entropy_commitment: EntropyCommitment,
31
32 #[serde(rename = "2")]
33 pub sources: Vec<SourceDescriptor>,
34
35 #[serde(rename = "3")]
36 pub summary: JitterSummary,
37
38 #[serde(rename = "4")]
39 pub binding_mac: BindingMac,
40
41 #[serde(rename = "5", skip_serializing_if = "Option::is_none")]
43 pub raw_intervals: Option<RawIntervals>,
44
45 #[serde(rename = "6", skip_serializing_if = "Option::is_none")]
47 pub active_probes: Option<ActiveProbes>,
48
49 #[serde(rename = "7", skip_serializing_if = "Option::is_none")]
51 pub labyrinth_structure: Option<LabyrinthStructure>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct EntropyCommitment {
65 #[serde(rename = "1", with = "super::serde_helpers::hex_bytes")]
66 pub hash: [u8; 32],
67
68 #[serde(rename = "2")]
69 pub timestamp_ms: u64,
70
71 #[serde(rename = "3", with = "super::serde_helpers::hex_bytes")]
73 pub previous_hash: [u8; 32],
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct SourceDescriptor {
86 #[serde(rename = "1")]
88 pub source_type: String,
89
90 #[serde(rename = "2")]
92 pub weight: u16,
93
94 #[serde(rename = "3", skip_serializing_if = "Option::is_none")]
95 pub device_fingerprint: Option<String>,
96
97 #[serde(rename = "4", skip_serializing_if = "Option::is_none")]
98 pub transport_calibration: Option<TransportCalibration>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct TransportCalibration {
113 #[serde(rename = "1")]
114 pub transport: String,
115
116 #[serde(rename = "2")]
117 pub baseline_latency_us: u64,
118
119 #[serde(rename = "3")]
120 pub latency_variance_us: u64,
121
122 #[serde(rename = "4")]
123 pub calibrated_at_ms: u64,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct JitterSummary {
139 #[serde(rename = "1")]
140 pub sample_count: u64,
141
142 #[serde(rename = "2")]
143 pub mean_interval_us: f64,
144
145 #[serde(rename = "3")]
146 pub std_dev: f64,
147
148 #[serde(rename = "4")]
150 pub coefficient_of_variation: f64,
151
152 #[serde(rename = "5")]
153 pub percentiles: [f64; 5],
154
155 #[serde(rename = "6")]
157 pub entropy_bits: f64,
158
159 #[serde(rename = "7", skip_serializing_if = "Option::is_none")]
161 pub hurst_exponent: Option<f64>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct BindingMac {
176 #[serde(rename = "1", with = "super::serde_helpers::hex_bytes")]
177 pub mac: [u8; 32],
178
179 #[serde(rename = "2", with = "super::serde_helpers::hex_bytes")]
180 pub document_hash: [u8; 32],
181
182 #[serde(rename = "3")]
184 pub keystroke_count: u64,
185
186 #[serde(rename = "4")]
187 pub timestamp_ms: u64,
188}
189
190impl BindingMac {
191 pub fn compute(
193 key: &[u8],
194 document_hash: [u8; 32],
195 keystroke_count: u64,
196 timestamp_ms: u64,
197 entropy_hash: &[u8; 32],
198 ) -> Self {
199 use hmac::{Hmac, Mac};
200 use sha2::Sha256;
201 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts any key size");
202 mac.update(&document_hash);
203 mac.update(&keystroke_count.to_be_bytes());
204 mac.update(×tamp_ms.to_be_bytes());
205 mac.update(entropy_hash);
206 Self {
207 mac: mac.finalize().into_bytes().into(),
208 document_hash,
209 keystroke_count,
210 timestamp_ms,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct RawIntervals {
224 #[serde(rename = "1")]
225 pub intervals: Vec<u32>,
226
227 #[serde(rename = "2")]
228 pub compression_method: u8,
229
230 #[serde(rename = "3", skip_serializing_if = "Option::is_none")]
231 pub compressed_data: Option<Vec<u8>>,
232}
233
234#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241pub struct ActiveProbes {
242 #[serde(rename = "1", skip_serializing_if = "Option::is_none")]
243 pub galton_invariant: Option<GaltonInvariant>,
244
245 #[serde(rename = "2", skip_serializing_if = "Option::is_none")]
246 pub reflex_gate: Option<ReflexGate>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct GaltonInvariant {
263 #[serde(rename = "1")]
264 pub absorption_coefficient: f64,
265
266 #[serde(rename = "2")]
267 pub stimulus_count: u32,
268
269 #[serde(rename = "3")]
270 pub expected_absorption: f64,
271
272 #[serde(rename = "4")]
273 pub z_score: f64,
274
275 #[serde(rename = "5")]
276 pub passed: bool,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ReflexGate {
293 #[serde(rename = "1")]
294 pub mean_latency_ms: f64,
295
296 #[serde(rename = "2")]
297 pub std_dev_ms: f64,
298
299 #[serde(rename = "3")]
300 pub event_count: u32,
301
302 #[serde(rename = "4")]
303 pub percentiles: [f64; 5],
304
305 #[serde(rename = "5")]
307 pub passed: bool,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct LabyrinthStructure {
324 #[serde(rename = "1")]
325 pub embedding_dimension: u8,
326
327 #[serde(rename = "2")]
328 pub time_delay: u16,
329
330 #[serde(rename = "3")]
332 pub attractor_points: Vec<Vec<f64>>,
333
334 #[serde(rename = "4")]
336 pub betti_numbers: Vec<u32>,
337
338 #[serde(rename = "5")]
341 pub lyapunov_exponent: Option<f64>,
342
343 #[serde(rename = "6")]
345 pub correlation_dimension: f64,
346}
347
348impl JitterBinding {
349 pub fn new(
351 entropy_commitment: EntropyCommitment,
352 sources: Vec<SourceDescriptor>,
353 summary: JitterSummary,
354 binding_mac: BindingMac,
355 ) -> Self {
356 Self {
357 entropy_commitment,
358 sources,
359 summary,
360 binding_mac,
361 raw_intervals: None,
362 active_probes: None,
363 labyrinth_structure: None,
364 }
365 }
366
367 pub fn with_raw_intervals(mut self, intervals: RawIntervals) -> Self {
369 self.raw_intervals = Some(intervals);
370 self
371 }
372
373 pub fn with_active_probes(mut self, probes: ActiveProbes) -> Self {
375 self.active_probes = Some(probes);
376 self
377 }
378
379 pub fn with_labyrinth(mut self, labyrinth: LabyrinthStructure) -> Self {
381 self.labyrinth_structure = Some(labyrinth);
382 self
383 }
384
385 pub fn verify_binding(&self, seed: &[u8]) -> bool {
387 let expected = BindingMac::compute(
388 seed,
389 self.binding_mac.document_hash,
390 self.binding_mac.keystroke_count,
391 self.binding_mac.timestamp_ms,
392 &self.entropy_commitment.hash,
393 );
394 subtle::ConstantTimeEq::ct_eq(&self.binding_mac.mac[..], &expected.mac[..]).unwrap_u8() == 1
395 }
396
397 pub fn is_hurst_valid(&self) -> bool {
399 if let Some(h) = self.summary.hurst_exponent {
400 h > 0.55 && h < 0.85
402 } else {
403 true
404 }
405 }
406
407 pub fn probes_passed(&self) -> bool {
409 if let Some(probes) = &self.active_probes {
410 let galton_ok = probes
411 .galton_invariant
412 .as_ref()
413 .map(|g| g.passed)
414 .unwrap_or(true);
415 let reflex_ok = probes
416 .reflex_gate
417 .as_ref()
418 .map(|r| r.passed)
419 .unwrap_or(true);
420 galton_ok && reflex_ok
421 } else {
422 true
423 }
424 }
425
426 pub fn validate(&self) -> Vec<String> {
428 let mut errors = Vec::new();
429
430 if self.entropy_commitment.hash == [0u8; 32] {
431 errors.push("entropy commitment hash is zero".into());
432 }
433 if self.entropy_commitment.timestamp_ms == 0 {
434 errors.push("entropy commitment timestamp is zero".into());
435 }
436
437 if self.sources.is_empty() {
438 errors.push("no entropy sources declared".into());
439 }
440 let total_weight: u32 = self.sources.iter().map(|s| s.weight as u32).sum();
441 if total_weight == 0 {
442 errors.push("total source weight is zero".into());
443 }
444 if total_weight > 1000 {
445 errors.push(format!("total source weight {} exceeds 1000", total_weight));
446 }
447 for source in &self.sources {
448 if source.source_type.is_empty() {
449 errors.push("empty source type".into());
450 }
451 }
452
453 if self.summary.sample_count == 0 {
454 errors.push("sample count is zero".into());
455 }
456 if self.summary.mean_interval_us <= 0.0 {
457 errors.push("mean interval is non-positive".into());
458 }
459 if self.summary.std_dev < 0.0 {
460 errors.push("standard deviation is negative".into());
461 }
462 if self.summary.coefficient_of_variation < 0.0 {
463 errors.push("coefficient of variation is negative".into());
464 }
465 if self.summary.entropy_bits < 0.0 {
466 errors.push("entropy bits is negative".into());
467 }
468
469 for i in 1..5 {
470 if self.summary.percentiles[i] < self.summary.percentiles[i - 1] {
471 errors.push(format!(
472 "percentiles not monotonic: index {} ({}) < index {} ({})",
473 i,
474 self.summary.percentiles[i],
475 i - 1,
476 self.summary.percentiles[i - 1]
477 ));
478 break;
479 }
480 }
481
482 if let Some(h) = self.summary.hurst_exponent {
483 if !(0.0..=1.0).contains(&h) {
484 errors.push(format!("Hurst exponent {} out of range [0, 1]", h));
485 }
486 }
487
488 if self.binding_mac.mac == [0u8; 32] {
489 errors.push("binding MAC is zero".into());
490 }
491 if self.binding_mac.document_hash == [0u8; 32] {
492 errors.push("document hash is zero".into());
493 }
494 if self.binding_mac.timestamp_ms == 0 {
495 errors.push("binding MAC timestamp is zero".into());
496 }
497
498 if let Some(probes) = &self.active_probes {
499 if let Some(galton) = &probes.galton_invariant {
500 if galton.absorption_coefficient < 0.0 || galton.absorption_coefficient > 1.0 {
501 errors.push(format!(
502 "Galton absorption coefficient {} out of range [0, 1]",
503 galton.absorption_coefficient
504 ));
505 }
506 if galton.stimulus_count == 0 {
507 errors.push("Galton stimulus count is zero".into());
508 }
509 }
510 if let Some(reflex) = &probes.reflex_gate {
511 if reflex.mean_latency_ms < 0.0 {
512 errors.push("reflex gate mean latency is negative".into());
513 }
514 if reflex.std_dev_ms < 0.0 {
515 errors.push("reflex gate std dev is negative".into());
516 }
517 }
518 }
519
520 if let Some(labyrinth) = &self.labyrinth_structure {
521 if labyrinth.embedding_dimension < 2 {
522 errors.push("labyrinth embedding dimension < 2".into());
523 }
524 if labyrinth.time_delay == 0 {
525 errors.push("labyrinth time delay is zero".into());
526 }
527 if labyrinth.betti_numbers.is_empty() {
528 errors.push("labyrinth betti numbers empty".into());
529 }
530 if labyrinth.correlation_dimension < 0.0 {
531 errors.push("correlation dimension is negative".into());
532 }
533 }
534
535 errors
536 }
537
538 pub fn is_valid(&self) -> bool {
540 self.validate().is_empty()
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 fn create_test_binding() -> JitterBinding {
549 let commitment = EntropyCommitment {
550 hash: [1u8; 32],
551 timestamp_ms: 1700000000000,
552 previous_hash: [0u8; 32],
553 };
554
555 let sources = vec![
556 SourceDescriptor {
557 source_type: "keyboard".to_string(),
558 weight: 700,
559 device_fingerprint: Some("usb:1234:5678".to_string()),
560 transport_calibration: None,
561 },
562 SourceDescriptor {
563 source_type: "mouse".to_string(),
564 weight: 300,
565 device_fingerprint: None,
566 transport_calibration: None,
567 },
568 ];
569
570 let summary = JitterSummary {
571 sample_count: 1000,
572 mean_interval_us: 150000.0,
573 std_dev: 50000.0,
574 coefficient_of_variation: 0.33,
575 percentiles: [50000.0, 80000.0, 140000.0, 200000.0, 300000.0],
576 entropy_bits: 8.5,
577 hurst_exponent: Some(0.72),
578 };
579
580 let binding_mac = BindingMac {
581 mac: [2u8; 32],
582 document_hash: [3u8; 32],
583 keystroke_count: 5000,
584 timestamp_ms: 1700000000000,
585 };
586
587 JitterBinding::new(commitment, sources, summary, binding_mac)
588 }
589
590 #[test]
591 fn test_jitter_binding_serialization() {
592 let binding = create_test_binding();
593
594 let json = serde_json::to_string_pretty(&binding).unwrap();
595 let decoded: JitterBinding = serde_json::from_str(&json).unwrap();
596
597 assert_eq!(binding.summary.sample_count, decoded.summary.sample_count);
598 assert_eq!(binding.sources.len(), decoded.sources.len());
599 }
600
601 #[test]
602 fn test_hurst_validation() {
603 let mut binding = create_test_binding();
604
605 binding.summary.hurst_exponent = Some(0.72);
606 assert!(binding.is_hurst_valid());
607
608 binding.summary.hurst_exponent = Some(0.5);
609 assert!(!binding.is_hurst_valid());
610
611 binding.summary.hurst_exponent = Some(1.0);
612 assert!(!binding.is_hurst_valid());
613
614 binding.summary.hurst_exponent = None;
615 assert!(binding.is_hurst_valid());
616 }
617
618 #[test]
619 fn test_active_probes() {
620 let mut binding = create_test_binding();
621
622 let probes = ActiveProbes {
623 galton_invariant: Some(GaltonInvariant {
624 absorption_coefficient: 0.65,
625 stimulus_count: 100,
626 expected_absorption: 0.63,
627 z_score: 0.5,
628 passed: true,
629 }),
630 reflex_gate: Some(ReflexGate {
631 mean_latency_ms: 250.0,
632 std_dev_ms: 50.0,
633 event_count: 50,
634 percentiles: [180.0, 210.0, 245.0, 285.0, 340.0],
635 passed: true,
636 }),
637 };
638
639 binding.active_probes = Some(probes);
640 assert!(binding.probes_passed());
641
642 binding
643 .active_probes
644 .as_mut()
645 .unwrap()
646 .galton_invariant
647 .as_mut()
648 .unwrap()
649 .passed = false;
650 assert!(!binding.probes_passed());
651 }
652
653 #[test]
654 fn test_jitter_binding_validation_valid() {
655 let binding = create_test_binding();
656 let errors = binding.validate();
657 assert!(errors.is_empty(), "expected no errors, got: {:?}", errors);
658 assert!(binding.is_valid());
659 }
660
661 #[test]
662 fn test_jitter_binding_validation_zero_hash() {
663 let mut binding = create_test_binding();
664 binding.entropy_commitment.hash = [0u8; 32];
665 let errors = binding.validate();
666 assert!(errors
667 .iter()
668 .any(|e| e.contains("entropy commitment hash is zero")));
669 assert!(!binding.is_valid());
670 }
671
672 #[test]
673 fn test_jitter_binding_validation_empty_sources() {
674 let mut binding = create_test_binding();
675 binding.sources.clear();
676 let errors = binding.validate();
677 assert!(errors.iter().any(|e| e.contains("no entropy sources")));
678 }
679
680 #[test]
681 fn test_jitter_binding_validation_excessive_weight() {
682 let mut binding = create_test_binding();
683 binding.sources[0].weight = 800;
684 binding.sources[1].weight = 500;
685 let errors = binding.validate();
686 assert!(errors.iter().any(|e| e.contains("exceeds 1000")));
687 }
688
689 #[test]
690 fn test_jitter_binding_validation_invalid_hurst() {
691 let mut binding = create_test_binding();
692 binding.summary.hurst_exponent = Some(1.5);
693 let errors = binding.validate();
694 assert!(errors.iter().any(|e| e.contains("Hurst exponent")));
695 }
696
697 #[test]
698 fn test_jitter_binding_validation_non_monotonic_percentiles() {
699 let mut binding = create_test_binding();
700 binding.summary.percentiles = [100.0, 50.0, 75.0, 80.0, 90.0];
701 let errors = binding.validate();
702 assert!(errors.iter().any(|e| e.contains("not monotonic")));
703 }
704}