From edf40976f00fb920372f391829a8b687a90a5c1e Mon Sep 17 00:00:00 2001 From: Timo Keller Date: Mon, 22 Jun 2026 13:06:53 +0200 Subject: [PATCH] pv: Add OpenSSL ML-KEM bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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` 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` 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 Acked-by: Steffen Eiden Signed-off-by: Timo Keller Signed-off-by: Jan Höppner --- rust/pv/src/openssl_extensions/ml_kem.rs | 595 +++++++++++++++++++++++ rust/pv/src/openssl_extensions/mod.rs | 3 + 2 files changed, 598 insertions(+) create mode 100644 rust/pv/src/openssl_extensions/ml_kem.rs diff --git a/rust/pv/src/openssl_extensions/ml_kem.rs b/rust/pv/src/openssl_extensions/ml_kem.rs new file mode 100644 index 00000000..f1aceaf0 --- /dev/null +++ b/rust/pv/src/openssl_extensions/ml_kem.rs @@ -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, + genkey: &mut Vec, + ) -> 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 PkeyEncapsulateContext for PkeyCtx { + #[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; + + /// 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, + ) -> Result { + 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 PkeyDecapsulateContext for PkeyCtx { + #[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 { + 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, +} + +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 { + 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, 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, 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" + ); + } +} diff --git a/rust/pv/src/openssl_extensions/mod.rs b/rust/pv/src/openssl_extensions/mod.rs index f6234e5d..81d74a4f 100644 --- a/rust/pv/src/openssl_extensions/mod.rs +++ b/rust/pv/src/openssl_extensions/mod.rs @@ -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::*;