From 5340d11165c6d7199bb8f033f3fa7b25e7b8b047 Mon Sep 17 00:00:00 2001 From: Marc Hartmayer Date: Fri, 26 Jun 2026 13:43:35 +0200 Subject: [PATCH] pv: test_utils: Add TEST-RAND generator for testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a test random number generator (RNG) using the TEST-RAND [1] generator provided by OpenSSL. This allows writing tests for OpenSSL functionality where OpenSSL internally uses RNGs, e.g. used for ML-KEM. The implementation uses RAND_set0_public [2] to set the thread-local RNG, which requires OpenSSL >= 3.1. [1] https://docs.openssl.org/3.0/man7/EVP_RAND-TEST-RAND/ [2] https://docs.openssl.org/3.1/man3/RAND_set0_public Assisted-by: IBM Bob:1.0.5 Acked-by: Steffen Eiden Acked-by: Timo Keller Signed-off-by: Marc Hartmayer Signed-off-by: Jan Höppner --- rust/pv/src/crypto.rs | 157 ++++++++++++++++ rust/pv/src/test_utils.rs | 381 +++++++++++++++++++++++++++++++++++++- 2 files changed, 537 insertions(+), 1 deletion(-) diff --git a/rust/pv/src/crypto.rs b/rust/pv/src/crypto.rs index 5a69a261..5ec90985 100644 --- a/rust/pv/src/crypto.rs +++ b/rust/pv/src/crypto.rs @@ -548,10 +548,167 @@ pub(crate) fn verify_signature( #[cfg(test)] mod tests { + use std::sync::Arc; + use std::thread; + use super::*; use crate::test_utils::*; use crate::{get_test_asset, PvCoreError}; + /// Test that deterministic RNG contexts are thread-local and don't interfere. + /// + /// Per OpenSSL documentation (RAND_get0_primary(3)): + /// "The public and private DRBG are thread-local instances, which are used by + /// RAND_bytes() and RAND_priv_bytes(), respectively." + /// + /// Reference: + /// + /// Note: RAND_set0_public() and RAND_set0_private() require OpenSSL >= 3.1. + #[test] + fn test_deterministic_rng_thread_isolation() { + use std::sync::Barrier; + + use openssl::rand::rand_bytes; + + // Barriers to synchronize: thread1 installs → thread2 installs → both generate → both + // complete + let barrier_after_t1_install = Arc::new(Barrier::new(2)); + let barrier_after_t2_install = Arc::new(Barrier::new(2)); + let barrier_after_rand_bytes = Arc::new(Barrier::new(2)); + + let barrier1_clone = Arc::clone(&barrier_after_t1_install); + let barrier2_clone = Arc::clone(&barrier_after_t2_install); + let barrier3_clone = Arc::clone(&barrier_after_rand_bytes); + + // Thread 1: Install deterministic RNG with specific entropy + let thread1 = thread::spawn(move || { + let entropy = [0x42u8; 4096]; + let nonce = [0x24u8; 48]; + + // Install thread-local deterministic RNG + let _rng = DeterministicTestRandGuard::install(&entropy, &nonce).unwrap(); + + // Signal thread2 that we've installed our RNG + barrier1_clone.wait(); + + // Wait for thread2 to install its RNG + barrier2_clone.wait(); + + // Now generate bytes while thread2 also has its RNG installed + let mut buf = [0u8; 32]; + rand_bytes(&mut buf).unwrap(); + + // Wait for thread2 to also complete rand_bytes + barrier3_clone.wait(); + + buf + }); + + // Thread 2: Install different deterministic RNG after thread1 + let thread2 = thread::spawn(move || { + // Wait for thread1 to install its RNG first + barrier_after_t1_install.wait(); + + // Now install our own thread-local deterministic RNG with different entropy + let entropy = [0xAAu8; 4096]; + let nonce = [0x55u8; 48]; + let _rng = DeterministicTestRandGuard::install(&entropy, &nonce).unwrap(); + + // Signal thread1 that we've installed our RNG + barrier_after_t2_install.wait(); + + // Generate bytes with our different entropy (concurrently with thread1) + let mut buf = [0u8; 32]; + rand_bytes(&mut buf).unwrap(); + + // Wait for thread1 to also complete rand_bytes + barrier_after_rand_bytes.wait(); + + buf + }); + + let t1_buf = thread1.join().unwrap(); + let t2_buf = thread2.join().unwrap(); + + // Expected deterministic values for thread 1 (entropy=0x42, nonce=0x24) + let expected_t1: [u8; 32] = [ + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + ]; + + // Expected deterministic values for thread 2 (entropy=0xAA, nonce=0x55) + let expected_t2: [u8; 32] = [ + 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, + 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, + ]; + + // Verify each thread produced its expected deterministic output + assert_eq!( + t1_buf, expected_t1, + "Thread 1 should produce deterministic output" + ); + assert_eq!( + t2_buf, expected_t2, + "Thread 2 should produce deterministic output" + ); + + // Also verify they are different (proves thread-local isolation) + assert_ne!( + t1_buf, t2_buf, + "Different thread-local entropy should produce different output" + ); + } + + /// Test that the original RNG is properly restored after DeterministicTestRandGuard is dropped + #[test] + fn test_deterministic_rng_restoration() { + use openssl::rand::rand_bytes; + + // Generate random bytes with system RNG before installing deterministic RNG + let mut before_buf = [0u8; 32]; + rand_bytes(&mut before_buf).unwrap(); + + let deterministic_buf = { + let entropy = [0x42u8; 4096]; + let nonce = [0x24u8; 48]; + + // Install deterministic RNG + let _rng = DeterministicTestRandGuard::install(&entropy, &nonce).unwrap(); + + // Generate deterministic bytes + let mut buf = [0u8; 32]; + rand_bytes(&mut buf).unwrap(); + + // Expected deterministic output + let expected: [u8; 32] = [ + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + ]; + assert_eq!(buf, expected, "Should produce deterministic output"); + + buf + // _rng is dropped here, should restore original RNG + }; + + // Generate random bytes again with restored system RNG + let mut after_buf = [0u8; 32]; + rand_bytes(&mut after_buf).unwrap(); + + // The system RNG should produce different random values each time + // (extremely unlikely to match the deterministic output) + assert_ne!( + after_buf, deterministic_buf, + "After restoration, system RNG should produce different random values" + ); + + // Also verify that before and after are different (system RNG produces random values) + // Note: This could theoretically fail with probability 1/2^256, but that's negligible + assert_ne!( + before_buf, after_buf, + "System RNG should produce different random values on each call" + ); + } + #[test] fn sign_ec() { let (ec_key, _) = get_test_keys(); diff --git a/rust/pv/src/test_utils.rs b/rust/pv/src/test_utils.rs index 91caffc8..fc09e352 100644 --- a/rust/pv/src/test_utils.rs +++ b/rust/pv/src/test_utils.rs @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT // -// Copyright IBM Corp. 2023 +// Copyright IBM Corp. // DO NOT USE ANY OF THESE ITEMS IN PRODUCTION CODE // USED FOR INTERNAL UNIT AND FVT TESTING ONLY!!! +use std::ffi::c_void; use std::fs; +use std::mem::{size_of, ManuallyDrop}; use std::path::{Path, PathBuf}; +use std::ptr::NonNull; use openssl::bn::BigNum; use openssl::ec::{EcGroup, EcKey}; @@ -118,3 +121,379 @@ fn get_keypair(pub_coords: &[u8], priv_num: &[u8]) -> Result, Erro key.check_key()?; PKey::from_ec_key(key) } + +// To regenerate these bindings, run: +// bindgen bindgen_wrapper.h -o bindgen_output.rs \ +// --allowlist-function "RAND_get0_public" \ +// --allowlist-function "RAND_get0_private" --allowlist-function "EVP_RAND_fetch" \ +// --allowlist-function "EVP_RAND_free" --allowlist-function "EVP_RAND_CTX_new" \ +// --allowlist-function "EVP_RAND_CTX_free" --allowlist-function "EVP_RAND_CTX_up_ref" \ +// --allowlist-function "EVP_RAND_instantiate" --allowlist-function "RAND_set0_public" \ +// --allowlist-function "RAND_set0_private" --allowlist-type "OSSL_PARAM" +// where bindgen_wrapper.h contains: +// #include +// #include +// #include +// #include +mod ffi { + use std::ffi::{c_char, c_int, c_uchar, c_uint, c_void}; + + #[repr(C)] + pub struct OsslParam { + pub key: *const c_char, + pub data_type: c_uint, + pub data: *mut c_void, + pub data_size: usize, + pub return_size: usize, + } + + pub enum OsslLibCtx {} + pub enum OsslProvider {} + pub enum EvpRand {} + pub enum EvpRandCtx {} + + unsafe extern "C" { + pub fn RAND_get0_public(ctx: *mut OsslLibCtx) -> *mut EvpRandCtx; + pub fn RAND_get0_private(ctx: *mut OsslLibCtx) -> *mut EvpRandCtx; + pub fn EVP_RAND_fetch( + libctx: *mut OsslLibCtx, + algorithm: *const c_char, + properties: *const c_char, + ) -> *mut EvpRand; + pub fn EVP_RAND_free(rand: *mut EvpRand); + pub fn EVP_RAND_CTX_new(rand: *mut EvpRand, parent: *mut EvpRandCtx) -> *mut EvpRandCtx; + pub fn EVP_RAND_CTX_free(ctx: *mut EvpRandCtx); + pub fn EVP_RAND_CTX_up_ref(ctx: *mut EvpRandCtx) -> c_int; + pub fn EVP_RAND_instantiate( + ctx: *mut EvpRandCtx, + strength: c_uint, + prediction_resistance: c_int, + pstr: *const c_uchar, + pstr_len: usize, + params: *const OsslParam, + ) -> c_int; + pub fn RAND_set0_public(ctx: *mut OsslLibCtx, rand: *mut EvpRandCtx) -> c_int; + pub fn RAND_set0_private(ctx: *mut OsslLibCtx, rand: *mut EvpRandCtx) -> c_int; + } +} + +// Constants for OSSL_PARAM construction +const OSSL_PARAM_OCTET_STRING: u32 = 5; +const OSSL_PARAM_UNSIGNED_INTEGER: u32 = 2; +const OSSL_PARAM_END: u32 = 0; + +fn ossl_param_end() -> ffi::OsslParam { + ffi::OsslParam { + key: std::ptr::null(), + data_type: OSSL_PARAM_END, + data: std::ptr::null_mut(), + data_size: 0, + return_size: 0, + } +} + +fn ossl_param_octet_string(name: &'static [u8], data: &mut [u8]) -> ffi::OsslParam { + // SAFETY: Constructing OSSL_PARAM for octet string. + // - name is a static null-terminated C string, valid for 'static + // - data is a valid mutable slice, pointer remains valid during param usage + // - Pointer casts are safe as they preserve alignment and validity + ffi::OsslParam { + key: name.as_ptr().cast(), + data_type: OSSL_PARAM_OCTET_STRING, + data: data.as_mut_ptr().cast(), + data_size: data.len(), + return_size: data.len(), + } +} + +fn ossl_param_uint(name: &'static [u8], value: &mut u32) -> ffi::OsslParam { + // SAFETY: Constructing OSSL_PARAM for unsigned integer. + // - name is a static null-terminated C string, valid for 'static + // - value is a valid mutable reference, pointer remains valid during param usage + // - Pointer cast to c_void is safe as it preserves alignment and validity + ffi::OsslParam { + key: name.as_ptr().cast(), + data_type: OSSL_PARAM_UNSIGNED_INTEGER, + data: (value as *mut u32).cast::(), + data_size: size_of::(), + return_size: size_of::(), + } +} + +#[derive(Debug)] +struct FetchedRand(NonNull); + +impl FetchedRand { + const TEST_RAND_NAME: &'static [u8] = b"TEST-RAND\0"; + + fn fetch_test_rand() -> Result { + // SAFETY: Calling OpenSSL C API with valid parameters. + // - null_mut() is valid for optional OSSL_LIB_CTX parameter + // - Self::TEST_RAND_NAME is a valid null-terminated C string + // - null() is valid for optional properties parameter + // - Returns null on error, which we handle via NonNull::new + let rand = unsafe { + ffi::EVP_RAND_fetch( + std::ptr::null_mut(), + Self::TEST_RAND_NAME.as_ptr().cast(), + std::ptr::null(), + ) + }; + NonNull::new(rand).map(Self).ok_or_else(ErrorStack::get) + } + + fn as_ptr(&self) -> *mut ffi::EvpRand { + self.0.as_ptr() + } +} + +impl Drop for FetchedRand { + fn drop(&mut self) { + // SAFETY: self.0 is a valid non-null EVP_RAND pointer that we own. + // This is the only place we call free, preventing double-free. + unsafe { + ffi::EVP_RAND_free(self.0.as_ptr()); + } + } +} + +#[derive(Debug)] +struct RandCtx(NonNull); + +impl RandCtx { + fn new(rand: &FetchedRand) -> Result { + // SAFETY: Calling OpenSSL C API with valid parameters. + // - rand.as_ptr() is a valid non-null EVP_RAND pointer + // - null_mut() is valid for optional parent parameter + // - Returns null on error, which we handle via NonNull::new + let ctx = unsafe { ffi::EVP_RAND_CTX_new(rand.as_ptr(), std::ptr::null_mut()) }; + NonNull::new(ctx).map(Self).ok_or_else(ErrorStack::get) + } + + fn up_ref(ptr: *mut ffi::EvpRandCtx) -> Result { + let ptr = NonNull::new(ptr).ok_or_else(ErrorStack::get)?; + // SAFETY: ptr is a valid non-null EVP_RAND_CTX pointer. + // EVP_RAND_CTX_up_ref increments the reference count. + // Returns 1 on success, 0 on failure. + let rc = unsafe { ffi::EVP_RAND_CTX_up_ref(ptr.as_ptr()) }; + if rc == 1 { + Ok(Self(ptr)) + } else { + Err(ErrorStack::get()) + } + } + + fn current_public() -> Result, ErrorStack> { + // SAFETY: RAND_get0_public returns a borrowed pointer (no ownership transfer). + // Returns null if no public RNG is set, which we handle. + let ptr = unsafe { ffi::RAND_get0_public(std::ptr::null_mut()) }; + if ptr.is_null() { + Ok(None) + } else { + Self::up_ref(ptr).map(Some) + } + } + + fn current_private() -> Result, ErrorStack> { + // SAFETY: RAND_get0_private returns a borrowed pointer (no ownership transfer). + // Returns null if no private RNG is set, which we handle. + let ptr = unsafe { ffi::RAND_get0_private(std::ptr::null_mut()) }; + if ptr.is_null() { + Ok(None) + } else { + Self::up_ref(ptr).map(Some) + } + } + + fn as_ptr(&self) -> *mut ffi::EvpRandCtx { + self.0.as_ptr() + } + + fn instantiate_test_rand(&self, entropy: &[u8], nonce: &[u8]) -> Result<(), ErrorStack> { + // See https://docs.openssl.org/3.1/man7/EVP_RAND-TEST-RAND/#description + // for the available parameters. + const TEST_ENTROPY_PARAM: &[u8] = b"test_entropy\0"; + const TEST_NONCE_PARAM: &[u8] = b"test_nonce\0"; + const STRENGTH_PARAM: &[u8] = b"strength\0"; + + let mut entropy = entropy.to_vec(); + let mut nonce = nonce.to_vec(); + let mut strength = 256u32; + let params = [ + ossl_param_uint(STRENGTH_PARAM, &mut strength), + ossl_param_octet_string(TEST_ENTROPY_PARAM, &mut entropy), + ossl_param_octet_string(TEST_NONCE_PARAM, &mut nonce), + ossl_param_end(), + ]; + + // SAFETY: Calling OpenSSL C API with valid parameters. + // - self.as_ptr() is a valid non-null EVP_RAND_CTX pointer + // - strength is a valid u32 value + // - prediction_resistance=0 is valid + // - pstr=null and pstr_len=0 indicate no personalization string + // - params points to a valid array of OSSL_PARAM with proper terminator + // - All mutable references in params remain valid for the call duration + let rc = unsafe { + ffi::EVP_RAND_instantiate( + self.as_ptr(), + strength, + 0, + std::ptr::null(), + 0, + params.as_ptr(), + ) + }; + if rc == 1 { + Ok(()) + } else { + Err(ErrorStack::get()) + } + } + + fn install_as_public(self) -> Result { + // SAFETY: Calling OpenSSL C API to transfer ownership. + // - self.as_ptr() is a valid non-null EVP_RAND_CTX pointer + // - RAND_set0_public takes ownership of the context on success (rc==1) + // - We wrap in ManuallyDrop to prevent double-free since OpenSSL now owns it + let rc = unsafe { ffi::RAND_set0_public(std::ptr::null_mut(), self.as_ptr()) }; + if rc == 1 { + Ok(InstalledRandCtx(ManuallyDrop::new(self))) + } else { + Err(ErrorStack::get()) + } + } + + fn install_as_private(self) -> Result { + // SAFETY: Calling OpenSSL C API to transfer ownership. + // - self.as_ptr() is a valid non-null EVP_RAND_CTX pointer + // - RAND_set0_private takes ownership of the context on success (rc==1) + // - We wrap in ManuallyDrop to prevent double-free since OpenSSL now owns it + let rc = unsafe { ffi::RAND_set0_private(std::ptr::null_mut(), self.as_ptr()) }; + if rc == 1 { + Ok(InstalledRandCtx(ManuallyDrop::new(self))) + } else { + Err(ErrorStack::get()) + } + } +} + +impl Drop for RandCtx { + fn drop(&mut self) { + // SAFETY: self.0 is a valid non-null EVP_RAND_CTX pointer that we own. + // This is only called when ownership was NOT transferred to OpenSSL. + // InstalledRandCtx uses ManuallyDrop to prevent this from running after transfer. + unsafe { + ffi::EVP_RAND_CTX_free(self.0.as_ptr()); + } + } +} + +#[derive(Debug)] +struct InstalledRandCtx(ManuallyDrop); + +impl Drop for InstalledRandCtx { + fn drop(&mut self) { + // SAFETY: Ownership of the EVP_RAND_CTX was transferred to OpenSSL + // via RAND_set0_public/private, so we must not call EVP_RAND_CTX_free. + // ManuallyDrop prevents RandCtx::drop from running automatically. + } +} + +#[derive(Debug)] +struct PreviousRandCtx(Option); + +impl PreviousRandCtx { + fn capture_public() -> Result { + RandCtx::current_public().map(Self) + } + + fn capture_private() -> Result { + RandCtx::current_private().map(Self) + } + + fn restore_public(&mut self) { + let Some(ctx) = self.0.take() else { + return; + }; + // SAFETY: Restoring previously captured RNG context. + // - ctx.as_ptr() is a valid non-null EVP_RAND_CTX pointer + // - RAND_set0_public takes ownership of the context + // - We forget ctx to prevent double-free since OpenSSL now owns it + // - Ignoring return value as restoration is best-effort during cleanup + let _ = unsafe { ffi::RAND_set0_public(std::ptr::null_mut(), ctx.as_ptr()) }; + std::mem::forget(ctx); + } + + fn restore_private(&mut self) { + let Some(ctx) = self.0.take() else { + return; + }; + // SAFETY: Restoring previously captured RNG context. + // - ctx.as_ptr() is a valid non-null EVP_RAND_CTX pointer + // - RAND_set0_private takes ownership of the context + + // - We forget ctx to prevent double-free since OpenSSL now owns it + // - Ignoring return value as restoration is best-effort during cleanup + let _ = unsafe { ffi::RAND_set0_private(std::ptr::null_mut(), ctx.as_ptr()) }; + std::mem::forget(ctx); + } +} + +#[derive(Debug)] +pub struct DeterministicTestRandGuard { + previous_public: PreviousRandCtx, + previous_private: PreviousRandCtx, + _public: InstalledRandCtx, + _private: InstalledRandCtx, +} + +impl DeterministicTestRandGuard { + /// Install OpenSSL >= 3 TEST-RAND as the thread-local public/private RNG for deterministic + /// tests. + /// + /// The supplied entropy is consumed across generate calls. The nonce is replayed for each + /// nonce request. Per OpenSSL documentation, the public and private DRBG instances are + /// thread-local, so each thread can safely install its own deterministic RNG without + /// affecting other threads. + /// + /// # Thread Safety + /// + /// From OpenSSL documentation (RAND_get0_primary(3)): + /// > "The public and private DRBG are thread-local instances, which are used by + /// > RAND_bytes() and RAND_priv_bytes(), respectively." + /// + /// Reference: + /// + /// **Note:** RAND_set0_public() and RAND_set0_private() require OpenSSL >= 3.1. + /// + /// # Errors + /// + /// Returns an OpenSSL error if the TEST-RAND provider cannot be configured. + pub fn install(entropy: &[u8], nonce: &[u8]) -> Result { + let previous_public = PreviousRandCtx::capture_public()?; + let previous_private = PreviousRandCtx::capture_private()?; + let rand = FetchedRand::fetch_test_rand()?; + + let public = RandCtx::new(&rand)?; + public.instantiate_test_rand(entropy, nonce)?; + let public = public.install_as_public()?; + + let private = RandCtx::new(&rand)?; + private.instantiate_test_rand(entropy, nonce)?; + let private = private.install_as_private()?; + + Ok(Self { + previous_public, + previous_private, + _public: public, + _private: private, + }) + } +} + +impl Drop for DeterministicTestRandGuard { + fn drop(&mut self) { + self.previous_private.restore_private(); + self.previous_public.restore_public(); + } +}