pv: Add OpenSSL ML-KEM bindings

Add interfaces for creating an ML-KEM keypair and encapsulation and
decapsulation.

The public part of it can be used to encapsulate a shared secret in a
ciphertext. The latter can be decapsulated with the private part of the
keypair to recover the shared secret.

In terms of code, a keypair of type `PKey<Private>` can be generated
with `generate_ml_kem` specifying `KeyType::ML_KEM_512`,
`KeyType::ML_KEM_768`, or `KeyType::ML_KEM_1024`.

To encapsulate a shared secret, generate a `PkeyCtx<Public>` with
`PkeyCtx::new`, initialize it with `encapsulate_init`, and encapsulate
with `encapsulate_to_vec`.

To decapsulate the ciphertext to the shared secret, generate a
`PkeyCtx<Private>` with `PkeyCtx::new`, initialize it with
`decapsulate_init`, and decapsulate with `decapsulate_to_vec`.

Note that when https://github.com/rust-openssl/rust-openssl/pull/2532
is getting merged into `rust-openssl`, these changes will become
unnecessary.

Assisted-by: IBM Bob:1.0.4
Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Acked-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Timo Keller <tkeller@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Timo Keller
2026-06-22 13:06:53 +02:00
committed by Jan Höppner
parent e557f41c6e
commit edf40976f0
2 changed files with 598 additions and 0 deletions

View File

@@ -0,0 +1,595 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp.
#![expect(unused)]
use std::ffi::CStr;
use std::ptr::NonNull;
use foreign_types::ForeignType;
use openssl::error::ErrorStack;
use openssl::pkey::{KeyType, PKey, Private};
use openssl::pkey_ctx::PkeyCtx;
// automatically generated by rust-bindgen 0.69.5
mod ffi {
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct evp_pkey_ctx_st {
_unused: [u8; 0],
}
pub type EvpPkeyCtx = evp_pkey_ctx_st;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ossl_param_st {
pub key: *const ::std::os::raw::c_char,
pub data_type: ::std::os::raw::c_uint,
pub data: *mut ::std::os::raw::c_void,
pub data_size: usize,
pub return_size: usize,
}
pub type OsslParam = ossl_param_st;
extern "C" {
pub fn EVP_PKEY_CTX_new_from_name(
libctx: *mut ::std::os::raw::c_void,
name: *const ::std::os::raw::c_char,
propquery: *const ::std::os::raw::c_char,
) -> *mut EvpPkeyCtx;
}
extern "C" {
pub fn EVP_PKEY_keygen_init(ctx: *mut EvpPkeyCtx) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn EVP_PKEY_generate(
ctx: *mut EvpPkeyCtx,
ppkey: *mut *mut ::std::os::raw::c_void,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn EVP_PKEY_CTX_free(ctx: *mut EvpPkeyCtx);
}
extern "C" {
pub fn EVP_PKEY_encapsulate_init(
ctx: *mut EvpPkeyCtx,
params: *const OsslParam,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn EVP_PKEY_encapsulate(
ctx: *mut EvpPkeyCtx,
wrappedkey: *mut ::std::os::raw::c_uchar,
wrappedkeylen: *mut usize,
genkey: *mut ::std::os::raw::c_uchar,
genkeylen: *mut usize,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn EVP_PKEY_decapsulate_init(
ctx: *mut EvpPkeyCtx,
params: *const OsslParam,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn EVP_PKEY_decapsulate(
ctx: *mut EvpPkeyCtx,
unwrapped: *mut ::std::os::raw::c_uchar,
unwrappedlen: *mut usize,
wrapped: *const ::std::os::raw::c_uchar,
wrappedlen: usize,
) -> ::std::os::raw::c_int;
}
}
const ML_KEM_512_NAME: &CStr = c"ML-KEM-512";
const ML_KEM_768_NAME: &CStr = c"ML-KEM-768";
const ML_KEM_1024_NAME: &CStr = c"ML-KEM-1024";
fn ml_kem_name(key_type: KeyType) -> Result<&'static CStr, ErrorStack> {
if key_type == KeyType::ML_KEM_512 {
Ok(ML_KEM_512_NAME)
} else if key_type == KeyType::ML_KEM_768 {
Ok(ML_KEM_768_NAME)
} else if key_type == KeyType::ML_KEM_1024 {
Ok(ML_KEM_1024_NAME)
} else {
Err(ErrorStack::get())
}
}
/// Encapsulate a key
pub trait PkeyEncapsulateContext {
/// Initialize the encapsulation operation.
///
/// # Errors
/// Returns an error if the OpenSSL operation fails.
fn encapsulate_init(&mut self) -> Result<(), ErrorStack>;
/// Perform the encapsulation operation.
///
/// # Parameters
/// - `wrappedkey`: Optional buffer to receive the wrapped key.
/// - `genkey`: Optional buffer to receive the generated key.
///
/// # Returns
/// A tuple of `(wrappedkey_len, genkey_len)` on success.
///
/// # Errors
/// Returns an error if the OpenSSL operation fails.
fn encapsulate(
&mut self,
wrappedkey: Option<&mut [u8]>,
genkey: Option<&mut [u8]>,
) -> Result<(usize, usize), ErrorStack>;
/// Convenience method to encapsulate into vectors.
///
/// # Parameters
/// - `wrappedkey`: Buffer to receive the wrapped key (ciphertext).
/// - `genkey`: Buffer to receive the generated key (shared secret).
///
/// # Returns
/// A tuple of `(wrappedkey_len, genkey_len)` on success.
///
/// # Errors
/// Returns an error if the OpenSSL operation fails.
fn encapsulate_to_vec(
&mut self,
wrappedkey: &mut Vec<u8>,
genkey: &mut Vec<u8>,
) -> Result<(usize, usize), ErrorStack> {
let wrappedkey_base = wrappedkey.len();
let genkey_base = genkey.len();
// Query the required output buffer sizes.
let (wrappedkey_len, genkey_len) = self.encapsulate(None, None)?;
wrappedkey.resize(wrappedkey_base + wrappedkey_len, 0);
genkey.resize(genkey_base + genkey_len, 0);
let (wrappedkey_len, genkey_len) = self.encapsulate(
Some(&mut wrappedkey[wrappedkey_base..]),
Some(&mut genkey[genkey_base..]),
)?;
wrappedkey.truncate(wrappedkey_base + wrappedkey_len);
genkey.truncate(genkey_base + genkey_len);
Ok((wrappedkey_len, genkey_len))
}
}
impl<T> PkeyEncapsulateContext for PkeyCtx<T> {
#[inline]
fn encapsulate_init(&mut self) -> Result<(), ErrorStack> {
// SAFETY: self.as_ptr() returns a valid EVP_PKEY_CTX pointer. Parameter-based
// configuration is intentionally unsupported here, so a null params pointer is passed.
let ret = unsafe {
ffi::EVP_PKEY_encapsulate_init(self.as_ptr() as *mut ffi::EvpPkeyCtx, std::ptr::null())
};
if ret == 1 {
Ok(())
} else {
Err(ErrorStack::get())
}
}
fn encapsulate(
&mut self,
wrappedkey: Option<&mut [u8]>,
genkey: Option<&mut [u8]>,
) -> Result<(usize, usize), ErrorStack> {
let mut wrappedkey_len = wrappedkey.as_ref().map_or(0, |buf| buf.len());
let mut genkey_len = genkey.as_ref().map_or(0, |buf| buf.len());
let wrappedkey_ptr = wrappedkey
.map(|buf| buf.as_mut_ptr())
.unwrap_or(std::ptr::null_mut());
let genkey_ptr = genkey
.map(|buf| buf.as_mut_ptr())
.unwrap_or(std::ptr::null_mut());
// SAFETY: All pointers are either valid mutable buffers or null.
let ret = unsafe {
ffi::EVP_PKEY_encapsulate(
self.as_ptr() as *mut ffi::EvpPkeyCtx,
wrappedkey_ptr,
&mut wrappedkey_len,
genkey_ptr,
&mut genkey_len,
)
};
if ret == 1 {
Ok((wrappedkey_len, genkey_len))
} else {
Err(ErrorStack::get())
}
}
}
/// Decapsulate a key
pub trait PkeyDecapsulateContext {
/// Initialize the decapsulation operation.
///
/// # Errors
/// Returns an error if the OpenSSL operation fails.
fn decapsulate_init(&mut self) -> Result<(), ErrorStack>;
/// Perform the decapsulation operation.
///
/// # Parameters
/// - `wrapped`: The wrapped key (ciphertext) to decapsulate.
/// - `unwrapped`: Optional buffer to receive the unwrapped shared secret.
///
/// # Returns
/// The length of the unwrapped shared secret on success.
///
/// # Errors
/// Returns an error if the OpenSSL operation fails.
fn decapsulate(
&mut self,
wrapped: &[u8],
unwrapped: Option<&mut [u8]>,
) -> Result<usize, ErrorStack>;
/// Convenience method to decapsulate into a vector.
///
/// # Parameters
/// - `wrapped`: The wrapped key (ciphertext) to decapsulate.
/// - `unwrapped`: Buffer to receive the unwrapped shared secret.
///
/// # Returns
/// The length of the unwrapped shared secret on success.
///
/// # Errors
/// Returns an error if the OpenSSL operation fails.
fn decapsulate_to_vec(
&mut self,
wrapped: &[u8],
unwrapped: &mut Vec<u8>,
) -> Result<usize, ErrorStack> {
let unwrapped_base = unwrapped.len();
// Query the required output buffer size.
let unwrapped_len = self.decapsulate(wrapped, None)?;
unwrapped.resize(unwrapped_base + unwrapped_len, 0);
let unwrapped_len = self.decapsulate(wrapped, Some(&mut unwrapped[unwrapped_base..]))?;
unwrapped.truncate(unwrapped_base + unwrapped_len);
Ok(unwrapped_len)
}
}
impl<T> PkeyDecapsulateContext for PkeyCtx<T> {
#[inline]
fn decapsulate_init(&mut self) -> Result<(), ErrorStack> {
// SAFETY: self.as_ptr() returns a valid EVP_PKEY_CTX pointer. Parameter-based
// configuration is intentionally unsupported here, so a null params pointer is passed.
let ret = unsafe {
ffi::EVP_PKEY_decapsulate_init(self.as_ptr() as *mut ffi::EvpPkeyCtx, std::ptr::null())
};
if ret == 1 {
Ok(())
} else {
Err(ErrorStack::get())
}
}
fn decapsulate(
&mut self,
wrapped: &[u8],
unwrapped: Option<&mut [u8]>,
) -> Result<usize, ErrorStack> {
let mut unwrapped_len = unwrapped.as_ref().map_or(0, |buf| buf.len());
let unwrapped_ptr = unwrapped
.map(|buf| buf.as_mut_ptr())
.unwrap_or(std::ptr::null_mut());
// SAFETY: All pointers are either valid mutable buffers or null, and wrapped is a valid
// slice.
let ret = unsafe {
ffi::EVP_PKEY_decapsulate(
self.as_ptr() as *mut ffi::EvpPkeyCtx,
unwrapped_ptr,
&mut unwrapped_len,
wrapped.as_ptr(),
wrapped.len(),
)
};
if ret == 1 {
Ok(unwrapped_len)
} else {
Err(ErrorStack::get())
}
}
}
/// ML-KEM key generation context.
///
/// The inner pointer is guaranteed to be non-null for the lifetime of the context.
pub struct MlKemKeyCtx {
ctx: NonNull<ffi::EvpPkeyCtx>,
}
impl MlKemKeyCtx {
/// Create a new ML-KEM key generation context.
///
/// # Parameters
/// - `key_type`: The ML-KEM key type (`KeyType::ML_KEM_512`, `KeyType::ML_KEM_768`, or
/// `KeyType::ML_KEM_1024`).
///
/// # Errors
/// Returns an error if the context cannot be created or the key type is unsupported.
pub fn new(key_type: KeyType) -> Result<Self, ErrorStack> {
let name = ml_kem_name(key_type)?;
// SAFETY: name is a valid null-terminated C string.
let ctx = unsafe {
ffi::EVP_PKEY_CTX_new_from_name(std::ptr::null_mut(), name.as_ptr(), std::ptr::null())
};
let Some(ctx) = NonNull::new(ctx) else {
return Err(ErrorStack::get());
};
// SAFETY: ctx is a valid non-null pointer.
let ret = unsafe { ffi::EVP_PKEY_keygen_init(ctx.as_ptr()) };
if ret != 1 {
// SAFETY: ctx is a valid non-null pointer.
unsafe { ffi::EVP_PKEY_CTX_free(ctx.as_ptr()) };
return Err(ErrorStack::get());
}
Ok(Self { ctx })
}
/// Generate an ML-KEM keypair.
///
/// # Returns
/// A `PKey` containing both the private and public key.
///
/// # Errors
/// Returns an error if key generation fails.
pub fn generate(&mut self) -> Result<PKey<Private>, ErrorStack> {
let mut pkey: *mut std::os::raw::c_void = std::ptr::null_mut();
// SAFETY: self.ctx is valid, and pkey is a valid mutable pointer.
let ret = unsafe { ffi::EVP_PKEY_generate(self.ctx.as_ptr(), &mut pkey) };
if ret != 1 || pkey.is_null() {
return Err(ErrorStack::get());
}
// SAFETY: pkey is a valid EVP_PKEY pointer created by OpenSSL.
unsafe { Ok(PKey::from_ptr(pkey as *mut openssl_sys::EVP_PKEY)) }
}
}
impl Drop for MlKemKeyCtx {
fn drop(&mut self) {
// SAFETY: self.ctx is guaranteed to be non-null and owned by this context.
unsafe {
ffi::EVP_PKEY_CTX_free(self.ctx.as_ptr());
}
}
}
/// Generate an ML-KEM keypair.
///
/// # Parameters
/// - `key_type`: The ML-KEM key type (`KeyType::ML_KEM_512`, `KeyType::ML_KEM_768`, or
/// `KeyType::ML_KEM_1024`).
///
/// # Returns
/// A `PKey` containing both the private and public key.
///
/// # Errors
/// Returns an error if key generation fails.
pub fn generate_ml_kem(key_type: KeyType) -> Result<PKey<Private>, ErrorStack> {
let mut ctx = MlKemKeyCtx::new(key_type)?;
ctx.generate()
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip_ml_kem(key_type: KeyType, expected_ciphertext_len: usize) {
let keypair = generate_ml_kem(key_type).expect("Failed to generate ML-KEM keypair");
// Extract public key for encapsulation
let public_key_der = keypair
.public_key_to_der()
.expect("Failed to export public key");
let public_key =
PKey::public_key_from_der(&public_key_der).expect("Failed to import public key");
// Encapsulate: generate ciphertext and shared secret using public key
let mut ctx_enc =
PkeyCtx::new(&public_key).expect("Failed to create encapsulation context");
ctx_enc
.encapsulate_init()
.expect("Failed to initialize encapsulation");
let mut ciphertext = Vec::new();
let mut shared_secret_enc = Vec::new();
ctx_enc
.encapsulate_to_vec(&mut ciphertext, &mut shared_secret_enc)
.expect("Failed to encapsulate");
assert!(!ciphertext.is_empty(), "Ciphertext should not be empty");
assert!(
!shared_secret_enc.is_empty(),
"Shared secret should not be empty"
);
// Decapsulate: recover shared secret from ciphertext using private key
let mut ctx_dec = PkeyCtx::new(&keypair).expect("Failed to create decapsulation context");
ctx_dec
.decapsulate_init()
.expect("Failed to initialize decapsulation");
let mut shared_secret_dec = Vec::new();
ctx_dec
.decapsulate_to_vec(&ciphertext, &mut shared_secret_dec)
.expect("Failed to decapsulate");
// Verify that the shared secrets match
assert_eq!(
shared_secret_enc, shared_secret_dec,
"Shared secrets from encapsulation and decapsulation should match"
);
assert_eq!(ciphertext.len(), expected_ciphertext_len);
assert_eq!(shared_secret_enc.len(), 32);
}
#[test]
fn test_encapsulate_decapsulate_roundtrip_ml_kem_512() {
roundtrip_ml_kem(KeyType::ML_KEM_512, 768);
}
#[test]
fn test_encapsulate_decapsulate_roundtrip_ml_kem_768() {
roundtrip_ml_kem(KeyType::ML_KEM_768, 1088);
}
#[test]
fn test_encapsulate_decapsulate_roundtrip_ml_kem_1024() {
roundtrip_ml_kem(KeyType::ML_KEM_1024, 1568);
}
#[test]
fn test_ml_kem_key_ctx_new_accepts_supported_key_types() {
MlKemKeyCtx::new(KeyType::ML_KEM_512).expect("ML-KEM-512 context creation must succeed");
MlKemKeyCtx::new(KeyType::ML_KEM_768).expect("ML-KEM-768 context creation must succeed");
MlKemKeyCtx::new(KeyType::ML_KEM_1024).expect("ML-KEM-1024 context creation must succeed");
}
#[test]
fn test_ml_kem_key_ctx_new_rejects_unsupported_key_type() {
assert!(
MlKemKeyCtx::new(KeyType::RSA).is_err(),
"Unsupported key type must fail"
);
}
#[test]
fn test_ml_kem_key_ctx_generate_produces_ml_kem_keypair() {
let mut ctx = MlKemKeyCtx::new(KeyType::ML_KEM_768)
.expect("ML-KEM-768 context creation must succeed");
let keypair = ctx
.generate()
.expect("ML-KEM-768 key generation must succeed");
let public_key_der = keypair
.public_key_to_der()
.expect("Generated keypair must export a public key");
let public_key =
PKey::public_key_from_der(&public_key_der).expect("Generated public key must reimport");
let mut ctx_enc =
PkeyCtx::new(&public_key).expect("Failed to create encapsulation context");
ctx_enc
.encapsulate_init()
.expect("Failed to initialize encapsulation");
let mut ciphertext = Vec::new();
let mut shared_secret = Vec::new();
ctx_enc
.encapsulate_to_vec(&mut ciphertext, &mut shared_secret)
.expect("Generated keypair must support encapsulation");
assert_eq!(ciphertext.len(), 1088);
assert_eq!(shared_secret.len(), 32);
}
#[test]
fn test_generate_ml_kem_rejects_unsupported_key_type() {
generate_ml_kem(KeyType::RSA).expect_err("Unsupported key type must fail");
}
#[test]
fn test_encapsulate_requires_init() {
let keypair =
generate_ml_kem(KeyType::ML_KEM_512).expect("Failed to generate ML-KEM-512 keypair");
let public_key_der = keypair
.public_key_to_der()
.expect("Failed to export public key");
let public_key =
PKey::public_key_from_der(&public_key_der).expect("Failed to import public key");
let mut ctx_enc =
PkeyCtx::new(&public_key).expect("Failed to create encapsulation context");
let err = ctx_enc
.encapsulate_to_vec(&mut Vec::new(), &mut Vec::new())
.expect_err("Encapsulation without init must fail");
assert!(
!err.errors().is_empty(),
"OpenSSL should report an error when encapsulate_init was not called"
);
}
#[test]
fn test_decapsulate_requires_init() {
let keypair =
generate_ml_kem(KeyType::ML_KEM_512).expect("Failed to generate ML-KEM-512 keypair");
let mut ctx_dec = PkeyCtx::new(&keypair).expect("Failed to create decapsulation context");
let err = ctx_dec
.decapsulate_to_vec(&[0_u8; 768], &mut Vec::new())
.expect_err("Decapsulation without init must fail");
assert!(
!err.errors().is_empty(),
"OpenSSL should report an error when decapsulate_init was not called"
);
}
#[test]
fn test_decapsulate_rejects_undersized_output_buffer() {
let keypair =
generate_ml_kem(KeyType::ML_KEM_768).expect("Failed to generate ML-KEM-768 keypair");
let public_key_der = keypair
.public_key_to_der()
.expect("Failed to export public key");
let public_key =
PKey::public_key_from_der(&public_key_der).expect("Failed to import public key");
let mut ctx_enc =
PkeyCtx::new(&public_key).expect("Failed to create encapsulation context");
ctx_enc
.encapsulate_init()
.expect("Failed to initialize encapsulation");
let mut ciphertext = Vec::new();
let mut shared_secret = Vec::new();
ctx_enc
.encapsulate_to_vec(&mut ciphertext, &mut shared_secret)
.expect("Failed to encapsulate");
let mut ctx_dec = PkeyCtx::new(&keypair).expect("Failed to create decapsulation context");
ctx_dec
.decapsulate_init()
.expect("Failed to initialize decapsulation");
let mut undersized = [0_u8; 31];
let err = ctx_dec
.decapsulate(&ciphertext, Some(&mut undersized))
.expect_err("Undersized output buffer must fail");
assert!(
!err.errors().is_empty(),
"OpenSSL should report an error for an undersized output buffer"
);
}
}

View File

@@ -8,8 +8,11 @@
mod akid;
mod bio;
mod crl;
mod ml_kem;
mod stackable_crl;
pub use akid::*;
pub use bio::*;
pub use crl::*;
#[expect(unused_imports)]
pub use ml_kem::*;