Skip to main content

rc_crypto/certificate/
serial_number.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, ops::RangeInclusive};
16
17use bytes::Bytes;
18use x509_parser::prelude::X509Certificate;
19
20use crate::{cached_string_repr::CachedStringRepr, hex::colon_string};
21
22/// The allowable [`SerialNumber`] byte lengths.
23const VALID_LENGTHS: RangeInclusive<usize> = 1..=20;
24
25/// A [`Certificate`] serial number, potentially non-unique, set by the
26/// certificate issuer.
27///
28/// X509 serial numbers are variable length byte arrays defined in RFC 5280 as
29/// up to 20 bytes ("octets"):
30///
31/// > Certificate users MUST be able to handle serialNumber values up to 20
32/// > octets.  Conforming CAs MUST NOT use serialNumber values longer than 20
33/// > octets.
34///
35/// This implementation accepts a serial number of arbitrary length as a byte
36/// array (as apposed to a using a fixed sized integer) up to a maximum of the
37/// specified 20 bytes. Users of this type MUST account for the variable length
38/// nature when storing or transmitting this value.
39///
40/// When rendered to a string (with [`Display`] or
41/// [`SerialNumber::as_hex_str()`]) this type formats the serial number as a
42/// colon delimited, lowercase hex string following the convention of the
43/// OpenSSL representation of serial numbers (example:
44/// `cc:cb:0f:63:f1:63:5e:f1:0e:26:e8:82:f7:7a:6e:f9`).
45///
46/// # Not Uniquely Identifying
47///
48/// Serial numbers are set by the certificate issuer. A certificate issuer
49/// SHOULD use a unique serial number for each certificate it issues, but code
50/// MUST NOT rely on a [`SerialNumber`] to uniquely identify a specific
51/// certificate as it can be re-used if the issuer is compromised.
52///
53/// [`Certificate`]: super::Certificate
54#[derive(Debug, Clone)] // NOTE: no PartialEq - not unique, do not compare.
55pub struct SerialNumber {
56    /// The raw BER serial bytes (a variable-length ASN.1 `INTEGER`).
57    ///
58    /// Invariant: immutable to ensure the cached rendering of the serial number
59    /// remains in-sync.
60    bytes: Bytes,
61
62    /// A lazily-rendered string representation of `bytes`.
63    ///
64    /// See [`Self::as_hex_str()`] for initialisation.
65    rendered: CachedStringRepr,
66}
67
68impl SerialNumber {
69    /// Construct a new [`SerialNumber`] from a raw byte array.
70    ///
71    /// # Panics
72    ///
73    /// This constructor panics if `value` is empty, or if the length exceeds 20
74    /// bytes.
75    fn new(value: impl Into<Bytes>) -> Self {
76        let bytes = value.into();
77
78        // Correctness: reject out-of-spec serial numbers to bound the data
79        // size of a serial number.
80        assert!(
81            VALID_LENGTHS.contains(&bytes.len()),
82            "serial number of length {} is invalid",
83            bytes.len()
84        );
85
86        Self {
87            bytes,
88            rendered: Default::default(),
89        }
90    }
91
92    /// Render the [`SerialNumber`] as a lowercase hex string delimited by
93    /// colons in the style of OpenSSL.
94    ///
95    /// Example: `cc:cb:0f:63:f1:63:5e:f1:0e:26:e8:82:f7:7a:6e:f9`
96    ///
97    /// This value is lazily rendered and cached for reuse.
98    pub fn as_hex_str(&self) -> &str {
99        self.rendered.get_or_init(|| colon_string(&self.bytes))
100    }
101
102    /// Return the raw serial number bytes.
103    pub fn as_bytes(&self) -> &[u8] {
104        self.bytes.as_ref()
105    }
106}
107
108impl Display for SerialNumber {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.write_str(self.as_hex_str())
111    }
112}
113
114impl<'a> From<&'a SerialNumber> for rcgen::SerialNumber {
115    fn from(value: &'a SerialNumber) -> Self {
116        Self::from_slice(value.as_bytes())
117    }
118}
119
120impl<'a> From<&'a X509Certificate<'a>> for SerialNumber {
121    fn from(cert: &'a X509Certificate<'a>) -> Self {
122        Self::new(cert.raw_serial().to_vec())
123    }
124}
125
126/// Render a [`SerialNumber`] as an encoded string in structured logging.
127impl valuable::Valuable for SerialNumber {
128    fn as_value(&self) -> valuable::Value<'_> {
129        valuable::Value::String(self.as_hex_str())
130    }
131
132    fn visit(&self, visit: &mut dyn valuable::Visit) {
133        visit.visit_value(self.as_value());
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use rc_x509_test_helpers::assert_valuable_repr;
140
141    use super::*;
142
143    use proptest::prelude::*;
144    use static_assertions::assert_not_impl_any;
145
146    // Why: a SerialNumber can be set to anything by the issuer, making it
147    // unreliable as a unique identifier, and should not be used to compare two
148    // certificates for equality.
149    assert_not_impl_any!(SerialNumber: PartialEq, Eq);
150
151    #[test]
152    fn test_fixture() {
153        let hex_str = "cc:cb:0f:63:f1:63:5e:f1:0e:26:e8:82:f7:7a:6e:f9";
154
155        let raw = hex::decode(hex_str.replace(':', "")).expect("valid hex");
156        let sn = SerialNumber::new(raw.clone());
157
158        assert_eq!(sn.to_string(), hex_str);
159        assert_eq!(sn.as_hex_str(), hex_str);
160        assert_eq!(sn.as_bytes(), &raw);
161    }
162
163    #[test]
164    #[should_panic(expected = "serial number of length 0 is invalid")]
165    fn test_empty() {
166        let _sn = SerialNumber::new([].as_slice());
167    }
168
169    #[test]
170    #[should_panic(expected = "serial number of length 21 is invalid")]
171    fn test_too_long() {
172        let _sn = SerialNumber::new(
173            [
174                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
175            ]
176            .as_slice(),
177        );
178    }
179
180    /// Assert how a serial number appears in structured logs.
181    #[test]
182    fn test_valuable_repr() {
183        let sn = SerialNumber::new(
184            [
185                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
186            ]
187            .as_slice(),
188        );
189
190        assert_valuable_repr(
191            &sn,
192            "01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:14\n",
193        );
194    }
195
196    proptest! {
197        #[test]
198        fn prop_render_serial_number(
199            value in prop::collection::vec(any::<u8>(), 1..20), // RFC 5280: 20 max
200        ) {
201            let serial = SerialNumber::new(value.clone());
202            let rendered = serial.as_hex_str();
203
204            let rcgen_serial = rcgen::SerialNumber::from(&serial);
205            let rcgen_rendered = rcgen_serial.to_string();
206
207            // Invariant: the rendered version matches the OpenSSL convention as
208            // implemented by the rcgen type.
209            assert_eq!(rendered, rcgen_rendered);
210
211            // Invariant: the Display impl uses the same OpenSSL convention.
212            assert_eq!(serial.to_string(), rendered);
213
214            // Invariant: the byte accessor returns the raw serial bytes.
215            assert_eq!(serial.as_bytes(), &value);
216        }
217    }
218}