Skip to main content
This is unreleased documentation for the main (development) branch of crypto-glue.

x509_cert/
serial_number.rs

1//! X.509 serial number
2
3use core::{fmt::Display, marker::PhantomData};
4
5use der::{
6    DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd,
7    Writer,
8    asn1::{self, Int},
9};
10#[cfg(feature = "builder")]
11use {alloc::vec, signature::rand_core::CryptoRng};
12
13use crate::certificate::{Profile, Rfc5280};
14
15/// [RFC 5280 Section 4.1.2.2.]  Serial Number
16///
17///   The serial number MUST be a positive integer assigned by the CA to
18///   each certificate.  It MUST be unique for each certificate issued by a
19///   given CA (i.e., the issuer name and serial number identify a unique
20///   certificate).  CAs MUST force the serialNumber to be a non-negative
21///   integer.
22///
23///   Given the uniqueness requirements above, serial numbers can be
24///   expected to contain long integers.  Certificate users MUST be able to
25///   handle serialNumber values up to 20 octets.  Conforming CAs MUST NOT
26///   use serialNumber values longer than 20 octets.
27///
28///   Note: Non-conforming CAs may issue certificates with serial numbers
29///   that are negative or zero.  Certificate users SHOULD be prepared to
30///   gracefully handle such certificates.
31#[derive(Clone, Debug, Eq, PartialEq, ValueOrd, PartialOrd, Ord)]
32pub struct SerialNumber<P: Profile = Rfc5280> {
33    pub(crate) inner: Int,
34    _profile: PhantomData<P>,
35}
36
37impl<P: Profile> SerialNumber<P> {
38    /// Maximum length in bytes for a [`SerialNumber`]
39    pub const MAX_LEN: Length = Length::new(20);
40
41    /// See notes in `SerialNumber::new` and `SerialNumber::decode_value`.
42    pub(crate) const MAX_DECODE_LEN: Length = Length::new(21);
43
44    /// Create a new [`SerialNumber`] from a byte slice.
45    ///
46    /// The byte slice **must** represent a positive integer.
47    pub fn new(bytes: &[u8]) -> Result<Self> {
48        let inner = asn1::Uint::new(bytes)?;
49
50        // The user might give us a 20 byte unsigned integer with a high MSB,
51        // which we'd then encode with 21 octets to preserve the sign bit.
52        // RFC 5280 is ambiguous about whether this is valid, so we limit
53        // `SerialNumber` *encodings* to 20 bytes or fewer while permitting
54        // `SerialNumber` *decodings* to have up to 21 bytes below.
55        if inner.value_len()? > Self::MAX_LEN {
56            return Err(ErrorKind::Overlength.into());
57        }
58
59        Ok(Self {
60            inner: inner.into(),
61            _profile: PhantomData,
62        })
63    }
64
65    /// Borrow the inner byte slice which contains the least significant bytes
66    /// of a big endian integer value with all leading zeros stripped.
67    pub fn as_bytes(&self) -> &[u8] {
68        self.inner.as_bytes()
69    }
70}
71
72#[cfg(feature = "builder")]
73impl<P: Profile> SerialNumber<P> {
74    /// Generates a random serial number from RNG.
75    ///
76    /// This follows the recommendation the CAB forum [ballot 164] and uses a minimum of 64 bits
77    /// of output from the CSPRNG. This currently defaults to a 17-bytes long serial number.
78    ///
79    /// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/
80    pub fn generate<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
81        Self::generate_with_prefix(&[], 17, rng)
82            .expect("a random of 17 is acceptable, and rng may not fail")
83    }
84
85    /// Generates a random serial number from RNG. Include a prefix value.
86    ///
87    /// This follows the recommendation the CAB forum [ballot 164] and uses a minimum of 64 bits
88    /// of output from the CSPRNG.
89    ///
90    /// The specified length does not include the length of the prefix, the maximum length must be
91    /// equal or below 19 (to account for leading sign disambiguation, and the maximum length of 20).
92    ///
93    /// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/
94    pub fn generate_with_prefix<R: CryptoRng + ?Sized>(
95        prefix: &[u8],
96        rand_len: usize,
97        rng: &mut R,
98    ) -> Result<Self> {
99        // CABF requires a minimum of 64 bits of random
100        if rand_len < 8 {
101            return Err(ErrorKind::Failed.into());
102        }
103
104        if rand_len + prefix.len() > 19 {
105            return Err(ErrorKind::Failed.into());
106        }
107
108        let mut buf = vec![0; prefix.len() + rand_len];
109        buf[..prefix.len()].copy_from_slice(prefix);
110
111        let rand_buf = &mut buf[prefix.len()..];
112
113        // Make sure the first byte isn't 0, [`Int`] will otherwise optimize out the leading zeros,
114        // shorten the value of the serial and trigger false positives in linters.
115        while rand_buf[0] == 0 {
116            rng.fill_bytes(rand_buf);
117        }
118
119        Self::new(&buf)
120    }
121}
122
123impl<P: Profile> EncodeValue for SerialNumber<P> {
124    fn value_len(&self) -> Result<Length> {
125        self.inner.value_len()
126    }
127
128    fn encode_value(&self, writer: &mut impl Writer) -> Result<()> {
129        self.inner.encode_value(writer)
130    }
131}
132
133impl<'a, P: Profile> DecodeValue<'a> for SerialNumber<P> {
134    type Error = der::Error;
135
136    fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> {
137        let inner = Int::decode_value(reader, header)?;
138        let serial = Self {
139            inner,
140            _profile: PhantomData,
141        };
142
143        P::check_serial_number(&serial)?;
144
145        Ok(serial)
146    }
147}
148
149impl<P: Profile> FixedTag for SerialNumber<P> {
150    const TAG: Tag = <Int as FixedTag>::TAG;
151}
152
153impl Display for SerialNumber {
154    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155        let mut iter = self.as_bytes().iter().peekable();
156
157        while let Some(byte) = iter.next() {
158            match iter.peek() {
159                Some(_) => write!(f, "{byte:02X}:")?,
160                None => write!(f, "{byte:02X}")?,
161            }
162        }
163
164        Ok(())
165    }
166}
167
168macro_rules! impl_from {
169    ($source:ty) => {
170        impl From<$source> for SerialNumber {
171            fn from(inner: $source) -> SerialNumber {
172                let serial_number = &inner.to_be_bytes()[..];
173                let serial_number = asn1::Uint::new(serial_number).unwrap();
174
175                // This could only fail if the big endian representation was to be more than 20
176                // bytes long. Because it's only implemented for up to u64 / usize (8 bytes).
177                SerialNumber::new(serial_number.as_bytes()).unwrap()
178            }
179        }
180    };
181}
182
183impl_from!(u8);
184impl_from!(u16);
185impl_from!(u32);
186impl_from!(u64);
187impl_from!(usize);
188
189// Implement by hand because the derive would create invalid values.
190// Use the constructor to create a valid value.
191#[cfg(feature = "arbitrary")]
192impl<'a, P: Profile> arbitrary::Arbitrary<'a> for SerialNumber<P> {
193    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
194        let len = u.int_in_range(0u32..=Self::MAX_LEN.into())?;
195
196        Self::new(u.bytes(len as usize)?).map_err(|_| arbitrary::Error::IncorrectFormat)
197    }
198
199    fn size_hint(depth: usize) -> (usize, Option<usize>) {
200        arbitrary::size_hint::and(u32::size_hint(depth), (0, None))
201    }
202}
203
204#[cfg(test)]
205#[allow(clippy::unwrap_used)]
206mod tests {
207    use alloc::string::ToString;
208
209    use super::*;
210
211    #[test]
212    fn serial_number_invariants() {
213        // Creating a new serial with an oversized encoding (due to high MSB) fails.
214        {
215            let too_big = [0x80; 20];
216            assert!(SerialNumber::<Rfc5280>::new(&too_big).is_err());
217        }
218
219        // Creating a new serial with the maximum encoding succeeds.
220        {
221            let just_enough = [
222                0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
223                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
224            ];
225            assert!(SerialNumber::<Rfc5280>::new(&just_enough).is_ok());
226        }
227    }
228
229    #[test]
230    fn serial_number_display() {
231        {
232            let sn = SerialNumber::new(&[0x11, 0x22, 0x33]).unwrap();
233
234            assert_eq!(sn.to_string(), "11:22:33")
235        }
236
237        {
238            let sn = SerialNumber::new(&[0xAA, 0xBB, 0xCC, 0x01, 0x10, 0x00, 0x11]).unwrap();
239
240            // We force the user's serial to be positive if they give us a negative one.
241            assert_eq!(sn.to_string(), "00:AA:BB:CC:01:10:00:11")
242        }
243
244        {
245            let sn = SerialNumber::new(&[0x00, 0x00, 0x01]).unwrap();
246
247            // Leading zeroes are ignored, due to canonicalization.
248            assert_eq!(sn.to_string(), "01")
249        }
250    }
251
252    #[cfg(feature = "builder")]
253    #[test]
254    fn serial_number_generate() {
255        let sn = SerialNumber::<Rfc5280>::generate(&mut rand::rng());
256
257        // Underlying storage uses signed int for compatibility reasons,
258        // we may need to prefix the value with 0x00 to make it an unsigned.
259        // in which case the length is going to be 18.
260        assert!(matches!(sn.as_bytes().len(), 17..=18));
261
262        let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 8, &mut rand::rng()).unwrap();
263        assert!(matches!(sn.as_bytes().len(), 8..=9));
264
265        let sn =
266            SerialNumber::<Rfc5280>::generate_with_prefix(&[1, 2, 3], 8, &mut rand::rng()).unwrap();
267        assert!(matches!(sn.as_bytes().len(), 11..=12));
268        assert_eq!(&sn.as_bytes()[..3], &[1, 2, 3]);
269
270        let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 7, &mut rand::rng());
271        assert!(sn.is_err());
272
273        let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 20, &mut rand::rng());
274        assert!(sn.is_err());
275
276        let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 19, &mut rand::rng());
277        assert!(sn.is_ok());
278
279        let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[1], 19, &mut rand::rng());
280        assert!(sn.is_err());
281    }
282}