rc_crypto/certificate/fingerprint.rs
1// Copyright 2026-Present Datadog, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Display;
16
17use aws_lc_rs::digest::{SHA256, digest};
18use x509_parser::prelude::X509Certificate;
19
20use crate::{cached_string_repr::CachedStringRepr, hex::colon_string};
21
22/// The byte length of a fingerprint is always 32 for SHA256 digests.
23const FINGERPRINT_LEN: usize = aws_lc_rs::digest::SHA256_OUTPUT_LEN;
24
25/// A [`Fingerprint`] is a deterministic, fixed-length SHA-256 hash that
26/// uniquely identifies a single issued [`Certificate`].
27///
28/// The fingerprint is constructed by hashing the (DER encoded) bytes of a
29/// [`Certificate`], a process that is lightly described in [RFC 4387 § 2.2] as
30/// a `certHash`. Unlike the RFC, we use SHA-256 as the hash instead of SHA-1 (a
31/// common modification for newer systems, including new versions of OpenSSL).
32///
33/// A [`Fingerprint`] should be used when checking if two certificates are
34/// identical.
35///
36/// [`Certificate`]: super::Certificate
37/// [RFC 4387 § 2.2]: https://datatracker.ietf.org/doc/html/rfc4387#section-2.2
38#[derive(Debug, PartialEq, Eq, Hash, Clone)]
39pub struct Fingerprint {
40 digest: [u8; FINGERPRINT_LEN],
41
42 /// A lazily-rendered string representation of `digest`.
43 ///
44 /// See [`Self::as_hex_str()`] for initialisation.
45 rendered: CachedStringRepr,
46}
47
48impl Fingerprint {
49 /// Render the [`Fingerprint`] as a lowercase hex string delimited by
50 /// colons in the style of OpenSSL.
51 ///
52 /// Example: `cc:cb:0f:63:f1:63:5e:f1:0e:26:e8:82:f7:7a:6e:f9`
53 ///
54 /// This value is lazily rendered and cached for reuse.
55 pub fn as_hex_str(&self) -> &str {
56 self.rendered.get_or_init(|| colon_string(self.as_bytes()))
57 }
58
59 /// Return the raw fingerprint digest bytes.
60 pub fn as_bytes(&self) -> &[u8] {
61 &self.digest
62 }
63}
64
65impl Display for Fingerprint {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.write_str(self.as_hex_str())
68 }
69}
70
71impl<'a> From<&'a X509Certificate<'a>> for Fingerprint {
72 fn from(cert: &'a X509Certificate<'a>) -> Self {
73 let hash = digest(&SHA256, cert.as_raw());
74
75 Self {
76 digest: hash
77 .as_ref()
78 .try_into()
79 .expect("sha256 digest length is fixed"),
80 rendered: Default::default(),
81 }
82 }
83}
84
85/// Render a [`Fingerprint`] as an encoded string in structured logging.
86impl valuable::Valuable for Fingerprint {
87 fn as_value(&self) -> valuable::Value<'_> {
88 valuable::Value::String(self.as_hex_str())
89 }
90
91 fn visit(&self, visit: &mut dyn valuable::Visit) {
92 visit.visit_value(self.as_value());
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn test_fixture() {
102 let input = [
103 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
104 25, 26, 27, 28, 29, 30, 31, 32,
105 ];
106 let f = Fingerprint {
107 digest: input,
108 rendered: Default::default(),
109 };
110
111 assert_eq!(f.as_bytes(), &input);
112
113 let want = "01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:14:15:16:17:18:19:1a:1b:1c:1d:1e:1f:20";
114 assert_eq!(f.as_hex_str(), want);
115 assert_eq!(f.to_string(), want);
116 }
117}