Skip to content

Commit

Permalink
x509-cert: adds serial numbers generator
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
baloo committed Jan 5, 2024
1 parent 734b7b1 commit 86c086b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion x509-cert/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ spki = { version = "0.7.3", features = ["alloc"] }

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

arbitrary = ["dep:arbitrary", "std", "der/arbitrary", "spki/arbitrary"]
builder = ["std", "sha1/default", "signature"]
builder = ["dep:hybrid-array", "std", "sha1/default", "signature"]
hazmat = []
pem = ["der/pem", "spki/pem"]
sct = ["dep:tls_codec"]
Expand Down
85 changes: 85 additions & 0 deletions x509-cert/src/serial_number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use der::{
DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd,
Writer,
};
#[cfg(feature = "builder")]
use {alloc::vec, signature::rand_core::CryptoRngCore};

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

Expand Down Expand Up @@ -67,6 +69,56 @@ impl<P: Profile> SerialNumber<P> {
}
}

#[cfg(feature = "builder")]
impl<P: Profile> SerialNumber<P> {
/// Generates a random serial number from RNG.
///
/// This follows the recommendation the CAB forum [ballot 164] and uses a minimum of 64 bits
/// of output from the CSPRNG. This currently defaults to a 17-bytes long serial number.
///
/// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/
pub fn generate(rng: &mut impl CryptoRngCore) -> Result<Self> {
Self::generate_with_prefix(&[], 17, rng)
}

/// Generates a random serial number from RNG. Include a prefix value.
///
/// This follows the recommendation the CAB forum [ballot 164] and uses a minimum of 64 bits
/// of output from the CSPRNG.
///
/// The specified length does not include the length of the prefix, the maximum length must be
/// equal or below 19 (to account for leading sign disembiguation, and the maximum length of 20).
///
/// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/
pub fn generate_with_prefix(
prefix: &[u8],
rand_len: usize,
rng: &mut impl CryptoRngCore,
) -> Result<Self> {
// CABF requires a minimum of 64 bits of random
if rand_len < 8 {
return Err(ErrorKind::Failed.into());
}

if rand_len + prefix.len() > 19 {
return Err(ErrorKind::Failed.into());
}

let mut buf = vec![0; prefix.len() + rand_len];
buf[..prefix.len()].copy_from_slice(prefix);

let rand_buf = &mut buf[prefix.len()..];

// Make sure the first byte isn't 0, [`Int`] will otherwise optimize out the leading zeros,
// shorten the value of the serial and trigger false positives in linters.
while rand_buf[0] == 0 {
rng.fill_bytes(rand_buf);
}

Self::new(&buf)
}
}

impl<P: Profile> EncodeValue for SerialNumber<P> {
fn value_len(&self) -> Result<Length> {
self.inner.value_len()
Expand Down Expand Up @@ -193,4 +245,37 @@ mod tests {
assert_eq!(sn.to_string(), "01")
}
}

#[cfg(feature = "builder")]
#[test]
fn serial_number_generate() {
let sn = SerialNumber::<Rfc5280>::generate(&mut rand::thread_rng()).unwrap();

// Underlying storage uses signed int for compatibility reasons,
// we may need to prefix the value with 0x00 to make it an unsigned.
// in which case the length is going to be 18.
assert!(matches!(sn.as_bytes().len(), 17..=18));

let sn =
SerialNumber::<Rfc5280>::generate_with_prefix(&[], 8, &mut rand::thread_rng()).unwrap();
assert!(matches!(sn.as_bytes().len(), 8..=9));

let sn =
SerialNumber::<Rfc5280>::generate_with_prefix(&[1, 2, 3], 8, &mut rand::thread_rng())
.unwrap();
assert!(matches!(sn.as_bytes().len(), 11..=12));
assert_eq!(&sn.as_bytes()[..3], &[1, 2, 3]);

let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 7, &mut rand::thread_rng());
assert!(sn.is_err());

let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 20, &mut rand::thread_rng());
assert!(sn.is_err());

let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[], 19, &mut rand::thread_rng());
assert!(sn.is_ok());

let sn = SerialNumber::<Rfc5280>::generate_with_prefix(&[1], 19, &mut rand::thread_rng());
assert!(sn.is_err());
}
}

0 comments on commit 86c086b

Please sign in to comment.