1use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11
12use parking_lot::RwLock;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
20pub struct CertFingerprint(pub String);
21
22impl CertFingerprint {
23 pub fn from_bytes(bytes: &[u8]) -> Self {
29 let hash = fnv1a_hash_bytes(bytes);
30 Self(format!(
31 "{:016x}{:016x}{:016x}{:016x}",
32 hash,
33 hash ^ 0xDEAD_BEEF_DEAD_BEEF_u64,
34 hash ^ 0xBEEF_CAFE_BEEF_CAFE_u64,
35 hash ^ 0xCAFE_DEAD_CAFE_DEAD_u64,
36 ))
37 }
38
39 pub fn as_str(&self) -> &str {
41 &self.0
42 }
43
44 pub fn len(&self) -> usize {
46 self.0.len()
47 }
48
49 pub fn is_valid_format(&self) -> bool {
51 self.0.len() == 64 && self.0.chars().all(|c| c.is_ascii_hexdigit())
52 }
53
54 pub fn is_empty(&self) -> bool {
56 self.0.is_empty()
57 }
58}
59
60fn fnv1a_hash_bytes(data: &[u8]) -> u64 {
62 let mut hash: u64 = 14_695_981_039_346_656_037;
63 for &byte in data {
64 hash ^= byte as u64;
65 hash = hash.wrapping_mul(1_099_511_628_211);
66 }
67 hash
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
74pub enum PinPolicy {
75 #[default]
77 TrustOnFirstUse,
78 Strict,
80 Observe,
82}
83
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
88pub struct PeerCertPin {
89 pub peer_id: String,
91 pub fingerprint: CertFingerprint,
93 pub pinned_at_ms: u64,
95 pub last_seen_ms: u64,
97 pub connection_count: u64,
99 pub tofu: bool,
101}
102
103impl PeerCertPin {
104 pub fn new(
106 peer_id: impl Into<String>,
107 fingerprint: CertFingerprint,
108 now_ms: u64,
109 tofu: bool,
110 ) -> Self {
111 Self {
112 peer_id: peer_id.into(),
113 fingerprint,
114 pinned_at_ms: now_ms,
115 last_seen_ms: now_ms,
116 connection_count: 0,
117 tofu,
118 }
119 }
120
121 pub fn record_connection(&mut self, now_ms: u64) {
123 self.last_seen_ms = now_ms;
124 self.connection_count += 1;
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
132pub enum VerificationResult {
133 Accepted,
135 PinnedNew { peer_id: String },
137 Mismatch { expected: String, got: String },
139 UnknownPeer { peer_id: String },
141}
142
143impl VerificationResult {
144 pub fn is_accepted(&self) -> bool {
146 matches!(
147 self,
148 VerificationResult::Accepted | VerificationResult::PinnedNew { .. }
149 )
150 }
151}
152
153pub struct CertPinStore {
165 pins: RwLock<HashMap<String, PeerCertPin>>,
166 policy: PinPolicy,
167 mismatch_count: AtomicU64,
168 tofu_count: AtomicU64,
169}
170
171impl CertPinStore {
172 pub fn new(policy: PinPolicy) -> Arc<Self> {
174 Arc::new(Self {
175 pins: RwLock::new(HashMap::new()),
176 policy,
177 mismatch_count: AtomicU64::new(0),
178 tofu_count: AtomicU64::new(0),
179 })
180 }
181
182 pub fn pin(&self, peer_id: impl Into<String>, fingerprint: CertFingerprint, now_ms: u64) {
186 let peer_id = peer_id.into();
187 let mut guard = self.pins.write();
188 guard
189 .entry(peer_id.clone())
190 .or_insert_with(|| PeerCertPin::new(peer_id, fingerprint, now_ms, false));
191 }
192
193 pub fn unpin(&self, peer_id: &str) -> bool {
197 self.pins.write().remove(peer_id).is_some()
198 }
199
200 pub fn verify(
202 &self,
203 peer_id: &str,
204 fingerprint: &CertFingerprint,
205 now_ms: u64,
206 ) -> VerificationResult {
207 match self.policy {
208 PinPolicy::TrustOnFirstUse => self.verify_tofu(peer_id, fingerprint, now_ms),
209 PinPolicy::Strict => self.verify_strict(peer_id, fingerprint, now_ms),
210 PinPolicy::Observe => self.verify_observe(peer_id, fingerprint, now_ms),
211 }
212 }
213
214 fn verify_tofu(
215 &self,
216 peer_id: &str,
217 fingerprint: &CertFingerprint,
218 now_ms: u64,
219 ) -> VerificationResult {
220 {
222 let guard = self.pins.read();
223 if let Some(pin) = guard.get(peer_id) {
224 if pin.fingerprint == *fingerprint {
225 drop(guard);
226 let mut wguard = self.pins.write();
228 if let Some(p) = wguard.get_mut(peer_id) {
229 p.record_connection(now_ms);
230 }
231 return VerificationResult::Accepted;
232 } else {
233 self.mismatch_count.fetch_add(1, Ordering::Relaxed);
234 return VerificationResult::Mismatch {
235 expected: pin.fingerprint.as_str().to_owned(),
236 got: fingerprint.as_str().to_owned(),
237 };
238 }
239 }
240 }
241 let mut new_pin = PeerCertPin::new(peer_id, fingerprint.clone(), now_ms, true);
243 new_pin.record_connection(now_ms);
244 self.pins.write().insert(peer_id.to_owned(), new_pin);
245 self.tofu_count.fetch_add(1, Ordering::Relaxed);
246 VerificationResult::PinnedNew {
247 peer_id: peer_id.to_owned(),
248 }
249 }
250
251 fn verify_strict(
252 &self,
253 peer_id: &str,
254 fingerprint: &CertFingerprint,
255 now_ms: u64,
256 ) -> VerificationResult {
257 let guard = self.pins.read();
258 if let Some(pin) = guard.get(peer_id) {
259 if pin.fingerprint == *fingerprint {
260 drop(guard);
261 let mut wguard = self.pins.write();
262 if let Some(p) = wguard.get_mut(peer_id) {
263 p.record_connection(now_ms);
264 }
265 VerificationResult::Accepted
266 } else {
267 self.mismatch_count.fetch_add(1, Ordering::Relaxed);
268 VerificationResult::Mismatch {
269 expected: pin.fingerprint.as_str().to_owned(),
270 got: fingerprint.as_str().to_owned(),
271 }
272 }
273 } else {
274 VerificationResult::UnknownPeer {
275 peer_id: peer_id.to_owned(),
276 }
277 }
278 }
279
280 fn verify_observe(
281 &self,
282 peer_id: &str,
283 fingerprint: &CertFingerprint,
284 _now_ms: u64,
285 ) -> VerificationResult {
286 let guard = self.pins.read();
287 if let Some(pin) = guard.get(peer_id) {
288 if pin.fingerprint != *fingerprint {
289 self.mismatch_count.fetch_add(1, Ordering::Relaxed);
290 }
291 }
292 VerificationResult::Accepted
293 }
294
295 pub fn get_pin(&self, peer_id: &str) -> Option<PeerCertPin> {
297 self.pins.read().get(peer_id).cloned()
298 }
299
300 pub fn pinned_peers(&self) -> Vec<String> {
302 self.pins.read().keys().cloned().collect()
303 }
304
305 pub fn pin_count(&self) -> usize {
307 self.pins.read().len()
308 }
309
310 pub fn mismatch_count(&self) -> u64 {
312 self.mismatch_count.load(Ordering::Relaxed)
313 }
314
315 pub fn tofu_count(&self) -> u64 {
317 self.tofu_count.load(Ordering::Relaxed)
318 }
319
320 pub fn export_pins(&self) -> Vec<PeerCertPin> {
322 self.pins.read().values().cloned().collect()
323 }
324
325 pub fn import_pins(&self, pins: Vec<PeerCertPin>) {
327 let mut guard = self.pins.write();
328 for pin in pins {
329 guard.entry(pin.peer_id.clone()).or_insert(pin);
330 }
331 }
332}
333
334#[cfg(test)]
337mod tests {
338 use super::*;
339
340 fn now_ms() -> u64 {
341 1_700_000_000_000
342 }
343
344 fn fp(seed: &[u8]) -> CertFingerprint {
345 CertFingerprint::from_bytes(seed)
346 }
347
348 #[test]
351 fn test_fingerprint_from_bytes_format() {
352 let f = fp(b"hello world");
353 assert_eq!(f.len(), 64, "fingerprint must be 64 hex chars");
354 assert!(f.is_valid_format());
355 }
356
357 #[test]
358 fn test_fingerprint_is_valid_format() {
359 let valid = CertFingerprint("a".repeat(64));
361 assert!(valid.is_valid_format());
362
363 let too_short = CertFingerprint("abc".to_owned());
364 assert!(!too_short.is_valid_format());
365
366 let non_hex = CertFingerprint("z".repeat(64));
367 assert!(!non_hex.is_valid_format());
368 }
369
370 #[test]
371 fn test_fingerprint_deterministic() {
372 let a = fp(b"deterministic input");
373 let b = fp(b"deterministic input");
374 assert_eq!(a, b);
375
376 let c = fp(b"different input");
377 assert_ne!(a, c);
378 }
379
380 #[test]
383 fn test_pin_and_get() {
384 let store = CertPinStore::new(PinPolicy::Strict);
385 let f = fp(b"peer-cert");
386
387 store.pin("peer1", f.clone(), now_ms());
388
389 let pin = store.get_pin("peer1").expect("pin should exist");
390 assert_eq!(pin.peer_id, "peer1");
391 assert_eq!(pin.fingerprint, f);
392 assert!(!pin.tofu);
393 }
394
395 #[test]
396 fn test_unpin() {
397 let store = CertPinStore::new(PinPolicy::Strict);
398 store.pin("peer1", fp(b"x"), now_ms());
399
400 assert!(store.unpin("peer1"));
401 assert!(!store.unpin("peer1")); assert!(store.get_pin("peer1").is_none());
403 }
404
405 #[test]
408 fn test_verify_tofu_new_peer() {
409 let store = CertPinStore::new(PinPolicy::TrustOnFirstUse);
410 let f = fp(b"new-peer-cert");
411
412 let result = store.verify("peer1", &f, now_ms());
413 assert!(
414 matches!(result, VerificationResult::PinnedNew { ref peer_id } if peer_id == "peer1")
415 );
416 assert!(result.is_accepted());
417 assert_eq!(store.pin_count(), 1);
418 }
419
420 #[test]
421 fn test_verify_tofu_known_peer_match() {
422 let store = CertPinStore::new(PinPolicy::TrustOnFirstUse);
423 let f = fp(b"known-cert");
424
425 store.pin("peer1", f.clone(), now_ms());
427
428 let result = store.verify("peer1", &f, now_ms() + 1);
429 assert_eq!(result, VerificationResult::Accepted);
430 }
431
432 #[test]
433 fn test_verify_tofu_known_peer_mismatch() {
434 let store = CertPinStore::new(PinPolicy::TrustOnFirstUse);
435 store.pin("peer1", fp(b"original"), now_ms());
436
437 let result = store.verify("peer1", &fp(b"different"), now_ms() + 1);
438 assert!(matches!(result, VerificationResult::Mismatch { .. }));
439 assert!(!result.is_accepted());
440 assert_eq!(store.mismatch_count(), 1);
441 }
442
443 #[test]
446 fn test_verify_strict_unknown_peer() {
447 let store = CertPinStore::new(PinPolicy::Strict);
448
449 let result = store.verify("unknown", &fp(b"anything"), now_ms());
450 assert!(
451 matches!(result, VerificationResult::UnknownPeer { ref peer_id } if peer_id == "unknown")
452 );
453 assert!(!result.is_accepted());
454 }
455
456 #[test]
457 fn test_verify_strict_known_peer() {
458 let store = CertPinStore::new(PinPolicy::Strict);
459 let f = fp(b"strict-cert");
460 store.pin("peer2", f.clone(), now_ms());
461
462 let result = store.verify("peer2", &f, now_ms() + 1);
463 assert_eq!(result, VerificationResult::Accepted);
464 }
465
466 #[test]
469 fn test_verify_observe_always_accepted() {
470 let store = CertPinStore::new(PinPolicy::Observe);
471
472 let r1 = store.verify("anyone", &fp(b"cert"), now_ms());
474 assert_eq!(r1, VerificationResult::Accepted);
475
476 store.pin("peer3", fp(b"original"), now_ms());
478 let r2 = store.verify("peer3", &fp(b"changed"), now_ms() + 1);
479 assert_eq!(r2, VerificationResult::Accepted);
480 }
481
482 #[test]
485 fn test_mismatch_count_increments() {
486 let store = CertPinStore::new(PinPolicy::TrustOnFirstUse);
487 store.pin("peer1", fp(b"a"), now_ms());
488
489 store.verify("peer1", &fp(b"b"), now_ms() + 1);
490 store.verify("peer1", &fp(b"c"), now_ms() + 2);
491 assert_eq!(store.mismatch_count(), 2);
492 }
493
494 #[test]
495 fn test_tofu_count_increments() {
496 let store = CertPinStore::new(PinPolicy::TrustOnFirstUse);
497
498 store.verify("peer1", &fp(b"cert1"), now_ms());
499 store.verify("peer2", &fp(b"cert2"), now_ms());
500 assert_eq!(store.tofu_count(), 2);
501 }
502
503 #[test]
506 fn test_export_import_roundtrip() {
507 let store_a = CertPinStore::new(PinPolicy::Strict);
508 store_a.pin("peer1", fp(b"alpha"), now_ms());
509 store_a.pin("peer2", fp(b"beta"), now_ms());
510
511 let exported = store_a.export_pins();
512 assert_eq!(exported.len(), 2);
513
514 let store_b = CertPinStore::new(PinPolicy::Strict);
515 store_b.import_pins(exported);
516 assert_eq!(store_b.pin_count(), 2);
517
518 let pin = store_b.get_pin("peer1").expect("peer1 must be present");
519 assert_eq!(pin.fingerprint, fp(b"alpha"));
520
521 let mut altered = store_b.export_pins();
523 for p in &mut altered {
524 p.fingerprint = fp(b"tampered");
525 }
526 store_b.import_pins(altered);
527 let pin_after = store_b
528 .get_pin("peer1")
529 .expect("peer1 must still be present");
530 assert_eq!(
531 pin_after.fingerprint,
532 fp(b"alpha"),
533 "existing pin must not be overwritten"
534 );
535 }
536
537 #[test]
540 fn test_verification_result_is_accepted() {
541 assert!(VerificationResult::Accepted.is_accepted());
542 assert!(VerificationResult::PinnedNew {
543 peer_id: "x".into()
544 }
545 .is_accepted());
546 assert!(!VerificationResult::Mismatch {
547 expected: "a".into(),
548 got: "b".into()
549 }
550 .is_accepted());
551 assert!(!VerificationResult::UnknownPeer {
552 peer_id: "y".into()
553 }
554 .is_accepted());
555 }
556
557 #[test]
560 fn test_pinned_peers_list() {
561 let store = CertPinStore::new(PinPolicy::Strict);
562 store.pin("alice", fp(b"a"), now_ms());
563 store.pin("bob", fp(b"b"), now_ms());
564
565 let mut peers = store.pinned_peers();
566 peers.sort();
567 assert_eq!(peers, vec!["alice", "bob"]);
568 }
569}