From 665b6e9c33917cb50bdb5d95784f352d7b6c1241 Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Sun, 21 Jun 2026 22:20:37 -0500 Subject: [PATCH 1/8] The RNG trait is now dyn-compatible so that instances can be handed around. --- crypto/factory/src/rng_factory.rs | 7 +++++-- crypto/rng/src/hash_drbg80090a.rs | 17 ++++++++++------- crypto/rng/src/lib.rs | 8 ++++---- crypto/rng/tests/hash_drbg80090a_tests.rs | 2 +- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crypto/factory/src/rng_factory.rs b/crypto/factory/src/rng_factory.rs index 9f1b8e0..18bf23a 100644 --- a/crypto/factory/src/rng_factory.rs +++ b/crypto/factory/src/rng_factory.rs @@ -95,7 +95,7 @@ impl AlgorithmFactory for RNGFactory { impl RNG for RNGFactory { fn add_seed_keymaterial( &mut self, - additional_seed: impl KeyMaterialTrait, + additional_seed: &dyn KeyMaterialTrait, ) -> Result<(), RNGError> { match self { Self::HashDRBG_SHA256(rng) => rng.add_seed_keymaterial(additional_seed), @@ -126,7 +126,10 @@ impl RNG for RNGFactory { } } - fn fill_keymaterial_out(&mut self, out: &mut impl KeyMaterialTrait) -> Result { + fn fill_keymaterial_out( + &mut self, + out: &mut dyn KeyMaterialTrait, + ) -> Result { match self { Self::HashDRBG_SHA256(rng) => rng.fill_keymaterial_out(out), Self::HashDRBG_SHA512(rng) => rng.fill_keymaterial_out(out), diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index ffac1db..5b10b86 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -268,9 +268,9 @@ impl Sp80090ADrbg for HashDRBG80090A { Ok(()) } - fn reseed( + fn reseed( &mut self, - seed: &impl KeyMaterialTrait, + seed: &K, additional_input: &[u8], ) -> Result<(), RNGError> { // Hash_DRBG Reseed Process: @@ -457,10 +457,10 @@ impl Sp80090ADrbg for HashDRBG80090A { Ok(out.len()) } - fn generate_keymaterial_out( + fn generate_keymaterial_out( &mut self, additional_input: &[u8], - out: &mut impl KeyMaterialTrait, + out: &mut K, ) -> Result { out.allow_hazardous_operations(); let bytes_written = self.generate_out(additional_input, out.ref_to_bytes_mut().unwrap())?; @@ -485,9 +485,9 @@ impl RNG for HashDRBG80090A { fn add_seed_keymaterial( &mut self, - additional_seed: impl KeyMaterialTrait, + additional_seed: &dyn KeyMaterialTrait, ) -> Result<(), RNGError> { - self.reseed(&additional_seed, "add_seed_keymaterial".as_bytes()) + self.reseed(additional_seed, "add_seed_keymaterial".as_bytes()) } fn next_int(&mut self) -> Result { @@ -506,7 +506,10 @@ impl RNG for HashDRBG80090A { self.generate_out("next_bytes_out".as_bytes(), out) } - fn fill_keymaterial_out(&mut self, out: &mut impl KeyMaterialTrait) -> Result { + fn fill_keymaterial_out( + &mut self, + out: &mut dyn KeyMaterialTrait, + ) -> Result { self.generate_keymaterial_out("fill_keymaterial".as_bytes(), out) } diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 43a65c1..19c60bc 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -92,9 +92,9 @@ pub trait Sp80090ADrbg { /// Reseeds the DRBG with the provided seed. /// TODO: this needs to be thought out to take some sort of EntropySource object that'll work well with DRBGs that require frequent reseeding. - fn reseed( + fn reseed( &mut self, - seed: &impl KeyMaterialTrait, + seed: &K, additional_input: &[u8], ) -> Result<(), RNGError>; @@ -125,10 +125,10 @@ pub trait Sp80090ADrbg { /// The output [KeyMaterialTrait] is filled to capacity. /// Throws a [RNGError::InsufficientSeedEntropy] if the capacity of the output KeyMaterial exceeds [SecurityStrength]. /// Retruns the number of bits output. - fn generate_keymaterial_out( + fn generate_keymaterial_out( &mut self, additional_input: &[u8], - out: &mut impl KeyMaterialTrait, + out: &mut K, ) -> Result; // TODO -- implement FIPS health tests diff --git a/crypto/rng/tests/hash_drbg80090a_tests.rs b/crypto/rng/tests/hash_drbg80090a_tests.rs index e4fd534..c6c2cef 100644 --- a/crypto/rng/tests/hash_drbg80090a_tests.rs +++ b/crypto/rng/tests/hash_drbg80090a_tests.rs @@ -301,7 +301,7 @@ mod tests { KeyMaterial256::from_bytes_as_type(&DUMMY_SEED_512[..32], KeyType::Seed).unwrap(); /* test add_seed_keymaterial */ - rng.add_seed_keymaterial(seed).unwrap(); + rng.add_seed_keymaterial(&seed).unwrap(); /* test next_int */ let out1: u32 = rng.next_int().unwrap(); From 2bb28845bd5297ead128991093c1c5d31173804c Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Sun, 21 Jun 2026 22:41:37 -0500 Subject: [PATCH 2/8] rng trait is now dyn-compatible --- crypto/core/src/traits.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 5247bbd..3576d62 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -432,7 +432,7 @@ pub trait RNG: Default { fn add_seed_keymaterial( &mut self, - additional_seed: impl KeyMaterialTrait, + additional_seed: &dyn KeyMaterialTrait, ) -> Result<(), RNGError>; fn next_int(&mut self) -> Result; @@ -443,7 +443,7 @@ pub trait RNG: Default { /// The entire output buffer is zeroized before the random bytes are written. fn next_bytes_out(&mut self, out: &mut [u8]) -> Result; - fn fill_keymaterial_out(&mut self, out: &mut impl KeyMaterialTrait) -> Result; + fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result; /// Returns the Security Strength of this RNG. fn security_strength(&self) -> SecurityStrength; From 2c2bb1c695162b29c29e46f63a8c9d1f7ccb0854 Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Sun, 21 Jun 2026 23:28:59 -0500 Subject: [PATCH 3/8] mldsa has a keygen_from_rng() --- crypto/core/src/traits.rs | 7 ++- crypto/mldsa/src/mldsa.rs | 25 ++++----- crypto/mldsa/tests/mldsa_tests.rs | 89 ++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 14 deletions(-) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 3576d62..131b1ea 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -426,7 +426,12 @@ impl SecurityStrength { /// be used by applications that intend to submit to FIPS certification as it more closely aligns with the /// requirements of SP 800-90A. /// Note: this interface produces bytes. If you want a [KeyMaterialTrait], then use [KeyMaterial::from_rng]. -pub trait RNG: Default { +/// +/// Implementors are expected to also implement [Default] (default-construction should produce a +/// securely OS-seeded instance), but this is intentionally *not* a supertrait bound: requiring +/// `Default` would make `RNG` not dyn-compatible, and `&mut dyn RNG` is needed so RNG instances +/// can be handed around as trait objects. +pub trait RNG { // TODO: add back once we figure out streaming interaction with entropy sources. // fn add_seed_bytes(&mut self, additional_seed: &[u8]) -> Result<(), RNGError>; diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index ca41c81..88a90d0 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -728,19 +728,11 @@ impl< GAMMA1_MASK_LEN, > { - /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. - /// - /// Key generation is intentionally not part of the [Signer] / [SignatureVerifier] traits; - /// it is provided as an inherent associated function directly on the algorithm struct. - /// Error condition: basically only on RNG failures. - pub fn keygen() -> Result<(PK, SK), SignatureError> { - Self::keygen_from_os_rng() - } - - /// Should still be ok in FIPS mode - pub fn keygen_from_os_rng() -> Result<(PK, SK), SignatureError> { + /// Run a keygen using the provided RNG implementation. + // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. + pub fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { let mut seed = KeyMaterial256::new(); - HashDRBG_SHA512::new_from_os().fill_keymaterial_out(&mut seed)?; + rng.fill_keymaterial_out(&mut seed)?; Self::keygen_internal(&seed) } /// Implements Algorithm 6 of FIPS 204 @@ -1932,6 +1924,15 @@ impl< GAMMA1_MASK_LEN, > { + /// Runs a key generation using the library's default RNG, seeded from the OS. + /// In environments where the default OS based RNG is not available, use instead [MLDSA::keygen_from_rng] + /// and explicitly provide a [RNG] implementation, or use [MLDSATrait::keygen_from_seed] and provide the + /// private key seed directly. + fn keygen() -> Result<(PK, SK), SignatureError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } + fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; Self::sign_out(sk, msg, ctx, &mut out)?; diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index b87a0a8..b404e32 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -1,8 +1,12 @@ +use bouncycastle_core::errors::RNGError; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{RNG, SecurityStrength}; + /// This performs tests using the public interfaces of the crate. #[cfg(test)] mod mldsa_tests { use crate::{MLDSA44_KAT1, MLDSA65_KAT1, MLDSA87_KAT1}; - use bouncycastle_core::errors::SignatureError; + use bouncycastle_core::errors::{RNGError, SignatureError}; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, @@ -198,6 +202,42 @@ mod mldsa_tests { } } + #[test] + fn keygen_from_rng_matches_keygen_from_seed() { + use super::FixedSeedRNG; + + // Same arbitrary fixed seed as rfc9881_keygen. + let seed_bytes: [u8; 32] = + hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + .unwrap() + .try_into() + .unwrap(); + + // The seed as fed directly to keygen_from_seed. + let seed = KeyMaterial256::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + // ML-DSA-44 + let (pk_seed, sk_seed) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG { seed: seed_bytes }; + let (pk_rng, sk_rng) = MLDSA44::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-DSA-44 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-DSA-44 sk from RNG must match sk from seed"); + + // ML-DSA-65 + let (pk_seed, sk_seed) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG { seed: seed_bytes }; + let (pk_rng, sk_rng) = MLDSA65::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-DSA-65 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-DSA-65 sk from RNG must match sk from seed"); + + // ML-DSA-87 + let (pk_seed, sk_seed) = MLDSA87::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG { seed: seed_bytes }; + let (pk_rng, sk_rng) = MLDSA87::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-DSA-87 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-DSA-87 sk from RNG must match sk from seed"); + } + #[test] fn keygen_error_cases() { /* @@ -909,6 +949,53 @@ mod mldsa_tests { } } +/// A test-only fake [RNG] that always emits the same fixed 32-byte seed. +/// +/// Only [RNG::fill_keymaterial_out] is implemented (it is the sole method `keygen_from_rng` +/// drives); the remaining trait methods panic if called so the test fails loudly should the +/// keygen path ever start relying on them. This lets us prove that +/// `keygen_from_rng(rng)` produces exactly the same keypair as `keygen_from_seed(seed)` +/// when the RNG hands back the bytes that `seed` was built from. +struct FixedSeedRNG { + seed: [u8; 32], +} + +impl RNG for FixedSeedRNG { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn KeyMaterialTrait, + ) -> Result<(), RNGError> { + unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") + } + fn next_int(&mut self) -> Result { + unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") + } + fn next_bytes(&mut self, _len: usize) -> Result, RNGError> { + unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") + } + fn next_bytes_out(&mut self, _out: &mut [u8]) -> Result { + unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") + } + + /// Fill `out` with the fixed seed, mirroring what a real DRBG's + /// `generate_keymaterial_out` sets: full-entropy-equivalent key type, the matching key + /// length, and a 256-bit security strength (enough for every ML-DSA parameter set). + fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { + out.allow_hazardous_operations(); + // mut_ref_to_bytes is infallible here because we just allowed hazardous operations. + out.mut_ref_to_bytes().unwrap()[..self.seed.len()].copy_from_slice(&self.seed); + out.set_key_len(self.seed.len())?; + out.set_key_type(KeyType::Seed)?; + out.set_security_strength(SecurityStrength::_256bit)?; + out.drop_hazardous_operations(); + Ok(self.seed.len()) + } + + fn security_strength(&self) -> SecurityStrength { + SecurityStrength::_256bit + } +} + struct Kat { _parameter_set: &'static str, deterministic: bool, From b8cd984634ffc58ec8cb2336f4d3f7ec314657b0 Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Mon, 22 Jun 2026 00:58:21 -0500 Subject: [PATCH 4/8] Refactored MLDSA and MLKEM to take dyn RNG anywhere that the algorithms consume randomness --- alpha_0.1.2_release_notes.md | 5 + .../core-test-framework/src/fixed_seed_rng.rs | 92 ++++++++++++++++++ crypto/core-test-framework/src/kem.rs | 19 ++++ crypto/core-test-framework/src/lib.rs | 3 + crypto/mldsa-lowmemory/src/mldsa.rs | 31 +++--- crypto/mldsa-lowmemory/tests/mldsa_tests.rs | 36 +++++++ crypto/mldsa/src/mldsa.rs | 31 +++--- crypto/mldsa/tests/mldsa_tests.rs | 62 +----------- crypto/mlkem-lowmemory/src/mlkem.rs | 39 ++++---- crypto/mlkem-lowmemory/tests/mlkem_tests.rs | 93 ++++++++++++++++++ crypto/mlkem/src/mlkem.rs | 54 +++++++---- crypto/mlkem/tests/mlkem_tests.rs | 97 +++++++++++++++++++ 12 files changed, 440 insertions(+), 122 deletions(-) create mode 100644 crypto/core-test-framework/src/fixed_seed_rng.rs diff --git a/alpha_0.1.2_release_notes.md b/alpha_0.1.2_release_notes.md index cebd420..59564bb 100644 --- a/alpha_0.1.2_release_notes.md +++ b/alpha_0.1.2_release_notes.md @@ -6,6 +6,11 @@ * Check the crate release checklist and run claude against the style guide (maybe Francis could cross-check me) * Run Crucible testing * Add factories for ML-DSA and ML-KEM (if we are keeping factories, see below) + * After merging the Signer/Verifier, Encrypter/Decrypter split, check if the keygen_from_rng() is still on the right + trait. +* Split the Signature trait into a Signer and a Verifier so that, for example, we can implement the verifier for MTC in + a different struct from the signer; or so that you can get FIPS compliance on old algorithms that are currently only + FIPS-allowed for verification of existing signatures but not for creation of new ones. * Check out Megan's email May 13 about KeyMaterial: "I was wondering if there might be scope for a closure based approach that could guarantee encapsulation of the state change from safe to hazardous back to safe again." * Go back to previous algs and apply memory optimization tricks like internal functions. And add a docs section "Memory diff --git a/crypto/core-test-framework/src/fixed_seed_rng.rs b/crypto/core-test-framework/src/fixed_seed_rng.rs new file mode 100644 index 0000000..244efed --- /dev/null +++ b/crypto/core-test-framework/src/fixed_seed_rng.rs @@ -0,0 +1,92 @@ +//! A deterministic fake [RNG] for reproducible tests. + +use bouncycastle_core::errors::RNGError; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{RNG, SecurityStrength}; + +/// A test-only fake [RNG] that produces a fixed, fully deterministic byte stream. +/// +/// The stream is the `SEED_LEN`-byte seed repeated indefinitely. A single internal counter is +/// shared across every [RNG] method, so each byte handed out — whether through +/// [RNG::next_bytes_out], [RNG::next_bytes], [RNG::next_int], or [RNG::fill_keymaterial_out] — +/// advances the same stream. Two instances built from the same seed therefore emit identical +/// streams, which is what makes RNG-driven operations reproducible (and comparable against their +/// seed/`m`-driven internal counterparts) in tests. +/// +/// This is a deterministic stub for tests only; it is in no way a secure RNG. +pub struct FixedSeedRNG { + seed: [u8; SEED_LEN], + counter: usize, +} + +impl FixedSeedRNG { + /// Create an instance that emits `seed` repeated indefinitely, starting from its first byte. + pub fn new(seed: [u8; SEED_LEN]) -> Self { + Self { seed, counter: 0 } + } + + /// Pull the next byte from the deterministic stream and advance the counter. + fn next_byte(&mut self) -> u8 { + let b = self.seed[self.counter % SEED_LEN]; + self.counter += 1; + b + } +} + +impl RNG for FixedSeedRNG { + /// No-op: this fake RNG ignores reseeding, since its stream is fixed by construction. + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn KeyMaterialTrait, + ) -> Result<(), RNGError> { + Ok(()) + } + + fn next_int(&mut self) -> Result { + let mut buf = [0u8; 4]; + for slot in buf.iter_mut() { + *slot = self.next_byte(); + } + Ok(u32::from_le_bytes(buf)) + } + + fn next_bytes(&mut self, len: usize) -> Result, RNGError> { + let mut out = vec![0u8; len]; + for slot in out.iter_mut() { + *slot = self.next_byte(); + } + Ok(out) + } + + fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { + for slot in out.iter_mut() { + *slot = self.next_byte(); + } + Ok(out.len()) + } + + /// Fill `out` to capacity from the stream and mark it as a full-entropy 256-bit seed, + /// mirroring what a real DRBG's `generate_keymaterial_out` produces. A 256-bit security + /// strength is enough for every ML-KEM / ML-DSA parameter set. + fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { + out.allow_hazardous_operations(); + let len = { + // mut_ref_to_bytes is infallible here because we just allowed hazardous operations. + let buf = out.mut_ref_to_bytes().unwrap(); + for slot in buf.iter_mut() { + *slot = self.seed[self.counter % SEED_LEN]; + self.counter += 1; + } + buf.len() + }; + out.set_key_len(len)?; + out.set_key_type(KeyType::Seed)?; + out.set_security_strength(SecurityStrength::_256bit)?; + out.drop_hazardous_operations(); + Ok(len) + } + + fn security_strength(&self) -> SecurityStrength { + SecurityStrength::_256bit + } +} diff --git a/crypto/core-test-framework/src/kem.rs b/crypto/core-test-framework/src/kem.rs index 4509684..0d6ad7b 100644 --- a/crypto/core-test-framework/src/kem.rs +++ b/crypto/core-test-framework/src/kem.rs @@ -1,3 +1,4 @@ +use crate::FixedSeedRNG; use bouncycastle_core::errors::KEMError; use bouncycastle_core::traits::{KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey}; @@ -41,6 +42,24 @@ impl TestFrameworkKEM { let ss1 = DECAPSULATOR::decaps(&sk, &ct).unwrap(); assert_eq!(ss, ss1); + // Test that encaps_rng is deterministic in its RNG input: two encapsulations against the + // same public key, each fed an RNG that emits identical bytes, must produce the same + // shared secret and ciphertext. + { + let mut rng_a = FixedSeedRNG::new([0x5A; 64]); + let mut rng_b = FixedSeedRNG::new([0x5A; 64]); + let (ss_a, ct_a) = KEMAlg::encaps_rng(&pk, &mut rng_a).unwrap(); + let (ss_b, ct_b) = KEMAlg::encaps_rng(&pk, &mut rng_b).unwrap(); + assert_eq!( + ss_a, ss_b, + "encaps_rng shared secret must be deterministic given fixed RNG output" + ); + assert_eq!( + ct_a, ct_b, + "encaps_rng ciphertext must be deterministic given fixed RNG output" + ); + } + // Test non-determinism if !self.alg_is_deterministic { let (ss1, ct1) = ENCAPSULATOR::encaps(&pk).unwrap(); diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index 92f7215..be769ae 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -15,6 +15,9 @@ pub mod kem; pub mod mac; pub mod signature; +mod fixed_seed_rng; +pub use fixed_seed_rng::FixedSeedRNG; + pub const DUMMY_SEED_512: &[u8; 512] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; pub const DUMMY_SEED_1024: &[u8; 1024] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index 5768e3f..91434d4 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -718,21 +718,6 @@ impl< GAMMA1_MASK_LEN, > { - /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. - /// - /// Key generation is intentionally not part of the [Signer] / [SignatureVerifier] traits; - /// it is provided as an inherent associated function directly on the algorithm struct. - /// Error condition: basically only on RNG failures. - pub fn keygen() -> Result<(PK, SK), SignatureError> { - Self::keygen_from_os_rng() - } - - /// Should still be ok in FIPS mode - pub fn keygen_from_os_rng() -> Result<(PK, SK), SignatureError> { - let mut seed = KeyMaterial::<32>::new(); - HashDRBG_SHA512::new_from_os().fill_keymaterial_out(&mut seed)?; - Self::keygen_internal(&seed) - } /// Performs the first step of key generation to transform the single provided seed into a set of internal intermediate seeds. /// /// Unlike other interfaces across the library that take an &impl KeyMaterial, this one @@ -1335,6 +1320,13 @@ pub trait MLDSATrait< const ETA: usize, >: Sized { + /// Run a keygen using the provided RNG implementation. + // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. + fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { + let mut seed = KeyMaterial::<32>::new(); + rng.fill_keymaterial_out(&mut seed)?; + Self::keygen_from_seed(&seed) + } /// Imports a secret key from a seed. fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError>; /// Imports a secret key from both a seed and an encoded_sk. @@ -1582,6 +1574,15 @@ impl< GAMMA1_MASK_LEN, > { + /// Runs a key generation using the library's default RNG, seeded from the OS. + /// In environments where the default OS based RNG is not available, use instead [MLDSA::keygen_from_rng] + /// and explicitly provide a [RNG] implementation, or use [MLDSATrait::keygen_from_seed] and provide the + /// private key seed directly. + fn keygen() -> Result<(PK, SK), SignatureError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } + fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; Self::sign_out(sk, msg, ctx, &mut out)?; diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index e45ed76..4b8a761 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -8,6 +8,7 @@ mod mldsa_tests { RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, }; use bouncycastle_core_test_framework::DUMMY_SEED_1024; + use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::signature::*; use bouncycastle_hex as hex; use bouncycastle_mldsa_lowmemory::{ @@ -18,6 +19,41 @@ mod mldsa_tests { use bouncycastle_mldsa_lowmemory::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; use bouncycastle_mldsa_lowmemory::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; use bouncycastle_mldsa_lowmemory::{MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait, MLDSATrait}; + + #[test] + fn keygen_from_rng_matches_keygen_from_seed() { + // Same arbitrary fixed seed as rfc9881_keygen. + let seed_bytes: [u8; 32] = + hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + .unwrap() + .try_into() + .unwrap(); + + // The seed as fed directly to keygen_from_seed. + let seed = KeyMaterial256::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + // ML-DSA-44 + let (pk_seed, sk_seed) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLDSA44::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-DSA-44 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-DSA-44 sk from RNG must match sk from seed"); + + // ML-DSA-65 + let (pk_seed, sk_seed) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLDSA65::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-DSA-65 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-DSA-65 sk from RNG must match sk from seed"); + + // ML-DSA-87 + let (pk_seed, sk_seed) = MLDSA87::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLDSA87::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-DSA-87 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-DSA-87 sk from RNG must match sk from seed"); + } + #[test] fn test_framework_signature() { let tf = TestFrameworkSignature::new(false, true); diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 88a90d0..37cf23c 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -728,13 +728,6 @@ impl< GAMMA1_MASK_LEN, > { - /// Run a keygen using the provided RNG implementation. - // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. - pub fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { - let mut seed = KeyMaterial256::new(); - rng.fill_keymaterial_out(&mut seed)?; - Self::keygen_internal(&seed) - } /// Implements Algorithm 6 of FIPS 204 /// Note: NIST has made a special exception in the FIPS 204 FAQ that this _internal function /// may in fact be exposed outside the crypto module. @@ -1652,6 +1645,21 @@ pub trait MLDSATrait< const ETA: usize, >: Sized { + /// Runs a key generation using the library's default RNG, seeded from the OS. + /// In environments where the default OS based RNG is not available, use instead [MLDSA::keygen_from_rng] + /// and explicitly provide a [RNG] implementation, or use [MLDSATrait::keygen_from_seed] and provide the + /// private key seed directly. + fn keygen() -> Result<(PK, SK), SignatureError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } + /// Run a keygen using the provided RNG implementation. + // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. + fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { + let mut seed = KeyMaterial256::new(); + rng.fill_keymaterial_out(&mut seed)?; + Self::keygen_from_seed(&seed) + } /// Imports a secret key from a seed. fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError>; /// Imports a secret key from both a seed and an encoded_sk. @@ -1924,15 +1932,6 @@ impl< GAMMA1_MASK_LEN, > { - /// Runs a key generation using the library's default RNG, seeded from the OS. - /// In environments where the default OS based RNG is not available, use instead [MLDSA::keygen_from_rng] - /// and explicitly provide a [RNG] implementation, or use [MLDSATrait::keygen_from_seed] and provide the - /// private key seed directly. - fn keygen() -> Result<(PK, SK), SignatureError> { - let mut os_rng = HashDRBG_SHA512::new_from_os(); - Self::keygen_from_rng(&mut os_rng) - } - fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; Self::sign_out(sk, msg, ctx, &mut out)?; diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index b404e32..773f0bb 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -1,17 +1,14 @@ -use bouncycastle_core::errors::RNGError; -use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{RNG, SecurityStrength}; - /// This performs tests using the public interfaces of the crate. #[cfg(test)] mod mldsa_tests { use crate::{MLDSA44_KAT1, MLDSA65_KAT1, MLDSA87_KAT1}; - use bouncycastle_core::errors::{RNGError, SignatureError}; + use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, }; use bouncycastle_core_test_framework::DUMMY_SEED_1024; + use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::signature::*; use bouncycastle_hex as hex; use bouncycastle_mldsa::{ @@ -204,8 +201,6 @@ mod mldsa_tests { #[test] fn keygen_from_rng_matches_keygen_from_seed() { - use super::FixedSeedRNG; - // Same arbitrary fixed seed as rfc9881_keygen. let seed_bytes: [u8; 32] = hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") @@ -218,21 +213,21 @@ mod mldsa_tests { // ML-DSA-44 let (pk_seed, sk_seed) = MLDSA44::keygen_from_seed(&seed).unwrap(); - let mut rng = FixedSeedRNG { seed: seed_bytes }; + let mut rng = FixedSeedRNG::new(seed_bytes); let (pk_rng, sk_rng) = MLDSA44::keygen_from_rng(&mut rng).unwrap(); assert_eq!(pk_rng, pk_seed, "ML-DSA-44 pk from RNG must match pk from seed"); assert_eq!(sk_rng, sk_seed, "ML-DSA-44 sk from RNG must match sk from seed"); // ML-DSA-65 let (pk_seed, sk_seed) = MLDSA65::keygen_from_seed(&seed).unwrap(); - let mut rng = FixedSeedRNG { seed: seed_bytes }; + let mut rng = FixedSeedRNG::new(seed_bytes); let (pk_rng, sk_rng) = MLDSA65::keygen_from_rng(&mut rng).unwrap(); assert_eq!(pk_rng, pk_seed, "ML-DSA-65 pk from RNG must match pk from seed"); assert_eq!(sk_rng, sk_seed, "ML-DSA-65 sk from RNG must match sk from seed"); // ML-DSA-87 let (pk_seed, sk_seed) = MLDSA87::keygen_from_seed(&seed).unwrap(); - let mut rng = FixedSeedRNG { seed: seed_bytes }; + let mut rng = FixedSeedRNG::new(seed_bytes); let (pk_rng, sk_rng) = MLDSA87::keygen_from_rng(&mut rng).unwrap(); assert_eq!(pk_rng, pk_seed, "ML-DSA-87 pk from RNG must match pk from seed"); assert_eq!(sk_rng, sk_seed, "ML-DSA-87 sk from RNG must match sk from seed"); @@ -949,53 +944,6 @@ mod mldsa_tests { } } -/// A test-only fake [RNG] that always emits the same fixed 32-byte seed. -/// -/// Only [RNG::fill_keymaterial_out] is implemented (it is the sole method `keygen_from_rng` -/// drives); the remaining trait methods panic if called so the test fails loudly should the -/// keygen path ever start relying on them. This lets us prove that -/// `keygen_from_rng(rng)` produces exactly the same keypair as `keygen_from_seed(seed)` -/// when the RNG hands back the bytes that `seed` was built from. -struct FixedSeedRNG { - seed: [u8; 32], -} - -impl RNG for FixedSeedRNG { - fn add_seed_keymaterial( - &mut self, - _additional_seed: &dyn KeyMaterialTrait, - ) -> Result<(), RNGError> { - unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") - } - fn next_int(&mut self) -> Result { - unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") - } - fn next_bytes(&mut self, _len: usize) -> Result, RNGError> { - unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") - } - fn next_bytes_out(&mut self, _out: &mut [u8]) -> Result { - unimplemented!("FixedSeedRNG only implements fill_keymaterial_out") - } - - /// Fill `out` with the fixed seed, mirroring what a real DRBG's - /// `generate_keymaterial_out` sets: full-entropy-equivalent key type, the matching key - /// length, and a 256-bit security strength (enough for every ML-DSA parameter set). - fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { - out.allow_hazardous_operations(); - // mut_ref_to_bytes is infallible here because we just allowed hazardous operations. - out.mut_ref_to_bytes().unwrap()[..self.seed.len()].copy_from_slice(&self.seed); - out.set_key_len(self.seed.len())?; - out.set_key_type(KeyType::Seed)?; - out.set_security_strength(SecurityStrength::_256bit)?; - out.drop_hazardous_operations(); - Ok(self.seed.len()) - } - - fn security_strength(&self) -> SecurityStrength { - SecurityStrength::_256bit - } -} - struct Kat { _parameter_set: &'static str, deterministic: bool, diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index 98eca08..3958408 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -235,22 +235,6 @@ impl< T_PACKED_LEN, > { - /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. - /// - /// Key generation is intentionally not part of the [KEMEncapsulator] / [KEMDecapsulator] traits; - /// it is provided as an inherent associated function directly on the algorithm struct. - /// Error condition: basically only on RNG failures. - pub fn keygen() -> Result<(PK, SK), KEMError> { - Self::keygen_from_os_rng() - } - - /// Should still be ok in FIPS mode - pub fn keygen_from_os_rng() -> Result<(PK, SK), KEMError> { - let mut seed = KeyMaterial::<64>::new(); - HashDRBG_SHA512::new_from_os().fill_keymaterial_out(&mut seed)?; - // Self::keygen_internal(&seed) - Self::keygen_internal(&seed) - } /// Performs the first step of key generation to transform the single provided seed into a set of internal intermediate seeds. /// /// Unlike other interfaces across the library that take an &impl KeyMaterial, this one @@ -629,6 +613,13 @@ pub trait MLKEMTrait< const T_PACKED_LEN: usize, >: Sized { + /// Run a keygen using the provided RNG implementation. + // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. + fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), KEMError> { + let mut seed = KeyMaterial::<64>::new(); + rng.fill_keymaterial_out(&mut seed)?; + Self::keygen_from_seed(&seed) + } /// Imports a secret key from a seed. fn keygen_from_seed(seed: &KeyMaterial<64>) -> Result<(PK, SK), KEMError>; /// Imports a secret key from both a seed and an encoded_sk. @@ -685,9 +676,23 @@ impl< T_PACKED_LEN, > { + /// Generates a fresh key pair. + fn keygen() -> Result<(PK, SK), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } + fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::encaps_rng(pk, &mut os_rng) + } + + fn encaps_rng( + pk: &PK, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { let mut m = [0u8; 32]; - HashDRBG_SHA512::new_from_os().next_bytes_out(&mut m)?; + rng.next_bytes_out(&mut m)?; let (ss_bytes, ct) = Self::encaps_internal(pk, m); diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 71cc449..12568c6 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -6,6 +6,7 @@ mod mlkem_tests { use bouncycastle_core::traits::{ KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, }; + use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; use bouncycastle_mlkem_lowmemory::mlkem::{ MLKEM512_FULL_SK_LEN, MLKEM768_FULL_SK_LEN, MLKEM1024_FULL_SK_LEN, @@ -22,6 +23,42 @@ mod mlkem_tests { MLKEM1024PrivateKey, MLKEM1024PublicKey, }; use bouncycastle_mlkem_lowmemory::{MLKEMPrivateKeyTrait, MLKEMTrait}; + + #[test] + fn keygen_from_rng_matches_keygen_from_seed() { + // An arbitrary fixed 64-byte seed (bytes 0x00..=0x3f). + let seed_bytes: [u8; 64] = hex::decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + ) + .unwrap() + .try_into() + .unwrap(); + + // The seed as fed directly to keygen_from_seed. + let seed = KeyMaterial512::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + // ML-KEM-512 + let (pk_seed, sk_seed) = MLKEM512::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLKEM512::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-KEM-512 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-KEM-512 sk from RNG must match sk from seed"); + + // ML-KEM-768 + let (pk_seed, sk_seed) = MLKEM768::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLKEM768::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-KEM-768 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-KEM-768 sk from RNG must match sk from seed"); + + // ML-KEM-1024 + let (pk_seed, sk_seed) = MLKEM1024::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLKEM1024::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-KEM-1024 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-KEM-1024 sk from RNG must match sk from seed"); + } use bouncycastle_sha3::SHAKE256; // #[test] @@ -552,6 +589,62 @@ mod mlkem_tests { _ => panic!("Expected error for different key"), }; } + + /// Proves that `encaps_rng` is just `encaps_internal` with the message `m` sourced from the + /// RNG: when the RNG hands back exactly the bytes that `m` would be, the two must produce the + /// same shared secret and ciphertext. + /// + /// Determinism of `encaps_rng` itself is covered by the shared KEM test + /// framework via `core_framework_tests`.) + #[test] + fn encaps_rng_matches_encaps_internal() { + // An arbitrary fixed 64-byte seed; FixedSeedRNG::next_bytes_out hands back its leading + // 32 bytes as the encapsulation message `m`. + let seed_bytes: [u8; 64] = hex::decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + ) + .unwrap() + .try_into() + .unwrap(); + let m: [u8; MLKEM_RND_LEN] = seed_bytes[..MLKEM_RND_LEN].try_into().unwrap(); + + // ML-KEM-512 + let (pk, _sk) = MLKEM512::keygen().unwrap(); + let (ss_ref, ct_ref) = MLKEM512::encaps_internal(&pk, m); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (ss, ct) = MLKEM512::encaps_rng(&pk, &mut rng).unwrap(); + assert_eq!(ct, ct_ref, "ML-KEM-512 ciphertext must match encaps_internal"); + assert_eq!( + ss_ref, + ss.ref_to_bytes(), + "ML-KEM-512 shared secret must match encaps_internal" + ); + + // ML-KEM-768 + let (pk, _sk) = MLKEM768::keygen().unwrap(); + let (ss_ref, ct_ref) = MLKEM768::encaps_internal(&pk, m); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (ss, ct) = MLKEM768::encaps_rng(&pk, &mut rng).unwrap(); + assert_eq!(ct, ct_ref, "ML-KEM-768 ciphertext must match encaps_internal"); + assert_eq!( + ss_ref, + ss.ref_to_bytes(), + "ML-KEM-768 shared secret must match encaps_internal" + ); + + // ML-KEM-1024 + let (pk, _sk) = MLKEM1024::keygen().unwrap(); + let (ss_ref, ct_ref) = MLKEM1024::encaps_internal(&pk, m); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (ss, ct) = MLKEM1024::encaps_rng(&pk, &mut rng).unwrap(); + assert_eq!(ct, ct_ref, "ML-KEM-1024 ciphertext must match encaps_internal"); + assert_eq!( + ss_ref, + ss.ref_to_bytes(), + "ML-KEM-1024 shared secret must match encaps_internal" + ); + } } // struct Kat { diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index d370ea4..24d940a 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -319,21 +319,6 @@ impl< const LAMBDA: i16, > MLKEM { - /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. - /// - /// Key generation is intentionally not part of the [KEMEncapsulator] / [KEMDecapsulator] traits; - /// it is provided as an inherent associated function directly on the algorithm struct. - /// Error condition: basically only on RNG failures. - pub fn keygen() -> Result<(PK, SK), KEMError> { - Self::keygen_from_os_rng() - } - - /// Should still be ok in FIPS mode - pub fn keygen_from_os_rng() -> Result<(PK, SK), KEMError> { - let mut seed = KeyMaterial::<64>::new(); - HashDRBG_SHA512::new_from_os().fill_keymaterial_out(&mut seed)?; - Self::keygen_internal(&seed) - } /// Algorithm 16 ML-KEM.KeyGen_internal(𝑑, 𝑧) /// Uses randomness to generate an encapsulation key and a corresponding decapsulation key. /// Input: randomness 𝑑 ∈ 𝔹32 . @@ -768,9 +753,17 @@ impl< fn encaps_for_expanded_key( pk: &MLKEMPublicKeyExpanded, + ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::encaps_for_expanded_key_rng(pk, &mut os_rng) + } + + fn encaps_for_expanded_key_rng( + pk: &MLKEMPublicKeyExpanded, + rng: &mut dyn RNG, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { let mut m = [0u8; 32]; - HashDRBG_SHA512::new_from_os().next_bytes_out(&mut m)?; + rng.next_bytes_out(&mut m)?; let (ss, ct) = Self::encaps_internal(&pk.ek, Some(&pk.A_hat), m); @@ -830,6 +823,13 @@ pub trait MLKEMTrait< const LAMBDA: i16, >: Sized { + /// Run a keygen using the provided RNG implementation. + // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. + fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), KEMError> { + let mut seed = KeyMaterial::<64>::new(); + rng.fill_keymaterial_out(&mut seed)?; + Self::keygen_from_seed(&seed) + } /// Imports a secret key from a seed. fn keygen_from_seed(seed: &KeyMaterial<64>) -> Result<(PK, SK), KEMError>; /// Imports a secret key from both a seed and an encoded_sk. @@ -857,6 +857,12 @@ pub trait MLKEMTrait< pk: &MLKEMPublicKeyExpanded, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; + /// Same as [KEM::encaps], but acts on an [MLKEMPublicKeyExpanded] and uses a provided RNG. + fn encaps_for_expanded_key_rng( + pk: &MLKEMPublicKeyExpanded, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; + /// Same as [KEMDecapsulator::decaps], but acts on an [MLKEMPrivateKeyExpanded]. fn decaps_with_expanded_key( sk: &MLKEMPrivateKeyExpanded, @@ -880,6 +886,12 @@ impl< > KEMEncapsulator for MLKEM { + /// Generates a fresh key pair. + fn keygen() -> Result<(PK, SK), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } + /// Performs an encapsulation against the given public key, using the library's default internal RNG. /// Returns (shared_secret_key, ciphertext) /// The derived shared secret key is returned as a KeyMaterial with the SecurityStrength set to @@ -891,7 +903,15 @@ impl< /// Output: shared secret key 𝐾 ∈ 𝔹32 . /// Output: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { - Self::encaps_for_expanded_key(&MLKEMPublicKeyExpanded::::from(pk)) + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::encaps_rng(pk, &mut os_rng) + } + + fn encaps_rng( + pk: &PK, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { + Self::encaps_for_expanded_key_rng(&MLKEMPublicKeyExpanded::::from(pk), rng) } } diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index d5aa274..3f01707 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -6,6 +6,7 @@ mod mlkem_tests { use bouncycastle_core::traits::{ KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, }; + use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; use bouncycastle_mlkem::{MLKEM_RND_LEN, MLKEM512, MLKEM768, MLKEM1024, Polynomial}; use bouncycastle_mlkem::{ @@ -19,6 +20,42 @@ mod mlkem_tests { use bouncycastle_mlkem::{MLKEMPrivateKeyTrait, MLKEMTrait}; use bouncycastle_sha3::SHAKE256; + #[test] + fn keygen_from_rng_matches_keygen_from_seed() { + // An arbitrary fixed 64-byte seed (bytes 0x00..=0x3f). + let seed_bytes: [u8; 64] = hex::decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + ) + .unwrap() + .try_into() + .unwrap(); + + // The seed as fed directly to keygen_from_seed. + let seed = KeyMaterial512::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + // ML-KEM-512 + let (pk_seed, sk_seed) = MLKEM512::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLKEM512::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-KEM-512 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-KEM-512 sk from RNG must match sk from seed"); + + // ML-KEM-768 + let (pk_seed, sk_seed) = MLKEM768::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLKEM768::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-KEM-768 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-KEM-768 sk from RNG must match sk from seed"); + + // ML-KEM-1024 + let (pk_seed, sk_seed) = MLKEM1024::keygen_from_seed(&seed).unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk_rng, sk_rng) = MLKEM1024::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk_rng, pk_seed, "ML-KEM-1024 pk from RNG must match pk from seed"); + assert_eq!(sk_rng, sk_seed, "ML-KEM-1024 sk from RNG must match sk from seed"); + } + // #[test] // fn generate_kats_for_low_mem() { // let seed = KeyMaterial256::from_bytes_as_type( @@ -635,6 +672,66 @@ mod mlkem_tests { }; assert_eq!(ss, ss1); } + + /// Proves that `encaps_for_expanded_key_rng` is just `encaps_internal` with the message `m` + /// sourced from the RNG: when the RNG hands back exactly the bytes that `m` would be, the two + /// must produce the same shared secret and ciphertext. + #[test] + fn encaps_for_expanded_key_rng_matches_encaps_internal() { + use bouncycastle_mlkem::{ + MLKEM512PublicKeyExpanded, MLKEM768PublicKeyExpanded, MLKEM1024PublicKeyExpanded, + }; + + // An arbitrary fixed 64-byte seed; FixedSeedRNG::next_bytes_out hands back its leading + // 32 bytes as the encapsulation message `m`. + let seed_bytes: [u8; 64] = hex::decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + ) + .unwrap() + .try_into() + .unwrap(); + let m: [u8; MLKEM_RND_LEN] = seed_bytes[..MLKEM_RND_LEN].try_into().unwrap(); + + // ML-KEM-512 + let (pk, _sk) = MLKEM512::keygen().unwrap(); + let (ss_ref, ct_ref) = MLKEM512::encaps_internal(&pk, None, m); + let pk_expanded = MLKEM512PublicKeyExpanded::from(&pk); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (ss, ct) = MLKEM512::encaps_for_expanded_key_rng(&pk_expanded, &mut rng).unwrap(); + assert_eq!(ct, ct_ref, "ML-KEM-512 ciphertext must match encaps_internal"); + assert_eq!( + ss_ref, + ss.ref_to_bytes(), + "ML-KEM-512 shared secret must match encaps_internal" + ); + + // ML-KEM-768 + let (pk, _sk) = MLKEM768::keygen().unwrap(); + let (ss_ref, ct_ref) = MLKEM768::encaps_internal(&pk, None, m); + let pk_expanded = MLKEM768PublicKeyExpanded::from(&pk); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (ss, ct) = MLKEM768::encaps_for_expanded_key_rng(&pk_expanded, &mut rng).unwrap(); + assert_eq!(ct, ct_ref, "ML-KEM-768 ciphertext must match encaps_internal"); + assert_eq!( + ss_ref, + ss.ref_to_bytes(), + "ML-KEM-768 shared secret must match encaps_internal" + ); + + // ML-KEM-1024 + let (pk, _sk) = MLKEM1024::keygen().unwrap(); + let (ss_ref, ct_ref) = MLKEM1024::encaps_internal(&pk, None, m); + let pk_expanded = MLKEM1024PublicKeyExpanded::from(&pk); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (ss, ct) = MLKEM1024::encaps_for_expanded_key_rng(&pk_expanded, &mut rng).unwrap(); + assert_eq!(ct, ct_ref, "ML-KEM-1024 ciphertext must match encaps_internal"); + assert_eq!( + ss_ref, + ss.ref_to_bytes(), + "ML-KEM-1024 shared secret must match encaps_internal" + ); + } } // struct Kat { From 3bdd97a206c627a665fde4f2c8f402e25746b451 Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Mon, 22 Jun 2026 09:08:02 -0500 Subject: [PATCH 5/8] Merged dyn RNG commits --- .../core-test-framework/src/fixed_seed_rng.rs | 2 +- crypto/core-test-framework/src/kem.rs | 27 +++++++++---------- crypto/core/src/traits.rs | 8 ++++++ crypto/mldsa-lowmemory/src/lib.rs | 2 +- crypto/mldsa-lowmemory/src/mldsa.rs | 17 ++++++------ crypto/mldsa/src/lib.rs | 2 +- crypto/mlkem-lowmemory/src/lib.rs | 2 +- crypto/mlkem-lowmemory/src/mlkem.rs | 11 ++++---- crypto/mlkem-lowmemory/tests/mlkem_tests.rs | 6 ++--- crypto/mlkem/src/lib.rs | 2 +- crypto/mlkem/src/mlkem.rs | 11 ++++---- crypto/mlkem/tests/mlkem_tests.rs | 6 ++--- 12 files changed, 50 insertions(+), 46 deletions(-) diff --git a/crypto/core-test-framework/src/fixed_seed_rng.rs b/crypto/core-test-framework/src/fixed_seed_rng.rs index 244efed..3e65ab9 100644 --- a/crypto/core-test-framework/src/fixed_seed_rng.rs +++ b/crypto/core-test-framework/src/fixed_seed_rng.rs @@ -72,7 +72,7 @@ impl RNG for FixedSeedRNG { out.allow_hazardous_operations(); let len = { // mut_ref_to_bytes is infallible here because we just allowed hazardous operations. - let buf = out.mut_ref_to_bytes().unwrap(); + let buf = out.ref_to_bytes_mut().unwrap(); for slot in buf.iter_mut() { *slot = self.seed[self.counter % SEED_LEN]; self.counter += 1; diff --git a/crypto/core-test-framework/src/kem.rs b/crypto/core-test-framework/src/kem.rs index 0d6ad7b..f0cec47 100644 --- a/crypto/core-test-framework/src/kem.rs +++ b/crypto/core-test-framework/src/kem.rs @@ -25,8 +25,7 @@ impl TestFrameworkKEM { pub fn test_kem< PK: KEMPublicKey, SK: KEMPrivateKey, - ENCAPSULATOR: KEMEncapsulator, - DECAPSULATOR: KEMDecapsulator, + KEMAlg: KEMEncapsulator + KEMDecapsulator, const PK_LEN: usize, const SK_LEN: usize, const CT_LEN: usize, @@ -38,8 +37,8 @@ impl TestFrameworkKEM { ) { // Basic test let (pk, sk) = keygen().unwrap(); - let (ss, ct) = ENCAPSULATOR::encaps(&pk).unwrap(); - let ss1 = DECAPSULATOR::decaps(&sk, &ct).unwrap(); + let (ss, ct) = KEMAlg::encaps(&pk).unwrap(); + let ss1 = KEMAlg::decaps(&sk, &ct).unwrap(); assert_eq!(ss, ss1); // Test that encaps_rng is deterministic in its RNG input: two encapsulations against the @@ -62,21 +61,21 @@ impl TestFrameworkKEM { // Test non-determinism if !self.alg_is_deterministic { - let (ss1, ct1) = ENCAPSULATOR::encaps(&pk).unwrap(); - let (ss2, ct2) = ENCAPSULATOR::encaps(&pk).unwrap(); + let (ss1, ct1) = KEMAlg::encaps(&pk).unwrap(); + let (ss2, ct2) = KEMAlg::encaps(&pk).unwrap(); assert_ne!(ss1, ss2); assert_ne!(ct1, ct2); } // Test that decaps fails for broken ct value let (pk, sk) = keygen().unwrap(); - let (ss, mut ct) = ENCAPSULATOR::encaps(&pk).unwrap(); + let (ss, mut ct) = KEMAlg::encaps(&pk).unwrap(); ct[17] ^= 0xFF; if self.is_implicitly_rejecting { - let ss2 = DECAPSULATOR::decaps(&sk, &ct).unwrap(); + let ss2 = KEMAlg::decaps(&sk, &ct).unwrap(); assert_ne!(ss, ss2); } else { - match DECAPSULATOR::decaps(&sk, &ct) { + match KEMAlg::decaps(&sk, &ct) { Err(KEMError::DecapsulationFailed) => /* good */ { @@ -95,10 +94,10 @@ impl TestFrameworkKEM { // should throw an Err if self.is_implicitly_rejecting { - let ss2 = DECAPSULATOR::decaps(&sk, &ct_copy).unwrap(); + let ss2 = KEMAlg::decaps(&sk, &ct_copy).unwrap(); assert_ne!(ss, ss2); } else { - match DECAPSULATOR::decaps(&sk, &ct) { + match KEMAlg::decaps(&sk, &ct) { Err(KEMError::DecapsulationFailed) => /* good */ { @@ -113,9 +112,9 @@ impl TestFrameworkKEM { // test ct the wrong length let (pk, sk) = keygen().unwrap(); - let (_ss, ct) = ENCAPSULATOR::encaps(&pk).unwrap(); + let (_ss, ct) = KEMAlg::encaps(&pk).unwrap(); // too short - match DECAPSULATOR::decaps(&sk, &ct[..CT_LEN - 1]) { + match KEMAlg::decaps(&sk, &ct[..CT_LEN - 1]) { Err(KEMError::LengthError(_)) => { /* good */ } _ => panic!("This should have thrown an error but it didn't."), }; @@ -123,7 +122,7 @@ impl TestFrameworkKEM { // too long let mut long_ct = vec![1u8; CT_LEN + 2]; long_ct.as_mut_slice()[..CT_LEN].copy_from_slice(&ct); - match DECAPSULATOR::decaps(&sk, &long_ct) { + match KEMAlg::decaps(&sk, &long_ct) { Err(KEMError::LengthError(_)) => { /* good */ } _ => panic!("This should have thrown an error but it didn't."), }; diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 131b1ea..034567c 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -206,8 +206,16 @@ pub trait KEMEncapsulator< >: Sized { /// Performs an encapsulation against the given public key. + /// Sources randomness from the library's default OS-backed RNG. /// Returns the ciphertext and derived shared secret. fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; + /// Performs an encapsulation against the given public key. + /// Sources randomness from the provided RNG. + /// Returns the ciphertext and derived shared secret. + fn encaps_rng( + pk: &PK, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; } /// A Key Encapsulation Mechanism (KEM) is defined as a set of three operations: diff --git a/crypto/mldsa-lowmemory/src/lib.rs b/crypto/mldsa-lowmemory/src/lib.rs index 5c23325..64a91f1 100644 --- a/crypto/mldsa-lowmemory/src/lib.rs +++ b/crypto/mldsa-lowmemory/src/lib.rs @@ -128,7 +128,7 @@ //! ## Generating Keys //! //! ```rust -//! use bouncycastle_mldsa_lowmemory::MLDSA65; +//! use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait}; //! //! let (pk, sk) = MLDSA65::keygen().unwrap(); //! ``` diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index 91434d4..6158c92 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -1320,6 +1320,14 @@ pub trait MLDSATrait< const ETA: usize, >: Sized { + /// Runs a key generation using the library's default RNG, seeded from the OS. + /// In environments where the default OS based RNG is not available, use instead [MLDSA::keygen_from_rng] + /// and explicitly provide a [RNG] implementation, or use [MLDSATrait::keygen_from_seed] and provide the + /// private key seed directly. + fn keygen() -> Result<(PK, SK), SignatureError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } /// Run a keygen using the provided RNG implementation. // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { @@ -1574,15 +1582,6 @@ impl< GAMMA1_MASK_LEN, > { - /// Runs a key generation using the library's default RNG, seeded from the OS. - /// In environments where the default OS based RNG is not available, use instead [MLDSA::keygen_from_rng] - /// and explicitly provide a [RNG] implementation, or use [MLDSATrait::keygen_from_seed] and provide the - /// private key seed directly. - fn keygen() -> Result<(PK, SK), SignatureError> { - let mut os_rng = HashDRBG_SHA512::new_from_os(); - Self::keygen_from_rng(&mut os_rng) - } - fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; Self::sign_out(sk, msg, ctx, &mut out)?; diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index ba33b1d..7480c3c 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -14,7 +14,7 @@ //! ## Generating Keys //! //! ```rust -//! use bouncycastle_mldsa::MLDSA65; +//! use bouncycastle_mldsa::{MLDSA65, MLDSATrait}; //! //! let (pk, sk) = MLDSA65::keygen().unwrap(); //! ``` diff --git a/crypto/mlkem-lowmemory/src/lib.rs b/crypto/mlkem-lowmemory/src/lib.rs index 6435f73..d371326 100644 --- a/crypto/mlkem-lowmemory/src/lib.rs +++ b/crypto/mlkem-lowmemory/src/lib.rs @@ -154,7 +154,7 @@ //! ## Generating Keys //! //! ```rust -//! use bouncycastle_mlkem_lowmemory::MLKEM768; +//! use bouncycastle_mlkem_lowmemory::{MLKEM768, MLKEMTrait}; //! //! let (pk, sk) = MLKEM768::keygen().unwrap(); //! ``` diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index 3958408..c9cca06 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -613,6 +613,11 @@ pub trait MLKEMTrait< const T_PACKED_LEN: usize, >: Sized { + /// Generates a fresh key pair. + fn keygen() -> Result<(PK, SK), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } /// Run a keygen using the provided RNG implementation. // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), KEMError> { @@ -676,12 +681,6 @@ impl< T_PACKED_LEN, > { - /// Generates a fresh key pair. - fn keygen() -> Result<(PK, SK), KEMError> { - let mut os_rng = HashDRBG_SHA512::new_from_os(); - Self::keygen_from_rng(&mut os_rng) - } - fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { let mut os_rng = HashDRBG_SHA512::new_from_os(); Self::encaps_rng(pk, &mut os_rng) diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 12568c6..3ba8dae 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -85,9 +85,9 @@ mod mlkem_tests { let tf = TestFrameworkKEM::new(false, true); - tf.test_kem::(MLKEM512::keygen, false); - tf.test_kem::(MLKEM768::keygen, false); - tf.test_kem::(MLKEM1024::keygen, false); + tf.test_kem::(MLKEM512::keygen, false); + tf.test_kem::(MLKEM768::keygen, false); + tf.test_kem::(MLKEM1024::keygen, false); } /// This runs the full bitflipping tests and takes about 1.5 mins.. diff --git a/crypto/mlkem/src/lib.rs b/crypto/mlkem/src/lib.rs index 3e4b9c6..c3dd524 100644 --- a/crypto/mlkem/src/lib.rs +++ b/crypto/mlkem/src/lib.rs @@ -45,7 +45,7 @@ //! ## Generating Keys //! //! ```rust -//! use bouncycastle_mlkem::MLKEM768; +//! use bouncycastle_mlkem::{MLKEM768, MLKEMTrait}; //! //! let (pk, sk) = MLKEM768::keygen().unwrap(); //! ``` diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index 24d940a..97ac843 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -823,6 +823,11 @@ pub trait MLKEMTrait< const LAMBDA: i16, >: Sized { + /// Generates a fresh key pair. + fn keygen() -> Result<(PK, SK), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } /// Run a keygen using the provided RNG implementation. // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), KEMError> { @@ -886,12 +891,6 @@ impl< > KEMEncapsulator for MLKEM { - /// Generates a fresh key pair. - fn keygen() -> Result<(PK, SK), KEMError> { - let mut os_rng = HashDRBG_SHA512::new_from_os(); - Self::keygen_from_rng(&mut os_rng) - } - /// Performs an encapsulation against the given public key, using the library's default internal RNG. /// Returns (shared_secret_key, ciphertext) /// The derived shared secret key is returned as a KeyMaterial with the SecurityStrength set to diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 3f01707..4b598c0 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -80,9 +80,9 @@ mod mlkem_tests { let tf = TestFrameworkKEM::new(false, true); - tf.test_kem::(MLKEM512::keygen, false); - tf.test_kem::(MLKEM768::keygen, false); - tf.test_kem::(MLKEM1024::keygen, false); + tf.test_kem::(MLKEM512::keygen, false); + tf.test_kem::(MLKEM768::keygen, false); + tf.test_kem::(MLKEM1024::keygen, false); } /// This runs the full bitflipping tests and takes about 30s.. From fed225734a508a3dd8f60d54f89079467c65673c Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Mon, 22 Jun 2026 09:08:02 -0500 Subject: [PATCH 6/8] rustfmt --- crypto/factory/src/rng_factory.rs | 5 +---- crypto/rng/src/hash_drbg80090a.rs | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/crypto/factory/src/rng_factory.rs b/crypto/factory/src/rng_factory.rs index 18bf23a..aa4bcb6 100644 --- a/crypto/factory/src/rng_factory.rs +++ b/crypto/factory/src/rng_factory.rs @@ -126,10 +126,7 @@ impl RNG for RNGFactory { } } - fn fill_keymaterial_out( - &mut self, - out: &mut dyn KeyMaterialTrait, - ) -> Result { + fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { match self { Self::HashDRBG_SHA256(rng) => rng.fill_keymaterial_out(out), Self::HashDRBG_SHA512(rng) => rng.fill_keymaterial_out(out), diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index 5b10b86..9b44a70 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -506,10 +506,7 @@ impl RNG for HashDRBG80090A { self.generate_out("next_bytes_out".as_bytes(), out) } - fn fill_keymaterial_out( - &mut self, - out: &mut dyn KeyMaterialTrait, - ) -> Result { + fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { self.generate_keymaterial_out("fill_keymaterial".as_bytes(), out) } From 8b724817b8169d267a387c1103d5f3d65bbe5344 Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Thu, 25 Jun 2026 06:27:49 -0500 Subject: [PATCH 7/8] tweaks --- crypto/core-test-framework/src/fixed_seed_rng.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crypto/core-test-framework/src/fixed_seed_rng.rs b/crypto/core-test-framework/src/fixed_seed_rng.rs index 3e65ab9..00efd39 100644 --- a/crypto/core-test-framework/src/fixed_seed_rng.rs +++ b/crypto/core-test-framework/src/fixed_seed_rng.rs @@ -70,15 +70,7 @@ impl RNG for FixedSeedRNG { /// strength is enough for every ML-KEM / ML-DSA parameter set. fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { out.allow_hazardous_operations(); - let len = { - // mut_ref_to_bytes is infallible here because we just allowed hazardous operations. - let buf = out.ref_to_bytes_mut().unwrap(); - for slot in buf.iter_mut() { - *slot = self.seed[self.counter % SEED_LEN]; - self.counter += 1; - } - buf.len() - }; + let len = self.next_bytes_out(out.mut_ref_to_bytes()?)?; out.set_key_len(len)?; out.set_key_type(KeyType::Seed)?; out.set_security_strength(SecurityStrength::_256bit)?; From 3fe1148f0bc605d424b98a56ed20f79053ef88f3 Mon Sep 17 00:00:00 2001 From: Mike Ounsworth Date: Thu, 25 Jun 2026 08:17:51 -0500 Subject: [PATCH 8/8] bug --- crypto/core-test-framework/src/fixed_seed_rng.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/core-test-framework/src/fixed_seed_rng.rs b/crypto/core-test-framework/src/fixed_seed_rng.rs index 00efd39..cd68ec7 100644 --- a/crypto/core-test-framework/src/fixed_seed_rng.rs +++ b/crypto/core-test-framework/src/fixed_seed_rng.rs @@ -70,7 +70,7 @@ impl RNG for FixedSeedRNG { /// strength is enough for every ML-KEM / ML-DSA parameter set. fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { out.allow_hazardous_operations(); - let len = self.next_bytes_out(out.mut_ref_to_bytes()?)?; + let len = self.next_bytes_out(out.ref_to_bytes_mut()?)?; out.set_key_len(len)?; out.set_key_type(KeyType::Seed)?; out.set_security_strength(SecurityStrength::_256bit)?;