From 84f8b5fa138efa525ba0ea0b6656e1bb778a589d Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Thu, 23 Apr 2026 10:49:44 -0400 Subject: [PATCH] Rust wrapper: implement kem traits --- wrapper/rust/wolfssl-wolfcrypt/Cargo.lock | 13 + wrapper/rust/wolfssl-wolfcrypt/Cargo.toml | 4 + wrapper/rust/wolfssl-wolfcrypt/Makefile | 2 +- wrapper/rust/wolfssl-wolfcrypt/src/lib.rs | 2 + .../rust/wolfssl-wolfcrypt/src/mlkem_kem.rs | 249 ++++++++++++++++++ .../wolfssl-wolfcrypt/tests/test_mlkem_kem.rs | 167 ++++++++++++ 6 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 wrapper/rust/wolfssl-wolfcrypt/src/mlkem_kem.rs create mode 100644 wrapper/rust/wolfssl-wolfcrypt/tests/test_mlkem_kem.rs diff --git a/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock b/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock index 0241697ad3..fc07fcd75f 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock +++ b/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock @@ -135,6 +135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", + "rand_core 0.10.0", ] [[package]] @@ -206,6 +207,16 @@ dependencies = [ "either", ] +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.1", + "rand_core 0.10.0", +] + [[package]] name = "libc" version = "0.2.175" @@ -464,6 +475,8 @@ dependencies = [ "bindgen", "cipher", "digest", + "hybrid-array", + "kem", "password-hash", "rand_core 0.10.0", "regex", diff --git a/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml b/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml index 70067dc978..c8f67144d6 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml +++ b/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml @@ -18,6 +18,7 @@ cipher = ["dep:cipher"] digest = ["dep:digest"] signature = ["dep:signature"] password-hash = ["dep:password-hash", "password-hash/phc"] +kem = ["dep:kem", "hybrid-array/extra-sizes"] [dependencies] rand_core = { version = "0.10", optional = true, default-features = false } @@ -27,6 +28,8 @@ digest = { version = "0.11", optional = true, default-features = false, features signature = { version = "2.2", optional = true, default-features = false } zeroize = { version = "1.3", default-features = false, features = ["derive"] } password-hash = { version = "0.6.1", optional = true, default-features = false } +kem = { version = "0.3", optional = true, default-features = false } +hybrid-array = { version = "0.4.7", optional = true, default-features = false } [dev-dependencies] aead = { version = "0.5", features = ["alloc", "dev"] } @@ -34,6 +37,7 @@ cipher = "0.5" digest = { version = "0.11", features = ["dev"] } signature = "2.2" password-hash = { version = "0.6.1", features = ["phc"] } +kem = "0.3" [build-dependencies] bindgen = "0.72.1" diff --git a/wrapper/rust/wolfssl-wolfcrypt/Makefile b/wrapper/rust/wolfssl-wolfcrypt/Makefile index 1ebb06bb97..1d58c4befc 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/Makefile +++ b/wrapper/rust/wolfssl-wolfcrypt/Makefile @@ -1,4 +1,4 @@ -FEATURES := rand_core,aead,cipher,digest,signature,password-hash +FEATURES := rand_core,aead,cipher,digest,signature,password-hash,kem CARGO_FEATURE_FLAGS := --features $(FEATURES) .PHONY: all diff --git a/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs b/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs index 6ab0488902..73d31fdb61 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs +++ b/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs @@ -58,6 +58,8 @@ pub mod hmac; pub mod kdf; pub mod lms; pub mod mlkem; +#[cfg(feature = "kem")] +pub mod mlkem_kem; pub mod prf; pub mod random; pub mod rsa; diff --git a/wrapper/rust/wolfssl-wolfcrypt/src/mlkem_kem.rs b/wrapper/rust/wolfssl-wolfcrypt/src/mlkem_kem.rs new file mode 100644 index 0000000000..177c13558f --- /dev/null +++ b/wrapper/rust/wolfssl-wolfcrypt/src/mlkem_kem.rs @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/*! +RustCrypto `kem` trait implementations for the wolfCrypt ML-KEM types. + +Provides [`kem::Kem`] marker types and associated encapsulation/decapsulation +key types for ML-KEM-512, ML-KEM-768, and ML-KEM-1024: + +| Marker | Encapsulation key | Decapsulation key | +|-----------------|---------------------------------|---------------------------------| +| [`MlKem512`] | [`MlKem512EncapsulationKey`] | [`MlKem512DecapsulationKey`] | +| [`MlKem768`] | [`MlKem768EncapsulationKey`] | [`MlKem768DecapsulationKey`] | +| [`MlKem1024`] | [`MlKem1024EncapsulationKey`] | [`MlKem1024DecapsulationKey`] | + +Each encapsulation key implements [`kem::Encapsulate`] (with +[`kem::TryKeyInit`] and [`kem::KeyExport`] for key serialization). + +Each decapsulation key implements [`kem::Decapsulate`] and +[`kem::Generate`] (for key generation from a [`rand_core::CryptoRng`]). + +Key generation and encapsulation bridge a caller-supplied +[`rand_core::CryptoRng`] to wolfCrypt's deterministic APIs by extracting the +required random bytes from the RNG. + +# Examples + +```rust +#[cfg(all(mlkem, random, feature = "kem", feature = "rand_core"))] +{ +use kem::{Kem, Encapsulate, Decapsulate}; +use kem::Generate; +use wolfssl_wolfcrypt::random::RNG; +use wolfssl_wolfcrypt::mlkem_kem::*; + +let mut rng = RNG::new().expect("RNG creation failed"); + +let (dk, ek) = MlKem768::generate_keypair_from_rng(&mut rng); +let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); +let k_recv = dk.decapsulate(&ct); +assert_eq!(k_send, k_recv); +} +``` +*/ + +#![cfg(all(feature = "kem", mlkem))] + +use kem::common::array::Array; +use kem::common::typenum::{U32, U768, U800}; +use hybrid_array::sizes::{U1088, U1184, U1568, U1632, U2400, U3168}; + +macro_rules! impl_mlkem_kem { + ( + kem = $kem:ident, + ek = $ek:ident, + dk = $dk:ident, + pk_typenum = $pk_tn:ty, + sk_typenum = $sk_tn:ty, + ct_typenum = $ct_tn:ty, + pk_len = $pk_len:expr, + sk_len = $sk_len:expr, + ct_len = $ct_len:expr, + key_type = $key_type:expr $(,)? + ) => { + /// ML-KEM parameter set marker implementing [`kem::Kem`]. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $kem; + + impl kem::Kem for $kem { + type DecapsulationKey = $dk; + type EncapsulationKey = $ek; + type SharedKeySize = U32; + type CiphertextSize = $ct_tn; + } + + /// ML-KEM encapsulation (public) key implementing [`kem::Encapsulate`]. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct $ek { + pk: Array, + } + + impl kem::KeySizeUser for $ek { + type KeySize = $pk_tn; + } + + impl kem::TryKeyInit for $ek { + fn new(key: &kem::Key) -> Result { + let mut wc_key = crate::mlkem::MlKem::new($key_type) + .map_err(|_| kem::InvalidKey)?; + wc_key.decode_public_key(key.as_ref()) + .map_err(|_| kem::InvalidKey)?; + Ok(Self { pk: key.clone() }) + } + } + + impl kem::KeyExport for $ek { + fn to_bytes(&self) -> kem::Key { + self.pk.clone() + } + } + + impl kem::Encapsulate for $ek { + type Kem = $kem; + + fn encapsulate_with_rng( + &self, + rng: &mut R, + ) -> (kem::Ciphertext<$kem>, kem::SharedKey<$kem>) { + let mut rand = [0u8; crate::mlkem::MlKem::ENC_RAND_SIZE]; + rng.fill_bytes(&mut rand); + + let mut wc_key = crate::mlkem::MlKem::new($key_type) + .expect("MlKem::new failed"); + wc_key.decode_public_key(self.pk.as_ref()) + .expect("decode_public_key failed"); + + let mut ct = [0u8; $ct_len]; + let mut ss = [0u8; crate::mlkem::MlKem::SHARED_SECRET_SIZE]; + wc_key.encapsulate_with_random(&mut ct, &mut ss, &rand) + .expect("encapsulate_with_random failed"); + + (ct.into(), ss.into()) + } + } + + /// ML-KEM decapsulation (private) key implementing [`kem::Decapsulate`]. + /// + /// The private key bytes are securely zeroized on drop. + pub struct $dk { + sk: Array, + ek: $ek, + } + + impl kem::Decapsulator for $dk { + type Kem = $kem; + + fn encapsulation_key(&self) -> &$ek { + &self.ek + } + } + + impl kem::Decapsulate for $dk { + fn decapsulate( + &self, + ct: &kem::Ciphertext<$kem>, + ) -> kem::SharedKey<$kem> { + let mut wc_key = crate::mlkem::MlKem::new($key_type) + .expect("MlKem::new failed"); + wc_key.decode_private_key(self.sk.as_ref()) + .expect("decode_private_key failed"); + + let mut ss = [0u8; crate::mlkem::MlKem::SHARED_SECRET_SIZE]; + wc_key.decapsulate(&mut ss, ct.as_ref()) + .expect("decapsulate failed"); + + ss.into() + } + } + + impl kem::Generate for $dk { + fn try_generate_from_rng( + rng: &mut R, + ) -> Result { + let mut rand = [0u8; crate::mlkem::MlKem::MAKEKEY_RAND_SIZE]; + rng.try_fill_bytes(&mut rand)?; + + let wc_key = crate::mlkem::MlKem::generate_with_random( + $key_type, &rand, + ).expect("generate_with_random failed"); + + let mut pk = [0u8; $pk_len]; + let mut sk = [0u8; $sk_len]; + wc_key.encode_public_key(&mut pk) + .expect("encode_public_key failed"); + wc_key.encode_private_key(&mut sk) + .expect("encode_private_key failed"); + + Ok(Self { + sk: sk.into(), + ek: $ek { pk: pk.into() }, + }) + } + } + + impl Drop for $dk { + fn drop(&mut self) { + use zeroize::Zeroize; + let sk_bytes: &mut [u8] = self.sk.as_mut(); + sk_bytes.zeroize(); + } + } + }; +} + +impl_mlkem_kem! { + kem = MlKem512, + ek = MlKem512EncapsulationKey, + dk = MlKem512DecapsulationKey, + pk_typenum = U800, + sk_typenum = U1632, + ct_typenum = U768, + pk_len = 800, + sk_len = 1632, + ct_len = 768, + key_type = crate::mlkem::MlKem::TYPE_512, +} + +impl_mlkem_kem! { + kem = MlKem768, + ek = MlKem768EncapsulationKey, + dk = MlKem768DecapsulationKey, + pk_typenum = U1184, + sk_typenum = U2400, + ct_typenum = U1088, + pk_len = 1184, + sk_len = 2400, + ct_len = 1088, + key_type = crate::mlkem::MlKem::TYPE_768, +} + +impl_mlkem_kem! { + kem = MlKem1024, + ek = MlKem1024EncapsulationKey, + dk = MlKem1024DecapsulationKey, + pk_typenum = U1568, + sk_typenum = U3168, + ct_typenum = U1568, + pk_len = 1568, + sk_len = 3168, + ct_len = 1568, + key_type = crate::mlkem::MlKem::TYPE_1024, +} diff --git a/wrapper/rust/wolfssl-wolfcrypt/tests/test_mlkem_kem.rs b/wrapper/rust/wolfssl-wolfcrypt/tests/test_mlkem_kem.rs new file mode 100644 index 0000000000..6b15be1365 --- /dev/null +++ b/wrapper/rust/wolfssl-wolfcrypt/tests/test_mlkem_kem.rs @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#![cfg(all(mlkem, random, feature = "kem", feature = "rand_core"))] + +mod common; + +use kem::{Decapsulate, Decapsulator, Encapsulate, Kem, TryKeyInit, KeyExport}; +use kem::Generate; +use wolfssl_wolfcrypt::mlkem::MlKem; +use wolfssl_wolfcrypt::mlkem_kem::*; +use wolfssl_wolfcrypt::random::RNG; + +/// Verify that the compile-time sizes used by the kem types match the runtime +/// sizes reported by wolfCrypt. +#[test] +fn test_sizes_match_runtime() { + common::setup(); + + let key512 = MlKem::new(MlKem::TYPE_512).expect("new TYPE_512"); + assert_eq!(key512.public_key_size().unwrap(), 800); + assert_eq!(key512.private_key_size().unwrap(), 1632); + assert_eq!(key512.cipher_text_size().unwrap(), 768); + + let key768 = MlKem::new(MlKem::TYPE_768).expect("new TYPE_768"); + assert_eq!(key768.public_key_size().unwrap(), 1184); + assert_eq!(key768.private_key_size().unwrap(), 2400); + assert_eq!(key768.cipher_text_size().unwrap(), 1088); + + let key1024 = MlKem::new(MlKem::TYPE_1024).expect("new TYPE_1024"); + assert_eq!(key1024.public_key_size().unwrap(), 1568); + assert_eq!(key1024.private_key_size().unwrap(), 3168); + assert_eq!(key1024.cipher_text_size().unwrap(), 1568); +} + +/// Generate, encapsulate, and decapsulate with ML-KEM-512 via the kem traits. +#[test] +fn test_kem_512_round_trip() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let (dk, ek) = MlKem512::generate_keypair_from_rng(&mut rng); + let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); + let k_recv = dk.decapsulate(&ct); + assert_eq!(k_send, k_recv); +} + +/// Generate, encapsulate, and decapsulate with ML-KEM-768 via the kem traits. +#[test] +fn test_kem_768_round_trip() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let (dk, ek) = MlKem768::generate_keypair_from_rng(&mut rng); + let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); + let k_recv = dk.decapsulate(&ct); + assert_eq!(k_send, k_recv); +} + +/// Generate, encapsulate, and decapsulate with ML-KEM-1024 via the kem traits. +#[test] +fn test_kem_1024_round_trip() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let (dk, ek) = MlKem1024::generate_keypair_from_rng(&mut rng); + let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); + let k_recv = dk.decapsulate(&ct); + assert_eq!(k_send, k_recv); +} + +/// Verify that `Generate::generate_from_rng` produces a usable decapsulation +/// key and that the associated encapsulation key is consistent. +#[test] +fn test_generate_from_rng() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let dk = MlKem768DecapsulationKey::generate_from_rng(&mut rng); + let ek = dk.encapsulation_key(); + + let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); + let k_recv = dk.decapsulate(&ct); + assert_eq!(k_send, k_recv); +} + +/// Verify that a tampered ciphertext produces a different shared secret +/// (ML-KEM implicit rejection). +#[test] +fn test_implicit_rejection() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let (dk, ek) = MlKem768::generate_keypair_from_rng(&mut rng); + let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); + + let mut ct_tampered = ct.clone(); + ct_tampered[0] ^= 0xFF; + let k_tampered = dk.decapsulate(&ct_tampered); + + assert_eq!(k_send, dk.decapsulate(&ct)); + assert_ne!(k_send, k_tampered); +} + +/// Verify that `TryKeyInit` and `KeyExport` round-trip the encapsulation key. +#[test] +fn test_ek_export_import() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let (dk, ek) = MlKem768::generate_keypair_from_rng(&mut rng); + + // Export and re-import the encapsulation key. + let exported = ek.to_bytes(); + let ek2 = MlKem768EncapsulationKey::new(&exported) + .expect("TryKeyInit failed"); + assert_eq!(ek, ek2); + + // Encapsulate with the re-imported key; the original DK must decapsulate. + let (ct, k_send) = ek2.encapsulate_with_rng(&mut rng); + let k_recv = dk.decapsulate(&ct); + assert_eq!(k_send, k_recv); +} + +/// Verify that `TryKeyInit` doesn't panic on a zeroed key. +#[test] +fn test_ek_try_new_zeroed_key() { + common::setup(); + + // A zero-filled buffer of the correct size. Whether this succeeds or fails + // depends on wolfCrypt's decode_public_key validation. The key point is it + // shouldn't panic. + let zeroed = kem::Key::::default(); + let _ = MlKem768EncapsulationKey::new(&zeroed); +} + +/// Verify the `Decapsulator::encapsulation_key` method returns a key that +/// can be used for encapsulation. +#[test] +fn test_decapsulator_encapsulation_key() { + common::setup(); + let mut rng = RNG::new().expect("RNG creation failed"); + + let dk = MlKem512DecapsulationKey::generate_from_rng(&mut rng); + let ek = dk.encapsulation_key().clone(); + + let (ct, k_send) = ek.encapsulate_with_rng(&mut rng); + let k_recv = dk.decapsulate(&ct); + assert_eq!(k_send, k_recv); +}