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}