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    /// Return the subject distinguished name as a [RFC 4514] string.
216    ///
217    /// # Expensive
218    ///
219    /// Generating the subject string is relatively expensive - cache the result
220    /// at this call appropriately.
221    ///
222    /// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514
223    pub fn subject(&self) -> String {
224        let (_, cert) = X509CertificateParser::new()
225            .with_deep_parse_extensions(false)
226            .parse(&self.der)
227            .expect("valid DER: already parsed at construction");
228        cert.subject().to_string()
229    }
230}
231
232impl From<rcgen::Certificate> for Certificate {
233    fn from(value: rcgen::Certificate) -> Self {
234        Certificate::from_pem(value.pem().as_bytes()).expect("valid cert round-trip")
235    }
236}
237
238#[cfg(test)]
239pub(super) mod tests {
240    use std::fmt::Display;
241
242    use rc_x509_test_helpers::assert_valuable_repr;
243
244    use super::*;
245
246    use proptest::prelude::*;
247    use valuable::Valuable;
248
249    /// An PEM-encoded example leaf certificate for testing (missing PEM
250    /// headers).
251    ///
252    /// ```text
253    ///   Serial: 00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
254    ///   Valid: 2025-08-13 14:58 UTC to 2035-08-11 14:59 UTC
255    ///   Signature: ECDSA-SHA256
256    ///   Subject Info:
257    ///           CommonName: itsallbroken.com
258    ///   Issuer Info:
259    ///           Organization: La Fábrica de Plátanos
260    ///           CommonName: La Fábrica de Plátanos Intermediate CA
261    ///   Subject Key ID: DC:8D:B6:27:52:78:58:4C:FD:A2:43:DB:CB:2B:E0:57:68:6E:2B:8E
262    ///   Authority Key ID: 20:6C:8E:CF:E4:21:A7:FF:ED:23:C8:3D:37:0F:77:81:84:71:0E:15
263    ///   Key Usage:
264    ///           Digital Signature
265    ///   Extended Key Usage:
266    ///           Server Auth
267    ///           Client Auth
268    ///   DNS Names:
269    ///           itsallbroken.com
270    /// ```
271    const CERT_PEM_DATA: &str = "\
272MIICWjCCAgCgAwIBAgIRAOJ7lLc8PQi630WNVnql4WQwCgYIKoZIzj0EAwIwVjEh
273MB8GA1UECgwYTGEgRsOhYnJpY2EgZGUgUGzDoXRhbm9zMTEwLwYDVQQDDChMYSBG
274w6FicmljYSBkZSBQbMOhdGFub3MgSW50ZXJtZWRpYXRlIENBMB4XDTI1MDgxMzE0
275NTg0MFoXDTM1MDgxMTE0NTk0MFowGzEZMBcGA1UEAxMQaXRzYWxsYnJva2VuLmNv
276bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHLJcMR9Px/OfC9kXFCOqxlPe4Z
277sQa9wW3V8mMwxzwdDCvH7PWfW+uKof7LPw9XZ6F1fmTTw1YxG1NZ56GPpUGjgekw
278geYwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
279AjAdBgNVHQ4EFgQU3I22J1J4WEz9okPbyyvgV2huK44wHwYDVR0jBBgwFoAUIGyO
280z+Qhp//tI8g9Nw93gYRxDhUwGwYDVR0RBBQwEoIQaXRzYWxsYnJva2VuLmNvbTBY
281BgwrBgEEAYKkZMYoQAEESDBGAgEBBBRkb21AaXRzYWxsYnJva2VuLmNvbQQrbGhN
282WDU2VVFVQjVlMnNvR1hzN2RRcE5wXy1jb19BUzd0dkpoQmstaHFJazAKBggqhkjO
283PQQDAgNIADBFAiANQrCtWI0ejFhyydcpsrqQ5vSlL26PIWBjurEsF7i9JwIhAMTX
284YxZ1HPGBZ43mYEaEdMR47YlQlNwwK+43yTDBRgd7\
285";
286
287    /// Assert two certificates contain the same content without implementing
288    /// PartialEq on the cert (fingerprints should be used for equality matching
289    /// in the public API).
290    fn assert_certs_equal(a: &Certificate, b: &Certificate) {
291        assert_eq!(a.der, b.der);
292        assert_eq!(a.public_key_der, b.public_key_der);
293        assert_eq!(a.fingerprint, b.fingerprint);
294    }
295
296    /// Return a [`Certificate`] from [`CERT_PEM_DATA`].
297    pub(crate) fn cert_fixture() -> Certificate {
298        let pem =
299            format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----");
300
301        Certificate::from_pem(pem.as_bytes()).expect("valid PEM")
302    }
303
304    #[test]
305    fn test_fixture() {
306        let pem =
307            format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----\n");
308
309        let cert = Certificate::from_pem(pem.as_bytes()).expect("valid PEM");
310
311        assert_eq!(
312            cert.serial_number().as_hex_str(),
313            "00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64"
314        );
315
316        // Fixture value extracted using OpenSSL:
317        //
318        //   % openssl x509 -in cert.pem -pubkey -noout | \
319        //      openssl pkey -pubin -outform DER | \
320        //      openssl dgst -sha256 -hex
321        //
322        // Converted from hex to decimal array for consistency with assert
323        // output.
324        assert_eq!(
325            *cert.public_key().key_id(),
326            [
327                79, 76, 105, 90, 163, 235, 170, 81, 228, 220, 126, 244, 31, 241, 56, 133, 220, 5,
328                215, 45, 202, 124, 72, 64, 131, 33, 152, 138, 94, 248, 14, 204
329            ]
330        );
331
332        // Assert the generated PEM (inc. line endings).
333        assert_eq!(cert.generate_pem(), pem);
334
335        // SKI & AKI value extraction.
336        assert_eq!(
337            cert.cert_id().as_hex_str(),
338            "dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e"
339        );
340        assert_eq!(
341            cert.issuer_cert_id().as_hex_str(),
342            "20:6c:8e:cf:e4:21:a7:ff:ed:23:c8:3d:37:0f:77:81:84:71:0e:15"
343        );
344    }
345
346    #[test]
347    fn test_valuable_repr() {
348        let cert = cert_fixture();
349
350        #[derive(Valuable)]
351        struct Wrapper {
352            cert: Certificate,
353        }
354
355        // Wrap the Certificate struct to capture the struct name in the
356        // rendered output (otherwise only fields are captured).
357        let cert = Wrapper { cert };
358
359        assert_valuable_repr(
360            &cert,
361            "\
362- cert:
363    Certificate {}:
364        - serial_number:
365            00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
366        - fingerprint:
367            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
368        - validity:
369            2025-08-13T14:58:40Z..2035-08-11T14:59:40Z
370",
371        );
372    }
373
374    #[test]
375    fn test_validity_fixture() {
376        let cert = cert_fixture();
377
378        assert_eq!(
379            cert.validity().not_before_as_timestamp().to_string(),
380            "2025-08-13T14:58:40Z"
381        );
382        assert_eq!(
383            cert.validity().not_after_as_timestamp().to_string(),
384            "2035-08-11T14:59:40Z"
385        );
386    }
387
388    #[test]
389    fn test_subject_fixture() {
390        let cert = cert_fixture();
391        assert_eq!(cert.subject(), "CN=itsallbroken.com");
392    }
393
394    #[test]
395    fn test_fingerprint_fixture() {
396        let cert = cert_fixture();
397
398        assert_eq!(
399            cert.fingerprint().as_hex_str(),
400            "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"
401        );
402    }
403
404    #[test]
405    fn test_round_trip_pem() {
406        let cert = cert_fixture();
407
408        let got = Certificate::from_pem(cert.generate_pem().as_bytes()).expect("valid cert");
409        assert_certs_equal(&got, &cert);
410    }
411
412    #[test]
413    fn test_der_zero_copy_construction() {
414        // Force a copy from a Vec to obtain a unique bytes buffer.
415        let buf = Bytes::from(cert_fixture().as_der().to_vec());
416        assert!(buf.is_unique());
417
418        // Pass a ref copy of the buffer to the constructor.
419        let cert = Certificate::from_der(buf.clone()).expect("valid DER");
420
421        // At this point, either:
422        //
423        //  - The der bytes buffer was retained by the Certificate constructor
424        //    and the original ref is no longer unique, or
425        //  - The constructor copied data from the byte buffer and then dropped
426        //    it, so the original ref is now unique again.
427        //
428        // We expect the constructor to be zero-copy and retain a ref to the
429        // original buffer.
430        assert!(!buf.is_unique());
431
432        // As an additional check, the buffer returned by the DER accessor is a
433        // zero-copy ref.
434        assert!(!cert.as_der().is_unique());
435    }
436
437    /// Fragments of a potentially invalid PEM block.
438    #[derive(Debug, Clone)]
439    enum PemPart {
440        /// The opening PEM header.
441        Start,
442        /// The closing PEM footer.
443        End,
444        // Random string data.
445        RandomData(String),
446        // A valid PEM block for a certificate.
447        ValidCertData,
448    }
449
450    // Render the PEM block fragments.
451    impl Display for PemPart {
452        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453            match self {
454                PemPart::Start => f.write_str("-----BEGIN CERTIFICATE-----\n"),
455                PemPart::End => f.write_str("-----END CERTIFICATE-----\n"),
456                PemPart::RandomData(s) => f.write_str(s),
457                PemPart::ValidCertData => f.write_str(CERT_PEM_DATA),
458            }
459        }
460    }
461
462    // Yield an arbitrary [`PemPart`].
463    fn arbitrary_pem_part() -> impl Strategy<Value = PemPart> {
464        prop_oneof![
465            Just(PemPart::Start),
466            Just(PemPart::End),
467            any::<String>().prop_map(PemPart::RandomData),
468            Just(PemPart::ValidCertData),
469        ]
470    }
471
472    proptest! {
473        /// Generate strings from the `PemPart` fragments and attempt to parse
474        /// it as a `Certificate`.
475        #[test]
476        fn prop_from_pem(
477            parts in prop::collection::vec(arbitrary_pem_part(), 0..5),
478        ) {
479            prop_from_pem_test(parts);
480        }
481
482        /// Parse a Certificate from random invalid bytes, ensuring no panic
483        /// occurs.
484        #[test]
485        fn prop_invalid_pem_bytes(
486            binary in prop::collection::vec(any::<u8>(), 0..200),
487        ) {
488            let _ = Certificate::from_pem(&binary).expect_err("non-pem input");
489        }
490    }
491
492    fn prop_from_pem_test(parts: Vec<PemPart>) {
493        // Build a string from the randomised parts.
494        let pem: String = parts.iter().map(ToString::to_string).collect();
495
496        // Attempt to parse the certificate from this string.
497        let cert = match Certificate::from_pem(pem.as_bytes()) {
498            Ok(cert) => {
499                // Continue and verify below.
500                cert
501            }
502            Err(_) => {
503                // Did not accept input, and did not panic.
504                return;
505            }
506        };
507
508        // Invariant: a cert that was parsed from PEM, should round trip.
509        assert_certs_equal(
510            &cert,
511            &Certificate::from_pem(cert.generate_pem().as_bytes()).unwrap(),
512        );
513
514        // Invariant: certs parsed from DER should also be equal.
515        assert_certs_equal(&cert, &Certificate::from_der(cert.as_der()).unwrap());
516
517        // The success case must be the only valid sequence of PEM fragments.
518        //
519        // (technically the random string fragment might produce a completely
520        // valid PEM certificate string, but the probability is so low it's
521        // effectively zero).
522        assert!(matches!(
523            parts.as_slice(),
524            [PemPart::Start, PemPart::ValidCertData, PemPart::End]
525        ));
526    }
527}