use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
pub trait TofuStore: Send + Sync {
fn fingerprint(&self, host: &str) -> Option<[u8; 32]>;
fn pin(&self, host: &str, fingerprint: [u8; 32]);
}
#[derive(Debug, Default)]
pub struct InMemoryTofu {
pins: Mutex<HashMap<String, [u8; 32]>>,
}
impl InMemoryTofu {
pub fn new() -> Self {
Self::default()
}
}
impl TofuStore for InMemoryTofu {
fn fingerprint(&self, host: &str) -> Option<[u8; 32]> {
self.pins.lock().unwrap().get(host).copied()
}
fn pin(&self, host: &str, fingerprint: [u8; 32]) {
self.pins.lock().unwrap().insert(host.to_string(), fingerprint);
}
}
#[derive(Debug, Default)]
pub struct PermissiveTofu;
impl TofuStore for PermissiveTofu {
fn fingerprint(&self, _host: &str) -> Option<[u8; 32]> {
None
}
fn pin(&self, _host: &str, _fingerprint: [u8; 32]) {}
}
static TRUST_STORE: RwLock<Option<Arc<dyn TofuStore>>> = RwLock::new(None);
pub fn set_trust_store(store: Arc<dyn TofuStore>) {
*TRUST_STORE.write().unwrap() = Some(store);
}
pub(crate) fn trust_store() -> Arc<dyn TofuStore> {
TRUST_STORE
.read()
.unwrap()
.clone()
.unwrap_or_else(|| Arc::new(PermissiveTofu))
}
pub(crate) fn fingerprint(cert_der: &[u8]) -> [u8; 32] {
let digest = ring::digest::digest(&ring::digest::SHA256, cert_der);
let mut out = [0u8; 32];
out.copy_from_slice(digest.as_ref());
out
}
pub(crate) fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_memory_pins_and_recalls_per_host() {
let tofu = InMemoryTofu::new();
assert!(tofu.fingerprint("a.example").is_none());
tofu.pin("a.example", [1u8; 32]);
assert_eq!(tofu.fingerprint("a.example"), Some([1u8; 32]));
assert!(tofu.fingerprint("b.example").is_none(), "pins are per host");
}
#[test]
fn permissive_never_pins() {
let tofu = PermissiveTofu;
tofu.pin("a.example", [1u8; 32]);
assert!(
tofu.fingerprint("a.example").is_none(),
"permissive store treats every visit as first contact"
);
}
#[test]
fn fingerprint_is_stable_and_hex_round_trips() {
assert_eq!(fingerprint(b"cert"), fingerprint(b"cert"));
assert_ne!(fingerprint(b"cert"), fingerprint(b"other"));
assert_eq!(hex(&[0xde, 0xad, 0x01]), "dead01");
}
}