From 7053afe913dced1eec945ab48cf6c73d5606786e Mon Sep 17 00:00:00 2001 From: sra Date: Wed, 24 Jun 2026 14:00:05 +0200 Subject: [PATCH 1/5] Enable ML-KEM and ML-DSA for rust applications --- ledger_device_sdk/Cargo.toml | 4 + ledger_device_sdk/src/lib.rs | 4 + ledger_device_sdk/src/mldsa.rs | 402 +++++++++++++++++++++++++++++++ ledger_device_sdk/src/mlkem.rs | 246 +++++++++++++++++++ ledger_secure_sdk_sys/Cargo.toml | 4 + ledger_secure_sdk_sys/build.rs | 63 +++++ 6 files changed, 723 insertions(+) create mode 100644 ledger_device_sdk/src/mldsa.rs create mode 100644 ledger_device_sdk/src/mlkem.rs diff --git a/ledger_device_sdk/Cargo.toml b/ledger_device_sdk/Cargo.toml index 32250455..b5b62bb2 100644 --- a/ledger_device_sdk/Cargo.toml +++ b/ledger_device_sdk/Cargo.toml @@ -40,6 +40,10 @@ nano_nbgl = [ "ledger_secure_sdk_sys/nano_nbgl" ] debug_csdk = [ "ledger_secure_sdk_sys/debug_csdk" ] io_new = [] # switch to new 'io' module stack_usage = [] +mlkem = [ "ledger_secure_sdk_sys/mlkem" ] +mldsa = [ "ledger_secure_sdk_sys/mldsa" ] +mldsa_87 = [ "mldsa", "ledger_secure_sdk_sys/mldsa_87" ] +mldsa_optimization = [ "mldsa", "ledger_secure_sdk_sys/mldsa_optimization" ] # Build a network/app variant. When enabled, build.rs overlays the # [package.metadata.ledger.variants.x] table onto the base diff --git a/ledger_device_sdk/src/lib.rs b/ledger_device_sdk/src/lib.rs index 2fc6201e..f1d118b4 100644 --- a/ledger_device_sdk/src/lib.rs +++ b/ledger_device_sdk/src/lib.rs @@ -30,6 +30,10 @@ pub mod io { pub mod libcall; pub mod log; pub mod math; +#[cfg(feature = "mldsa")] +pub mod mldsa; +#[cfg(feature = "mlkem")] +pub mod mlkem; pub mod nvm; pub mod pki; pub mod random; diff --git a/ledger_device_sdk/src/mldsa.rs b/ledger_device_sdk/src/mldsa.rs new file mode 100644 index 00000000..52e48350 --- /dev/null +++ b/ledger_device_sdk/src/mldsa.rs @@ -0,0 +1,402 @@ +//! ML-DSA (Module-Lattice Digital Signature Algorithm) support (FIPS 204). +//! +//! Provides safe Rust wrappers around the ML-DSA C implementation from the +//! Ledger C SDK (`lib_cxng`). Supports ML-DSA-44 and ML-DSA-65 parameter sets, +//! plus ML-DSA-87 when the `mldsa_87` Cargo feature is enabled. +//! +//! The `mldsa_optimization` feature enables an alternative implementation that +//! trades RAM for speed. +//! +//! This module is only available when the `mldsa` Cargo feature is enabled. + +use ledger_secure_sdk_sys::*; + +/// ML-DSA parameter set selector. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MlDsaParam { + /// ML-DSA-44 (NIST security level 2). + MlDsa44, + /// ML-DSA-65 (NIST security level 3). + MlDsa65, + /// ML-DSA-87 (NIST security level 5). Requires the `mldsa_87` feature. + #[cfg(feature = "mldsa_87")] + MlDsa87, +} + +impl MlDsaParam { + const fn as_c(self) -> MLDSA_param_t { + match self { + MlDsaParam::MlDsa44 => MLDSA_44, + MlDsaParam::MlDsa65 => MLDSA_65, + #[cfg(feature = "mldsa_87")] + MlDsaParam::MlDsa87 => MLDSA_87, + } + } +} + +/// Pre-hash algorithm selector for HashML-DSA (FIPS 204, Section 5.4). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MlDsaPrehash { + Sha256, + Sha512, + Sha3_256, + Sha3_512, + Shake128, + Shake256, +} + +impl MlDsaPrehash { + const fn as_c(self) -> MLDSA_prehash_t { + match self { + MlDsaPrehash::Sha256 => MLDSA_PREHASH_SHA256, + MlDsaPrehash::Sha512 => MLDSA_PREHASH_SHA512, + MlDsaPrehash::Sha3_256 => MLDSA_PREHASH_SHA3_256, + MlDsaPrehash::Sha3_512 => MLDSA_PREHASH_SHA3_512, + MlDsaPrehash::Shake128 => MLDSA_PREHASH_SHAKE128, + MlDsaPrehash::Shake256 => MLDSA_PREHASH_SHAKE256, + } + } +} + +/// ML-DSA error type. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MlDsaError { + InvalidParameter, + InvalidParameterValue, + InternalError, +} + +impl From for MlDsaError { + fn from(code: u32) -> Self { + match code { + CX_INVALID_PARAMETER => MlDsaError::InvalidParameter, + CX_INVALID_PARAMETER_VALUE => MlDsaError::InvalidParameterValue, + _ => MlDsaError::InternalError, + } + } +} + +/// ML-DSA-44 public key size in bytes. +pub const MLDSA44_PK_LEN: usize = MLDSA44_PUBLICKEYBYTES as usize; +/// ML-DSA-44 secret key size in bytes. +pub const MLDSA44_SK_LEN: usize = MLDSA44_SECRETKEYBYTES as usize; +/// ML-DSA-44 signature size in bytes. +pub const MLDSA44_SIG_LEN: usize = MLDSA44_SIGBYTES as usize; + +/// ML-DSA-65 public key size in bytes. +pub const MLDSA65_PK_LEN: usize = MLDSA65_PUBLICKEYBYTES as usize; +/// ML-DSA-65 secret key size in bytes. +pub const MLDSA65_SK_LEN: usize = MLDSA65_SECRETKEYBYTES as usize; +/// ML-DSA-65 signature size in bytes. +pub const MLDSA65_SIG_LEN: usize = MLDSA65_SIGBYTES as usize; + +/// ML-DSA-87 public key size in bytes. +#[cfg(feature = "mldsa_87")] +pub const MLDSA87_PK_LEN: usize = MLDSA87_PUBLICKEYBYTES as usize; +/// ML-DSA-87 secret key size in bytes. +#[cfg(feature = "mldsa_87")] +pub const MLDSA87_SK_LEN: usize = MLDSA87_SECRETKEYBYTES as usize; +/// ML-DSA-87 signature size in bytes. +#[cfg(feature = "mldsa_87")] +pub const MLDSA87_SIG_LEN: usize = MLDSA87_SIGBYTES as usize; + +/// Maximum context string length as per FIPS 204. +pub const MAX_CTX_LEN: usize = 255; + +impl MlDsaParam { + /// Returns the public key size in bytes for this parameter set. + pub const fn pk_len(self) -> usize { + match self { + MlDsaParam::MlDsa44 => MLDSA44_PK_LEN, + MlDsaParam::MlDsa65 => MLDSA65_PK_LEN, + #[cfg(feature = "mldsa_87")] + MlDsaParam::MlDsa87 => MLDSA87_PK_LEN, + } + } + + /// Returns the secret key size in bytes for this parameter set. + pub const fn sk_len(self) -> usize { + match self { + MlDsaParam::MlDsa44 => MLDSA44_SK_LEN, + MlDsaParam::MlDsa65 => MLDSA65_SK_LEN, + #[cfg(feature = "mldsa_87")] + MlDsaParam::MlDsa87 => MLDSA87_SK_LEN, + } + } + + /// Returns the signature size in bytes for this parameter set. + pub const fn sig_len(self) -> usize { + match self { + MlDsaParam::MlDsa44 => MLDSA44_SIG_LEN, + MlDsaParam::MlDsa65 => MLDSA65_SIG_LEN, + #[cfg(feature = "mldsa_87")] + MlDsaParam::MlDsa87 => MLDSA87_SIG_LEN, + } + } +} + +/// Generates an ML-DSA key pair using internal randomness. +/// +/// # Arguments +/// * `pk` - Output buffer for the public key (size must match `param.pk_len()`). +/// * `sk` - Output buffer for the secret key (size must match `param.sk_len()`). +/// * `param` - The ML-DSA parameter set to use. +pub fn keygen(pk: &mut [u8], sk: &mut [u8], param: MlDsaParam) -> Result<(), MlDsaError> { + let err = unsafe { + MLDSA_keygen( + pk.as_mut_ptr(), + pk.len(), + sk.as_mut_ptr(), + sk.len(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(()) + } +} + +/// Signs a message using ML-DSA. +/// +/// # Arguments +/// * `sig` - Output buffer for the signature (size must match `param.sig_len()`). +/// * `msg` - The message to sign. +/// * `ctx` - Optional context string (at most [`MAX_CTX_LEN`] bytes, or empty slice). +/// * `sk` - The signer's secret key. +/// * `param` - The ML-DSA parameter set to use. +/// +/// # Returns +/// The actual signature length on success. +pub fn sign( + sig: &mut [u8], + msg: &[u8], + ctx: &[u8], + sk: &[u8], + param: MlDsaParam, +) -> Result { + if ctx.len() > MAX_CTX_LEN { + return Err(MlDsaError::InvalidParameterValue); + } + let mut sig_actual_len: usize = 0; + let ctx_ptr = if ctx.is_empty() { + core::ptr::null() + } else { + ctx.as_ptr() + }; + let err = unsafe { + MLDSA_sign( + sig.as_mut_ptr(), + sig.len(), + &mut sig_actual_len, + msg.as_ptr(), + msg.len(), + ctx_ptr, + ctx.len(), + sk.as_ptr(), + sk.len(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(sig_actual_len) + } +} + +/// Verifies an ML-DSA signature. +/// +/// # Arguments +/// * `sig` - The signature to verify. +/// * `msg` - The message that was signed. +/// * `ctx` - Optional context string (must match what was used during signing). +/// * `pk` - The signer's public key. +/// * `param` - The ML-DSA parameter set to use. +pub fn verify( + sig: &[u8], + msg: &[u8], + ctx: &[u8], + pk: &[u8], + param: MlDsaParam, +) -> Result<(), MlDsaError> { + if ctx.len() > MAX_CTX_LEN { + return Err(MlDsaError::InvalidParameterValue); + } + let ctx_ptr = if ctx.is_empty() { + core::ptr::null() + } else { + ctx.as_ptr() + }; + let err = unsafe { + MLDSA_verify( + sig.as_ptr(), + sig.len(), + msg.as_ptr(), + msg.len(), + ctx_ptr, + ctx.len(), + pk.as_ptr(), + pk.len(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(()) + } +} + +/// Signs a pre-hashed message using HashML-DSA (FIPS 204, Algorithm 4). +/// +/// The caller must hash the message with the chosen algorithm before calling +/// this function. +/// +/// # Arguments +/// * `sig` - Output buffer for the signature (size must match `param.sig_len()`). +/// * `ph` - The pre-hashed message digest. +/// * `ctx` - Optional context string (at most [`MAX_CTX_LEN`] bytes, or empty slice). +/// * `sk` - The signer's secret key. +/// * `prehash_alg` - The hash algorithm used to produce `ph`. +/// * `param` - The ML-DSA parameter set to use. +/// +/// # Returns +/// The actual signature length on success. +pub fn sign_prehash( + sig: &mut [u8], + ph: &[u8], + ctx: &[u8], + sk: &[u8], + prehash_alg: MlDsaPrehash, + param: MlDsaParam, +) -> Result { + if ctx.len() > MAX_CTX_LEN { + return Err(MlDsaError::InvalidParameterValue); + } + let mut sig_actual_len: usize = 0; + let ctx_ptr = if ctx.is_empty() { + core::ptr::null() + } else { + ctx.as_ptr() + }; + let err = unsafe { + MLDSA_sign_prehash( + sig.as_mut_ptr(), + sig.len(), + &mut sig_actual_len, + ph.as_ptr(), + ph.len(), + ctx_ptr, + ctx.len(), + sk.as_ptr(), + sk.len(), + prehash_alg.as_c(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(sig_actual_len) + } +} + +/// Verifies a HashML-DSA pre-hash signature (FIPS 204, Algorithm 5). +/// +/// The caller must hash the message with the chosen algorithm before calling +/// this function. +/// +/// # Arguments +/// * `sig` - The signature to verify. +/// * `ph` - The pre-hashed message digest. +/// * `ctx` - Optional context string (must match what was used during signing). +/// * `pk` - The signer's public key. +/// * `prehash_alg` - The hash algorithm used to produce `ph`. +/// * `param` - The ML-DSA parameter set to use. +pub fn verify_prehash( + sig: &[u8], + ph: &[u8], + ctx: &[u8], + pk: &[u8], + prehash_alg: MlDsaPrehash, + param: MlDsaParam, +) -> Result<(), MlDsaError> { + if ctx.len() > MAX_CTX_LEN { + return Err(MlDsaError::InvalidParameterValue); + } + let ctx_ptr = if ctx.is_empty() { + core::ptr::null() + } else { + ctx.as_ptr() + }; + let err = unsafe { + MLDSA_verify_prehash( + sig.as_ptr(), + sig.len(), + ph.as_ptr(), + ph.len(), + ctx_ptr, + ctx.len(), + pk.as_ptr(), + pk.len(), + prehash_alg.as_c(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_eq_err as assert_eq; + use crate::testing::TestType; + use testmacro::test_item as test; + + const TEST_MSG: &[u8] = b"Test message"; + + #[test] + fn test_mldsa44_sign_verify() { + let mut pk = [0u8; MLDSA44_PK_LEN]; + let mut sk = [0u8; MLDSA44_SK_LEN]; + keygen(&mut pk, &mut sk, MlDsaParam::MlDsa44).unwrap(); + + let mut sig = [0u8; MLDSA44_SIG_LEN]; + let sig_len = sign(&mut sig, TEST_MSG, &[], &sk, MlDsaParam::MlDsa44).unwrap(); + assert_eq!(sig_len, MLDSA44_SIG_LEN); + + verify(&sig[..sig_len], TEST_MSG, &[], &pk, MlDsaParam::MlDsa44).unwrap(); + } + + #[test] + fn test_mldsa65_sign_verify() { + let mut pk = [0u8; MLDSA65_PK_LEN]; + let mut sk = [0u8; MLDSA65_SK_LEN]; + keygen(&mut pk, &mut sk, MlDsaParam::MlDsa65).unwrap(); + + let mut sig = [0u8; MLDSA65_SIG_LEN]; + let sig_len = sign(&mut sig, TEST_MSG, &[], &sk, MlDsaParam::MlDsa65).unwrap(); + assert_eq!(sig_len, MLDSA65_SIG_LEN); + + verify(&sig[..sig_len], TEST_MSG, &[], &pk, MlDsaParam::MlDsa65).unwrap(); + } + + #[test] + fn test_mldsa44_sign_verify_with_context() { + let mut pk = [0u8; MLDSA44_PK_LEN]; + let mut sk = [0u8; MLDSA44_SK_LEN]; + keygen(&mut pk, &mut sk, MlDsaParam::MlDsa44).unwrap(); + + let ctx = b"test context"; + let mut sig = [0u8; MLDSA44_SIG_LEN]; + let sig_len = sign(&mut sig, TEST_MSG, ctx, &sk, MlDsaParam::MlDsa44).unwrap(); + + verify(&sig[..sig_len], TEST_MSG, ctx, &pk, MlDsaParam::MlDsa44).unwrap(); + } +} diff --git a/ledger_device_sdk/src/mlkem.rs b/ledger_device_sdk/src/mlkem.rs new file mode 100644 index 00000000..f777fef9 --- /dev/null +++ b/ledger_device_sdk/src/mlkem.rs @@ -0,0 +1,246 @@ +//! ML-KEM (Module-Lattice Key Encapsulation Mechanism) support (FIPS 203). +//! +//! Provides safe Rust wrappers around the ML-KEM C implementation from the +//! Ledger C SDK (`lib_cxng`). Supports ML-KEM-512, ML-KEM-768, and ML-KEM-1024 +//! parameter sets. +//! +//! This module is only available when the `mlkem` Cargo feature is enabled. + +use ledger_secure_sdk_sys::*; + +/// ML-KEM parameter set selector. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MlKemParam { + /// ML-KEM-512 (NIST security level 1). + MlKem512, + /// ML-KEM-768 (NIST security level 3). + MlKem768, + /// ML-KEM-1024 (NIST security level 5). + MlKem1024, +} + +impl MlKemParam { + const fn as_c(self) -> MLKEM_param_t { + match self { + MlKemParam::MlKem512 => MLKEM_512, + MlKemParam::MlKem768 => MLKEM_768, + MlKemParam::MlKem1024 => MLKEM_1024, + } + } +} + +/// ML-KEM error type. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MlKemError { + InvalidParameter, + InvalidParameterValue, + InternalError, +} + +impl From for MlKemError { + fn from(code: u32) -> Self { + match code { + CX_INVALID_PARAMETER => MlKemError::InvalidParameter, + CX_INVALID_PARAMETER_VALUE => MlKemError::InvalidParameterValue, + _ => MlKemError::InternalError, + } + } +} + +/// Size of the shared secret for all ML-KEM parameter sets (32 bytes). +pub const SHARED_SECRET_LEN: usize = MLKEM_SSBYTES as usize; + +/// ML-KEM-512 public key size in bytes. +pub const MLKEM512_PK_LEN: usize = MLKEM512_PUBLICKEYBYTES as usize; +/// ML-KEM-512 secret key size in bytes. +pub const MLKEM512_SK_LEN: usize = MLKEM512_SECRETKEYBYTES as usize; +/// ML-KEM-512 ciphertext size in bytes. +pub const MLKEM512_CT_LEN: usize = MLKEM512_CIPHERTEXTBYTES as usize; + +/// ML-KEM-768 public key size in bytes. +pub const MLKEM768_PK_LEN: usize = MLKEM768_PUBLICKEYBYTES as usize; +/// ML-KEM-768 secret key size in bytes. +pub const MLKEM768_SK_LEN: usize = MLKEM768_SECRETKEYBYTES as usize; +/// ML-KEM-768 ciphertext size in bytes. +pub const MLKEM768_CT_LEN: usize = MLKEM768_CIPHERTEXTBYTES as usize; + +/// ML-KEM-1024 public key size in bytes. +pub const MLKEM1024_PK_LEN: usize = MLKEM1024_PUBLICKEYBYTES as usize; +/// ML-KEM-1024 secret key size in bytes. +pub const MLKEM1024_SK_LEN: usize = MLKEM1024_SECRETKEYBYTES as usize; +/// ML-KEM-1024 ciphertext size in bytes. +pub const MLKEM1024_CT_LEN: usize = MLKEM1024_CIPHERTEXTBYTES as usize; + +impl MlKemParam { + /// Returns the public key size in bytes for this parameter set. + pub const fn pk_len(self) -> usize { + match self { + MlKemParam::MlKem512 => MLKEM512_PK_LEN, + MlKemParam::MlKem768 => MLKEM768_PK_LEN, + MlKemParam::MlKem1024 => MLKEM1024_PK_LEN, + } + } + + /// Returns the secret key size in bytes for this parameter set. + pub const fn sk_len(self) -> usize { + match self { + MlKemParam::MlKem512 => MLKEM512_SK_LEN, + MlKemParam::MlKem768 => MLKEM768_SK_LEN, + MlKemParam::MlKem1024 => MLKEM1024_SK_LEN, + } + } + + /// Returns the ciphertext size in bytes for this parameter set. + pub const fn ct_len(self) -> usize { + match self { + MlKemParam::MlKem512 => MLKEM512_CT_LEN, + MlKemParam::MlKem768 => MLKEM768_CT_LEN, + MlKemParam::MlKem1024 => MLKEM1024_CT_LEN, + } + } +} + +/// Generates an ML-KEM key pair using internal randomness. +/// +/// # Arguments +/// * `pk` - Output buffer for the public key (size must match `param.pk_len()`). +/// * `sk` - Output buffer for the secret key (size must match `param.sk_len()`). +/// * `param` - The ML-KEM parameter set to use. +pub fn keypair(pk: &mut [u8], sk: &mut [u8], param: MlKemParam) -> Result<(), MlKemError> { + let err = unsafe { + MLKEM_crypto_kem_keypair( + pk.as_mut_ptr(), + pk.len(), + sk.as_mut_ptr(), + sk.len(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(()) + } +} + +/// Performs ML-KEM encapsulation using internal randomness. +/// +/// Produces a ciphertext and a shared secret from a public key. +/// +/// # Arguments +/// * `ct` - Output buffer for the ciphertext (size must match `param.ct_len()`). +/// * `ss` - Output buffer for the shared secret ([`SHARED_SECRET_LEN`] bytes). +/// * `pk` - The recipient's public key. +/// * `param` - The ML-KEM parameter set to use. +pub fn encapsulate( + ct: &mut [u8], + ss: &mut [u8; SHARED_SECRET_LEN], + pk: &[u8], + param: MlKemParam, +) -> Result<(), MlKemError> { + let err = unsafe { + MLKEM_crypto_kem_enc( + ct.as_mut_ptr(), + ct.len(), + ss.as_mut_ptr(), + ss.len(), + pk.as_ptr(), + pk.len(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(()) + } +} + +/// Performs ML-KEM decapsulation. +/// +/// Recovers the shared secret from a ciphertext and a secret key. +/// +/// # Arguments +/// * `ss` - Output buffer for the shared secret ([`SHARED_SECRET_LEN`] bytes). +/// * `ct` - The ciphertext to decapsulate. +/// * `sk` - The recipient's secret key. +/// * `param` - The ML-KEM parameter set to use. +pub fn decapsulate( + ss: &mut [u8; SHARED_SECRET_LEN], + ct: &[u8], + sk: &[u8], + param: MlKemParam, +) -> Result<(), MlKemError> { + let err = unsafe { + MLKEM_crypto_kem_dec( + ss.as_mut_ptr(), + ss.len(), + ct.as_ptr(), + ct.len(), + sk.as_ptr(), + sk.len(), + param.as_c(), + ) + }; + if err != CX_OK { + Err(err.into()) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_eq_err as assert_eq; + use crate::testing::TestType; + use testmacro::test_item as test; + + #[test] + fn test_mlkem512_keygen_encaps_decaps() { + let mut pk = [0u8; MLKEM512_PK_LEN]; + let mut sk = [0u8; MLKEM512_SK_LEN]; + keypair(&mut pk, &mut sk, MlKemParam::MlKem512).unwrap(); + + let mut ct = [0u8; MLKEM512_CT_LEN]; + let mut ss_enc = [0u8; SHARED_SECRET_LEN]; + encapsulate(&mut ct, &mut ss_enc, &pk, MlKemParam::MlKem512).unwrap(); + + let mut ss_dec = [0u8; SHARED_SECRET_LEN]; + decapsulate(&mut ss_dec, &ct, &sk, MlKemParam::MlKem512).unwrap(); + + assert_eq!(&ss_enc, &ss_dec); + } + + #[test] + fn test_mlkem768_keygen_encaps_decaps() { + let mut pk = [0u8; MLKEM768_PK_LEN]; + let mut sk = [0u8; MLKEM768_SK_LEN]; + keypair(&mut pk, &mut sk, MlKemParam::MlKem768).unwrap(); + + let mut ct = [0u8; MLKEM768_CT_LEN]; + let mut ss_enc = [0u8; SHARED_SECRET_LEN]; + encapsulate(&mut ct, &mut ss_enc, &pk, MlKemParam::MlKem768).unwrap(); + + let mut ss_dec = [0u8; SHARED_SECRET_LEN]; + decapsulate(&mut ss_dec, &ct, &sk, MlKemParam::MlKem768).unwrap(); + + assert_eq!(&ss_enc, &ss_dec); + } + + #[test] + fn test_mlkem1024_keygen_encaps_decaps() { + let mut pk = [0u8; MLKEM1024_PK_LEN]; + let mut sk = [0u8; MLKEM1024_SK_LEN]; + keypair(&mut pk, &mut sk, MlKemParam::MlKem1024).unwrap(); + + let mut ct = [0u8; MLKEM1024_CT_LEN]; + let mut ss_enc = [0u8; SHARED_SECRET_LEN]; + encapsulate(&mut ct, &mut ss_enc, &pk, MlKemParam::MlKem1024).unwrap(); + + let mut ss_dec = [0u8; SHARED_SECRET_LEN]; + decapsulate(&mut ss_dec, &ct, &sk, MlKemParam::MlKem1024).unwrap(); + + assert_eq!(&ss_enc, &ss_dec); + } +} diff --git a/ledger_secure_sdk_sys/Cargo.toml b/ledger_secure_sdk_sys/Cargo.toml index 52cc1957..987f846e 100644 --- a/ledger_secure_sdk_sys/Cargo.toml +++ b/ledger_secure_sdk_sys/Cargo.toml @@ -20,6 +20,10 @@ critical-section = { version = "1.2.0", optional = true } heap = ["dep:embedded-alloc", "dep:critical-section"] nano_nbgl = [] debug_csdk = [] +mlkem = [] +mldsa = [] +mldsa_87 = ["mldsa"] +mldsa_optimization = ["mldsa"] [lints.rust.unexpected_cfgs] level = "warn" diff --git a/ledger_secure_sdk_sys/build.rs b/ledger_secure_sdk_sys/build.rs index 1b58c9a9..56dfaefb 100644 --- a/ledger_secure_sdk_sys/build.rs +++ b/ledger_secure_sdk_sys/build.rs @@ -422,6 +422,27 @@ impl SDKBuilder<'_> { } } + // Configure PQC algorithms (compiled app-side) + if env::var_os("CARGO_FEATURE_MLKEM").is_some() { + configure_lib_mlkem(&mut command, &self.device.c_sdk); + } + if env::var_os("CARGO_FEATURE_MLDSA").is_some() { + configure_lib_mldsa(&mut command, &self.device.c_sdk); + if env::var_os("CARGO_FEATURE_MLDSA_87").is_some() { + command.define("HAVE_MLDSA_87", None); + } + if env::var_os("CARGO_FEATURE_MLDSA_OPTIMIZATION").is_some() { + command.define("HAVE_MLDSA_OPTIMIZATION", None); + } + } + if env::var_os("CARGO_FEATURE_MLKEM").is_some() + || env::var_os("CARGO_FEATURE_MLDSA").is_some() + { + command + .file(self.device.c_sdk.join("src/cx_hash_iovec.c")) + .include(&self.device.c_sdk); + } + // Add the defines found in the Makefile.conf.cx to our build command. for define in self.cxdefines.iter() { command.define(define, None); @@ -535,6 +556,15 @@ impl SDKBuilder<'_> { bindings = bindings.clang_arg(format!("-D{define}")); } + // ML-DSA feature-gated defines for bindgen + if env::var_os("CARGO_FEATURE_MLDSA_87").is_some() { + bindings = bindings.clang_arg("-DHAVE_MLDSA_87"); + } + + if env::var_os("CARGO_FEATURE_MLDSA_OPTIMIZATION").is_some() { + bindings = bindings.clang_arg("-DHAVE_MLDSA_OPTIMIZATION"); + } + let bindings = bindings .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate() @@ -983,3 +1013,36 @@ fn header2define(headername: &str) -> Vec<(String, Option)> { }) .collect() } + +fn configure_lib_mlkem(command: &mut cc::Build, c_sdk: &Path) { + let src = c_sdk.join("lib_cxng/src"); + command + .file(src.join("cx_mlkem.c")) + .file(src.join("cx_mlkem_internal.c")) + .file(src.join("cx_mlkem_indcpa.c")) + .file(src.join("cx_mlkem_poly.c")) + .file(src.join("cx_mlkem_polymat.c")) + .file(src.join("cx_mlkem_polyvec.c")) + .file(src.join("cx_mlkem_sample.c")) + .file(src.join("cx_mlkem_util.c")) + .file(src.join("cx_mlkem_params.c")) + .include(&src); +} + +fn configure_lib_mldsa(command: &mut cc::Build, c_sdk: &Path) { + let src = c_sdk.join("lib_cxng/src"); + command + .file(src.join("cx_mldsa.c")) + .file(src.join("cx_mldsa_internal.c")) + .file(src.join("cx_mldsa_lowram.c")) + .file(src.join("cx_mldsa_packing.c")) + .file(src.join("cx_mldsa_poly.c")) + .file(src.join("cx_mldsa_polymat.c")) + .file(src.join("cx_mldsa_polyvec.c")) + .file(src.join("cx_mldsa_rounding.c")) + .file(src.join("cx_mldsa_sample.c")) + .file(src.join("cx_mldsa_smallpoly.c")) + .file(src.join("cx_mldsa_util.c")) + .file(src.join("cx_mldsa_params.c")) + .include(&src); +} From 13527edd2591c0c9e4de7dcb4035ae7bd02bc83d Mon Sep 17 00:00:00 2001 From: sra Date: Mon, 29 Jun 2026 18:36:59 +0200 Subject: [PATCH 2/5] Enable ML-KEM and ML-DSA unitary tests in the CI --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92ef195f..e167b7e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,27 @@ jobs: run: | cargo test --target ${{ matrix.target }} --features unit_test --tests + test-pqc: + name: Run ML-KEM and ML-DSA unit tests + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest + strategy: + matrix: + target: ["nanox", "nanosplus", "stax", "flex", "apex_p"] + steps: + - name: Clone + uses: actions/checkout@v6 + - name: Setup C SDK (API_LEVEL_26) + run: | + git clone https://github.com/LedgerHQ/ledger-secure-sdk --branch API_LEVEL_26 --single-branch /tmp/c_sdk + - name: ML-KEM and ML-DSA unit tests + working-directory: ledger_device_sdk + env: + LEDGER_SDK_PATH: /tmp/c_sdk + run: | + cargo test --target ${{ matrix.target }} --features unit_test,mlkem,mldsa --tests + build-apps: name: Build all Rust apps if: github.event_name != 'workflow_dispatch' From d8d0b629496543b13fe219f0d0618ee5bb987524 Mon Sep 17 00:00:00 2001 From: sra Date: Fri, 3 Jul 2026 21:44:32 +0200 Subject: [PATCH 3/5] Reduce available heap for Nano X to leave stack for ML-KEM and ML-DSA --- .github/workflows/ci.yml | 2 ++ ledger_device_sdk/src/mldsa.rs | 10 ++++++++++ ledger_device_sdk/src/mlkem.rs | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e167b7e7..e0a7b1ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,8 @@ jobs: working-directory: ledger_device_sdk env: LEDGER_SDK_PATH: /tmp/c_sdk + # Shrink the heap for Nano X only, other targets keep the default. + HEAP_SIZE: "nanox: 2048" run: | cargo test --target ${{ matrix.target }} --features unit_test,mlkem,mldsa --tests diff --git a/ledger_device_sdk/src/mldsa.rs b/ledger_device_sdk/src/mldsa.rs index 52e48350..dfe248ce 100644 --- a/ledger_device_sdk/src/mldsa.rs +++ b/ledger_device_sdk/src/mldsa.rs @@ -7,6 +7,16 @@ //! The `mldsa_optimization` feature enables an alternative implementation that //! trades RAM for speed. //! +//! # Memory considerations +//! +//! The underlying C routines use large stack-allocated workspaces. On the +//! memory-constrained Nano X (28 KB of SRAM), the default 8 KB heap leaves too +//! little stack: a key-generation, signing, or verification call can overflow +//! the stack into the heap and corrupt it. Apps enabling `mldsa` on Nano X must +//! therefore budget a smaller heap, for example by setting +//! `HEAP_SIZE="nanox: 2048"` (the per-target syntax only affects Nano X and +//! leaves the default heap on the other devices). +//! //! This module is only available when the `mldsa` Cargo feature is enabled. use ledger_secure_sdk_sys::*; diff --git a/ledger_device_sdk/src/mlkem.rs b/ledger_device_sdk/src/mlkem.rs index f777fef9..235b67a4 100644 --- a/ledger_device_sdk/src/mlkem.rs +++ b/ledger_device_sdk/src/mlkem.rs @@ -4,6 +4,16 @@ //! Ledger C SDK (`lib_cxng`). Supports ML-KEM-512, ML-KEM-768, and ML-KEM-1024 //! parameter sets. //! +//! # Memory considerations +//! +//! The underlying C routines use large stack-allocated workspaces. On the +//! memory-constrained Nano X (28 KB of SRAM), the default 8 KB heap leaves too +//! little stack: a key-generation, encapsulation, or decapsulation call can +//! overflow the stack into the heap and corrupt it. Apps enabling `mlkem` on +//! Nano X must therefore budget a smaller heap, for example by setting +//! `HEAP_SIZE="nanox: 2048"` (the per-target syntax only affects Nano X and +//! leaves the default heap on the other devices). +//! //! This module is only available when the `mlkem` Cargo feature is enabled. use ledger_secure_sdk_sys::*; From 875b5d5c8991eec7baa27f52430fb47c0a0fd949 Mon Sep 17 00:00:00 2001 From: Mathias BROUSSET Date: Fri, 17 Jul 2026 17:25:16 +0200 Subject: [PATCH 4/5] add missing symbol for pic relocation --- ledger_secure_sdk_sys/link.ld | 1 + 1 file changed, 1 insertion(+) diff --git a/ledger_secure_sdk_sys/link.ld b/ledger_secure_sdk_sys/link.ld index 2a213252..7c27de08 100644 --- a/ledger_secure_sdk_sys/link.ld +++ b/ledger_secure_sdk_sys/link.ld @@ -105,6 +105,7 @@ SECTIONS .bss : { + _ram = .; _bss = .; *(.bss*) _ebss = .; From fdff75b4b6564c6b66bb83ef9dd0c71110ebecd2 Mon Sep 17 00:00:00 2001 From: Mathias BROUSSET Date: Mon, 3 Aug 2026 10:18:08 +0200 Subject: [PATCH 5/5] bump SDK version --- ledger_secure_sdk_sys/CHANGELOG.md | 11 ++++++++--- ledger_secure_sdk_sys/Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ledger_secure_sdk_sys/CHANGELOG.md b/ledger_secure_sdk_sys/CHANGELOG.md index 5b852cc5..6bbad0e5 100644 --- a/ledger_secure_sdk_sys/CHANGELOG.md +++ b/ledger_secure_sdk_sys/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.16.3] - 2026-08-03 + +### Added +- added `_ram` symbol in link.ld following update of BSS wipe function in C SDK + ## [1.16.2] - 2026-06-04 ### Changed @@ -32,14 +37,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Make the link_pass_nvram resilient to the nvram being wiped - C SDK: Remove usage of checks.c and checks.h - - + - ## [1.14.0] - 2026-01-15 ### Changed - Updated linker script link.ld (add install_parameters and app_flags sections) - Fix cargo-audit - - + - ## [1.13.0] - 2026-01-05 @@ -55,7 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.12.0] - 2025-11-19 ### Changed - - Can be accessed by activating the sys feature from ledger_device_sdk package. + - Can be accessed by activating the sys feature from ledger_device_sdk package. ## [1.11.6] - 2025-11-04 diff --git a/ledger_secure_sdk_sys/Cargo.toml b/ledger_secure_sdk_sys/Cargo.toml index 987f846e..b21dbae0 100644 --- a/ledger_secure_sdk_sys/Cargo.toml +++ b/ledger_secure_sdk_sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ledger_secure_sdk_sys" -version = "1.16.2" +version = "1.16.3" authors = ["Ledger"] edition = "2024" license.workspace = true