diff --git a/Cargo.lock b/Cargo.lock index 8179f1ce6..4285ec8a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1841,6 +1841,7 @@ dependencies = [ "const-oid 0.9.5", "der", "ecdsa", + "generic-array", "hex-literal 0.4.1", "p256", "rand", diff --git a/x509-cert/Cargo.toml b/x509-cert/Cargo.toml index 033f48b3a..ce1ed56e3 100644 --- a/x509-cert/Cargo.toml +++ b/x509-cert/Cargo.toml @@ -21,6 +21,7 @@ spki = { version = "0.7.3", features = ["alloc"] } # optional dependencies arbitrary = { version = "1.3", features = ["derive"], optional = true } +generic-array = { version = "0.14.7", default-features = false, optional = true } sha1 = { version = "0.10.6", optional = true } signature = { version = "2.1.0", features = ["rand_core"], optional = true } @@ -40,7 +41,7 @@ default = ["pem", "std"] std = ["const-oid/std", "der/std", "spki/std"] arbitrary = ["dep:arbitrary", "std", "der/arbitrary", "spki/arbitrary"] -builder = ["std", "sha1/default", "signature"] +builder = ["dep:generic-array", "std", "sha1/default", "signature"] hazmat = [] pem = ["der/pem", "spki/pem"] diff --git a/x509-cert/src/serial_number.rs b/x509-cert/src/serial_number.rs index 42952fd8c..8fa537720 100644 --- a/x509-cert/src/serial_number.rs +++ b/x509-cert/src/serial_number.rs @@ -7,6 +7,20 @@ use der::{ DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd, Writer, }; +#[cfg(feature = "builder")] +use { + core::ops::Add, + generic_array::{ + typenum::{ + consts::{U20, U8}, + marker_traits::Unsigned, + type_operators::{Max, Min}, + uint::UTerm, + }, + ArrayLength, GenericArray, + }, + signature::rand_core::CryptoRngCore, +}; use crate::certificate::{Profile, Rfc5280}; @@ -65,6 +79,48 @@ impl SerialNumber

{ pub fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() } + + /// 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. + /// + /// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/ + #[cfg(feature = "builder")] + pub fn generate(rng: &mut impl CryptoRngCore) -> Result + where + N: Unsigned + ArrayLength, + N: Min, // Rand minimum is 64 bits + N: Max, // Max length is 20 bytes + { + Self::generate_with_prefix::(GenericArray::default(), 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. + /// + /// [ballot 164]: https://cabforum.org/2016/03/31/ballot-164/ + #[cfg(feature = "builder")] + pub fn generate_with_prefix( + prefix: GenericArray, + rng: &mut impl CryptoRngCore, + ) -> Result + where + N: Unsigned, + Prefix: Unsigned + ArrayLength, + Prefix: Add, + >::Output: ArrayLength, + N: Min, // Rand minimum is 64 bits + >::Output: Max, // Max length is 20 bytes + { + let mut buf = GenericArray::<_, >::Output>::default(); + buf[..Prefix::USIZE].copy_from_slice(&prefix); + rng.fill_bytes(&mut buf[Prefix::USIZE..]); + + Self::new(&buf) + } } impl EncodeValue for SerialNumber

{ @@ -151,6 +207,9 @@ impl<'a, P: Profile> arbitrary::Arbitrary<'a> for SerialNumber

{ mod tests { use alloc::string::ToString; + #[cfg(feature = "builder")] + use generic_array::{arr, typenum::consts::U17}; + use super::*; #[test] @@ -193,4 +252,22 @@ mod tests { assert_eq!(sn.to_string(), "01") } } + + #[cfg(feature = "builder")] + #[test] + fn serial_number_generate() { + let sn = SerialNumber::::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::::generate_with_prefix::<_, U17>( + arr![u8; 1,2,3], + &mut rand::thread_rng(), + ) + .unwrap(); + assert_eq!(sn.as_bytes().len(), 20); + } }