1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[repr(u64)]
12pub enum ConfidenceTier {
13 PopulationReference = 1,
14 Emerging = 2,
15 Established = 3,
16 Mature = 4,
17}
18
19impl ConfidenceTier {
20 pub fn from_session_count(count: u64) -> Self {
21 match count {
22 0..=4 => Self::PopulationReference,
23 5..=9 => Self::Emerging,
24 10..=19 => Self::Established,
25 _ => Self::Mature,
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct StreamingStats {
33 #[serde(rename = "1")]
34 pub count: u64,
35 #[serde(rename = "2")]
36 pub mean: f64,
37 #[serde(rename = "3")]
38 pub m2: f64,
39 #[serde(rename = "4")]
40 pub min: f64,
41 #[serde(rename = "5")]
42 pub max: f64,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SessionBehavioralSummary {
47 #[serde(rename = "1")]
49 pub iki_histogram: [f64; 9],
50 #[serde(rename = "2")]
51 pub iki_cv: f64,
52 #[serde(rename = "3")]
54 pub hurst: f64,
55 #[serde(rename = "4")]
56 pub pause_frequency: f64,
57 #[serde(rename = "5")]
58 pub duration_secs: u64,
59 #[serde(rename = "6")]
60 pub keystroke_count: u64,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct BaselineDigest {
65 #[serde(rename = "1")]
66 pub version: u32,
67 #[serde(rename = "2")]
68 pub session_count: u64,
69 #[serde(rename = "3")]
70 pub total_keystrokes: u64,
71 #[serde(rename = "4")]
72 pub iki_stats: StreamingStats,
73 #[serde(rename = "5")]
74 pub cv_stats: StreamingStats,
75 #[serde(rename = "6")]
76 pub hurst_stats: StreamingStats,
77 #[serde(rename = "7")]
78 pub aggregate_iki_histogram: [f64; 9],
79 #[serde(rename = "8")]
80 pub pause_stats: StreamingStats,
81 #[serde(rename = "9", with = "serde_bytes")]
83 pub session_merkle_root: Vec<u8>,
84 #[serde(rename = "10")]
85 pub confidence_tier: ConfidenceTier,
86 #[serde(rename = "11")]
87 pub computed_at: u64,
88 #[serde(rename = "12", with = "serde_bytes")]
90 pub identity_fingerprint: Vec<u8>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct BaselineVerification {
95 #[serde(rename = "1", default, skip_serializing_if = "Option::is_none")]
97 pub digest: Option<BaselineDigest>,
98 #[serde(rename = "2")]
99 pub session_summary: SessionBehavioralSummary,
100 #[serde(
102 rename = "3",
103 default,
104 skip_serializing_if = "Option::is_none",
105 with = "serde_bytes_opt"
106 )]
107 pub digest_signature: Option<Vec<u8>>,
108}
109
110impl Default for SessionBehavioralSummary {
111 fn default() -> Self {
112 Self {
113 iki_histogram: [0.0; 9],
114 iki_cv: 0.0,
115 hurst: 0.5,
116 pause_frequency: 0.0,
117 duration_secs: 0,
118 keystroke_count: 0,
119 }
120 }
121}
122
123mod serde_bytes_opt {
124 use serde::{Deserialize, Deserializer, Serializer};
125
126 pub fn serialize<S>(val: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
127 where
128 S: Serializer,
129 {
130 match val {
131 Some(v) => serde_bytes::serialize(v, serializer),
132 None => serializer.serialize_none(),
133 }
134 }
135
136 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
137 where
138 D: Deserializer<'de>,
139 {
140 Option::<serde_bytes::ByteBuf>::deserialize(deserializer)
141 .map(|opt| opt.map(|buf| buf.into_vec()))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_confidence_tier_from_session_count() {
151 assert_eq!(
152 ConfidenceTier::from_session_count(0),
153 ConfidenceTier::PopulationReference
154 );
155 assert_eq!(
156 ConfidenceTier::from_session_count(4),
157 ConfidenceTier::PopulationReference
158 );
159 assert_eq!(
160 ConfidenceTier::from_session_count(5),
161 ConfidenceTier::Emerging
162 );
163 assert_eq!(
164 ConfidenceTier::from_session_count(10),
165 ConfidenceTier::Established
166 );
167 assert_eq!(
168 ConfidenceTier::from_session_count(20),
169 ConfidenceTier::Mature
170 );
171 assert_eq!(
172 ConfidenceTier::from_session_count(100),
173 ConfidenceTier::Mature
174 );
175 }
176
177 #[test]
178 fn test_baseline_verification_cbor_roundtrip_enrollment() {
179 let summary = SessionBehavioralSummary {
180 iki_histogram: [0.1, 0.2, 0.15, 0.1, 0.1, 0.15, 0.1, 0.05, 0.05],
181 iki_cv: 0.45,
182 hurst: 0.72,
183 pause_frequency: 3.5,
184 duration_secs: 1800,
185 keystroke_count: 5000,
186 };
187
188 let bv = BaselineVerification {
189 digest: None,
190 session_summary: summary,
191 digest_signature: None,
192 };
193
194 let mut buf = Vec::new();
195 ciborium::into_writer(&bv, &mut buf).expect("CBOR encode");
196 let decoded: BaselineVerification = ciborium::from_reader(&buf[..]).expect("CBOR decode");
197
198 assert!(decoded.digest.is_none());
199 assert!((decoded.session_summary.iki_cv - 0.45).abs() < 1e-10);
200 assert_eq!(decoded.session_summary.keystroke_count, 5000);
201 assert!(
202 buf.len() < 200,
203 "Enrollment wire overhead: {} bytes",
204 buf.len()
205 );
206 }
207
208 #[test]
209 fn test_baseline_verification_cbor_roundtrip_with_digest() {
210 let digest = BaselineDigest {
211 version: 1,
212 session_count: 10,
213 total_keystrokes: 50000,
214 iki_stats: StreamingStats {
215 count: 10,
216 mean: 150.0,
217 m2: 500.0,
218 min: 80.0,
219 max: 300.0,
220 },
221 cv_stats: StreamingStats {
222 count: 10,
223 mean: 0.45,
224 m2: 0.02,
225 min: 0.3,
226 max: 0.6,
227 },
228 hurst_stats: StreamingStats {
229 count: 10,
230 mean: 0.72,
231 m2: 0.01,
232 min: 0.65,
233 max: 0.8,
234 },
235 aggregate_iki_histogram: [0.1, 0.2, 0.15, 0.1, 0.1, 0.15, 0.1, 0.05, 0.05],
236 pause_stats: StreamingStats {
237 count: 10,
238 mean: 3.5,
239 m2: 2.0,
240 min: 1.0,
241 max: 7.0,
242 },
243 session_merkle_root: vec![0xAA; 32],
244 confidence_tier: ConfidenceTier::Established,
245 computed_at: 1708790400,
246 identity_fingerprint: vec![0xBB; 32],
247 };
248
249 let bv = BaselineVerification {
250 digest: Some(digest),
251 session_summary: SessionBehavioralSummary::default(),
252 digest_signature: Some(vec![0xCC; 64]),
253 };
254
255 let mut buf = Vec::new();
256 ciborium::into_writer(&bv, &mut buf).expect("CBOR encode");
257 let decoded: BaselineVerification = ciborium::from_reader(&buf[..]).expect("CBOR decode");
258
259 let d = decoded.digest.as_ref().unwrap();
260 assert_eq!(d.session_count, 10);
261 assert_eq!(d.confidence_tier, ConfidenceTier::Established);
262 assert_eq!(d.identity_fingerprint, vec![0xBB; 32]);
263 assert_eq!(decoded.digest_signature.as_ref().unwrap().len(), 64);
264 assert!(buf.len() < 600, "Full wire overhead: {} bytes", buf.len());
265 }
266}