From 5fddaf2a9f22a97d97897e5445675403f1e57457 Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Mon, 16 Mar 2026 20:59:31 +0100 Subject: [PATCH 01/10] feat(binding): add rust binding Signed-off-by: Aaron Kaiser --- .gitignore | 1 + Cargo.lock | 586 ++++++++++++++++++ Cargo.toml | 26 + bindings/rust/mlkem-native-sys/build.rs | 50 ++ bindings/rust/mlkem-native-sys/src/lib.rs | 36 ++ bindings/rust/mlkem-native/Cargo.toml | 16 + bindings/rust/mlkem-native/examples/kem.rs | 43 ++ .../mlkem-native/examples/serialization.rs | 37 ++ bindings/rust/mlkem-native/src/lib.rs | 273 ++++++++ flake.nix | 14 +- 10 files changed, 1079 insertions(+), 3 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 bindings/rust/mlkem-native-sys/build.rs create mode 100644 bindings/rust/mlkem-native-sys/src/lib.rs create mode 100644 bindings/rust/mlkem-native/Cargo.toml create mode 100644 bindings/rust/mlkem-native/examples/kem.rs create mode 100644 bindings/rust/mlkem-native/examples/serialization.rs create mode 100644 bindings/rust/mlkem-native/src/lib.rs diff --git a/.gitignore b/.gitignore index 3f9aeb0d0..4ea3e0e37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .vscode .idea test/build +target __pycache__/ # Downloaded ACVP test data test/acvp/.acvp-data/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..fa0685fd6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,586 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "getrandom", + "hybrid-array", + "rand_core", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "kem" +version = "0.3.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ae2c3347ff4a7af4f679a9e397c2c7e6034a00b773dd2dd3c001d7f40897c9" +dependencies = [ + "crypto-common", + "rand_core", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mlkem-native" +version = "0.1.0" +dependencies = [ + "hybrid-array", + "kem", + "mlkem-native-sys", + "rand_core", +] + +[[package]] +name = "mlkem-native-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..5fe89408f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +members = ["bindings/rust/mlkem-native"] +resolver = "2" + +[package] +name = "mlkem-native-sys" +version = "0.1.0" +edition = "2021" +description = "Raw FFI bindings to mlkem-native, a C90 implementation of ML-KEM (FIPS 203)" +repository = "https://github.com/pq-code-package/mlkem-native" +license = "Apache-2.0 OR ISC OR MIT" +links = "mlkem-native" +build = "bindings/rust/mlkem-native-sys/build.rs" +include = [ + "Cargo.toml", + "bindings/rust/mlkem-native-sys/src/**", + "bindings/rust/mlkem-native-sys/build.rs", + "mlkem/**", +] + +[lib] +path = "bindings/rust/mlkem-native-sys/src/lib.rs" + +[build-dependencies] +bindgen = "0.71" +cc = "1" diff --git a/bindings/rust/mlkem-native-sys/build.rs b/bindings/rust/mlkem-native-sys/build.rs new file mode 100644 index 000000000..42474a05a --- /dev/null +++ b/bindings/rust/mlkem-native-sys/build.rs @@ -0,0 +1,50 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +use std::env; +use std::path::PathBuf; + +fn main() { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let mlkem_dir = manifest_dir.join("mlkem"); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + println!("cargo:rerun-if-changed={}", mlkem_dir.display()); + + // Compile the C library for each parameter set using the monolithic build file. + // We build without the randomized API so no randombytes() implementation is needed. + for &level in &[512u32, 768, 1024] { + cc::Build::new() + .file(mlkem_dir.join("mlkem_native.c")) + .include(&mlkem_dir) + .define("MLK_CONFIG_PARAMETER_SET", level.to_string().as_str()) + .define("MLK_CONFIG_NO_RANDOMIZED_API", None) + .opt_level(3) + .compile(&format!("mlkem_native_{}", level)); + } + + // Generate bindings for each parameter set. + let header = mlkem_dir.join("mlkem_native.h"); + for &level in &[512u32, 768, 1024] { + let bindings = bindgen::Builder::default() + .header(header.to_str().unwrap()) + .clang_arg(format!("-I{}", mlkem_dir.display())) + .clang_arg(format!("-DMLK_CONFIG_PARAMETER_SET={}", level)) + // Suppress SUPERCOP crypto_kem_* aliases — use namespaced names only. + .clang_arg("-DMLK_CONFIG_API_NO_SUPERCOP") + // Must match the compiled library. + .clang_arg("-DMLK_CONFIG_NO_RANDOMIZED_API") + .allowlist_function(format!("PQCP_MLKEM_NATIVE_MLKEM{}_.*", level)) + .allowlist_var(format!("MLKEM{}_.*", level)) + .allowlist_var("MLKEM_BYTES") + .allowlist_var("MLKEM_SYMBYTES") + .allowlist_var("MLK_ERR_.*") + .use_core() + .generate() + .unwrap_or_else(|_| panic!("Unable to generate bindings for MLKEM-{}", level)); + + bindings + .write_to_file(out_dir.join(format!("bindings_{}.rs", level))) + .unwrap_or_else(|_| panic!("Couldn't write bindings for MLKEM-{}", level)); + } +} diff --git a/bindings/rust/mlkem-native-sys/src/lib.rs b/bindings/rust/mlkem-native-sys/src/lib.rs new file mode 100644 index 000000000..a719a9025 --- /dev/null +++ b/bindings/rust/mlkem-native-sys/src/lib.rs @@ -0,0 +1,36 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +//! Raw FFI bindings to [mlkem-native], a C90 implementation of ML-KEM ([FIPS 203]). +//! +//! This crate exposes the deterministic (`_derand`) API for all three parameter +//! sets. The randomized API (`keypair` / `enc`) is intentionally omitted here; +//! it is provided by the higher-level `mlkem-native` crate using Rust's RNG +//! ecosystem. +//! +//! # Safety +//! +//! All functions in this crate are `unsafe` — callers must ensure that buffers +//! are valid, correctly-sized, and non-overlapping, as documented by each +//! function in the C header `mlkem/mlkem_native.h`. +//! +//! [mlkem-native]: https://github.com/pq-code-package/mlkem-native +//! [FIPS 203]: https://csrc.nist.gov/pubs/fips/203/final + +#![no_std] +#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case, dead_code)] + +/// Bindings for ML-KEM-512 (NIST security level 1). +pub mod mlkem512 { + include!(concat!(env!("OUT_DIR"), "/bindings_512.rs")); +} + +/// Bindings for ML-KEM-768 (NIST security level 3). +pub mod mlkem768 { + include!(concat!(env!("OUT_DIR"), "/bindings_768.rs")); +} + +/// Bindings for ML-KEM-1024 (NIST security level 5). +pub mod mlkem1024 { + include!(concat!(env!("OUT_DIR"), "/bindings_1024.rs")); +} diff --git a/bindings/rust/mlkem-native/Cargo.toml b/bindings/rust/mlkem-native/Cargo.toml new file mode 100644 index 000000000..e8447a3f9 --- /dev/null +++ b/bindings/rust/mlkem-native/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mlkem-native" +version = "0.1.0" +edition = "2021" +description = "ML-KEM (FIPS 203) via mlkem-native, implementing the RustCrypto KEM traits" +repository = "https://github.com/pq-code-package/mlkem-native" +license = "Apache-2.0 OR ISC OR MIT" + +[dependencies] +mlkem-native-sys = { path = "../../..", version = "0.1.0" } +kem = "0.3.0-rc.6" +hybrid-array = { version = "0.4", features = ["extra-sizes"] } +rand_core = { version = "0.10", default-features = false } + +[dev-dependencies] +kem = { version = "0.3.0-rc.6", features = ["getrandom"] } diff --git a/bindings/rust/mlkem-native/examples/kem.rs b/bindings/rust/mlkem-native/examples/kem.rs new file mode 100644 index 000000000..f1ffac266 --- /dev/null +++ b/bindings/rust/mlkem-native/examples/kem.rs @@ -0,0 +1,43 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +//! Demonstrates key generation, encapsulation, and decapsulation for all +//! three ML-KEM parameter sets using the system's secure RNG. +//! +//! Run with: +//! cargo run --example kem + +use mlkem_native::{ + kem::{Decapsulate, Encapsulate, Kem}, + mlkem1024, mlkem512, mlkem768, +}; + +fn demo(name: &str) +where + K::EncapsulationKey: Encapsulate, + K::DecapsulationKey: Decapsulate, +{ + // Alice generates a keypair. The decapsulation key is kept secret; + // the encapsulation key is shared with the sender. + let (dk, ek) = K::generate_keypair(); + + // Bob encapsulates a fresh shared secret to Alice's encapsulation key. + // `ct` is the ciphertext sent to Alice; `ss_sender` is Bob's share. + let (ct, ss_sender) = ek.encapsulate(); + + // Alice decapsulates the ciphertext to recover the shared secret. + let ss_receiver = dk.decapsulate(&ct); + + assert_eq!(ss_sender, ss_receiver, "{name}: shared secrets must match"); + + println!( + "{name}: shared secret ({} bytes) established successfully", + ss_sender.as_slice().len() + ); +} + +fn main() { + demo::("ML-KEM-512"); + demo::("ML-KEM-768"); + demo::("ML-KEM-1024"); +} diff --git a/bindings/rust/mlkem-native/examples/serialization.rs b/bindings/rust/mlkem-native/examples/serialization.rs new file mode 100644 index 000000000..fd9db87f7 --- /dev/null +++ b/bindings/rust/mlkem-native/examples/serialization.rs @@ -0,0 +1,37 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +//! Demonstrates serialization and deserialization of ML-KEM keys. +//! +//! Run with: +//! cargo run --example serialization + +use mlkem_native::{ + kem::{Decapsulate, Encapsulate, KeyExport, Kem}, + mlkem768::{DecapsulationKey, EncapsulationKey, MlKem}, +}; + +fn main() { + // Generate a fresh keypair. + let (dk, ek) = MlKem::generate_keypair(); + + // --- Serialize --- + let ek_bytes: Vec = ek.to_bytes().to_vec(); // 1184 bytes + let dk_bytes: Vec = dk.as_bytes().to_vec(); // 2400 bytes + + println!("ek: {} bytes", ek_bytes.len()); + println!("dk: {} bytes", dk_bytes.len()); + + // --- Deserialize --- + // Both constructors validate the key (check_pk / check_sk) and return + // Err(InvalidKey) if the bytes are the wrong length or fail validation. + let ek2 = EncapsulationKey::try_from(ek_bytes.as_slice()).expect("valid ek"); + let dk2 = DecapsulationKey::try_from(dk_bytes.as_slice()).expect("valid dk"); + + // Verify the round-tripped keys still work correctly. + let (ct, ss_sender) = ek2.encapsulate(); + let ss_receiver = dk2.decapsulate(&ct); + assert_eq!(ss_sender, ss_receiver, "shared secrets must match after round-trip"); + + println!("Round-trip successful — shared secret established."); +} diff --git a/bindings/rust/mlkem-native/src/lib.rs b/bindings/rust/mlkem-native/src/lib.rs new file mode 100644 index 000000000..40c3b82f7 --- /dev/null +++ b/bindings/rust/mlkem-native/src/lib.rs @@ -0,0 +1,273 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +//! ML-KEM (FIPS 203) implemented via [mlkem-native], exposing the +//! [RustCrypto KEM traits]. +//! +//! All three parameter sets are provided as submodules: +//! - [`mlkem512`] — NIST security level 1 +//! - [`mlkem768`] — NIST security level 3 +//! - [`mlkem1024`] — NIST security level 5 +//! +//! Each submodule exposes a marker struct `MlKem` implementing [`kem::Kem`], +//! plus `EncapsulationKey` and `DecapsulationKey` types. +//! +//! # Example +//! +//! ```rust,ignore +//! use mlkem_native::{kem::{Decapsulate, Encapsulate, Kem}, mlkem768}; +//! +//! // Key generation (uses system RNG; requires the `getrandom` feature on `kem`). +//! let (dk, ek) = mlkem768::MlKem::generate_keypair(); +//! +//! // Encapsulate a shared secret to the recipient's public key. +//! let (ct, ss_sender) = ek.encapsulate(); +//! +//! // Decapsulate to recover the same shared secret. +//! let ss_receiver = dk.decapsulate(&ct); +//! +//! assert_eq!(ss_sender.as_slice(), ss_receiver.as_slice()); +//! ``` +//! +//! [mlkem-native]: https://github.com/pq-code-package/mlkem-native +//! [RustCrypto KEM traits]: https://crates.io/crates/kem + +#![no_std] + +pub use kem; + +macro_rules! define_mlkem { + ( + module = $module:ident, + sys_mod = $sys_mod:ident, + pk_len = $pk_len:literal, + sk_len = $sk_len:literal, + ek_start = $ek_start:literal, + pk_size = $pk_size:ty, + ct_size = $ct_size:ty, + keypair_derand = $keypair_derand:ident, + enc_derand = $enc_derand:ident, + dec = $dec:ident, + check_pk = $check_pk:ident, + check_sk = $check_sk:ident $(,)? + ) => { + pub mod $module { + use hybrid_array::Array; + use kem::{ + common::{Generate, InvalidKey, KeyExport, KeySizeUser, TryKeyInit}, + Ciphertext, Decapsulate, Encapsulate, Kem, SharedKey, + }; + use mlkem_native_sys::$sys_mod::{ + $check_pk, $check_sk, $dec, $enc_derand, $keypair_derand, + }; + use rand_core::{CryptoRng, TryCryptoRng}; + + /// Marker struct for this ML-KEM parameter set; implements [`Kem`]. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct MlKem; + + impl Kem for MlKem { + type DecapsulationKey = DecapsulationKey; + type EncapsulationKey = EncapsulationKey; + type SharedKeySize = hybrid_array::typenum::U32; + type CiphertextSize = $ct_size; + } + + /// The encapsulation (public) key. + #[derive(Clone, PartialEq, Eq)] + pub struct EncapsulationKey([u8; $pk_len]); + + impl core::fmt::Debug for EncapsulationKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("EncapsulationKey").field(&self.0.as_ref()).finish() + } + } + + impl KeySizeUser for EncapsulationKey { + type KeySize = $pk_size; + } + + impl TryKeyInit for EncapsulationKey { + fn new(key: &Array) -> Result { + let mut arr = [0u8; $pk_len]; + arr.copy_from_slice(key.as_slice()); + let ek = Self(arr); + let ret = unsafe { $check_pk(ek.0.as_ptr()) }; + if ret != 0 { Err(InvalidKey) } else { Ok(ek) } + } + } + + impl KeyExport for EncapsulationKey { + fn to_bytes(&self) -> Array { + let mut out = Array::::default(); + out.copy_from_slice(&self.0); + out + } + } + + impl Encapsulate for EncapsulationKey { + type Kem = MlKem; + + fn encapsulate_with_rng( + &self, + rng: &mut R, + ) -> (Ciphertext, SharedKey) { + let mut ct = Ciphertext::::default(); + let mut ss = SharedKey::::default(); + let mut coins = [0u8; 32]; // MLKEM_SYMBYTES + rng.fill_bytes(&mut coins); + let ret = unsafe { + $enc_derand( + ct.as_mut_slice().as_mut_ptr(), + ss.as_mut_slice().as_mut_ptr(), + self.0.as_ptr(), + coins.as_ptr(), + ) + }; + assert_eq!(ret, 0, "enc_derand failed — key may have been corrupted"); + (ct, ss) + } + } + + /// The decapsulation (secret) key. + /// + /// The encapsulation key is stored alongside to support + /// [`kem::Decapsulator::encapsulation_key`]. + pub struct DecapsulationKey { + sk: [u8; $sk_len], + ek: EncapsulationKey, + } + + impl DecapsulationKey { + /// Returns the raw byte encoding of this secret key. + pub fn as_bytes(&self) -> &[u8] { + &self.sk + } + } + + impl core::fmt::Debug for DecapsulationKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("DecapsulationKey([redacted])") + } + } + + impl kem::Decapsulator for DecapsulationKey { + type Kem = MlKem; + + fn encapsulation_key(&self) -> &EncapsulationKey { + &self.ek + } + } + + impl Decapsulate for DecapsulationKey { + fn decapsulate(&self, ct: &Ciphertext) -> SharedKey { + let mut ss = SharedKey::::default(); + // ML-KEM decapsulation always produces a value (implicit rejection + // for invalid ciphertexts per FIPS 203 §8.3); non-zero only if the + // hash check on the secret key fails, which is guarded against on + // construction. + let ret = unsafe { + $dec( + ss.as_mut_slice().as_mut_ptr(), + ct.as_slice().as_ptr(), + self.sk.as_ptr(), + ) + }; + assert_eq!(ret, 0, "dec failed — key may have been corrupted"); + ss + } + } + + impl Generate for DecapsulationKey { + fn try_generate_from_rng( + rng: &mut R, + ) -> Result { + let mut pk = [0u8; $pk_len]; + let mut sk = [0u8; $sk_len]; + let mut coins = [0u8; 64]; // 2 × MLKEM_SYMBYTES + rng.try_fill_bytes(&mut coins)?; + let ret = unsafe { + $keypair_derand(pk.as_mut_ptr(), sk.as_mut_ptr(), coins.as_ptr()) + }; + assert_eq!(ret, 0, "keypair_derand failed unexpectedly"); + Ok(Self { sk, ek: EncapsulationKey(pk) }) + } + } + + impl TryFrom<&[u8]> for DecapsulationKey { + type Error = InvalidKey; + + /// Parse and validate a secret key from bytes. + /// + /// The encapsulation key is extracted from its embedded position + /// in the secret key (per FIPS 203 §3.3). + fn try_from(bytes: &[u8]) -> Result { + let arr: [u8; $sk_len] = bytes.try_into().map_err(|_| InvalidKey)?; + let ret = unsafe { $check_sk(arr.as_ptr()) }; + if ret != 0 { + return Err(InvalidKey); + } + let mut pk = [0u8; $pk_len]; + pk.copy_from_slice(&arr[$ek_start..$ek_start + $pk_len]); + Ok(Self { sk: arr, ek: EncapsulationKey(pk) }) + } + } + + impl TryFrom<&[u8]> for EncapsulationKey { + type Error = InvalidKey; + + fn try_from(bytes: &[u8]) -> Result { + let arr: [u8; $pk_len] = bytes.try_into().map_err(|_| InvalidKey)?; + let ek = Self(arr); + let ret = unsafe { $check_pk(ek.0.as_ptr()) }; + if ret != 0 { Err(InvalidKey) } else { Ok(ek) } + } + } + } + }; +} + +define_mlkem! { + module = mlkem512, + sys_mod = mlkem512, + pk_len = 800, + sk_len = 1632, + ek_start = 768, + pk_size = hybrid_array::typenum::U800, + ct_size = hybrid_array::typenum::U768, + keypair_derand = PQCP_MLKEM_NATIVE_MLKEM512_keypair_derand, + enc_derand = PQCP_MLKEM_NATIVE_MLKEM512_enc_derand, + dec = PQCP_MLKEM_NATIVE_MLKEM512_dec, + check_pk = PQCP_MLKEM_NATIVE_MLKEM512_check_pk, + check_sk = PQCP_MLKEM_NATIVE_MLKEM512_check_sk, +} + +define_mlkem! { + module = mlkem768, + sys_mod = mlkem768, + pk_len = 1184, + sk_len = 2400, + ek_start = 1152, + pk_size = hybrid_array::sizes::U1184, + ct_size = hybrid_array::sizes::U1088, + keypair_derand = PQCP_MLKEM_NATIVE_MLKEM768_keypair_derand, + enc_derand = PQCP_MLKEM_NATIVE_MLKEM768_enc_derand, + dec = PQCP_MLKEM_NATIVE_MLKEM768_dec, + check_pk = PQCP_MLKEM_NATIVE_MLKEM768_check_pk, + check_sk = PQCP_MLKEM_NATIVE_MLKEM768_check_sk, +} + +define_mlkem! { + module = mlkem1024, + sys_mod = mlkem1024, + pk_len = 1568, + sk_len = 3168, + ek_start = 1536, + pk_size = hybrid_array::sizes::U1568, + ct_size = hybrid_array::sizes::U1568, + keypair_derand = PQCP_MLKEM_NATIVE_MLKEM1024_keypair_derand, + enc_derand = PQCP_MLKEM_NATIVE_MLKEM1024_enc_derand, + dec = PQCP_MLKEM_NATIVE_MLKEM1024_dec, + check_pk = PQCP_MLKEM_NATIVE_MLKEM1024_check_pk, + check_sk = PQCP_MLKEM_NATIVE_MLKEM1024_check_sk, +} diff --git a/flake.nix b/flake.nix index 02a55e76b..afb2ca4d4 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,8 @@ export PATH=$PWD/scripts:$PATH export PROOF_DIR="$PWD/proofs/hol_light" ''; + rustPackages = [ pkgs.rustup pkgs.llvmPackages.libclang ]; + rustEnv = { LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; }; in { _module.args.pkgs = import inputs.nixpkgs { @@ -83,7 +85,7 @@ packages.toolchain_aarch64_be = util.toolchain_aarch64_be; packages.gcc-arm-embedded = pkgs.gcc-arm-embedded; - devShells.default = util.mkShell { + devShells.default = (util.mkShell { packages = builtins.attrValues { inherit (config.packages) linters cbmc hol_light s2n_bignum slothy toolchains_native hol_server; @@ -91,8 +93,9 @@ direnv nix-direnv zig_0_13; - } ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ config.packages.valgrind_varlat ]; - }; + } ++ rustPackages + ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ config.packages.valgrind_varlat ]; + }).overrideAttrs (_: rustEnv); packages.hol_server = util.hol_server.hol_server_start; devShells.hol_light = (util.mkShell { @@ -166,6 +169,11 @@ ]; }; + devShells.rust = (util.mkShell { + packages = builtins.attrValues { inherit (config.packages) toolchains_native; } + ++ rustPackages; + }).overrideAttrs (_: rustEnv); + devShells.cross-avr = util.mkShell (import ./nix/avr { inherit pkgs; }); devShells.linter = util.mkShellNoCC { From 770fef4a9312f7ef294687a282f65fb6d5739fe7 Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Tue, 17 Mar 2026 11:03:18 +0100 Subject: [PATCH 02/10] feat(binding): add CI actions to test rust bindings Signed-off-by: Aaron Kaiser --- .github/workflows/all.yml | 6 ++++++ .github/workflows/rust.yml | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index ee37224f1..9bf0fa950 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -101,3 +101,9 @@ jobs: needs: [ base ] uses: ./.github/workflows/baremetal.yml secrets: inherit + rust: + name: Rust + permissions: + contents: 'read' + id-token: 'write' + uses: ./.github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 000000000..40a6b2790 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,37 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +name: Rust +permissions: + contents: read +on: + workflow_call: + workflow_dispatch: + +jobs: + rust: + name: Rust bindings (${{ matrix.target.name }}) + strategy: + fail-fast: false + matrix: + target: + - runner: ubuntu-latest + name: 'x86_64' + - runner: ubuntu-24.04-arm + name: 'aarch64' + - runner: macos-latest + name: 'macos (aarch64)' + - runner: macos-15-intel + name: 'macos (x86_64)' + runs-on: ${{ matrix.target.runner }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/setup-shell + with: + nix-shell: rust + gh_token: ${{ secrets.GITHUB_TOKEN }} + script: | + cargo build --workspace + cargo test --workspace + cargo publish --dry-run + cargo publish --dry-run --manifest-path bindings/rust/mlkem-native/Cargo.toml From ee9e9da6cf085be3b393a0fb74cb266b8204279f Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Mon, 13 Apr 2026 15:43:25 +0200 Subject: [PATCH 03/10] feat(binding): add acvp testing for rust create and add avx2 feature Signed-off-by: Aaron Kaiser --- .github/workflows/rust.yml | 24 +- Cargo.lock | 55 ++-- Cargo.toml | 27 +- bindings/rust/mlkem-native-sys/build.rs | 50 --- bindings/rust/mlkem-native-sys/src/lib.rs | 36 --- bindings/rust/mlkem-native/Cargo.toml | 8 +- bindings/rust/mlkem-native/build.rs | 89 ++++++ .../rust/mlkem-native/src/bin/acvp_mlkem.rs | 284 ++++++++++++++++++ bindings/rust/mlkem-native/src/lib.rs | 202 +++++++++++-- bindings/rust/mlkem-native/src/sys.rs | 14 + test/acvp/acvp_client.py | 40 ++- 11 files changed, 646 insertions(+), 183 deletions(-) delete mode 100644 bindings/rust/mlkem-native-sys/build.rs delete mode 100644 bindings/rust/mlkem-native-sys/src/lib.rs create mode 100644 bindings/rust/mlkem-native/build.rs create mode 100644 bindings/rust/mlkem-native/src/bin/acvp_mlkem.rs create mode 100644 bindings/rust/mlkem-native/src/sys.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 40a6b2790..4903781a3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,13 +16,13 @@ jobs: matrix: target: - runner: ubuntu-latest - name: 'x86_64' + name: "x86_64" - runner: ubuntu-24.04-arm - name: 'aarch64' + name: "aarch64" - runner: macos-latest - name: 'macos (aarch64)' + name: "macos (aarch64)" - runner: macos-15-intel - name: 'macos (x86_64)' + name: "macos (x86_64)" runs-on: ${{ matrix.target.runner }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -32,6 +32,18 @@ jobs: gh_token: ${{ secrets.GITHUB_TOKEN }} script: | cargo build --workspace + RUSTFLAGS="-C target-feature=+avx2" cargo build --workspace cargo test --workspace - cargo publish --dry-run - cargo publish --dry-run --manifest-path bindings/rust/mlkem-native/Cargo.toml + cargo publish --dry-run -p mlkem-native + - uses: ./.github/actions/setup-shell + name: ACVP test + with: + nix-shell: rust + gh_token: ${{ secrets.GITHUB_TOKEN }} + script: | + cargo build --release --bin acvp_mlkem + for version in v1.1.0.39 v1.1.0.40 v1.1.0.41; do + python3 test/acvp/acvp_client.py \ + --binary ./target/release/acvp_mlkem \ + --version $version + done diff --git a/Cargo.lock b/Cargo.lock index fa0685fd6..fb4422389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", @@ -45,9 +45,9 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cc" -version = "1.2.57" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -157,9 +157,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hybrid-array" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ "typenum", ] @@ -172,12 +172,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -193,15 +193,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "kem" -version = "0.3.0-rc.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ae2c3347ff4a7af4f679a9e397c2c7e6034a00b773dd2dd3c001d7f40897c9" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" dependencies = [ "crypto-common", "rand_core", @@ -215,9 +215,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libloading" @@ -249,22 +249,15 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mlkem-native" -version = "0.1.0" +version = "1.1.0-rc.1" dependencies = [ + "bindgen", + "cc", "hybrid-array", "kem", - "mlkem-native-sys", "rand_core", ] -[[package]] -name = "mlkem-native-sys" -version = "0.1.0" -dependencies = [ - "bindgen", - "cc", -] - [[package]] name = "nom" version = "7.1.3" @@ -346,15 +339,15 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" diff --git a/Cargo.toml b/Cargo.toml index 5fe89408f..a8d5253e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,5 @@ [workspace] -members = ["bindings/rust/mlkem-native"] -resolver = "2" - -[package] -name = "mlkem-native-sys" -version = "0.1.0" -edition = "2021" -description = "Raw FFI bindings to mlkem-native, a C90 implementation of ML-KEM (FIPS 203)" -repository = "https://github.com/pq-code-package/mlkem-native" -license = "Apache-2.0 OR ISC OR MIT" -links = "mlkem-native" -build = "bindings/rust/mlkem-native-sys/build.rs" -include = [ - "Cargo.toml", - "bindings/rust/mlkem-native-sys/src/**", - "bindings/rust/mlkem-native-sys/build.rs", - "mlkem/**", +members = [ + "bindings/rust/mlkem-native", ] - -[lib] -path = "bindings/rust/mlkem-native-sys/src/lib.rs" - -[build-dependencies] -bindgen = "0.71" -cc = "1" +resolver = "2" diff --git a/bindings/rust/mlkem-native-sys/build.rs b/bindings/rust/mlkem-native-sys/build.rs deleted file mode 100644 index 42474a05a..000000000 --- a/bindings/rust/mlkem-native-sys/build.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) The mlkem-native project authors -// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -use std::env; -use std::path::PathBuf; - -fn main() { - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - let mlkem_dir = manifest_dir.join("mlkem"); - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - - println!("cargo:rerun-if-changed={}", mlkem_dir.display()); - - // Compile the C library for each parameter set using the monolithic build file. - // We build without the randomized API so no randombytes() implementation is needed. - for &level in &[512u32, 768, 1024] { - cc::Build::new() - .file(mlkem_dir.join("mlkem_native.c")) - .include(&mlkem_dir) - .define("MLK_CONFIG_PARAMETER_SET", level.to_string().as_str()) - .define("MLK_CONFIG_NO_RANDOMIZED_API", None) - .opt_level(3) - .compile(&format!("mlkem_native_{}", level)); - } - - // Generate bindings for each parameter set. - let header = mlkem_dir.join("mlkem_native.h"); - for &level in &[512u32, 768, 1024] { - let bindings = bindgen::Builder::default() - .header(header.to_str().unwrap()) - .clang_arg(format!("-I{}", mlkem_dir.display())) - .clang_arg(format!("-DMLK_CONFIG_PARAMETER_SET={}", level)) - // Suppress SUPERCOP crypto_kem_* aliases — use namespaced names only. - .clang_arg("-DMLK_CONFIG_API_NO_SUPERCOP") - // Must match the compiled library. - .clang_arg("-DMLK_CONFIG_NO_RANDOMIZED_API") - .allowlist_function(format!("PQCP_MLKEM_NATIVE_MLKEM{}_.*", level)) - .allowlist_var(format!("MLKEM{}_.*", level)) - .allowlist_var("MLKEM_BYTES") - .allowlist_var("MLKEM_SYMBYTES") - .allowlist_var("MLK_ERR_.*") - .use_core() - .generate() - .unwrap_or_else(|_| panic!("Unable to generate bindings for MLKEM-{}", level)); - - bindings - .write_to_file(out_dir.join(format!("bindings_{}.rs", level))) - .unwrap_or_else(|_| panic!("Couldn't write bindings for MLKEM-{}", level)); - } -} diff --git a/bindings/rust/mlkem-native-sys/src/lib.rs b/bindings/rust/mlkem-native-sys/src/lib.rs deleted file mode 100644 index a719a9025..000000000 --- a/bindings/rust/mlkem-native-sys/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) The mlkem-native project authors -// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -//! Raw FFI bindings to [mlkem-native], a C90 implementation of ML-KEM ([FIPS 203]). -//! -//! This crate exposes the deterministic (`_derand`) API for all three parameter -//! sets. The randomized API (`keypair` / `enc`) is intentionally omitted here; -//! it is provided by the higher-level `mlkem-native` crate using Rust's RNG -//! ecosystem. -//! -//! # Safety -//! -//! All functions in this crate are `unsafe` — callers must ensure that buffers -//! are valid, correctly-sized, and non-overlapping, as documented by each -//! function in the C header `mlkem/mlkem_native.h`. -//! -//! [mlkem-native]: https://github.com/pq-code-package/mlkem-native -//! [FIPS 203]: https://csrc.nist.gov/pubs/fips/203/final - -#![no_std] -#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case, dead_code)] - -/// Bindings for ML-KEM-512 (NIST security level 1). -pub mod mlkem512 { - include!(concat!(env!("OUT_DIR"), "/bindings_512.rs")); -} - -/// Bindings for ML-KEM-768 (NIST security level 3). -pub mod mlkem768 { - include!(concat!(env!("OUT_DIR"), "/bindings_768.rs")); -} - -/// Bindings for ML-KEM-1024 (NIST security level 5). -pub mod mlkem1024 { - include!(concat!(env!("OUT_DIR"), "/bindings_1024.rs")); -} diff --git a/bindings/rust/mlkem-native/Cargo.toml b/bindings/rust/mlkem-native/Cargo.toml index e8447a3f9..ad1ecbb95 100644 --- a/bindings/rust/mlkem-native/Cargo.toml +++ b/bindings/rust/mlkem-native/Cargo.toml @@ -1,16 +1,20 @@ [package] name = "mlkem-native" -version = "0.1.0" +version = "1.1.0-rc.1" edition = "2021" description = "ML-KEM (FIPS 203) via mlkem-native, implementing the RustCrypto KEM traits" repository = "https://github.com/pq-code-package/mlkem-native" license = "Apache-2.0 OR ISC OR MIT" [dependencies] -mlkem-native-sys = { path = "../../..", version = "0.1.0" } kem = "0.3.0-rc.6" hybrid-array = { version = "0.4", features = ["extra-sizes"] } rand_core = { version = "0.10", default-features = false } [dev-dependencies] kem = { version = "0.3.0-rc.6", features = ["getrandom"] } +rand_core = "0.10" + +[build-dependencies] +bindgen = "0.72.1" +cc = "1.2.60" diff --git a/bindings/rust/mlkem-native/build.rs b/bindings/rust/mlkem-native/build.rs new file mode 100644 index 000000000..6627a06bd --- /dev/null +++ b/bindings/rust/mlkem-native/build.rs @@ -0,0 +1,89 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +use std::env; +use std::path::PathBuf; + +fn main() { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + // The mlkem C sources live at the repository root, three directories above + // this crate (bindings/rust/mlkem-native/ → repo root). + let repo_root = manifest_dir + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap(); + let mlkem_dir = repo_root.join("mlkem"); + let header = mlkem_dir.join("mlkem_native.h"); + let header_str = header.to_str().unwrap(); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + println!("cargo:rerun-if-changed={}", mlkem_dir.display()); + + // Enable AVX2 native backends when the target is x86_64 and the `avx2` + // CPU feature is present at compile time (e.g. via + // `RUSTFLAGS="-C target-cpu=native"` or + // `RUSTFLAGS="-C target-feature=+avx2"`). + let target_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); + let use_avx2 = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default() == "x86_64" + && target_features.split(',').any(|f| f == "avx2"); + + if use_avx2 { + println!("cargo:rustc-cfg=mlkem_native_avx2"); + } + + for &level in &[512u32, 768, 1024] { + let level_str = level.to_string(); + + let mut c_build = cc::Build::new(); + c_build + .file(mlkem_dir.join("mlkem_native.c")) + .include(&mlkem_dir) + .define("MLK_CONFIG_PARAMETER_SET", level_str.as_str()) + .define("MLK_CONFIG_NO_RANDOMIZED_API", None) + .opt_level(3); + + if use_avx2 { + c_build + .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) + .define("MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202", None) + .flag("-mavx2"); + } + + c_build.compile(&format!("mlkem_native_{}", level)); + + if use_avx2 { + cc::Build::new() + .file(mlkem_dir.join("mlkem_native_asm.S")) + .include(&mlkem_dir) + .define("MLK_CONFIG_PARAMETER_SET", level_str.as_str()) + .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) + .define("MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202", None) + .flag("-mavx2") + .compile(&format!("mlkem_native_asm_{}", level)); + } + + let bindings = bindgen::Builder::default() + .header(header_str) + .clang_arg(format!("-I{}", mlkem_dir.display())) + .clang_arg(format!("-DMLK_CONFIG_PARAMETER_SET={}", level)) + // Suppress SUPERCOP crypto_kem_* aliases — use namespaced names only. + .clang_arg("-DMLK_CONFIG_API_NO_SUPERCOP") + // Must match the compiled library. + .clang_arg("-DMLK_CONFIG_NO_RANDOMIZED_API") + .allowlist_function(format!("PQCP_MLKEM_NATIVE_MLKEM{}_.*", level)) + .allowlist_var(format!("MLKEM{}_.*", level)) + .allowlist_var("MLKEM_BYTES") + .allowlist_var("MLKEM_SYMBYTES") + .allowlist_var("MLK_ERR_.*") + .use_core() + .generate() + .unwrap_or_else(|_| panic!("Unable to generate bindings for MLKEM-{}", level)); + + bindings + .write_to_file(out_dir.join(format!("bindings_{}.rs", level))) + .unwrap_or_else(|_| panic!("Couldn't write bindings for MLKEM-{}", level)); + } +} diff --git a/bindings/rust/mlkem-native/src/bin/acvp_mlkem.rs b/bindings/rust/mlkem-native/src/bin/acvp_mlkem.rs new file mode 100644 index 000000000..c23d86608 --- /dev/null +++ b/bindings/rust/mlkem-native/src/bin/acvp_mlkem.rs @@ -0,0 +1,284 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +//! ACVP test harness for ML-KEM using the safe `mlkem-native` Rust API. +//! +//! Usage: acvp_mlkem <512|768|1024> [encapDecap|keyGen] [AFT|VAL] {test specific arguments} +//! +//! Matches the CLI interface of test/acvp/acvp_mlkem.c, with the security +//! level prepended as an additional leading argument (since this single binary +//! covers all three parameter sets). + +use std::process; + +use rand_core::{TryCryptoRng, TryRng}; + +const SYMBYTES: usize = 32; + +fn decode_hex_char(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'A'..=b'F' => Some(10 + c - b'A'), + b'a'..=b'f' => Some(10 + c - b'a'), + _ => None, + } +} + +/// Decode a `prefix=HEXSTRING` CLI argument into exactly `expected_len` bytes. +/// Returns `Err(())` and prints a diagnostic to stderr on failure. +fn decode_hex(prefix: &str, arg: &str, expected_len: usize) -> Result, ()> { + let full_prefix = format!("{}=", prefix); + let hex = match arg.strip_prefix(&full_prefix) { + Some(h) => h, + None => { + eprintln!( + "Argument {} invalid: Expected argument of the form '{}=HEX' with \ + HEX being a hex encoding of {} bytes", + arg, prefix, expected_len + ); + return Err(()); + } + }; + + if hex.len() != 2 * expected_len { + eprintln!( + "Argument {} invalid: Expected {}=HEX with HEX being a hex encoding of {} bytes", + arg, prefix, expected_len + ); + return Err(()); + } + + hex.as_bytes() + .chunks(2) + .map(|chunk| match (decode_hex_char(chunk[0]), decode_hex_char(chunk[1])) { + (Some(hi), Some(lo)) => Ok((hi << 4) | lo), + _ => { + eprintln!("Argument {} invalid: non-hex character in value", arg); + Err(()) + } + }) + .collect() +} + +fn die_usage(msg: &str) -> ! { + eprintln!("{}", msg); + process::exit(1); +} + +fn print_hex(name: &str, data: &[u8]) { + print!("{}=", name); + for b in data { + print!("{:02X}", b); + } + println!(); +} + +/// A deterministic RNG that replays a fixed byte string exactly once. +/// +/// Used to feed caller-supplied randomness into the standard `encapsulate_with_rng` +/// and `try_generate_from_rng` APIs, giving the same result as the dedicated +/// `_derand` entry points in the C library. +struct FixedRng<'a>(&'a [u8]); + +impl TryRng for FixedRng<'_> { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + let mut b = [0u8; 4]; + self.try_fill_bytes(&mut b)?; + Ok(u32::from_le_bytes(b)) + } + + fn try_next_u64(&mut self) -> Result { + let mut b = [0u8; 8]; + self.try_fill_bytes(&mut b)?; + Ok(u64::from_le_bytes(b)) + } + + fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> { + let n = dst.len(); + dst.copy_from_slice(&self.0[..n]); + self.0 = &self.0[n..]; + Ok(()) + } +} + +// Not a real CSPRNG — only used with caller-supplied test vectors where the +// ACVP protocol requires deterministic, reproduced output. +impl TryCryptoRng for FixedRng<'_> {} + +/// Core ACVP logic for one security level, using only the safe mlkem-native API. +macro_rules! run_level { + ( + module = $module:ident, + pk_len = $pk_len:literal, + sk_len = $sk_len:literal, + ct_len = $ct_len:literal, + args = $args:expr + ) => {{ + use mlkem_native::kem::common::{Generate, KeyExport}; + use mlkem_native::kem::{Ciphertext, Decapsulate, Decapsulator, Encapsulate}; + use mlkem_native::$module::{DecapsulationKey, EncapsulationKey, MlKem}; + + let args: &[String] = $args; + + const USAGE: &str = + "acvp_mlkem [encapDecap|keyGen] [AFT|VAL] {test specific arguments}"; + const ENCAPS_USAGE: &str = + "acvp_mlkem encapDecap AFT encapsulation ek=HEX m=HEX"; + const DECAPS_USAGE: &str = + "acvp_mlkem encapDecap VAL decapsulation dk=HEX c=HEX"; + const EK_CHECK_USAGE: &str = + "acvp_mlkem encapDecap VAL encapsulationKeyCheck ek=HEX"; + const DK_CHECK_USAGE: &str = + "acvp_mlkem encapDecap VAL decapsulationKeyCheck dk=HEX"; + const KEYGEN_USAGE: &str = "acvp_mlkem keyGen AFT z=HEX d=HEX"; + + if args.is_empty() { + die_usage(USAGE); + } + + match args[0].as_str() { + "encapDecap" => { + let args = &args[1..]; + if args.len() < 2 { + die_usage( + "acvp_mlkem encapDecap [AFT|VAL] \ + [encapsulation|decapsulation|\ + encapsulationKeyCheck|decapsulationKeyCheck] ...", + ); + } + let test_type = args[0].as_str(); + let function = args[1].as_str(); + let args = &args[2..]; + + match (test_type, function) { + ("AFT", "encapsulation") => { + if args.len() < 2 { + die_usage(ENCAPS_USAGE); + } + let ek_bytes = decode_hex("ek", &args[0], $pk_len) + .unwrap_or_else(|_| die_usage(ENCAPS_USAGE)); + let m_bytes = decode_hex("m", &args[1], SYMBYTES) + .unwrap_or_else(|_| die_usage(ENCAPS_USAGE)); + let ek = EncapsulationKey::try_from(ek_bytes.as_slice()) + .unwrap_or_else(|_| die_usage(ENCAPS_USAGE)); + let (ct, ss) = ek.encapsulate_with_rng(&mut FixedRng(&m_bytes)); + print_hex("c", ct.as_slice()); + print_hex("k", ss.as_slice()); + } + ("VAL", "decapsulation") => { + if args.len() < 2 { + die_usage(DECAPS_USAGE); + } + let dk_bytes = decode_hex("dk", &args[0], $sk_len) + .unwrap_or_else(|_| die_usage(DECAPS_USAGE)); + let c_bytes = decode_hex("c", &args[1], $ct_len) + .unwrap_or_else(|_| die_usage(DECAPS_USAGE)); + let dk = DecapsulationKey::try_from(dk_bytes.as_slice()) + .unwrap_or_else(|_| die_usage(DECAPS_USAGE)); + let mut ct = Ciphertext::::default(); + ct.as_mut_slice().copy_from_slice(&c_bytes); + let ss = dk.decapsulate(&ct); + print_hex("k", ss.as_slice()); + } + ("VAL", "encapsulationKeyCheck") => { + if args.is_empty() { + die_usage(EK_CHECK_USAGE); + } + // ACVP 1.1.0.40+ sends keys of wrong length to test rejection; + // a decode failure is reported as testPassed=0. + let ek_bytes = match decode_hex("ek", &args[0], $pk_len) { + Ok(v) => v, + Err(_) => { + println!("testPassed=0"); + return; + } + }; + // Importing via try_from runs the FIPS 203 §7.2 modulus check. + let passed = EncapsulationKey::try_from(ek_bytes.as_slice()).is_ok(); + println!("testPassed={}", if passed { 1 } else { 0 }); + } + ("VAL", "decapsulationKeyCheck") => { + if args.is_empty() { + die_usage(DK_CHECK_USAGE); + } + // ACVP 1.1.0.40+ sends keys of wrong length to test rejection. + let dk_bytes = match decode_hex("dk", &args[0], $sk_len) { + Ok(v) => v, + Err(_) => { + println!("testPassed=0"); + return; + } + }; + // Importing via try_from runs the FIPS 203 §7.3 hash check. + let passed = DecapsulationKey::try_from(dk_bytes.as_slice()).is_ok(); + println!("testPassed={}", if passed { 1 } else { 0 }); + } + _ => die_usage(USAGE), + } + } + "keyGen" => { + let args = &args[1..]; + if args.is_empty() || args[0] != "AFT" || args.len() < 3 { + die_usage(KEYGEN_USAGE); + } + let z = decode_hex("z", &args[1], SYMBYTES) + .unwrap_or_else(|_| die_usage(KEYGEN_USAGE)); + let d = decode_hex("d", &args[2], SYMBYTES) + .unwrap_or_else(|_| die_usage(KEYGEN_USAGE)); + // coins = d || z (matches the C reference: zd[0..32]=d, zd[32..64]=z) + let mut coins = [0u8; 64]; + coins[..32].copy_from_slice(&d); + coins[32..].copy_from_slice(&z); + let dk = DecapsulationKey::try_generate_from_rng(&mut FixedRng(&coins)) + .unwrap_or_else(|e| match e {}); + print_hex("ek", dk.encapsulation_key().to_bytes().as_slice()); + print_hex("dk", dk.as_bytes()); + } + _ => die_usage(USAGE), + } + }}; +} + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() < 2 { + die_usage( + "Usage: acvp_mlkem <512|768|1024> [encapDecap|keyGen] [AFT|VAL] \ + {test specific arguments}", + ); + } + + let level: u32 = args[1] + .parse() + .unwrap_or_else(|_| die_usage("Invalid security level: expected 512, 768, or 1024")); + + let rest = &args[2..]; + + match level { + 512 => run_level!( + module = mlkem512, + pk_len = 800, + sk_len = 1632, + ct_len = 768, + args = rest + ), + 768 => run_level!( + module = mlkem768, + pk_len = 1184, + sk_len = 2400, + ct_len = 1088, + args = rest + ), + 1024 => run_level!( + module = mlkem1024, + pk_len = 1568, + sk_len = 3168, + ct_len = 1568, + args = rest + ), + _ => die_usage("Invalid security level: expected 512, 768, or 1024"), + } +} diff --git a/bindings/rust/mlkem-native/src/lib.rs b/bindings/rust/mlkem-native/src/lib.rs index 40c3b82f7..0fdcb537b 100644 --- a/bindings/rust/mlkem-native/src/lib.rs +++ b/bindings/rust/mlkem-native/src/lib.rs @@ -36,6 +36,8 @@ pub use kem; +mod sys; + macro_rules! define_mlkem { ( module = $module:ident, @@ -49,17 +51,15 @@ macro_rules! define_mlkem { enc_derand = $enc_derand:ident, dec = $dec:ident, check_pk = $check_pk:ident, - check_sk = $check_sk:ident $(,)? + check_sk = $check_sk:ident, ) => { pub mod $module { + use crate::sys::$sys_mod::{$check_pk, $check_sk, $dec, $enc_derand, $keypair_derand}; use hybrid_array::Array; use kem::{ common::{Generate, InvalidKey, KeyExport, KeySizeUser, TryKeyInit}, Ciphertext, Decapsulate, Encapsulate, Kem, SharedKey, }; - use mlkem_native_sys::$sys_mod::{ - $check_pk, $check_sk, $dec, $enc_derand, $keypair_derand, - }; use rand_core::{CryptoRng, TryCryptoRng}; /// Marker struct for this ML-KEM parameter set; implements [`Kem`]. @@ -79,7 +79,9 @@ macro_rules! define_mlkem { impl core::fmt::Debug for EncapsulationKey { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("EncapsulationKey").field(&self.0.as_ref()).finish() + f.debug_tuple("EncapsulationKey") + .field(&self.0.as_ref()) + .finish() } } @@ -89,11 +91,7 @@ macro_rules! define_mlkem { impl TryKeyInit for EncapsulationKey { fn new(key: &Array) -> Result { - let mut arr = [0u8; $pk_len]; - arr.copy_from_slice(key.as_slice()); - let ek = Self(arr); - let ret = unsafe { $check_pk(ek.0.as_ptr()) }; - if ret != 0 { Err(InvalidKey) } else { Ok(ek) } + Self::try_from(key.as_slice()) } } @@ -139,7 +137,6 @@ macro_rules! define_mlkem { } impl DecapsulationKey { - /// Returns the raw byte encoding of this secret key. pub fn as_bytes(&self) -> &[u8] { &self.sk } @@ -190,26 +187,27 @@ macro_rules! define_mlkem { $keypair_derand(pk.as_mut_ptr(), sk.as_mut_ptr(), coins.as_ptr()) }; assert_eq!(ret, 0, "keypair_derand failed unexpectedly"); - Ok(Self { sk, ek: EncapsulationKey(pk) }) + Ok(Self { + sk, + ek: EncapsulationKey(pk), + }) } } impl TryFrom<&[u8]> for DecapsulationKey { type Error = InvalidKey; - /// Parse and validate a secret key from bytes. - /// - /// The encapsulation key is extracted from its embedded position - /// in the secret key (per FIPS 203 §3.3). fn try_from(bytes: &[u8]) -> Result { let arr: [u8; $sk_len] = bytes.try_into().map_err(|_| InvalidKey)?; - let ret = unsafe { $check_sk(arr.as_ptr()) }; - if ret != 0 { + if unsafe { $check_sk(arr.as_ptr()) } != 0 { return Err(InvalidKey); } let mut pk = [0u8; $pk_len]; pk.copy_from_slice(&arr[$ek_start..$ek_start + $pk_len]); - Ok(Self { sk: arr, ek: EncapsulationKey(pk) }) + Ok(Self { + sk: arr, + ek: EncapsulationKey::try_from(pk.as_slice())?, + }) } } @@ -218,9 +216,11 @@ macro_rules! define_mlkem { fn try_from(bytes: &[u8]) -> Result { let arr: [u8; $pk_len] = bytes.try_into().map_err(|_| InvalidKey)?; - let ek = Self(arr); - let ret = unsafe { $check_pk(ek.0.as_ptr()) }; - if ret != 0 { Err(InvalidKey) } else { Ok(ek) } + let key = Self(arr); + if unsafe { $check_pk(key.0.as_ptr()) } != 0 { + return Err(InvalidKey); + } + Ok(key) } } } @@ -271,3 +271,161 @@ define_mlkem! { check_pk = PQCP_MLKEM_NATIVE_MLKEM1024_check_pk, check_sk = PQCP_MLKEM_NATIVE_MLKEM1024_check_sk, } + +#[cfg(test)] +mod tests { + // Each parameter set gets the same suite of tests, generated by this macro. + // + // Arguments: + // $module — one of mlkem512 / mlkem768 / mlkem1024 + // $pk_len — public-key (encapsulation key) byte length + // $sk_len — secret-key (decapsulation key) byte length + macro_rules! define_kem_tests { + ($module:ident, $pk_len:literal, $sk_len:literal) => { + mod $module { + use crate::$module::{DecapsulationKey, EncapsulationKey, MlKem}; + use kem::{Decapsulate, Encapsulate, Kem, KeyExport}; + + // ── Core KEM correctness ────────────────────────────────── + + /// Encapsulation followed by decapsulation must recover the + /// same shared secret. + #[test] + fn roundtrip() { + let (dk, ek) = MlKem::generate_keypair(); + let (ct, ss_enc) = ek.encapsulate(); + let ss_dec = dk.decapsulate(&ct); + assert_eq!(ss_enc, ss_dec); + } + + /// Decapsulating with the *wrong* secret key must not recover + /// the sender's shared secret (FIPS 203 implicit rejection). + #[test] + fn wrong_key_implicit_rejection() { + let (dk1, _) = MlKem::generate_keypair(); + let (_, ek2) = MlKem::generate_keypair(); + let (ct, ss_enc) = ek2.encapsulate(); + let ss_wrong = dk1.decapsulate(&ct); + assert_ne!(ss_enc, ss_wrong); + } + + /// Flipping a bit in the ciphertext must not recover the + /// sender's shared secret (FIPS 203 implicit rejection). + #[test] + fn tampered_ciphertext_implicit_rejection() { + let (dk, ek) = MlKem::generate_keypair(); + let (mut ct, ss_enc) = ek.encapsulate(); + ct.as_mut_slice()[0] ^= 0x01; + let ss_dec = dk.decapsulate(&ct); + assert_ne!(ss_enc, ss_dec); + } + + // ── Key sizes ───────────────────────────────────────────── + + #[test] + fn key_byte_lengths() { + let (dk, ek) = MlKem::generate_keypair(); + assert_eq!(ek.to_bytes().len(), $pk_len); + assert_eq!(dk.as_bytes().len(), $sk_len); + } + + // ── Serialization round-trips ───────────────────────────── + + /// A serialized then deserialized encapsulation key must be + /// equal to the original and must still produce a ciphertext + /// that the original decapsulation key can open. + #[test] + fn encapsulation_key_serialization() { + let (dk, ek) = MlKem::generate_keypair(); + let ek_bytes = ek.to_bytes(); + let ek2 = EncapsulationKey::try_from(ek_bytes.as_slice()) + .expect("deserialization of a freshly exported key must succeed"); + assert_eq!(ek, ek2); + let (ct, ss_enc) = ek2.encapsulate(); + let ss_dec = dk.decapsulate(&ct); + assert_eq!(ss_enc, ss_dec); + } + + /// A serialized then deserialized decapsulation key must still + /// open ciphertexts produced for the corresponding + /// encapsulation key. + #[test] + fn decapsulation_key_serialization() { + let (dk, ek) = MlKem::generate_keypair(); + let dk2 = DecapsulationKey::try_from(dk.as_bytes()) + .expect("deserialization of a freshly exported key must succeed"); + let (ct, ss_enc) = ek.encapsulate(); + let ss_dec = dk2.decapsulate(&ct); + assert_eq!(ss_enc, ss_dec); + } + + // ── Invalid-length inputs ───────────────────────────────── + + #[test] + fn encapsulation_key_wrong_length() { + assert!( + EncapsulationKey::try_from(&[0u8; $pk_len - 1][..]).is_err(), + "one byte short should be InvalidKey" + ); + assert!( + EncapsulationKey::try_from(&[0u8; $pk_len + 1][..]).is_err(), + "one byte long should be InvalidKey" + ); + } + + #[test] + fn decapsulation_key_wrong_length() { + assert!( + DecapsulationKey::try_from(&[0u8; $sk_len - 1][..]).is_err(), + "one byte short should be InvalidKey" + ); + assert!( + DecapsulationKey::try_from(&[0u8; $sk_len + 1][..]).is_err(), + "one byte long should be InvalidKey" + ); + } + + // ── Key corruption rejection ────────────────────────────── + + /// Flipping a bit in the stored H(ek) section of a decapsulation + /// key must cause `try_from` to return `Err` (FIPS 203 §7.3). + /// + /// H(ek) occupies the 32 bytes at `sk_len - 64`. Any single-bit + /// change there makes the hash comparison fail. + #[test] + fn corrupted_decapsulation_key_rejected() { + let (dk, _) = MlKem::generate_keypair(); + let mut dk_bytes = dk.as_bytes().to_vec(); + dk_bytes[$sk_len - 64] ^= 1; + assert!( + DecapsulationKey::try_from(dk_bytes.as_slice()).is_err(), + "a bit-flipped H(ek) must be rejected by the hash check" + ); + } + + /// Corrupting an encapsulation key so that a polynomial + /// coefficient exceeds q must cause `try_from` to return `Err` + /// (FIPS 203 §7.2). + /// + /// Byte 1 holds bits [11:8] of the first coefficient in its + /// lower nibble. Valid values are at most 0xD (q-1 = 3328 = + /// 0xD00), so ORing with 0x0E forces the nibble to be at least + /// 0xE (≥ 0xE00 = 3584 > q), regardless of the original value. + #[test] + fn corrupted_encapsulation_key_rejected() { + let (_, ek) = MlKem::generate_keypair(); + let mut ek_bytes = ek.to_bytes().to_vec(); + ek_bytes[1] |= 0x0E; + assert!( + EncapsulationKey::try_from(ek_bytes.as_slice()).is_err(), + "an out-of-range coefficient must be rejected by the modulus check" + ); + } + } + }; + } + + define_kem_tests!(mlkem512, 800, 1632); + define_kem_tests!(mlkem768, 1184, 2400); + define_kem_tests!(mlkem1024, 1568, 3168); +} diff --git a/bindings/rust/mlkem-native/src/sys.rs b/bindings/rust/mlkem-native/src/sys.rs new file mode 100644 index 000000000..17bcc835f --- /dev/null +++ b/bindings/rust/mlkem-native/src/sys.rs @@ -0,0 +1,14 @@ +// Copyright (c) The mlkem-native project authors +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case, dead_code)] + +pub mod mlkem512 { + include!(concat!(env!("OUT_DIR"), "/bindings_512.rs")); +} +pub mod mlkem768 { + include!(concat!(env!("OUT_DIR"), "/bindings_768.rs")); +} +pub mod mlkem1024 { + include!(concat!(env!("OUT_DIR"), "/bindings_1024.rs")); +} diff --git a/test/acvp/acvp_client.py b/test/acvp/acvp_client.py index d9958e877..ef22ff182 100755 --- a/test/acvp/acvp_client.py +++ b/test/acvp/acvp_client.py @@ -102,16 +102,24 @@ def info(msg, **kwargs): def get_acvp_binary(tg): - """Convert JSON dict for ACVP test group to suitable ACVP binary.""" + """Return the command prefix (list) for the binary handling this test group. + + Without --binary: one-element list with the level-specific C binary path. + With --binary: two-element list [binary, level] for a combined binary + such as the Rust acvp_mlkem that takes the level as its + first argument. + """ parameterSetToLevel = { "ML-KEM-512": 512, "ML-KEM-768": 768, "ML-KEM-1024": 1024, } level = parameterSetToLevel[tg["parameterSet"]] + if custom_binary is not None: + return [custom_binary, str(level)] basedir = f"./test/build/mlkem{level}/bin" acvp_bin = f"acvp_mlkem{level}" - return f"{basedir}/{acvp_bin}" + return [f"{basedir}/{acvp_bin}"] def run_encapDecap_test(tg, tc): @@ -120,8 +128,7 @@ def run_encapDecap_test(tg, tc): results = {"tcId": tc["tcId"]} if tg["function"] == "encapsulation": acvp_bin = get_acvp_binary(tg) - acvp_call = exec_prefix + [ - acvp_bin, + acvp_call = exec_prefix + acvp_bin + [ "encapDecap", "AFT", "encapsulation", @@ -143,8 +150,7 @@ def run_encapDecap_test(tg, tc): # TODO: Remove this fallback workaround. v.1.1.0.40 moved the dk from the # tg to the tc. This can be removed when v1.1.0.39 is removed. dk_value = tc.get("dk", tg.get("dk")) - acvp_call = exec_prefix + [ - acvp_bin, + acvp_call = exec_prefix + acvp_bin + [ "encapDecap", "VAL", "decapsulation", @@ -163,8 +169,7 @@ def run_encapDecap_test(tg, tc): results[k] = v elif tg["function"] == "encapsulationKeyCheck": acvp_bin = get_acvp_binary(tg) - acvp_call = exec_prefix + [ - acvp_bin, + acvp_call = exec_prefix + acvp_bin + [ "encapDecap", "VAL", "encapsulationKeyCheck", @@ -183,8 +188,7 @@ def run_encapDecap_test(tg, tc): elif tg["function"] == "decapsulationKeyCheck": acvp_bin = get_acvp_binary(tg) - acvp_call = exec_prefix + [ - acvp_bin, + acvp_call = exec_prefix + acvp_bin + [ "encapDecap", "VAL", "decapsulationKeyCheck", @@ -209,8 +213,7 @@ def run_keyGen_test(tg, tc): results = {"tcId": tc["tcId"]} acvp_bin = get_acvp_binary(tg) - acvp_call = exec_prefix + [ - acvp_bin, + acvp_call = exec_prefix + acvp_bin + [ "keyGen", "AFT", f"z={tc['z']}", @@ -345,8 +348,21 @@ def test(prompt, expected, output, version): default="v1.1.0.41", help="ACVP test vector version (default: v1.1.0.41)", ) +parser.add_argument( + "--binary", + "-b", + default=None, + help=( + "Path to a combined ACVP binary that accepts the security level " + "(512, 768, or 1024) as its first argument, e.g. the Rust acvp_mlkem " + "binary. When omitted, the default per-level C binaries are used." + ), +) args = parser.parse_args() +# Set by --binary; read by get_acvp_binary(). +custom_binary = args.binary + if args.prompt is None: print(f"Using ACVP test vectors version {args.version}", file=sys.stderr) From 9056a4cc05a87d26dbde47102bbd4bb238c14f95 Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Thu, 16 Apr 2026 10:30:02 +0200 Subject: [PATCH 04/10] fix(bindings): exclude Rust files from macro typos check Signed-off-by: Aaron Kaiser --- scripts/autogen | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/autogen b/scripts/autogen index 7a611b791..09c52cbe3 100755 --- a/scripts/autogen +++ b/scripts/autogen @@ -2638,6 +2638,10 @@ def check_macro_typos(): if filename.endswith(".ml"): return True + # Exclude Rust source files + if filename.endswith(".rs"): + return True + # Exclude regexp patterns in `autogen` if is_autogen: if rest.startswith("\\") or m in ["MLK_XXX", "MLK_SOURCE_XXX"]: From f30f87c483bad968ad903efa4011d37de6df97d8 Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Fri, 17 Apr 2026 09:34:17 +0200 Subject: [PATCH 05/10] misc(binding): reduce amount of dependencies by not generating the mlkem-native bindings automatically Signed-off-by: Aaron Kaiser --- Cargo.lock | 146 +------------------------- bindings/rust/mlkem-native/Cargo.toml | 1 - bindings/rust/mlkem-native/build.rs | 24 ----- bindings/rust/mlkem-native/src/sys.rs | 116 +++++++++++++++++++- scripts/autogen | 4 - 5 files changed, 117 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb4422389..99a9ca73b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,46 +2,17 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "cc" @@ -53,32 +24,12 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "crypto-common" version = "0.2.1" @@ -90,12 +41,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "equivalent" version = "1.0.2" @@ -128,12 +73,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "hashbrown" version = "0.15.5" @@ -182,15 +121,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -219,16 +149,6 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - [[package]] name = "log" version = "0.4.29" @@ -241,33 +161,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "mlkem-native" version = "1.1.0-rc.1" dependencies = [ - "bindgen", "cc", "hybrid-array", "kem", "rand_core", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -304,44 +207,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "semver" @@ -478,12 +346,6 @@ dependencies = [ "semver", ] -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/bindings/rust/mlkem-native/Cargo.toml b/bindings/rust/mlkem-native/Cargo.toml index ad1ecbb95..1e8182b22 100644 --- a/bindings/rust/mlkem-native/Cargo.toml +++ b/bindings/rust/mlkem-native/Cargo.toml @@ -16,5 +16,4 @@ kem = { version = "0.3.0-rc.6", features = ["getrandom"] } rand_core = "0.10" [build-dependencies] -bindgen = "0.72.1" cc = "1.2.60" diff --git a/bindings/rust/mlkem-native/build.rs b/bindings/rust/mlkem-native/build.rs index 6627a06bd..71983f881 100644 --- a/bindings/rust/mlkem-native/build.rs +++ b/bindings/rust/mlkem-native/build.rs @@ -16,9 +16,6 @@ fn main() { .parent() .unwrap(); let mlkem_dir = repo_root.join("mlkem"); - let header = mlkem_dir.join("mlkem_native.h"); - let header_str = header.to_str().unwrap(); - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); println!("cargo:rerun-if-changed={}", mlkem_dir.display()); @@ -64,26 +61,5 @@ fn main() { .flag("-mavx2") .compile(&format!("mlkem_native_asm_{}", level)); } - - let bindings = bindgen::Builder::default() - .header(header_str) - .clang_arg(format!("-I{}", mlkem_dir.display())) - .clang_arg(format!("-DMLK_CONFIG_PARAMETER_SET={}", level)) - // Suppress SUPERCOP crypto_kem_* aliases — use namespaced names only. - .clang_arg("-DMLK_CONFIG_API_NO_SUPERCOP") - // Must match the compiled library. - .clang_arg("-DMLK_CONFIG_NO_RANDOMIZED_API") - .allowlist_function(format!("PQCP_MLKEM_NATIVE_MLKEM{}_.*", level)) - .allowlist_var(format!("MLKEM{}_.*", level)) - .allowlist_var("MLKEM_BYTES") - .allowlist_var("MLKEM_SYMBYTES") - .allowlist_var("MLK_ERR_.*") - .use_core() - .generate() - .unwrap_or_else(|_| panic!("Unable to generate bindings for MLKEM-{}", level)); - - bindings - .write_to_file(out_dir.join(format!("bindings_{}.rs", level))) - .unwrap_or_else(|_| panic!("Couldn't write bindings for MLKEM-{}", level)); } } diff --git a/bindings/rust/mlkem-native/src/sys.rs b/bindings/rust/mlkem-native/src/sys.rs index 17bcc835f..6715f6d0b 100644 --- a/bindings/rust/mlkem-native/src/sys.rs +++ b/bindings/rust/mlkem-native/src/sys.rs @@ -4,11 +4,121 @@ #![allow(non_upper_case_globals, non_camel_case_types, non_snake_case, dead_code)] pub mod mlkem512 { - include!(concat!(env!("OUT_DIR"), "/bindings_512.rs")); + pub const MLKEM512_SECRETKEYBYTES: usize = 1632; + pub const MLKEM512_PUBLICKEYBYTES: usize = 800; + pub const MLKEM512_CIPHERTEXTBYTES: usize = 768; + pub const MLKEM512_SYMBYTES: usize = 32; + pub const MLKEM512_BYTES: usize = 32; + + pub const MLKEM_SYMBYTES: usize = 32; + pub const MLKEM_BYTES: usize = 32; + + pub const MLK_ERR_FAIL: i32 = -1; + pub const MLK_ERR_OUT_OF_MEMORY: i32 = -2; + pub const MLK_ERR_RNG_FAIL: i32 = -3; + + extern "C" { + pub fn PQCP_MLKEM_NATIVE_MLKEM512_keypair_derand( + pk: *mut u8, + sk: *mut u8, + coins: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM512_enc_derand( + ct: *mut u8, + ss: *mut u8, + pk: *const u8, + coins: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM512_dec( + ss: *mut u8, + ct: *const u8, + sk: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM512_check_pk(pk: *const u8) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM512_check_sk(sk: *const u8) -> i32; + } } + pub mod mlkem768 { - include!(concat!(env!("OUT_DIR"), "/bindings_768.rs")); + pub const MLKEM768_SECRETKEYBYTES: usize = 2400; + pub const MLKEM768_PUBLICKEYBYTES: usize = 1184; + pub const MLKEM768_CIPHERTEXTBYTES: usize = 1088; + pub const MLKEM768_SYMBYTES: usize = 32; + pub const MLKEM768_BYTES: usize = 32; + + pub const MLKEM_SYMBYTES: usize = 32; + pub const MLKEM_BYTES: usize = 32; + + pub const MLK_ERR_FAIL: i32 = -1; + pub const MLK_ERR_OUT_OF_MEMORY: i32 = -2; + pub const MLK_ERR_RNG_FAIL: i32 = -3; + + extern "C" { + pub fn PQCP_MLKEM_NATIVE_MLKEM768_keypair_derand( + pk: *mut u8, + sk: *mut u8, + coins: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM768_enc_derand( + ct: *mut u8, + ss: *mut u8, + pk: *const u8, + coins: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM768_dec( + ss: *mut u8, + ct: *const u8, + sk: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM768_check_pk(pk: *const u8) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM768_check_sk(sk: *const u8) -> i32; + } } + pub mod mlkem1024 { - include!(concat!(env!("OUT_DIR"), "/bindings_1024.rs")); + pub const MLKEM1024_SECRETKEYBYTES: usize = 3168; + pub const MLKEM1024_PUBLICKEYBYTES: usize = 1568; + pub const MLKEM1024_CIPHERTEXTBYTES: usize = 1568; + pub const MLKEM1024_SYMBYTES: usize = 32; + pub const MLKEM1024_BYTES: usize = 32; + + pub const MLKEM_SYMBYTES: usize = 32; + pub const MLKEM_BYTES: usize = 32; + + pub const MLK_ERR_FAIL: i32 = -1; + pub const MLK_ERR_OUT_OF_MEMORY: i32 = -2; + pub const MLK_ERR_RNG_FAIL: i32 = -3; + + extern "C" { + pub fn PQCP_MLKEM_NATIVE_MLKEM1024_keypair_derand( + pk: *mut u8, + sk: *mut u8, + coins: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM1024_enc_derand( + ct: *mut u8, + ss: *mut u8, + pk: *const u8, + coins: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM1024_dec( + ss: *mut u8, + ct: *const u8, + sk: *const u8, + ) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM1024_check_pk(pk: *const u8) -> i32; + + pub fn PQCP_MLKEM_NATIVE_MLKEM1024_check_sk(sk: *const u8) -> i32; + } } diff --git a/scripts/autogen b/scripts/autogen index 09c52cbe3..7a611b791 100755 --- a/scripts/autogen +++ b/scripts/autogen @@ -2638,10 +2638,6 @@ def check_macro_typos(): if filename.endswith(".ml"): return True - # Exclude Rust source files - if filename.endswith(".rs"): - return True - # Exclude regexp patterns in `autogen` if is_autogen: if rest.startswith("\\") or m in ["MLK_XXX", "MLK_SOURCE_XXX"]: From 84098865df048065ee854401b37e409b840adecc Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Fri, 17 Apr 2026 09:53:24 +0200 Subject: [PATCH 06/10] misc(binding): add missing copyright and license note, format flake.nix and exclude Cargo.lock from copyright and license check Signed-off-by: Aaron Kaiser --- Cargo.toml | 3 +++ bindings/rust/mlkem-native/Cargo.toml | 2 ++ flake.nix | 2 +- scripts/lint | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8d5253e0..8b5fc30d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + [workspace] members = [ "bindings/rust/mlkem-native", diff --git a/bindings/rust/mlkem-native/Cargo.toml b/bindings/rust/mlkem-native/Cargo.toml index 1e8182b22..a7d967057 100644 --- a/bindings/rust/mlkem-native/Cargo.toml +++ b/bindings/rust/mlkem-native/Cargo.toml @@ -1,3 +1,5 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT [package] name = "mlkem-native" version = "1.1.0-rc.1" diff --git a/flake.nix b/flake.nix index afb2ca4d4..fc2cbe687 100644 --- a/flake.nix +++ b/flake.nix @@ -94,7 +94,7 @@ nix-direnv zig_0_13; } ++ rustPackages - ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ config.packages.valgrind_varlat ]; + ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ config.packages.valgrind_varlat ]; }).overrideAttrs (_: rustEnv); packages.hol_server = util.hol_server.hol_server_start; diff --git a/scripts/lint b/scripts/lint index 46f4f0c5f..23472412e 100755 --- a/scripts/lint +++ b/scripts/lint @@ -206,7 +206,7 @@ gh_group_end check-spdx() { local success=true - for file in $(git ls-files -- ":/" ":/!:*.json" ":/!:*.png" ":/!:*.patch" ":/!:*LICENSE*" ":/!:.git*" ":/!:flake.lock"); do + for file in $(git ls-files -- ":/" ":/!:*.json" ":/!:*.png" ":/!:*.patch" ":/!:*LICENSE*" ":/!:.git*" ":/!:flake.lock" ":/!:Cargo.lock"); do # Ignore symlinks if [[ ! -L $file && $(grep "SPDX-License-Identifier:" "$file" | wc -l) == 0 ]]; then gh_error "$file" "${line:-1}" "Missing license header error" "$file is missing SPDX License header" From 3475585977b001c284da62d76a019e6f03da4cb9 Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Fri, 17 Apr 2026 11:24:06 +0200 Subject: [PATCH 07/10] misc(binding): Remove cargo workspace, move package definition to root, add metadata to package and update the rust edition Signed-off-by: Aaron Kaiser --- Cargo.toml | 45 ++++++++++++++++++++++++--- bindings/rust/mlkem-native/Cargo.toml | 21 ------------- bindings/rust/mlkem-native/build.rs | 14 +++------ bindings/rust/mlkem-native/src/sys.rs | 31 +++++++----------- 4 files changed, 57 insertions(+), 54 deletions(-) delete mode 100644 bindings/rust/mlkem-native/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 8b5fc30d8..8e57b78e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,45 @@ # Copyright (c) The mlkem-native project authors # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT -[workspace] -members = [ - "bindings/rust/mlkem-native", +[package] +name = "mlkem-native" +version = "1.1.0-rc.1" +edition = "2024" +description = "ML-KEM (FIPS 203) via mlkem-native" +categories = ["cryptography"] +keywords = ["cryptography", "post-quantum", "security", "mlkem", "ml-kem"] +repository = "https://github.com/pq-code-package/mlkem-native" +readme = "README.md" +license = "Apache-2.0 OR ISC OR MIT" +build = "bindings/rust/mlkem-native/build.rs" +include = [ + "bindings/rust/mlkem-native/build.rs", + "bindings/rust/mlkem-native/src/lib.rs", + "bindings/rust/mlkem-native/src/sys.rs", + "bindings/rust/mlkem-native/examples/kem.rs", + "bindings/rust/mlkem-native/examples/serialization.rs", + "mlkem/**", ] -resolver = "2" + +[lib] +path = "bindings/rust/mlkem-native/src/lib.rs" + +[[example]] +name = "kem" +path = "bindings/rust/mlkem-native/examples/kem.rs" + +[[example]] +name = "serialization" +path = "bindings/rust/mlkem-native/examples/serialization.rs" + +[dependencies] +kem = "0.3.0-rc.6" +hybrid-array = { version = "0.4", features = ["extra-sizes"] } +rand_core = { version = "0.10", default-features = false } + +[dev-dependencies] +kem = { version = "0.3.0-rc.6", features = ["getrandom"] } +rand_core = "0.10" + +[build-dependencies] +cc = "1.2.60" diff --git a/bindings/rust/mlkem-native/Cargo.toml b/bindings/rust/mlkem-native/Cargo.toml deleted file mode 100644 index a7d967057..000000000 --- a/bindings/rust/mlkem-native/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) The mlkem-native project authors -# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT -[package] -name = "mlkem-native" -version = "1.1.0-rc.1" -edition = "2021" -description = "ML-KEM (FIPS 203) via mlkem-native, implementing the RustCrypto KEM traits" -repository = "https://github.com/pq-code-package/mlkem-native" -license = "Apache-2.0 OR ISC OR MIT" - -[dependencies] -kem = "0.3.0-rc.6" -hybrid-array = { version = "0.4", features = ["extra-sizes"] } -rand_core = { version = "0.10", default-features = false } - -[dev-dependencies] -kem = { version = "0.3.0-rc.6", features = ["getrandom"] } -rand_core = "0.10" - -[build-dependencies] -cc = "1.2.60" diff --git a/bindings/rust/mlkem-native/build.rs b/bindings/rust/mlkem-native/build.rs index 71983f881..e0e22fbc4 100644 --- a/bindings/rust/mlkem-native/build.rs +++ b/bindings/rust/mlkem-native/build.rs @@ -6,16 +6,10 @@ use std::path::PathBuf; fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - // The mlkem C sources live at the repository root, three directories above - // this crate (bindings/rust/mlkem-native/ → repo root). - let repo_root = manifest_dir - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap(); - let mlkem_dir = repo_root.join("mlkem"); + // The mlkem C sources are bundled alongside this crate in the `mlkem/` + // directory (a symlink to the repository root's mlkem/ in the source tree, + // and the resolved files in published packages). + let mlkem_dir = manifest_dir.join("mlkem"); println!("cargo:rerun-if-changed={}", mlkem_dir.display()); diff --git a/bindings/rust/mlkem-native/src/sys.rs b/bindings/rust/mlkem-native/src/sys.rs index 6715f6d0b..ab7888c75 100644 --- a/bindings/rust/mlkem-native/src/sys.rs +++ b/bindings/rust/mlkem-native/src/sys.rs @@ -1,7 +1,12 @@ // Copyright (c) The mlkem-native project authors // SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT -#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case, dead_code)] +#![allow( + non_upper_case_globals, + non_camel_case_types, + non_snake_case, + dead_code +)] pub mod mlkem512 { pub const MLKEM512_SECRETKEYBYTES: usize = 1632; @@ -17,7 +22,7 @@ pub mod mlkem512 { pub const MLK_ERR_OUT_OF_MEMORY: i32 = -2; pub const MLK_ERR_RNG_FAIL: i32 = -3; - extern "C" { + unsafe extern "C" { pub fn PQCP_MLKEM_NATIVE_MLKEM512_keypair_derand( pk: *mut u8, sk: *mut u8, @@ -31,11 +36,7 @@ pub mod mlkem512 { coins: *const u8, ) -> i32; - pub fn PQCP_MLKEM_NATIVE_MLKEM512_dec( - ss: *mut u8, - ct: *const u8, - sk: *const u8, - ) -> i32; + pub fn PQCP_MLKEM_NATIVE_MLKEM512_dec(ss: *mut u8, ct: *const u8, sk: *const u8) -> i32; pub fn PQCP_MLKEM_NATIVE_MLKEM512_check_pk(pk: *const u8) -> i32; @@ -57,7 +58,7 @@ pub mod mlkem768 { pub const MLK_ERR_OUT_OF_MEMORY: i32 = -2; pub const MLK_ERR_RNG_FAIL: i32 = -3; - extern "C" { + unsafe extern "C" { pub fn PQCP_MLKEM_NATIVE_MLKEM768_keypair_derand( pk: *mut u8, sk: *mut u8, @@ -71,11 +72,7 @@ pub mod mlkem768 { coins: *const u8, ) -> i32; - pub fn PQCP_MLKEM_NATIVE_MLKEM768_dec( - ss: *mut u8, - ct: *const u8, - sk: *const u8, - ) -> i32; + pub fn PQCP_MLKEM_NATIVE_MLKEM768_dec(ss: *mut u8, ct: *const u8, sk: *const u8) -> i32; pub fn PQCP_MLKEM_NATIVE_MLKEM768_check_pk(pk: *const u8) -> i32; @@ -97,7 +94,7 @@ pub mod mlkem1024 { pub const MLK_ERR_OUT_OF_MEMORY: i32 = -2; pub const MLK_ERR_RNG_FAIL: i32 = -3; - extern "C" { + unsafe extern "C" { pub fn PQCP_MLKEM_NATIVE_MLKEM1024_keypair_derand( pk: *mut u8, sk: *mut u8, @@ -111,11 +108,7 @@ pub mod mlkem1024 { coins: *const u8, ) -> i32; - pub fn PQCP_MLKEM_NATIVE_MLKEM1024_dec( - ss: *mut u8, - ct: *const u8, - sk: *const u8, - ) -> i32; + pub fn PQCP_MLKEM_NATIVE_MLKEM1024_dec(ss: *mut u8, ct: *const u8, sk: *const u8) -> i32; pub fn PQCP_MLKEM_NATIVE_MLKEM1024_check_pk(pk: *const u8) -> i32; From c93123510ff06aeab33bc712abb53198500c91ba Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Mon, 4 May 2026 18:25:48 +0200 Subject: [PATCH 08/10] feat(binding): zeroize secret key on drop and restructure build.rs Signed-off-by: Aaron Kaiser --- .cargo/config.toml | 15 +++ Cargo.lock | 7 ++ Cargo.toml | 5 + bindings/rust/mlkem-native/build.rs | 170 ++++++++++++++++++++------ bindings/rust/mlkem-native/src/lib.rs | 36 +++++- 5 files changed, 190 insertions(+), 43 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..931ee417b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,15 @@ + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-unknown-linux-gnu-gcc" + +[target.aarch64_be-unknown-linux-gnu] +linker = "aarch64_be-none-linux-gnu-gcc" + +[target.riscv64gc-unknown-linux-gnu] +linker = "riscv64-unknown-linux-gnu-gcc" + +[target.riscv32gc-unknown-linux-gnu] +linker = "riscv32-unknown-linux-gnu-gcc" + +[target.powerpc64le-unknown-linux-gnu] +linker = "powerpc64le-unknown-linux-gnu-gcc" diff --git a/Cargo.lock b/Cargo.lock index 99a9ca73b..361233124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,7 @@ dependencies = [ "hybrid-array", "kem", "rand_core", + "zeroize", ] [[package]] @@ -434,6 +435,12 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 8e57b78e7..f26dd5e90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,10 +32,15 @@ path = "bindings/rust/mlkem-native/examples/kem.rs" name = "serialization" path = "bindings/rust/mlkem-native/examples/serialization.rs" +[[bin]] +name = "acvp_mlkem" +path = "bindings/rust/mlkem-native/src/bin/acvp_mlkem.rs" + [dependencies] kem = "0.3.0-rc.6" hybrid-array = { version = "0.4", features = ["extra-sizes"] } rand_core = { version = "0.10", default-features = false } +zeroize = { version = "1.8.2" } [dev-dependencies] kem = { version = "0.3.0-rc.6", features = ["getrandom"] } diff --git a/bindings/rust/mlkem-native/build.rs b/bindings/rust/mlkem-native/build.rs index e0e22fbc4..e533654b5 100644 --- a/bindings/rust/mlkem-native/build.rs +++ b/bindings/rust/mlkem-native/build.rs @@ -2,7 +2,125 @@ // SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +// x86_64: enable when AVX2 is present (e.g. RUSTFLAGS="-C target-feature=+avx2"). +// AArch64 LE: enable when NEON is present (mandatory on ARMv8-A, but check to be safe). +// Big-endian AArch64 is excluded: the assembly backend only supports little-endian. +// SHA3 (ARMv8.4-A): enables optimized Keccak x1/x2/x4 backends via __ARM_FEATURE_SHA3. +// RISC-V 64: arith-only backend (no RVV FIPS202 backend); enable with RUSTFLAGS="-C target-feature=+v". +enum Backend { + Generic, + Avx2, + Aarch64 { sha3: bool }, + Riscv64Rvv, +} + +impl Backend { + fn detect() -> Self { + let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + let endian = env::var("CARGO_CFG_TARGET_ENDIAN").unwrap_or_default(); + let features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); + let has = |f: &str| features.split(',').any(|t| t == f); + + match arch.as_str() { + "x86_64" if has("avx2") => Backend::Avx2, + "aarch64" if endian == "little" && has("neon") => Backend::Aarch64 { sha3: has("sha3") }, + "riscv64" if has("v") => Backend::Riscv64Rvv, + _ => Backend::Generic, + } + } + + // AArch64 and x86_64 have assembly backends bundled in mlkem_native_asm.S; + // RISC-V uses only C intrinsics so no separate asm compilation is needed. + fn needs_asm(&self) -> bool { + matches!(self, Backend::Avx2 | Backend::Aarch64 { .. }) + } + + fn apply(&self, build: &mut cc::Build) { + let is_msvc = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "msvc"; + match self { + Backend::Avx2 => { + build + .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) + .define("MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202", None) + .flag(if is_msvc { "/arch:AVX2" } else { "-mavx2" }); + } + Backend::Aarch64 { sha3 } => { + build + .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) + .define("MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202", None); + if *sha3 { + build.flag("-march=armv8.4-a+sha3"); + } + } + Backend::Riscv64Rvv => { + build + .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) + .flag("-march=rv64gcv"); + } + Backend::Generic => {} + } + } + + fn emit_cfg(&self) { + for key in [ + "mlkem_native_backend_avx2", + "mlkem_native_backend_aarch64", + "mlkem_native_backend_aarch64_sha3", + "mlkem_native_backend_riscv64_rvv", + ] { + println!("cargo:rustc-check-cfg=cfg({key})"); + } + match self { + Backend::Avx2 => println!("cargo:rustc-cfg=mlkem_native_backend_avx2"), + Backend::Aarch64 { sha3 } => { + println!("cargo:rustc-cfg=mlkem_native_backend_aarch64"); + if *sha3 { + println!("cargo:rustc-cfg=mlkem_native_backend_aarch64_sha3"); + } + } + Backend::Riscv64Rvv => println!("cargo:rustc-cfg=mlkem_native_backend_riscv64_rvv"), + Backend::Generic => {} + } + } +} + +// When cross-compiling, derive the C compiler explicitly so that a CC env +// var pointing at the host compiler (common in Nix shells) doesn't +// silently produce host-arch object files. CC_ takes precedence +// over this fallback; CC on its own is ignored when cross-compiling +// because it typically names the host compiler, not the cross-compiler. +// +// RISC-V triples include "gc" (e.g. riscv64gc-unknown-linux-gnu) but the +// gcc binary omits it (riscv64-unknown-linux-gnu-gcc). +fn cross_compiler(host: &str, target: &str) -> Option { + if host == target { + return None; + } + let cc_env = format!("CC_{}", target.replace('-', "_")); + if env::var(&cc_env).is_ok() { + return None; + } + let gcc_triple = target + .replace("riscv64gc-", "riscv64-") + .replace("riscv32gc-", "riscv32-"); + Some(format!("{}-gcc", gcc_triple)) +} + +fn base_build(mlkem_dir: &Path, level: u32, compiler: Option<&str>) -> cc::Build { + let level_str = level.to_string(); + let mut build = cc::Build::new(); + build + .include(mlkem_dir) + .define("MLK_CONFIG_PARAMETER_SET", level_str.as_str()) + .define("MLK_CONFIG_NO_RANDOMIZED_API", None) + .opt_level(3); + if let Some(cc) = compiler { + build.compiler(cc); + } + build +} fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -13,47 +131,23 @@ fn main() { println!("cargo:rerun-if-changed={}", mlkem_dir.display()); - // Enable AVX2 native backends when the target is x86_64 and the `avx2` - // CPU feature is present at compile time (e.g. via - // `RUSTFLAGS="-C target-cpu=native"` or - // `RUSTFLAGS="-C target-feature=+avx2"`). - let target_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); - let use_avx2 = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default() == "x86_64" - && target_features.split(',').any(|f| f == "avx2"); - - if use_avx2 { - println!("cargo:rustc-cfg=mlkem_native_avx2"); - } + let host = env::var("HOST").unwrap_or_default(); + let target = env::var("TARGET").unwrap_or_default(); + let compiler = cross_compiler(&host, &target); + let backend = Backend::detect(); + backend.emit_cfg(); for &level in &[512u32, 768, 1024] { - let level_str = level.to_string(); - - let mut c_build = cc::Build::new(); - c_build - .file(mlkem_dir.join("mlkem_native.c")) - .include(&mlkem_dir) - .define("MLK_CONFIG_PARAMETER_SET", level_str.as_str()) - .define("MLK_CONFIG_NO_RANDOMIZED_API", None) - .opt_level(3); - - if use_avx2 { - c_build - .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) - .define("MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202", None) - .flag("-mavx2"); - } - + let mut c_build = base_build(&mlkem_dir, level, compiler.as_deref()); + c_build.file(mlkem_dir.join("mlkem_native.c")); + backend.apply(&mut c_build); c_build.compile(&format!("mlkem_native_{}", level)); - if use_avx2 { - cc::Build::new() - .file(mlkem_dir.join("mlkem_native_asm.S")) - .include(&mlkem_dir) - .define("MLK_CONFIG_PARAMETER_SET", level_str.as_str()) - .define("MLK_CONFIG_USE_NATIVE_BACKEND_ARITH", None) - .define("MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202", None) - .flag("-mavx2") - .compile(&format!("mlkem_native_asm_{}", level)); + if backend.needs_asm() { + let mut asm_build = base_build(&mlkem_dir, level, compiler.as_deref()); + asm_build.file(mlkem_dir.join("mlkem_native_asm.S")); + backend.apply(&mut asm_build); + asm_build.compile(&format!("mlkem_native_asm_{}", level)); } } } diff --git a/bindings/rust/mlkem-native/src/lib.rs b/bindings/rust/mlkem-native/src/lib.rs index 0fdcb537b..367af25b7 100644 --- a/bindings/rust/mlkem-native/src/lib.rs +++ b/bindings/rust/mlkem-native/src/lib.rs @@ -57,10 +57,11 @@ macro_rules! define_mlkem { use crate::sys::$sys_mod::{$check_pk, $check_sk, $dec, $enc_derand, $keypair_derand}; use hybrid_array::Array; use kem::{ - common::{Generate, InvalidKey, KeyExport, KeySizeUser, TryKeyInit}, Ciphertext, Decapsulate, Encapsulate, Kem, SharedKey, + common::{Generate, InvalidKey, KeyExport, KeySizeUser, TryKeyInit}, }; use rand_core::{CryptoRng, TryCryptoRng}; + use zeroize::Zeroize; /// Marker struct for this ML-KEM parameter set; implements [`Kem`]. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] @@ -114,6 +115,11 @@ macro_rules! define_mlkem { let mut ss = SharedKey::::default(); let mut coins = [0u8; 32]; // MLKEM_SYMBYTES rng.fill_bytes(&mut coins); + // SAFETY: All pointers are derived from correctly-sized, + // initialized arrays (`ct` = $ct_size bytes, `ss` = 32 + // bytes, `self.0` = $pk_len bytes, `coins` = 32 bytes). + // None of the buffers overlap, and the C function only + // reads/writes within the advertised sizes. let ret = unsafe { $enc_derand( ct.as_mut_slice().as_mut_ptr(), @@ -136,6 +142,12 @@ macro_rules! define_mlkem { ek: EncapsulationKey, } + impl Drop for DecapsulationKey { + fn drop(&mut self) { + self.sk.zeroize(); + } + } + impl DecapsulationKey { pub fn as_bytes(&self) -> &[u8] { &self.sk @@ -159,10 +171,11 @@ macro_rules! define_mlkem { impl Decapsulate for DecapsulationKey { fn decapsulate(&self, ct: &Ciphertext) -> SharedKey { let mut ss = SharedKey::::default(); - // ML-KEM decapsulation always produces a value (implicit rejection - // for invalid ciphertexts per FIPS 203 §8.3); non-zero only if the - // hash check on the secret key fails, which is guarded against on - // construction. + // SAFETY: All pointers are derived from correctly-sized, + // initialized arrays (`ss` = 32 bytes, `ct` = $ct_size + // bytes, `self.sk` = $sk_len bytes). None of the buffers + // overlap, and the C function only reads/writes within + // the advertised sizes. let ret = unsafe { $dec( ss.as_mut_slice().as_mut_ptr(), @@ -170,6 +183,10 @@ macro_rules! define_mlkem { self.sk.as_ptr(), ) }; + // ML-KEM decapsulation always produces a value (implicit rejection + // for invalid ciphertexts per FIPS 203 §8.3); non-zero only if the + // hash check on the secret key fails, which is guarded against on + // construction. assert_eq!(ret, 0, "dec failed — key may have been corrupted"); ss } @@ -183,6 +200,11 @@ macro_rules! define_mlkem { let mut sk = [0u8; $sk_len]; let mut coins = [0u8; 64]; // 2 × MLKEM_SYMBYTES rng.try_fill_bytes(&mut coins)?; + // SAFETY: All pointers are derived from correctly-sized, + // initialized arrays (`pk` = $pk_len bytes, `sk` = $sk_len + // bytes, `coins` = 64 bytes). None of the buffers overlap, + // and the C function only reads/writes within the + // advertised sizes. let ret = unsafe { $keypair_derand(pk.as_mut_ptr(), sk.as_mut_ptr(), coins.as_ptr()) }; @@ -199,6 +221,8 @@ macro_rules! define_mlkem { fn try_from(bytes: &[u8]) -> Result { let arr: [u8; $sk_len] = bytes.try_into().map_err(|_| InvalidKey)?; + // SAFETY: `arr` is a correctly-sized [$sk_len-byte] array; + // the C function reads exactly $sk_len bytes from the pointer. if unsafe { $check_sk(arr.as_ptr()) } != 0 { return Err(InvalidKey); } @@ -217,6 +241,8 @@ macro_rules! define_mlkem { fn try_from(bytes: &[u8]) -> Result { let arr: [u8; $pk_len] = bytes.try_into().map_err(|_| InvalidKey)?; let key = Self(arr); + // SAFETY: `key.0` is a correctly-sized [$pk_len-byte] array; + // the C function reads exactly $pk_len bytes from the pointer. if unsafe { $check_pk(key.0.as_ptr()) } != 0 { return Err(InvalidKey); } From 04a57a3c94e7f5bfb1c5277e74ce4ae87b50f53e Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Mon, 4 May 2026 20:11:22 +0200 Subject: [PATCH 09/10] feat(binding): add CI test Signed-off-by: Aaron Kaiser --- .github/workflows/rust.yml | 99 +++++++++++++++++++++++++++++++++++++- flake.nix | 15 ++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4903781a3..44a812802 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,9 +32,29 @@ jobs: gh_token: ${{ secrets.GITHUB_TOKEN }} script: | cargo build --workspace - RUSTFLAGS="-C target-feature=+avx2" cargo build --workspace cargo test --workspace cargo publish --dry-run -p mlkem-native + - name: Vector extension tests + uses: ./.github/actions/setup-shell + with: + nix-shell: rust + gh_token: ${{ secrets.GITHUB_TOKEN }} + script: | + ARCH=$(uname -m) + OS=$(uname -s) + if [ "$ARCH" = "x86_64" ]; then + RUSTFLAGS="-C target-feature=+avx2" cargo build --workspace + RUSTFLAGS="-C target-feature=+avx2" cargo test --workspace + elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + # Test generic backend (explicitly disable NEON) + RUSTFLAGS="-C target-feature=-neon" cargo build --workspace + RUSTFLAGS="-C target-feature=-neon" cargo test --workspace + # Test SHA3 backend; Apple Silicon (Darwin arm64) guarantees ARMv8.4-A+SHA3 + if [ "$OS" = "Darwin" ]; then + RUSTFLAGS="-C target-feature=+sha3" cargo build --workspace + RUSTFLAGS="-C target-feature=+sha3" cargo test --workspace + fi + fi - uses: ./.github/actions/setup-shell name: ACVP test with: @@ -47,3 +67,80 @@ jobs: --binary ./target/release/acvp_mlkem \ --version $version done + + rust_cross: + name: Rust cross-compilation (${{ matrix.target.name }}) + # Mirror the C CI: skip cross jobs on fork PRs to avoid long queue times. + if: github.repository_owner == 'pq-code-package' || !github.event.pull_request.head.repo.fork + strategy: + fail-fast: false + matrix: + target: + # Run riscv64 and ppc64le from the aarch64 runner + - name: "riscv64" + runner: ubuntu-24.04-arm + nix_shell: cross-rust-riscv64 + rust_target: riscv64gc-unknown-linux-gnu + cargo_runner_var: CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_RUNNER + qemu_bin: qemu-riscv64 + - name: "ppc64le" + runner: ubuntu-24.04-arm + nix_shell: cross-rust-ppc64le + rust_target: powerpc64le-unknown-linux-gnu + cargo_runner_var: CARGO_TARGET_POWERPC64LE_UNKNOWN_LINUX_GNU_RUNNER + qemu_bin: qemu-ppc64le + # Run aarch64 cross-compilation from the x86_64 runner + - name: "aarch64" + runner: ubuntu-latest + nix_shell: cross-rust-aarch64 + rust_target: aarch64-unknown-linux-gnu + cargo_runner_var: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER + qemu_bin: qemu-aarch64 + runs-on: ${{ matrix.target.runner }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/setup-shell + with: + nix-shell: ${{ matrix.target.nix_shell }} + nix-cache: true + gh_token: ${{ secrets.GITHUB_TOKEN }} + script: | + rustup target add ${{ matrix.target.rust_target }} + + # Build first to extract the nix-store sysroot from the ELF interpreter. + # Nix cross-toolchains don't expose a usable --sysroot; the target glibc + # lives at a hash-addressed path in the nix store that is only visible in + # the .interp section of a compiled binary. + cargo build --workspace --target ${{ matrix.target.rust_target }} + SAMPLE_BIN=$(find target/${{ matrix.target.rust_target }}/debug -maxdepth 2 -executable -type f -name 'acvp_mlkem' | head -1) + SYSROOT=$(readelf -l "$SAMPLE_BIN" | grep -oP '(?<=\[)[^\]]+' | grep -E 'ld-linux|ld-musl|ld64|ld\.so' | sed 's|/lib[^/]*/ld.*||') + + # Helper: sets the QEMU runner env var and runs cargo test. + # $1 = extra qemu flags (empty for none), $2 = RUSTFLAGS (empty for none). + run_tests() { + export ${{ matrix.target.cargo_runner_var }}="${{ matrix.target.qemu_bin }}${1:+ $1} -L $SYSROOT" + RUSTFLAGS="$2" cargo test --workspace --target ${{ matrix.target.rust_target }} + } + + case ${{ matrix.target.rust_target }} in + riscv64gc-unknown-linux-gnu) + # Generic backend (no RVV), then RVV backend at each VLEN. + # VLENs match the C CI (multi-functest uses 128/256/512/1024). + run_tests "" "" + for vlen in 128 256 512 1024; do + run_tests "-cpu rv64,v=true,vlen=$vlen" "-C target-feature=+v" + done + ;; + powerpc64le-unknown-linux-gnu) + # Generic backend only (no vector backend for ppc64le yet). + run_tests "" "" + ;; + aarch64-unknown-linux-gnu) + # Generic backend (NEON disabled), analogous to C CI no_opt. + run_tests "" "-C target-feature=-neon" + # NEON backend (default on aarch64), analogous to C CI opt. + run_tests "" "" + # SHA3 backend (ARMv8.4-A); qemu-aarch64 -cpu max emulates it. + run_tests "-cpu max" "-C target-feature=+sha3" + ;; + esac diff --git a/flake.nix b/flake.nix index fc2cbe687..f843db0ec 100644 --- a/flake.nix +++ b/flake.nix @@ -174,6 +174,21 @@ ++ rustPackages; }).overrideAttrs (_: rustEnv); + devShells.cross-rust-aarch64 = (util.mkShell { + packages = builtins.attrValues { inherit (config.packages) toolchain_aarch64; } + ++ rustPackages; + }).overrideAttrs (_: rustEnv); + + devShells.cross-rust-riscv64 = (util.mkShell { + packages = builtins.attrValues { inherit (config.packages) toolchain_riscv64; } + ++ rustPackages; + }).overrideAttrs (_: rustEnv); + + devShells.cross-rust-ppc64le = (util.mkShell { + packages = builtins.attrValues { inherit (config.packages) toolchain_ppc64le; } + ++ rustPackages; + }).overrideAttrs (_: rustEnv); + devShells.cross-avr = util.mkShell (import ./nix/avr { inherit pkgs; }); devShells.linter = util.mkShellNoCC { From a65c2cc9f3772a84d3d2ca96d1cd63aa97c7d249 Mon Sep 17 00:00:00 2001 From: Aaron Kaiser Date: Tue, 5 May 2026 15:29:14 +0200 Subject: [PATCH 10/10] misc(binding): Remove v1.1.0.39 test vectors in acvp test Signed-off-by: Aaron Kaiser --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 44a812802..c59c02a4e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -62,7 +62,7 @@ jobs: gh_token: ${{ secrets.GITHUB_TOKEN }} script: | cargo build --release --bin acvp_mlkem - for version in v1.1.0.39 v1.1.0.40 v1.1.0.41; do + for version in v1.1.0.40 v1.1.0.41; do python3 test/acvp/acvp_client.py \ --binary ./target/release/acvp_mlkem \ --version $version