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 Dec 3, 2023
1 parent 1d825c1 commit a81e1ac
Show file tree
Hide file tree
Showing 3 changed files with 80 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 }
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 }

Expand All @@ -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 = ["generic-array", "std", "sha1/default", "signature"]
hazmat = []
pem = ["der/pem", "spki/pem"]

Expand Down
77 changes: 77 additions & 0 deletions x509-cert/src/serial_number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -65,6 +79,48 @@ impl<P: Profile> SerialNumber<P> {
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<N>(rng: &mut impl CryptoRngCore) -> Result<Self>
where
N: Unsigned + ArrayLength<u8>,
N: Min<U8>, // Rand minimum is 64 bits
N: Max<U20>, // Max length is 20 bytes
{
Self::generate_with_prefix::<UTerm, N>(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, N>(
prefix: GenericArray<u8, Prefix>,
rng: &mut impl CryptoRngCore,
) -> Result<Self>
where
N: Unsigned,
Prefix: Unsigned + ArrayLength<u8>,
Prefix: Add<N>,
<Prefix as Add<N>>::Output: ArrayLength<u8>,
N: Min<U8>, // Rand minimum is 64 bits
<Prefix as Add<N>>::Output: Max<U20>, // Max length is 20 bytes
{
let mut buf = GenericArray::<_, <Prefix as Add<N>>::Output>::default();
buf[..Prefix::USIZE].copy_from_slice(&prefix);
rng.fill_bytes(&mut buf[Prefix::USIZE..]);

Self::new(&buf)
}
}

impl<P: Profile> EncodeValue for SerialNumber<P> {
Expand Down Expand Up @@ -151,6 +207,9 @@ impl<'a, P: Profile> arbitrary::Arbitrary<'a> for SerialNumber<P> {
mod tests {
use alloc::string::ToString;

#[cfg(feature = "builder")]
use generic_array::{arr, typenum::consts::U17};

use super::*;

#[test]
Expand Down Expand Up @@ -193,4 +252,22 @@ mod tests {
assert_eq!(sn.to_string(), "01")
}
}

#[cfg(feature = "builder")]
#[test]
fn serial_number_generate() {
let sn = SerialNumber::<Rfc5280>::generate::<U17>(&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::<_, U17>(
arr![u8; 1,2,3],
&mut rand::thread_rng(),
)
.unwrap();
assert_eq!(sn.as_bytes().len(), 20);
}
}

0 comments on commit a81e1ac

Please sign in to comment.