Skip to content

Commit 86c086b

Browse files
committed
x509-cert: adds serial numbers generator
This follows the CABF ballot 164 recommendation to have serials use 64 bits from a CSPRNG. This also provides the user with the option to prefix its values (for use of an instance id).
1 parent 734b7b1 commit 86c086b

File tree

3 files changed

+88
-1
lines changed

3 files changed

+88
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x509-cert/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ spki = { version = "0.7.3", features = ["alloc"] }
2121

2222
# optional dependencies
2323
arbitrary = { version = "1.3", features = ["derive"], optional = true }
24+
hybrid-array = { version = "=0.2.0-pre.8", optional = true, default-features = false }
2425
sha1 = { version = "0.10.6", optional = true }
2526
signature = { version = "2.1.0", features = ["rand_core"], optional = true }
2627
tls_codec = { version = "0.4.0", default-features = false, features = ["derive"], optional = true }
@@ -41,7 +42,7 @@ default = ["pem", "std"]
4142
std = ["const-oid/std", "der/std", "spki/std", "tls_codec?/std"]
4243

4344
arbitrary = ["dep:arbitrary", "std", "der/arbitrary", "spki/arbitrary"]
44-
builder = ["std", "sha1/default", "signature"]
45+
builder = ["dep:hybrid-array", "std", "sha1/default", "signature"]
4546
hazmat = []
4647
pem = ["der/pem", "spki/pem"]
4748
sct = ["dep:tls_codec"]

x509-cert/src/serial_number.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use der::{
77
DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd,
88
Writer,
99
};
10+
#[cfg(feature = "builder")]
11+
use {alloc::vec, signature::rand_core::CryptoRngCore};
1012

1113
use crate::certificate::{Profile, Rfc5280};
1214

@@ -67,6 +69,56 @@ impl<P: Profile> SerialNumber<P> {
6769
}
6870
}
6971

72+
#[cfg(feature = "builder")]
73+
impl<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(rng: &mut impl CryptoRngCore) -> Result<Self> {
81+
Self::generate_with_prefix(&[], 17, rng)
82+
}
83+
84+
/// Generates a random serial number from RNG. Include a prefix value.
85+
///
86+
/// This follows the recommendation the CAB forum [ballot 164] and uses a minimum of 64 bits
87+
/// of output from the CSPRNG.
88+
///
89+
/// The specified length does not include the length of the prefix, the maximum length must be
90+
/// equal or below 19 (to account for leading sign disembiguation, and the maximum length of 20).
91+
///
92+
/// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/
93+
pub fn generate_with_prefix(
94+
prefix: &[u8],
95+
rand_len: usize,
96+
rng: &mut impl CryptoRngCore,
97+
) -> Result<Self> {
98+
// CABF requires a minimum of 64 bits of random
99+
if rand_len < 8 {
100+
return Err(ErrorKind::Failed.into());
101+
}
102+
103+
if rand_len + prefix.len() > 19 {
104+
return Err(ErrorKind::Failed.into());
105+
}
106+
107+
let mut buf = vec![0; prefix.len() + rand_len];
108+
buf[..prefix.len()].copy_from_slice(prefix);
109+
110+
let rand_buf = &mut buf[prefix.len()..];
111+
112+
// Make sure the first byte isn't 0, [`Int`] will otherwise optimize out the leading zeros,
113+
// shorten the value of the serial and trigger false positives in linters.
114+
while rand_buf[0] == 0 {
115+
rng.fill_bytes(rand_buf);
116+
}
117+
118+
Self::new(&buf)
119+
}
120+
}
121+
70122
impl<P: Profile> EncodeValue for SerialNumber<P> {
71123
fn value_len(&self) -> Result<Length> {
72124
self.inner.value_len()
@@ -193,4 +245,37 @@ mod tests {
193245
assert_eq!(sn.to_string(), "01")
194246
}
195247
}
248+
249+
#[cfg(feature = "builder")]
250+
#[test]
251+
fn serial_number_generate() {
252+
let sn = SerialNumber::<Rfc5280>::generate(&mut rand::thread_rng()).unwrap();
253+
254+
// Underlying storage uses signed int for compatibility reasons,
255+
// we may need to prefix the value with 0x00 to make it an unsigned.
256+
// in which case the length is going to be 18.
257+
assert!(matches!(sn.as_bytes().len(), 17..=18));
258+
259+
let sn =
260+
SerialNumber::<Rfc5280>::generate_with_prefix(&[], 8, &mut rand::thread_rng()).unwrap();
261+
assert!(matches!(sn.as_bytes().len(), 8..=9));
262+
263+
let sn =
264+
SerialNumber::<Rfc5280>::generate_with_prefix(&[1, 2, 3], 8, &mut rand::thread_rng())
265+
.unwrap();
266+
assert!(matches!(sn.as_bytes().len(), 11..=12));
267+
assert_eq!(&sn.as_bytes()[..3], &[1, 2, 3]);
268+
269+
let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 7, &mut rand::thread_rng());
270+
assert!(sn.is_err());
271+
272+
let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 20, &mut rand::thread_rng());
273+
assert!(sn.is_err());
274+
275+
let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 19, &mut rand::thread_rng());
276+
assert!(sn.is_ok());
277+
278+
let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[1], 19, &mut rand::thread_rng());
279+
assert!(sn.is_err());
280+
}
196281
}

0 commit comments

Comments
 (0)