pv: test_utils: Add TEST-RAND generator for testing

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 <seiden@linux.ibm.com>
Acked-by: Timo Keller <tkeller@linux.ibm.com>
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Marc Hartmayer
2026-06-26 13:43:35 +02:00
committed by Jan Höppner
parent 162a041077
commit 5340d11165
2 changed files with 537 additions and 1 deletions
+380 -1
View File
@@ -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<PKey<Private>, 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 <openssl/provider.h>
// #include <openssl/rand.h>
// #include <openssl/evp.h>
// #include <openssl/params.h>
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::<c_void>(),
data_size: size_of::<u32>(),
return_size: size_of::<u32>(),
}
}
#[derive(Debug)]
struct FetchedRand(NonNull<ffi::EvpRand>);
impl FetchedRand {
const TEST_RAND_NAME: &'static [u8] = b"TEST-RAND\0";
fn fetch_test_rand() -> Result<Self, ErrorStack> {
// 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<ffi::EvpRandCtx>);
impl RandCtx {
fn new(rand: &FetchedRand) -> Result<Self, ErrorStack> {
// 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<Self, ErrorStack> {
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<Option<Self>, 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<Option<Self>, 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<InstalledRandCtx, ErrorStack> {
// 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<InstalledRandCtx, ErrorStack> {
// 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<RandCtx>);
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<RandCtx>);
impl PreviousRandCtx {
fn capture_public() -> Result<Self, ErrorStack> {
RandCtx::current_public().map(Self)
}
fn capture_private() -> Result<Self, ErrorStack> {
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: <https://docs.openssl.org/3.1/man3/RAND_get0_primary/>
///
/// **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<Self, ErrorStack> {
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();
}
}