1use crate::error::CryptoError;
7use sha2::{Digest, Sha256};
8use std::fmt;
9use subtle::ConstantTimeEq;
10
11#[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 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 pub fn from_bytes(bytes: [u8; 32]) -> Self {
44 Self { bytes }
45 }
46
47 pub fn parse(s: &str) -> Result<Self, CryptoError> {
52 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 pub fn as_bytes(&self) -> &[u8; 32] {
77 &self.bytes
78 }
79
80 pub fn to_hex(&self) -> String {
82 hex::encode(self.bytes)
83 }
84
85 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 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()); }
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}