Skip to main content

rc_crypto/certificate/id/
cert_id.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 smallvec::SmallVec;
18use thiserror::Error;
19use valuable::Valuable;
20use x509_parser::prelude::{ParsedExtension, X509Certificate};
21
22use crate::{
23    cached_string_repr::CachedStringRepr, certificate::id::DangerousComparableId, hex::colon_string,
24};
25
26/// No Subject Key Identifier extension was found in the certificate.
27#[derive(Debug, Error)]
28#[error("no Subject Key Identifier found")]
29pub struct ErrorNoSKI;
30
31/// Certificate ID was an invalid length.
32#[derive(Debug, Error)]
33#[error("certificate ID is an invalid length")]
34pub struct InvalidLength {
35    actual_len: usize,
36}
37
38/// Error extracting a [`CertId`] from an [`X509Certificate`].
39#[derive(Debug, Error)]
40pub enum InvalidCertId {
41    /// No Subject Key Identifier extension was found in the certificate.
42    #[error(transparent)]
43    NoSKI(#[from] ErrorNoSKI),
44
45    /// Certificate ID was an invalid length.
46    #[error(transparent)]
47    InvalidLength(#[from] InvalidLength),
48}
49
50/// An opaque identifier for the [`Certificate`] this value was extracted from.
51///
52/// This is an untrusted value, and can be set to anything the cert issuer
53/// wishes. Derived values such as a [`KeyId`] or certificate [`Fingerprint`])
54/// SHOULD be preferred for general use. The
55/// [`CertId::into_dangerous_comparable()`] method can be used to obtain a
56/// handle that implements [`PartialEq`].
57///
58/// The [`CertId`] is a user friendly rename of the [Subject Key Identifier]
59/// (commonly abbreviated SKI) within an X509 certificate. While the SKI claims
60/// to be an identifier of the key in the cert, it does not always identify the
61/// key material specifically (`hash(cert_dn + cert_serial)` is not uncommon).
62///
63/// [`KeyId`]: crate::keys::KeyId
64/// [`Certificate`]: crate::certificate::Certificate
65/// [`Fingerprint`]: crate::certificate::Fingerprint
66/// [Subject Key Identifier]:
67///     https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2
68#[derive(Debug, Hash, Clone)] // NOTE: no PartialEq - not trusted, do not compare.
69pub struct CertId {
70    bytes: SmallVec<[u8; 20]>,
71
72    /// A lazily-rendered string representation of `bytes`.
73    ///
74    /// See [`Self::as_hex_str()`] for initialisation.
75    rendered: CachedStringRepr,
76}
77
78impl CertId {
79    /// Minimum bytes CertId MUST be.
80    const MIN_LENGTH: usize = 16;
81
82    /// Render this value following the conventions of OpenSSL's colon-delimited
83    /// string representation.
84    pub fn as_hex_str(&self) -> &str {
85        self.rendered.get_or_init(|| colon_string(&self.bytes))
86    }
87
88    /// Return the raw bytes for this ID (private to this module).
89    pub(super) fn as_bytes(&self) -> &[u8] {
90        &self.bytes
91    }
92
93    /// Obtain a borrowed wrapper type that has a [`PartialEq`] implementation,
94    /// allowing this value to be compared to other values with the correctness
95    /// caveats documented for this type.
96    pub fn as_dangerous_comparable(&self) -> DangerousComparableId<'_, Self> {
97        DangerousComparableId::from(self)
98    }
99}
100
101impl Display for CertId {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.write_str(self.as_hex_str())
104    }
105}
106
107impl From<CertId> for Vec<u8> {
108    fn from(id: CertId) -> Vec<u8> {
109        id.bytes.into_vec()
110    }
111}
112
113impl TryFrom<&[u8]> for CertId {
114    type Error = InvalidLength;
115
116    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
117        if value.len() < Self::MIN_LENGTH {
118            return Err(InvalidLength {
119                actual_len: value.len(),
120            });
121        }
122        Ok(Self {
123            bytes: SmallVec::from_slice(value),
124            rendered: Default::default(),
125        })
126    }
127}
128
129impl TryFrom<Vec<u8>> for CertId {
130    type Error = InvalidLength;
131
132    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
133        if value.len() < Self::MIN_LENGTH {
134            return Err(InvalidLength {
135                actual_len: value.len(),
136            });
137        }
138        Ok(Self {
139            bytes: SmallVec::from_vec(value),
140            rendered: Default::default(),
141        })
142    }
143}
144
145impl<'a> TryFrom<&X509Certificate<'a>> for CertId {
146    type Error = InvalidCertId;
147
148    fn try_from(cert: &X509Certificate<'a>) -> Result<Self, Self::Error> {
149        let bytes = cert
150            .iter_extensions()
151            .find_map(|v| match v.parsed_extension() {
152                ParsedExtension::SubjectKeyIdentifier(ski) => Some(ski.0),
153                _ => None,
154            })
155            .ok_or(ErrorNoSKI)?;
156        Ok(CertId::try_from(bytes)?)
157    }
158}
159
160impl Valuable for CertId {
161    fn as_value(&self) -> valuable::Value<'_> {
162        valuable::Value::String(self.as_hex_str())
163    }
164
165    fn visit(&self, visit: &mut dyn valuable::Visit) {
166        visit.visit_value(self.as_value());
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use rc_x509_test_helpers::assert_valuable_repr;
173    use static_assertions::assert_not_impl_any;
174    use x509_parser::prelude::FromDer;
175
176    use super::*;
177
178    use crate::certificate::tests::cert_fixture;
179
180    const FIXTURE_SKI_STR: &str = "dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e";
181
182    // Why: a CertId can be set to anything by the issuer, making it unreliable
183    // as a unique identifier, and should not be used to compare two
184    // certificates for equality (outside of chain building which is then
185    // cryptographically verified).
186    assert_not_impl_any!(CertId: PartialEq, Eq);
187
188    fn fixture_ski() -> CertId {
189        let der = cert_fixture().as_der();
190        let cert = X509Certificate::from_der(&der).expect("valid DER").1;
191
192        CertId::try_from(&cert).expect("extract SKI")
193    }
194
195    #[test]
196    fn test_fixture() {
197        let aki = fixture_ski();
198
199        assert_eq!(aki.as_hex_str(), FIXTURE_SKI_STR,);
200    }
201
202    #[test]
203    fn test_valuable_repr() {
204        let aki = fixture_ski();
205
206        assert_valuable_repr(&aki, FIXTURE_SKI_STR);
207    }
208
209    #[test]
210    fn test_danger_eq() {
211        let ski = fixture_ski();
212
213        assert_eq!(ski.as_dangerous_comparable(), ski);
214    }
215}