Skip to main content

ferrox_security/
fingerprint.rs

1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3
4/// Zero-Trust Device & Client Fingerprint Binder for PASETO v4 Session Tokens
5pub struct ClientFingerprint;
6
7impl ClientFingerprint {
8    /// Computes a deterministic 64-bit fingerprint hash for a client HTTP session
9    pub fn compute_fingerprint(user_agent: &str, accept_language: &str, subnet_ip: &str) -> u64 {
10        let mut hasher = DefaultHasher::new();
11        user_agent.hash(&mut hasher);
12        accept_language.hash(&mut hasher);
13
14        // Normalize IP subnet (e.g. 192.168.1.xxx -> 192.168.1.0)
15        let subnet = if let Some(idx) = subnet_ip.rfind('.') {
16            &subnet_ip[..idx]
17        } else {
18            subnet_ip
19        };
20        subnet.hash(&mut hasher);
21
22        hasher.finish()
23    }
24
25    /// Verifies if a token binding fingerprint matches current client request
26    pub fn verify_binding(bound_fingerprint: u64, current_user_agent: &str, current_lang: &str, current_ip: &str) -> bool {
27        let current_fp = Self::compute_fingerprint(current_user_agent, current_lang, current_ip);
28        bound_fingerprint == current_fp
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    #[test]
37    fn test_client_fingerprint_binding() {
38        let fp = ClientFingerprint::compute_fingerprint("Mozilla/5.0", "it-IT", "192.168.1.50");
39        assert!(ClientFingerprint::verify_binding(fp, "Mozilla/5.0", "it-IT", "192.168.1.99")); // Same subnet
40        assert!(!ClientFingerprint::verify_binding(fp, "MaliciousBot/1.0", "it-IT", "192.168.1.50"));
41    }
42}