Skip to main content

ans_types/
fingerprint.rs

1//! Certificate fingerprint computation and comparison.
2//!
3//! All fingerprint comparisons use constant-time equality to prevent
4//! timing side-channel attacks that could leak fingerprint values.
5
6use crate::error::CryptoError;
7use sha2::{Digest, Sha256};
8use std::fmt;
9use subtle::ConstantTimeEq;
10
11/// SHA-256 certificate fingerprint in `SHA256:<hex>` format.
12///
13/// Equality comparisons are constant-time to prevent timing side-channels.
14#[derive(Clone)]
15pub struct CertFingerprint {
16    bytes: [u8; 32],
17}
18
19impl PartialEq for CertFingerprint {
20    fn eq(&self, other: &Self) -> bool {
21        self.bytes.ct_eq(&other.bytes).into()
22    }
23}
24
25impl Eq for CertFingerprint {}
26
27impl std::hash::Hash for CertFingerprint {
28    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
29        self.bytes.hash(state);
30    }
31}
32
33impl CertFingerprint {
34    /// Compute fingerprint from DER-encoded certificate.
35    pub fn from_der(der: &[u8]) -> Self {
36        let hash = Sha256::digest(der);
37        let mut bytes = [0u8; 32];
38        bytes.copy_from_slice(&hash);
39        Self { bytes }
40    }
41
42    /// Create from raw bytes.
43    pub fn from_bytes(bytes: [u8; 32]) -> Self {
44        Self { bytes }
45    }
46
47    /// Parse from `SHA256:<hex>` string format.
48    ///
49    /// # Errors
50    /// Returns `CryptoError::InvalidFingerprint` if the format is invalid.
51    pub fn parse(s: &str) -> Result<Self, CryptoError> {
52        // Handle both "SHA256:" and "sha256:" prefixes
53        let hex_str = s
54            .strip_prefix("SHA256:")
55            .or_else(|| s.strip_prefix("sha256:"))
56            .ok_or_else(|| CryptoError::InvalidFingerprint {
57                fingerprint: s.to_string(),
58            })?;
59
60        let bytes = hex::decode(hex_str).map_err(|_| CryptoError::InvalidFingerprint {
61            fingerprint: s.to_string(),
62        })?;
63
64        if bytes.len() != 32 {
65            return Err(CryptoError::InvalidFingerprint {
66                fingerprint: s.to_string(),
67            });
68        }
69
70        let mut arr = [0u8; 32];
71        arr.copy_from_slice(&bytes);
72        Ok(Self { bytes: arr })
73    }
74
75    /// Get the raw bytes.
76    pub fn as_bytes(&self) -> &[u8; 32] {
77        &self.bytes
78    }
79
80    /// Format as hex string without prefix.
81    pub fn to_hex(&self) -> String {
82        hex::encode(self.bytes)
83    }
84
85    /// Check if this fingerprint matches a string representation.
86    pub fn matches(&self, other: &str) -> bool {
87        match Self::parse(other) {
88            Ok(parsed) => self == &parsed,
89            Err(_) => false,
90        }
91    }
92}
93
94impl fmt::Display for CertFingerprint {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(f, "SHA256:{}", hex::encode(self.bytes))
97    }
98}
99
100impl fmt::Debug for CertFingerprint {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "CertFingerprint({self})")
103    }
104}
105
106impl std::str::FromStr for CertFingerprint {
107    type Err = CryptoError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        Self::parse(s)
111    }
112}
113
114impl serde::Serialize for CertFingerprint {
115    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
116        serializer.serialize_str(&self.to_string())
117    }
118}
119
120impl<'de> serde::Deserialize<'de> for CertFingerprint {
121    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
122        let s = String::deserialize(deserializer)?;
123        Self::parse(&s).map_err(serde::de::Error::custom)
124    }
125}
126
127impl TryFrom<&str> for CertFingerprint {
128    type Error = CryptoError;
129
130    fn try_from(s: &str) -> Result<Self, Self::Error> {
131        Self::parse(s)
132    }
133}
134
135impl TryFrom<String> for CertFingerprint {
136    type Error = CryptoError;
137
138    fn try_from(s: String) -> Result<Self, Self::Error> {
139        Self::parse(&s)
140    }
141}
142
143#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_fingerprint_from_der() {
150        let der = b"test certificate data";
151        let fp = CertFingerprint::from_der(der);
152        assert_eq!(fp.as_bytes().len(), 32);
153    }
154
155    #[test]
156    fn test_fingerprint_parse_uppercase() {
157        let fp_str = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
158        let fp = CertFingerprint::parse(fp_str).unwrap();
159        assert_eq!(fp.to_string(), fp_str);
160    }
161
162    #[test]
163    fn test_fingerprint_parse_lowercase() {
164        let fp_str = "sha256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
165        let fp = CertFingerprint::parse(fp_str).unwrap();
166        // Output is always uppercase prefix
167        assert_eq!(
168            fp.to_string(),
169            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904"
170        );
171    }
172
173    #[test]
174    fn test_fingerprint_roundtrip() {
175        let original = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
176        let fp = CertFingerprint::parse(original).unwrap();
177        let formatted = fp.to_string();
178        let reparsed = CertFingerprint::parse(&formatted).unwrap();
179        assert_eq!(fp, reparsed);
180    }
181
182    #[test]
183    fn test_fingerprint_matches() {
184        let fp = CertFingerprint::parse(
185            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
186        )
187        .unwrap();
188
189        assert!(
190            fp.matches("SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
191        );
192        assert!(
193            fp.matches("sha256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
194        );
195        assert!(
196            !fp.matches("SHA256:0000000000000000000000000000000000000000000000000000000000000000")
197        );
198    }
199
200    #[test]
201    fn test_fingerprint_invalid_format() {
202        assert!(CertFingerprint::parse("MD5:abc123").is_err());
203        assert!(CertFingerprint::parse("SHA256:toolshort").is_err());
204        assert!(CertFingerprint::parse("invalid").is_err());
205        assert!(CertFingerprint::parse("SHA256:gggg").is_err()); // invalid hex
206    }
207
208    #[test]
209    fn test_fingerprint_equality() {
210        let fp1 = CertFingerprint::parse(
211            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
212        )
213        .unwrap();
214        let fp2 = CertFingerprint::parse(
215            "sha256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
216        )
217        .unwrap();
218        assert_eq!(fp1, fp2);
219    }
220
221    #[test]
222    fn test_to_hex() {
223        let fp = CertFingerprint::parse(
224            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
225        )
226        .unwrap();
227        assert_eq!(
228            fp.to_hex(),
229            "e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904"
230        );
231    }
232
233    #[test]
234    fn test_from_bytes() {
235        let bytes = [0xab_u8; 32];
236        let fp = CertFingerprint::from_bytes(bytes);
237        assert_eq!(fp.as_bytes(), &bytes);
238    }
239
240    #[test]
241    fn test_debug_formatting() {
242        let fp = CertFingerprint::from_bytes([0u8; 32]);
243        let dbg = format!("{fp:?}");
244        assert!(dbg.starts_with("CertFingerprint(SHA256:"));
245    }
246
247    #[test]
248    fn test_from_str_trait() {
249        let fp: CertFingerprint =
250            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904"
251                .parse()
252                .unwrap();
253        assert_eq!(fp.as_bytes().len(), 32);
254    }
255
256    #[test]
257    fn test_try_from_str() {
258        let fp = CertFingerprint::try_from(
259            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
260        )
261        .unwrap();
262        assert_eq!(fp.as_bytes().len(), 32);
263    }
264
265    #[test]
266    fn test_try_from_string() {
267        let fp = CertFingerprint::try_from(
268            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904".to_string(),
269        )
270        .unwrap();
271        assert_eq!(fp.as_bytes().len(), 32);
272    }
273
274    #[test]
275    fn test_serde_deserialization_error() {
276        let result = serde_json::from_str::<CertFingerprint>(r#""MD5:abc""#);
277        assert!(result.is_err());
278    }
279
280    #[test]
281    fn test_matches_with_invalid_input() {
282        let fp = CertFingerprint::from_bytes([0u8; 32]);
283        assert!(!fp.matches("invalid-no-prefix"));
284        assert!(!fp.matches(""));
285    }
286
287    #[test]
288    fn test_hash_consistency() {
289        use std::collections::HashSet;
290        let fp1 = CertFingerprint::parse(
291            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
292        )
293        .unwrap();
294        let fp2 = fp1.clone();
295        let mut set = HashSet::new();
296        set.insert(fp1);
297        assert!(set.contains(&fp2));
298    }
299
300    #[test]
301    fn test_serde_roundtrip() {
302        let fp = CertFingerprint::parse(
303            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
304        )
305        .unwrap();
306        let json = serde_json::to_string(&fp).unwrap();
307        let deserialized: CertFingerprint = serde_json::from_str(&json).unwrap();
308        assert_eq!(fp, deserialized);
309    }
310}