diff --git a/libcrux-iot/ml-kem/Cargo.toml b/libcrux-iot/ml-kem/Cargo.toml index f1602d6d..e6fbcdfe 100644 --- a/libcrux-iot/ml-kem/Cargo.toml +++ b/libcrux-iot/ml-kem/Cargo.toml @@ -31,11 +31,18 @@ libcrux-iot-sha3 = { path = "../sha3" , features = [ ] } hax-lib.workspace = true libcrux-secrets.workspace = true +libcrux-traits = { version = "0.0.4", features = ["generic-tests"]} +tls_codec = { version = "0.4.2", features = [ + "derive", +], default-features = false, optional = true } [features] # By default all variants and std are enabled. default = ["mlkem512", "mlkem768", "mlkem1024"] +# Serialization & Deserialization using tls_codec +codec = ["dep:tls_codec"] + # Features for the different key sizes of ML-KEM mlkem512 = [] mlkem768 = [] @@ -48,9 +55,18 @@ kyber = [] rand = ["dep:rand"] # std support -std = [] +std = ["alloc", "rand/std", "tls_codec/std"] +alloc = [] + + +check-secret-independence = [ + "libcrux-secrets/check-secret-independence", + "libcrux-iot-sha3/check-secret-independence", + "libcrux-traits/check-secret-independence" + ] -check-secret-independence = ["libcrux-secrets/check-secret-independence", "libcrux-iot-sha3/check-secret-independence"] +# PQCP Common APIs +pqcp = [] [dev-dependencies] rand = { version = "0.9" } diff --git a/libcrux-iot/ml-kem/hax.py b/libcrux-iot/ml-kem/hax.py index 84f7f481..761dba21 100755 --- a/libcrux-iot/ml-kem/hax.py +++ b/libcrux-iot/ml-kem/hax.py @@ -61,10 +61,6 @@ def __call__(self, parser, args, values, option_string=None) -> None: cargo_hax_into = [ "cargo", "hax", - "-C", - "--features", - "std", - ";", "into", "-i", include_str, diff --git a/libcrux-iot/ml-kem/src/lib.rs b/libcrux-iot/ml-kem/src/lib.rs index 35561589..eba10299 100644 --- a/libcrux-iot/ml-kem/src/lib.rs +++ b/libcrux-iot/ml-kem/src/lib.rs @@ -72,7 +72,7 @@ // Enable doc cfg feature for doc builds. They use nightly. #![cfg_attr(doc_cfg, feature(doc_cfg))] -#[cfg(feature = "std")] +#[cfg(any(feature = "std", hax))] extern crate std; /// Feature gating helper macros @@ -108,6 +108,9 @@ mod utils; mod variant; mod vector; +#[cfg(feature = "pqcp")] +pub(crate) mod pqcp; + #[cfg(feature = "mlkem512")] #[cfg_attr(docsrs, doc(cfg(feature = "mlkem512")))] pub mod mlkem512; @@ -133,6 +136,7 @@ cfg_kyber! { pub mod kyber512 { //! Kyber 512 (NIST PQC Round 3) cfg_no_eurydice! { + pub use crate::mlkem512::kyber::Kyber512; pub use crate::mlkem512::kyber::generate_key_pair; pub use crate::mlkem512::kyber::decapsulate; pub use crate::mlkem512::kyber::encapsulate; @@ -146,6 +150,7 @@ cfg_kyber! { pub mod kyber768 { //! Kyber 768 (NIST PQC Round 3) cfg_no_eurydice! { + pub use crate::mlkem768::kyber::Kyber768; pub use crate::mlkem768::kyber::generate_key_pair; pub use crate::mlkem768::kyber::decapsulate; pub use crate::mlkem768::kyber::encapsulate; @@ -159,6 +164,7 @@ cfg_kyber! { pub mod kyber1024 { //! Kyber 1024 (NIST PQC Round 3) cfg_no_eurydice! { + pub use crate::mlkem1024::kyber::Kyber1024; pub use crate::mlkem1024::kyber::generate_key_pair; pub use crate::mlkem1024::kyber::decapsulate; pub use crate::mlkem1024::kyber::encapsulate; @@ -167,3 +173,69 @@ cfg_kyber! { } } } + +#[cfg(not(hax))] +macro_rules! impl_kem_trait { + ($variant:ty, $pk:ty, $sk:ty, $ct:ty) => { + impl + libcrux_traits::kem::arrayref::Kem< + CPA_PKE_PUBLIC_KEY_SIZE, + SECRET_KEY_SIZE, + CPA_PKE_CIPHERTEXT_SIZE, + SHARED_SECRET_SIZE, + KEY_GENERATION_SEED_SIZE, + SHARED_SECRET_SIZE, + > for $variant + { + fn keygen( + ek: &mut [u8; CPA_PKE_PUBLIC_KEY_SIZE], + dk: &mut [libcrux_secrets::U8; SECRET_KEY_SIZE], + rand: &[libcrux_secrets::U8; KEY_GENERATION_SEED_SIZE], + ) -> Result<(), libcrux_traits::kem::owned::KeyGenError> { + let key_pair = generate_key_pair(*rand); + ek.copy_from_slice(key_pair.pk()); + dk.copy_from_slice(key_pair.sk()); + + Ok(()) + } + + fn encaps( + ct: &mut [u8; CPA_PKE_CIPHERTEXT_SIZE], + ss: &mut [libcrux_secrets::U8; SHARED_SECRET_SIZE], + ek: &[u8; CPA_PKE_PUBLIC_KEY_SIZE], + rand: &[libcrux_secrets::U8; SHARED_SECRET_SIZE], + ) -> Result<(), libcrux_traits::kem::owned::EncapsError> { + let public_key: $pk = ek.into(); + + let (ct_, ss_) = encapsulate(&public_key, *rand); + ct.copy_from_slice(ct_.as_slice()); + ss.copy_from_slice(ss_.as_slice()); + + Ok(()) + } + + fn decaps( + ss: &mut [libcrux_secrets::U8; SHARED_SECRET_SIZE], + ct: &[u8; CPA_PKE_CIPHERTEXT_SIZE], + dk: &[libcrux_secrets::U8; SECRET_KEY_SIZE], + ) -> Result<(), libcrux_traits::kem::owned::DecapsError> { + let secret_key: $sk = dk.into(); + let ciphertext: $ct = ct.into(); + + let ss_ = decapsulate(&secret_key, &ciphertext); + + ss.copy_from_slice(ss_.as_slice()); + + Ok(()) + } + } + + libcrux_traits::kem::slice::impl_trait!($variant => + CPA_PKE_PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, + CPA_PKE_CIPHERTEXT_SIZE, SHARED_SECRET_SIZE, + KEY_GENERATION_SEED_SIZE, SHARED_SECRET_SIZE); + }; +} + +#[cfg(not(hax))] +use impl_kem_trait; diff --git a/libcrux-iot/ml-kem/src/mlkem1024.rs b/libcrux-iot/ml-kem/src/mlkem1024.rs index cfd1abfb..1928371c 100644 --- a/libcrux-iot/ml-kem/src/mlkem1024.rs +++ b/libcrux-iot/ml-kem/src/mlkem1024.rs @@ -29,6 +29,25 @@ const PRF_OUTPUT_SIZE2: usize = ETA2_RANDOMNESS_SIZE * RANK; const IMPLICIT_REJECTION_HASH_INPUT_SIZE: usize = SHARED_SECRET_SIZE + CPA_PKE_CIPHERTEXT_SIZE; +/// The ML-KEM 1024 algorithms +pub struct MlKem1024; + +#[cfg(not(any(hax, eurydice)))] +crate::impl_kem_trait!( + MlKem1024, + MlKem1024PublicKey, + MlKem1024PrivateKey, + MlKem1024Ciphertext +); + +// Provide the (packed) PQCP APIs +#[cfg(feature = "pqcp")] +crate::pqcp::pqcp_api!( + "use libcrux_iot_ml_kem::mlkem1024::pqcp::*;", + MlKem1024, + " 1024 " +); + /// An ML-KEM 1024 Ciphertext pub type MlKem1024Ciphertext = MlKemCiphertext; /// An ML-KEM 1024 Private key @@ -410,6 +429,16 @@ pub(crate) mod kyber { use super::*; + /// The Kyber 1024 algorithms + pub struct Kyber1024; + + crate::impl_kem_trait!( + Kyber1024, + MlKem1024PublicKey, + MlKem1024PrivateKey, + MlKem1024Ciphertext + ); + /// Generate Kyber 1024 Key Pair /// /// Generate an ML-KEM key pair. The input is a byte array of size diff --git a/libcrux-iot/ml-kem/src/mlkem512.rs b/libcrux-iot/ml-kem/src/mlkem512.rs index 742ac15e..53bffa84 100644 --- a/libcrux-iot/ml-kem/src/mlkem512.rs +++ b/libcrux-iot/ml-kem/src/mlkem512.rs @@ -31,6 +31,25 @@ const PRF_OUTPUT_SIZE2: usize = ETA2_RANDOMNESS_SIZE * RANK; const IMPLICIT_REJECTION_HASH_INPUT_SIZE: usize = SHARED_SECRET_SIZE + CPA_PKE_CIPHERTEXT_SIZE; +/// The ML-KEM 512 algorithms +pub struct MlKem512; + +#[cfg(not(any(hax, eurydice)))] +crate::impl_kem_trait!( + MlKem512, + MlKem512PublicKey, + MlKem512PrivateKey, + MlKem512Ciphertext +); + +// Provide the (packed) PQCP APIs +#[cfg(feature = "pqcp")] +crate::pqcp::pqcp_api!( + "use libcrux_iot_ml_kem::mlkem512::pqcp::*;", + MlKem512, + " 512 " +); + /// An ML-KEM 512 Ciphertext pub type MlKem512Ciphertext = MlKemCiphertext; /// An ML-KEM 512 Private key @@ -409,6 +428,16 @@ pub mod rand { pub(crate) mod kyber { use super::*; + /// The Kyber 512 algorithms + pub struct Kyber512; + + crate::impl_kem_trait!( + Kyber512, + MlKem512PublicKey, + MlKem512PrivateKey, + MlKem512Ciphertext + ); + /// Generate Kyber 512 Key Pair /// /// The input is a byte array of size diff --git a/libcrux-iot/ml-kem/src/mlkem768.rs b/libcrux-iot/ml-kem/src/mlkem768.rs index 1aae0e00..8bc80fec 100644 --- a/libcrux-iot/ml-kem/src/mlkem768.rs +++ b/libcrux-iot/ml-kem/src/mlkem768.rs @@ -38,6 +38,25 @@ const PRF_OUTPUT_SIZE2: usize = ETA2_RANDOMNESS_SIZE * RANK; const IMPLICIT_REJECTION_HASH_INPUT_SIZE: usize = SHARED_SECRET_SIZE + CPA_PKE_CIPHERTEXT_SIZE; +/// The ML-KEM 768 algorithms +pub struct MlKem768; + +#[cfg(not(any(hax, eurydice)))] +crate::impl_kem_trait!( + MlKem768, + MlKem768PublicKey, + MlKem768PrivateKey, + MlKem768Ciphertext +); + +// Provide the (packed) PQCP APIs +#[cfg(feature = "pqcp")] +crate::pqcp::pqcp_api!( + "use libcrux_iot_ml_kem::mlkem768::pqcp::*;", + MlKem768, + " 768 " +); + /// An ML-KEM 768 Ciphertext pub type MlKem768Ciphertext = MlKemCiphertext; /// An ML-KEM 768 Private key @@ -401,6 +420,16 @@ pub mod rand { pub(crate) mod kyber { use super::*; + /// The Kyber 768 algorithms + pub struct Kyber768; + + crate::impl_kem_trait!( + Kyber768, + MlKem768PublicKey, + MlKem768PrivateKey, + MlKem768Ciphertext + ); + /// Generate Kyber 768 Key Pair /// /// Generate a Kyber key pair. The input is a byte array of size diff --git a/libcrux-iot/ml-kem/src/pqcp.rs b/libcrux-iot/ml-kem/src/pqcp.rs new file mode 100644 index 00000000..6a5b359e --- /dev/null +++ b/libcrux-iot/ml-kem/src/pqcp.rs @@ -0,0 +1,165 @@ +#[derive(Debug)] +/// Errors generated by the PQCP API. +pub enum PQCPError { + /// An error during key generation. + KeyGeneration, + /// An error during encapsulation. + Encapsulation, + /// An error during decapsulation. + Decapsulation, + /// An error during public key deserialization. + InvalidPublicKey, + /// An error during private key deserialization. + InvalidPrivateKey, +} + +/// This macro implements the "packed" PQCP common API. +/// +/// - `$use_mod` must be the path to the module that will be generated by +/// this macro. This way, the doc test can `use` the required constants. +/// (Unfortunate to have to include this as a string, but doing it in +/// another way inserts an ugly linebreak in the doc output, +/// cf. https://stackoverflow.com/questions/60905060/prevent-line-break-in-doc-test) +/// - `$trait_implementer` must be a type implementing the +/// `libcrux_traits::kem::arrayref::Kem` trait. +/// - `$variant` must be a string containing the numeric parameter set +/// identifier with a space in front and one after, e.g. " 512 ". It is +/// used only for generating documentation. +macro_rules! pqcp_api { + ($use_mod:literal, $trait_implementer:ty, $variant:literal) => { + /// Common APIs shared between PQCP implementations. + /// + /// ``` + /// use rand::RngCore; + #[doc = $use_mod] + /// + /// // Key Generation + /// let mut pk = [0u8; PK_LEN]; + /// let mut sk = [0u8; SK_LEN]; + /// let mut coins = [0u8; KEYGEN_SEED_LEN]; + /// + /// let mut rng = rand::rng(); + /// rng.fill_bytes(&mut coins); + /// + /// crypto_kem_keypair_derand(&mut pk, &mut sk, coins).unwrap(); + /// + /// // Encapsulation + /// let mut encaps_coins = [0u8; ENCAPS_SEED_LEN]; + /// let mut ct = [0u8; CT_LEN]; + /// let mut ss_encaps = [0u8; SS_LEN]; + /// + /// rng.fill_bytes(&mut encaps_coins); + /// + /// crypto_kem_enc_derand(&mut ct, &mut ss_encaps, &pk, encaps_coins).unwrap(); + /// + /// // Decapsulation + /// let mut ss_decaps = [0u8; SS_LEN]; + /// + /// crypto_kem_dec(&mut ss_decaps, &ct, &sk).unwrap(); + /// + /// assert_eq!(ss_encaps, ss_decaps); + /// ``` + #[cfg(all(not(eurydice), feature = "pqcp"))] + pub mod pqcp { + use crate::pqcp::PQCPError; + use libcrux_secrets::{Classify, ClassifyRefMut}; + use libcrux_traits::kem::arrayref::Kem; + + #[cfg(feature = "rand")] + use ::rand::CryptoRng; + + use super::*; + + /// Length of the public key in bytes. + pub const PK_LEN: usize = CPA_PKE_PUBLIC_KEY_SIZE; + /// Length of the private key in bytes. + pub const SK_LEN: usize = SECRET_KEY_SIZE; + /// Length of the key generation randomness in bytes. + pub const KEYGEN_SEED_LEN: usize = KEY_GENERATION_SEED_SIZE; + /// Length of the key generation randomness in bytes. + pub const ENCAPS_SEED_LEN: usize = ENCAPS_SEED_SIZE; + /// Length of the ciphertext in bytes. + pub const CT_LEN: usize = CPA_PKE_CIPHERTEXT_SIZE; + /// Length of the shared secret in bytes. + pub const SS_LEN: usize = SHARED_SECRET_SIZE; + + // NIST APIs + + /// Generate ML-KEM + #[doc = $variant] + /// Key Pair (randomness generated externally) + pub fn crypto_kem_keypair_derand( + pk: &mut [u8; PK_LEN], + sk: &mut [u8; SK_LEN], + coins: [u8; KEYGEN_SEED_LEN], + ) -> Result<(), PQCPError> { + <$trait_implementer>::keygen(pk, sk.classify_ref_mut(), &coins.classify()) + .map_err(|_| PQCPError::KeyGeneration) + } + + #[doc = "Generate ML-KEM "] + #[doc = $variant] + #[doc = " Key Pair (randomness generated internally)"] + #[cfg(feature = "rand")] + pub fn crypto_kem_keypair( + pk: &mut [u8; PK_LEN], + sk: &mut [u8; SK_LEN], + rng: &mut impl CryptoRng, + ) -> Result<(), PQCPError> { + let mut rand = [0u8; KEYGEN_SEED_LEN]; + rng.fill_bytes(&mut rand); + <$trait_implementer>::keygen(pk, sk.classify_ref_mut(), &rand.classify()) + .map_err(|_| PQCPError::KeyGeneration) + } + + /// Encapsulate ML-KEM + #[doc = $variant] + /// shared secret (randomness generated externally) + /// + /// Does not perform public key validation. + pub fn crypto_kem_enc_derand( + ct: &mut [u8; CT_LEN], + ss: &mut [u8; SS_LEN], + pk: &[u8; PK_LEN], + coins: [u8; ENCAPS_SEED_LEN], + ) -> Result<(), PQCPError> { + <$trait_implementer>::encaps(ct, ss.classify_ref_mut(), pk, &coins.classify()) + .map_err(|_| PQCPError::Encapsulation) + } + + #[cfg(feature = "rand")] + /// Encapsulate ML-KEM + #[doc = $variant] + /// shared secret (randomness generated internally) + /// + /// Does not perform public key validation. + pub fn crypto_kem_enc( + ct: &mut [u8; CT_LEN], + ss: &mut [u8; SS_LEN], + pk: &[u8; PK_LEN], + rng: &mut impl CryptoRng, + ) -> Result<(), PQCPError> { + let mut rand = [0u8; ENCAPS_SEED_LEN]; + rng.fill_bytes(&mut rand); + <$trait_implementer>::encaps(ct, ss.classify_ref_mut(), pk, &rand.classify()) + .map_err(|_| PQCPError::Encapsulation) + } + + /// Decapsulate ML-KEM + #[doc = $variant] + /// shared secret + /// + /// Does not perform private key validation. + pub fn crypto_kem_dec( + ss: &mut [u8; SS_LEN], + ct: &[u8; CT_LEN], + sk: &[u8; SK_LEN], + ) -> Result<(), PQCPError> { + <$trait_implementer>::decaps(ss.classify_ref_mut(), ct, &sk.classify()) + .map_err(|_| PQCPError::Decapsulation) + } + } + }; +} + +pub(crate) use pqcp_api; diff --git a/libcrux-iot/ml-kem/src/types.rs b/libcrux-iot/ml-kem/src/types.rs index 1769bda5..babfd469 100644 --- a/libcrux-iot/ml-kem/src/types.rs +++ b/libcrux-iot/ml-kem/src/types.rs @@ -139,6 +139,122 @@ impl MlKemPrivateKey { impl_generic_struct!(MlKemCiphertext, "An ML-KEM Ciphertext"); impl_generic_struct!(MlKemPublicKey, "An ML-KEM Public key"); +#[cfg(all(feature = "codec", feature = "alloc"))] +mod codec { + use super::*; + + macro_rules! impl_tls_codec_for_generic_struct { + ($name:ident) => { + // XXX: `tls_codec::{Serialize, Deserialize}` are only + // available for feature `std`. For `no_std` scenarios, we + // need to implement `tls_codec::{SerializeBytes, + // DeserializeBytes}`, but `SerializeBytes` is not + // implemented for `VLByteSlice`. + impl tls_codec::DeserializeBytes for $name { + fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), tls_codec::Error> { + let (bytes, remainder) = tls_codec::VLBytes::tls_deserialize_bytes(bytes)?; + Ok(( + Self { + value: bytes + .as_ref() + .try_into() + .map_err(|_| tls_codec::Error::InvalidInput)?, + }, + remainder, + )) + } + } + + #[cfg(feature = "std")] + impl tls_codec::Serialize for $name { + fn tls_serialize( + &self, + writer: &mut W, + ) -> Result { + let out = tls_codec::VLByteSlice(self.as_ref()); + out.tls_serialize(writer) + } + } + + #[cfg(feature = "std")] + impl tls_codec::Serialize for &$name { + fn tls_serialize( + &self, + writer: &mut W, + ) -> Result { + (*self).tls_serialize(writer) + } + } + + #[cfg(feature = "std")] + impl tls_codec::Deserialize for $name { + fn tls_deserialize( + bytes: &mut R, + ) -> Result { + let bytes = tls_codec::VLBytes::tls_deserialize(bytes)?; + Ok(Self { + value: bytes + .as_ref() + .try_into() + .map_err(|_| tls_codec::Error::InvalidInput)?, + }) + } + } + + impl tls_codec::Size for $name { + fn tls_serialized_len(&self) -> usize { + tls_codec::VLByteSlice(self.as_ref()).tls_serialized_len() + } + } + + impl tls_codec::Size for &$name { + fn tls_serialized_len(&self) -> usize { + (*self).tls_serialized_len() + } + } + }; + } + + impl_tls_codec_for_generic_struct!(MlKemCiphertext); + impl_tls_codec_for_generic_struct!(MlKemPublicKey); + + #[cfg(test)] + mod test { + use tls_codec::{Deserialize, Serialize, Size}; + + use super::*; + + #[test] + #[cfg(feature = "std")] + fn ser_de() { + use tls_codec::DeserializeBytes; + + const SIZE: usize = 1568; + let test_struct = MlKemCiphertext::::default(); + + assert_eq!(test_struct.tls_serialized_len(), SIZE + 2); + let test_struct_serialized = test_struct.tls_serialize_detached().unwrap(); + assert_eq!( + test_struct_serialized.len(), + test_struct.tls_serialized_len() + ); + + let test_struct_deserialized = + MlKemCiphertext::::tls_deserialize_exact(&test_struct_serialized).unwrap(); + + let test_struct_deserialized_bytes = + MlKemCiphertext::::tls_deserialize_exact_bytes(&test_struct_serialized) + .unwrap(); + + assert_eq!(test_struct.as_ref(), test_struct_deserialized.as_ref()); + assert_eq!( + test_struct.as_ref(), + test_struct_deserialized_bytes.as_ref() + ) + } + } +} + /// An ML-KEM key pair pub struct MlKemKeyPair { pub(crate) sk: MlKemPrivateKey,