Skip to main content

rc_crypto/certificate/
certificate.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 bytes::Bytes;
16use pem::{self as pem_crate, LineEnding};
17use thiserror::Error;
18use valuable::Valuable;
19use x509_parser::{
20    error::{PEMError, X509Error},
21    nom::Parser,
22    pem::Pem,
23    prelude::X509CertificateParser,
24};
25
26use crate::{
27    certificate::{
28        Fingerprint, SerialNumber, Validity,
29        id::{CertId, InvalidCertId, InvalidIssuerCertId, IssuerCertId},
30    },
31    keys::PublicKey,
32};
33
34/// A [`Certificate`] cannot be parsed from the invalid PEM data provided.
35#[derive(Debug, Error)]
36pub enum InvalidPem {
37    /// The provided PEM contained no PEM.
38    #[error("no PEM block found")]
39    NoPEM,
40
41    /// The PEM was not valid PEM.
42    #[error("invalid PEM block: {0}")]
43    DeserialisePEM(#[from] PEMError),
44
45    /// The PEM was valid, but the encoded certificate was not.
46    #[error("pem deserialised to: {0}")]
47    ParseX509(#[from] InvalidDer),
48
49    /// More than one PEM-encoded something was provided.
50    #[error("expected 1 PEM block but more provided")]
51    TooManyBlocks,
52}
53
54/// Errors when parsing a DER certificate.
55#[derive(Debug, Error)]
56#[error("invalid der when parsing x509 cert: {0}")]
57pub enum InvalidDer {
58    /// DER bytes provided to a [`Certificate`] constructor did not contain a valid
59    /// X509 certificate.
60    Parse(#[from] x509_parser::nom::Err<X509Error>),
61
62    /// After parsing the X509 certificate, there was unparsed data remaining
63    /// (parser error).
64    #[error("excess der bytes")]
65    ExcessDER,
66
67    /// [`Validity`] in a [`Certificate`] is invalid.
68    #[error("invalid timestamp in certificate validity: {0}")]
69    InvalidTimestamp(#[from] jiff::Error),
70
71    /// The [`CertId`] (Subject Key Identifier) field is missing or has an invalid
72    /// length in the X509 certificate.
73    ///
74    /// This is allowed by the X509 spec, but it is required as an invariant of
75    /// our system.
76    #[error("cert ID missing or invalid length in certificate: {0}")]
77    CertId(#[from] InvalidCertId),
78
79    /// The [`IssuerCertId`] (Authority Key Identifier) field is missing or has an invalid
80    /// length in the X509 certificate.
81    ///
82    /// This is allowed by the X509 spec, but it is required as an invariant of
83    /// our system.
84    #[error("issuer cert ID missing or invalid length in certificate: {0}")]
85    IssuerCertId(#[from] InvalidIssuerCertId),
86}
87
88/// An X509 [`Certificate`].
89///
90/// # Untrusted
91///
92/// A [`Certificate`] is untrusted input: it cannot be determined if the
93/// certificate is from a trusted source and / or modified by an attacker unless
94/// verified to cryptographically chain to a trust anchor / known root.
95#[derive(Debug, Clone, Valuable)]
96pub struct Certificate {
97    /// DER encoded certificate.
98    #[valuable(skip)]
99    der: Bytes,
100
101    /// A copy of the raw public key DER bytes in `cert`.
102    #[valuable(skip)]
103    public_key_der: Bytes,
104
105    /// The parsed [`SerialNumber`] for this certificate.
106    serial_number: SerialNumber,
107
108    /// The parsed [`Fingerprint`] for this certificate.
109    fingerprint: Fingerprint,
110
111    /// The parsed [`Validity`] for this certificate.
112    validity: Validity,
113
114    /// The Subject Key Identity value in the certificate.
115    #[valuable(skip)] // Untrusted, used only for chain building.
116    cert_id: CertId,
117
118    /// The Authority Key Identity value in the certificate.
119    #[valuable(skip)] // Untrusted, used only for chain building.
120    issuer_cert_id: IssuerCertId,
121}
122
123impl Certificate {
124    /// Construct this certificate from a PEM string.
125    pub fn from_pem(pem: &[u8]) -> Result<Self, InvalidPem> {
126        let mut pem_iter = Pem::iter_from_buffer(pem);
127        let pem = pem_iter.next().ok_or(InvalidPem::NoPEM)??;
128
129        // It is an error to provide multiple certificates to this constructor.
130        if pem_iter.next().is_some() {
131            return Err(InvalidPem::TooManyBlocks);
132        }
133
134        Self::from_der(pem.contents).map_err(InvalidPem::from)
135    }
136
137    /// Construct a [`Certificate`] by parsing DER bytes that contain an X509
138    /// certificate.
139    pub fn from_der(der: impl Into<Bytes>) -> Result<Self, InvalidDer> {
140        let der = der.into();
141
142        let (rem, cert) = X509CertificateParser::new()
143            .with_deep_parse_extensions(true)
144            .parse(&der)
145            .map_err(InvalidDer::Parse)?;
146        if !rem.is_empty() {
147            // The provided PEM has trailing data after parsing the certificate.
148            return Err(InvalidDer::ExcessDER);
149        }
150
151        let fingerprint = Fingerprint::from(&cert);
152        let serial_number = SerialNumber::from(&cert);
153        let validity = Validity::try_from(&cert)?;
154        let cert_id = CertId::try_from(&cert)?;
155        let issuer_cert_id = IssuerCertId::try_from(&cert)?;
156
157        // Extract the raw public key DER bytes.
158        let public_key_der = Bytes::from(cert.public_key().subject_public_key.data.to_vec());
159
160        Ok(Self {
161            der,
162            serial_number,
163            fingerprint,
164            public_key_der,
165            validity,
166            cert_id,
167            issuer_cert_id,
168        })
169    }
170
171    /// Return the raw DER bytes for this certificate.
172    pub fn as_der(&self) -> Bytes {
173        self.der.clone() // ref copy
174    }
175
176    /// Return this [`Certificate`] as a PEM string.
177    pub fn generate_pem(&self) -> String {
178        let pem = pem_crate::Pem::new("CERTIFICATE", self.der.as_ref());
179        pem_crate::encode_config(
180            &pem,
181            pem_crate::EncodeConfig::new().set_line_ending(LineEnding::LF),
182        )
183    }
184
185    /// Return the serial number of this certificate.
186    pub fn serial_number(&self) -> &SerialNumber {
187        &self.serial_number
188    }
189
190    /// Return the unique fingerprint of this certificate.
191    pub fn fingerprint(&self) -> &Fingerprint {
192        &self.fingerprint
193    }
194
195    /// Return the [`Validity`] period of this certificate.
196    pub fn validity(&self) -> &Validity {
197        &self.validity
198    }
199
200    /// Return the [`PublicKey`] embedded in this [`Certificate`].
201    pub fn public_key<'a>(&'a self) -> PublicKey<'a> {
202        PublicKey::new(self.public_key_der.as_ref())
203    }
204
205    /// Return the [`CertId`] for this certificate.
206    pub fn cert_id(&self) -> &CertId {
207        &self.cert_id
208    }
209
210    /// Return the [`IssuerCertId`] for this certificate.
211    pub fn issuer_cert_id(&self) -> &IssuerCertId {
212        &self.issuer_cert_id
213    }
214}
215
216impl From<rcgen::Certificate> for Certificate {
217    fn from(value: rcgen::Certificate) -> Self {
218        Certificate::from_pem(value.pem().as_bytes()).expect("valid cert round-trip")
219    }
220}
221
222#[cfg(test)]
223pub(super) mod tests {
224    use std::fmt::Display;
225
226    use rc_x509_test_helpers::assert_valuable_repr;
227
228    use super::*;
229
230    use proptest::prelude::*;
231    use valuable::Valuable;
232
233    /// An PEM-encoded example leaf certificate for testing (missing PEM
234    /// headers).
235    ///
236    /// ```text
237    ///   Serial: 00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
238    ///   Valid: 2025-08-13 14:58 UTC to 2035-08-11 14:59 UTC
239    ///   Signature: ECDSA-SHA256
240    ///   Subject Info:
241    ///           CommonName: itsallbroken.com
242    ///   Issuer Info:
243    ///           Organization: La Fábrica de Plátanos
244    ///           CommonName: La Fábrica de Plátanos Intermediate CA
245    ///   Subject Key ID: DC:8D:B6:27:52:78:58:4C:FD:A2:43:DB:CB:2B:E0:57:68:6E:2B:8E
246    ///   Authority Key ID: 20:6C:8E:CF:E4:21:A7:FF:ED:23:C8:3D:37:0F:77:81:84:71:0E:15
247    ///   Key Usage:
248    ///           Digital Signature
249    ///   Extended Key Usage:
250    ///           Server Auth
251    ///           Client Auth
252    ///   DNS Names:
253    ///           itsallbroken.com
254    /// ```
255    const CERT_PEM_DATA: &str = "\
256MIICWjCCAgCgAwIBAgIRAOJ7lLc8PQi630WNVnql4WQwCgYIKoZIzj0EAwIwVjEh
257MB8GA1UECgwYTGEgRsOhYnJpY2EgZGUgUGzDoXRhbm9zMTEwLwYDVQQDDChMYSBG
258w6FicmljYSBkZSBQbMOhdGFub3MgSW50ZXJtZWRpYXRlIENBMB4XDTI1MDgxMzE0
259NTg0MFoXDTM1MDgxMTE0NTk0MFowGzEZMBcGA1UEAxMQaXRzYWxsYnJva2VuLmNv
260bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHLJcMR9Px/OfC9kXFCOqxlPe4Z
261sQa9wW3V8mMwxzwdDCvH7PWfW+uKof7LPw9XZ6F1fmTTw1YxG1NZ56GPpUGjgekw
262geYwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
263AjAdBgNVHQ4EFgQU3I22J1J4WEz9okPbyyvgV2huK44wHwYDVR0jBBgwFoAUIGyO
264z+Qhp//tI8g9Nw93gYRxDhUwGwYDVR0RBBQwEoIQaXRzYWxsYnJva2VuLmNvbTBY
265BgwrBgEEAYKkZMYoQAEESDBGAgEBBBRkb21AaXRzYWxsYnJva2VuLmNvbQQrbGhN
266WDU2VVFVQjVlMnNvR1hzN2RRcE5wXy1jb19BUzd0dkpoQmstaHFJazAKBggqhkjO
267PQQDAgNIADBFAiANQrCtWI0ejFhyydcpsrqQ5vSlL26PIWBjurEsF7i9JwIhAMTX
268YxZ1HPGBZ43mYEaEdMR47YlQlNwwK+43yTDBRgd7\
269";
270
271    /// Assert two certificates contain the same content without implementing
272    /// PartialEq on the cert (fingerprints should be used for equality matching
273    /// in the public API).
274    fn assert_certs_equal(a: &Certificate, b: &Certificate) {
275        assert_eq!(a.der, b.der);
276        assert_eq!(a.public_key_der, b.public_key_der);
277        assert_eq!(a.fingerprint, b.fingerprint);
278    }
279
280    /// Return a [`Certificate`] from [`CERT_PEM_DATA`].
281    pub(crate) fn cert_fixture() -> Certificate {
282        let pem =
283            format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----");
284
285        Certificate::from_pem(pem.as_bytes()).expect("valid PEM")
286    }
287
288    #[test]
289    fn test_fixture() {
290        let pem =
291            format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----\n");
292
293        let cert = Certificate::from_pem(pem.as_bytes()).expect("valid PEM");
294
295        assert_eq!(
296            cert.serial_number().as_hex_str(),
297            "00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64"
298        );
299
300        // Fixture value extracted using OpenSSL:
301        //
302        //   % openssl x509 -in cert.pem -pubkey -noout | \
303        //      openssl pkey -pubin -outform DER | \
304        //      openssl dgst -sha256 -hex
305        //
306        // Converted from hex to decimal array for consistency with assert
307        // output.
308        assert_eq!(
309            *cert.public_key().key_id(),
310            [
311                79, 76, 105, 90, 163, 235, 170, 81, 228, 220, 126, 244, 31, 241, 56, 133, 220, 5,
312                215, 45, 202, 124, 72, 64, 131, 33, 152, 138, 94, 248, 14, 204
313            ]
314        );
315
316        // Assert the generated PEM (inc. line endings).
317        assert_eq!(cert.generate_pem(), pem);
318
319        // SKI & AKI value extraction.
320        assert_eq!(
321            cert.cert_id().as_hex_str(),
322            "dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e"
323        );
324        assert_eq!(
325            cert.issuer_cert_id().as_hex_str(),
326            "20:6c:8e:cf:e4:21:a7:ff:ed:23:c8:3d:37:0f:77:81:84:71:0e:15"
327        );
328    }
329
330    #[test]
331    fn test_valuable_repr() {
332        let cert = cert_fixture();
333
334        #[derive(Valuable)]
335        struct Wrapper {
336            cert: Certificate,
337        }
338
339        // Wrap the Certificate struct to capture the struct name in the
340        // rendered output (otherwise only fields are captured).
341        let cert = Wrapper { cert };
342
343        assert_valuable_repr(
344            &cert,
345            "\
346- cert:
347    Certificate {}:
348        - serial_number:
349            00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
350        - fingerprint:
351            49:ef:bb:e5:7f:3d:ff:9c:6d:b5:6a:15:b7:24:ba:8b:78:76:9c:16:a6:58:75:f9:b7:76:ae:ee:21:53:e5:e5
352        - validity:
353            2025-08-13T14:58:40Z..2035-08-11T14:59:40Z
354",
355        );
356    }
357
358    #[test]
359    fn test_validity_fixture() {
360        let cert = cert_fixture();
361
362        assert_eq!(
363            cert.validity().not_before_as_timestamp().to_string(),
364            "2025-08-13T14:58:40Z"
365        );
366        assert_eq!(
367            cert.validity().not_after_as_timestamp().to_string(),
368            "2035-08-11T14:59:40Z"
369        );
370    }
371
372    #[test]
373    fn test_fingerprint_fixture() {
374        let cert = cert_fixture();
375
376        assert_eq!(
377            cert.fingerprint().as_hex_str(),
378            "49:ef:bb:e5:7f:3d:ff:9c:6d:b5:6a:15:b7:24:ba:8b:78:76:9c:16:a6:58:75:f9:b7:76:ae:ee:21:53:e5:e5"
379        );
380    }
381
382    #[test]
383    fn test_round_trip_pem() {
384        let cert = cert_fixture();
385
386        let got = Certificate::from_pem(cert.generate_pem().as_bytes()).expect("valid cert");
387        assert_certs_equal(&got, &cert);
388    }
389
390    #[test]
391    fn test_der_zero_copy_construction() {
392        // Force a copy from a Vec to obtain a unique bytes buffer.
393        let buf = Bytes::from(cert_fixture().as_der().to_vec());
394        assert!(buf.is_unique());
395
396        // Pass a ref copy of the buffer to the constructor.
397        let cert = Certificate::from_der(buf.clone()).expect("valid DER");
398
399        // At this point, either:
400        //
401        //  - The der bytes buffer was retained by the Certificate constructor
402        //    and the original ref is no longer unique, or
403        //  - The constructor copied data from the byte buffer and then dropped
404        //    it, so the original ref is now unique again.
405        //
406        // We expect the constructor to be zero-copy and retain a ref to the
407        // original buffer.
408        assert!(!buf.is_unique());
409
410        // As an additional check, the buffer returned by the DER accessor is a
411        // zero-copy ref.
412        assert!(!cert.as_der().is_unique());
413    }
414
415    /// Fragments of a potentially invalid PEM block.
416    #[derive(Debug, Clone)]
417    enum PemPart {
418        /// The opening PEM header.
419        Start,
420        /// The closing PEM footer.
421        End,
422        // Random string data.
423        RandomData(String),
424        // A valid PEM block for a certificate.
425        ValidCertData,
426    }
427
428    // Render the PEM block fragments.
429    impl Display for PemPart {
430        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431            match self {
432                PemPart::Start => f.write_str("-----BEGIN CERTIFICATE-----\n"),
433                PemPart::End => f.write_str("-----END CERTIFICATE-----\n"),
434                PemPart::RandomData(s) => f.write_str(s),
435                PemPart::ValidCertData => f.write_str(CERT_PEM_DATA),
436            }
437        }
438    }
439
440    // Yield an arbitrary [`PemPart`].
441    fn arbitrary_pem_part() -> impl Strategy<Value = PemPart> {
442        prop_oneof![
443            Just(PemPart::Start),
444            Just(PemPart::End),
445            any::<String>().prop_map(PemPart::RandomData),
446            Just(PemPart::ValidCertData),
447        ]
448    }
449
450    proptest! {
451        /// Generate strings from the `PemPart` fragments and attempt to parse
452        /// it as a `Certificate`.
453        #[test]
454        fn prop_from_pem(
455            parts in prop::collection::vec(arbitrary_pem_part(), 0..5),
456        ) {
457            prop_from_pem_test(parts);
458        }
459
460        /// Parse a Certificate from random invalid bytes, ensuring no panic
461        /// occurs.
462        #[test]
463        fn prop_invalid_pem_bytes(
464            binary in prop::collection::vec(any::<u8>(), 0..200),
465        ) {
466            let _ = Certificate::from_pem(&binary).expect_err("non-pem input");
467        }
468    }
469
470    fn prop_from_pem_test(parts: Vec<PemPart>) {
471        // Build a string from the randomised parts.
472        let pem: String = parts.iter().map(ToString::to_string).collect();
473
474        // Attempt to parse the certificate from this string.
475        let cert = match Certificate::from_pem(pem.as_bytes()) {
476            Ok(cert) => {
477                // Continue and verify below.
478                cert
479            }
480            Err(_) => {
481                // Did not accept input, and did not panic.
482                return;
483            }
484        };
485
486        // Invariant: a cert that was parsed from PEM, should round trip.
487        assert_certs_equal(
488            &cert,
489            &Certificate::from_pem(cert.generate_pem().as_bytes()).unwrap(),
490        );
491
492        // Invariant: certs parsed from DER should also be equal.
493        assert_certs_equal(&cert, &Certificate::from_der(cert.as_der()).unwrap());
494
495        // The success case must be the only valid sequence of PEM fragments.
496        //
497        // (technically the random string fragment might produce a completely
498        // valid PEM certificate string, but the probability is so low it's
499        // effectively zero).
500        assert!(matches!(
501            parts.as_slice(),
502            [PemPart::Start, PemPart::ValidCertData, PemPart::End]
503        ));
504    }
505}