Embedded Software | 07/06/2026
Post-Quantum Cryptography In The Ledger SDK
Learn how Ledger has built two new security tools into its SDK replacing older security methods with ones that quantum computers can't crack.
Before You Dive In
- The threat is real, but not here yet: A powerful enough quantum computer could break the math that protects most digital security today, including crypto wallets. It doesn’t exist yet, but the industry is preparing now so defenses are ready when it does.
- Ledger has added two new algorithms in its SDK: These are called ML-KEM and ML-DSA. They replace older security methods with ones that even quantum computers can’t crack.
- ML-KEM is for sharing secrets securely: Think of it like a lockbox: the recipient publishes an open lock (public key), the sender puts a secret inside and snaps it shut (encapsulation), and only the recipient can open it (decapsulation). Nobody else, including a quantum computer, can read what’s inside.
- ML-DSA is for proving something is genuine: It works like a digital signature on a document: it proves a message came from who it says it came from, and hasn’t been tampered with. It’s the quantum-safe replacement for the signature method most blockchains use today.
- The tradeoff is size, not security: These new methods are secure against future threats, but their keys and signatures are much larger than the old ones, sometimes 30–50x bigger. That’s a known cost of quantum resistance.
- Ledger’s devices are small and have limited memory: So the implementation required clever engineering. Rather than loading everything into memory at once, the code rebuilds what it needs piece by piece, staying within tight hardware limits without cutting any security corners.
- One important caveat the document flags openly: the current version doesn’t yet include protection against physical attacks (like someone probing the chip while it runs). That’s noted as future work.
- The output is bit-for-bit identical to the global NIST standard: This means Ledger’s implementation passes the same test vectors as the reference, just using less RAM
ML-KEM & ML-DSA | FIPS 203 / FIPS 204
Implementation Guide | Ledger Embedded OS
June 2026
The Quantum Threat: Why It Matters
Today’s public-key cryptography relies on mathematical problems that are computationally infeasible for classical computers to solve: factoring large integers (RSA), or computing discrete logarithms on elliptic curves (ECDSA, ECDH). A sufficiently powerful quantum computer running Shor’s algorithm would solve these problems in polynomial time, breaking the security guarantees that protect wallets, transactions, and key derivation schemes.
In 2016 NIST ran a multi-year competition to standardize quantum-resistant algorithms, culminating in the publication of FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) in August 2024. Ledger’s Embedded OS team has implemented both standards in the SDK.
| What changes for applications? Both ML-KEM and ML-DSA expose a familiar API structure, but the underlying concepts differ from their classical counterparts in important ways. ML-KEM is the more conceptually different one. Key generation looks similar to ECDH, you produce a public/private key pair, but the exchange mechanism is fundamentally different. In ECDH, both parties contribute a key pair and compute a shared secret symmetrically. In ML-KEM, only the recipient generates a key pair; the sender runs encapsulation, which takes the recipient’s public key and produces both a ciphertext and a shared secret in one shot. The recipient then runs decapsulation on that ciphertext to recover the same shared secret. There is no back-and-forth scalar multiplication: the sender and recipient play asymmetric roles. ML-DSA maps more naturally onto ECDSA: key generation produces a signing key and a verification key, sign takes a message and secret key and produces a signature, and verify takes the signature, message, and public key and returns a pass/fail. The call signatures will feel familiar. What changes is everything underneath: the parameters, the hash functions involved and the mathematical structure. |
What does ML-KEM replace?
ML-KEM (Module-Lattice Key Encapsulation Mechanism) replaces key-exchange protocols like ECDH. Its goal is to let two parties derive a shared secret over an untrusted channel. It does not directly encrypt data: it produces a symmetric key, which you then use with AES for instance.
What does ML-DSA replace?
ML-DSA (Module-Lattice Digital Signature Algorithm) replaces ECDSA. It produces a signature over a message that any holder of the corresponding public key can verify, without revealing the secret key.
From ECC to Lattices: A Gentle Comparison
Elliptic curve cryptography is built on a one-way function: scalar multiplication on a curve is fast to compute forward but infeasible to reverse. On a quantum computer, that reversal is easy via Shor’s algorithm.
Lattice-based cryptography uses a completely different hard problem: adding small noise to a structured system of linear equations over integer rings makes the system hard to solve, even for quantum computers. This is the Learning With Errors (LWE) problem.

Key Size Comparison
| ALGORITHM | SECURITY LEVEL | PUBLIC KEY | SECRET KEY | CIPHERTEXT / SIG | QUANTUM SAFE |
|---|---|---|---|---|---|
| P-256 / ECDH | 128-bit | 64 B | 32 B | 64 B | No |
| P-256 / ECDSA | 128-bit | 64 B | 32 B | 64 B | No |
| ML-KEM-512 | L1 (~128) | 800 B | 1 632 B | 768 B (ct) | Yes |
| ML-KEM-768 | L3 (~192) | 1 184 B | 2 400 B | 1 088 B (ct) | Yes |
| ML-KEM-1024 | L5 (~256) | 1 568 B | 3 168 B | 1 568 B (ct) | Yes |
| ML-DSA-44 | L2 | 1 312 B | 2 560 B | 2 420 B (sig) | Yes |
| ML-DSA-65 | L3 | 1 952 B | 4 032 B | 3 309 B (sig) | Yes |
| ML-DSA-87 | L5 | 2 592 B | 4 896 B | 4 627 B (sig) | Yes |
ML-KEM Algorithm Construction (FIPS 203)
ML-KEM is built from two layers. The inner layer is an IND-CPA PKE scheme (K-PKE) constructed over Module-LWE. The outer layer applies a Fujisaki-Okamoto (FO) transform to lift it to IND-CCA2 security, which is the standard required for a KEM.
The IND-CPA Core: K-PKE
K-PKE works in the polynomial ring Zq[X]/(X^n+1) with n=256, q=3329. The module dimension k varies by parameter set. Key generation expands a 32-byte seed via SHA3-512 into a public seed ρ and a noise seed σ, then samples the matrix A (via SHAKE-128) and secret/error vectors s and e following the centered binomial distribution (CBD) with SHAKE-256.

Building Blocks
| BUILDING BLOCK | ROLE IN ML-KEM | PRIMITIVE |
|---|---|---|
| NTT | Fast polynomial multiplication in Zq[X]/(X^n+1) | In-place Cooley-Tukey, zeta table |
| Montgomery / Barrett | Modular reduction during NTT and base multiplication | Custom 16-bit arithmetic |
| CBD (eta=2,3) | Sample small-norm secret and error polynomials | SHAKE-256 PRF |
| Rejection sampling | Sample uniform polynomials for matrix A | SHAKE-128 XOF |
| Compression/Decompression | Quantize polynomial coefficients for compact ciphertext | Bit-packing, du/dv parameters |
| H, G, J | Hash functions for FO transform, key derivation | SHA3-256, SHA3-512, SHAKE-256 |
| FO Transform | Lift IND-CPA to IND-CCA2 | Re-encryption + CT comparison |
The FO Transform & IND-CCA2 Decapsulation
The outer KEM layer performs decapsulation in a constant-time sequence: it re-encrypts the decrypted plaintext and performs a constant-time comparison against the received ciphertext. On mismatch, it outputs a pseudorandom value from an implicit-reject seed rather than an error, to prevent oracle attacks.

Parameter Sets
| ML-KEM-512k: 2η₁: 3η₂: 2du/dv: 10/4pk: 800 Bct: 768 Blevel: NIST 1 | ML-KEM-768k: 3η₁: 2η₂: 2du/dv: 10/4pk: 1 184 Bct: 1 088 Blevel: NIST 3 | ML-KEM-1024k: 4η₁: 2η₂: 2du/dv: 11/5pk: 1 568 Bct: 1 568 Blevel: NIST 5 |
ML-DSA Algorithm Construction (FIPS 204)
ML-DSA is a Fiat-Shamir with aborts scheme over Module-LWE and Module-SIS. Its signature security rests on the hardness of finding short vectors in a lattice: there is no known quantum speedup for this problem.
Signing At A High Level

Building Blocks
| BUILDING BLOCK | ROLE IN ML-DSA | PRIMITIVE |
|---|---|---|
| NTT (32-bit) | Fast polynomial multiplication over Zq, q=8380417 | In-place Cooley-Tukey, zeta table |
| Montgomery (64-bit) | Reduce int64 intermediate products mod q | MLDSA_POLY_montgomery_reduce |
| Power2Round | Split t into (t1, t0) at bit D=13 for pk compression | Arithmetic shift + masking |
| Decompose / HighBits / LowBits | Rounding for hint computation | Modulo gamma2 arithmetic |
| MakeHint / UseHint | Carry hints for verifier reconstruction of w1 | Bit vector, omega-bounded |
| SampleInBall | Generate sparse +-1 challenge polynomial | SHAKE-256 + rejection |
| ExpandMask | Sample masking vector y from seed | SHAKE-256, gamma1-range |
| ExpandA | Expand matrix A from rho | SHAKE-128 uniform rejection |
Parameter Sets
| ML-DSA-44k/l: 4/4η: 2τ/β: 39/78γ₁: 2^17pk/sk: 1312/2560 Bsig: 2420 B | ML-DSA-65k/l: 6/5η: 4τ/β: 49/196γ₁: 2^19pk/sk: 1952/4032 Bsig: 3309 B | ML-DSA-87 (opt-in)k/l: 8/7η: 2τ/β: 60/120γ₁: 2^19pk/sk: 2592/4896 Bsig: 4627 B |
| ML-DSA-87 is not enabled by default. It requires HAVE_MLDSA_87 but without the low-RAM optimization, key generation and signing with ML-DSA-87 exceeds the stack budget on LNX devices. The flag HAVE_MLDSA_OPTIMIZATION must be enabled to use ML-DSA-87. |
Implementation Deep-dive
Ledger devices use resource-constrained secure elements. Both algorithms were implemented with a primary goal: keep peak stack usage within at most 16 kB (see Application available memory) without sacrificing correctness or security. Without optimizations, ML-DSA signing would exceed this budget on larger parameter sets. This section walks through the key implementation choices and how they compare to the NIST reference implementations.
NTT Implementation
Both algorithms use a from-scratch in-place Cooley-Tukey NTT over their respective polynomial rings, sharing the same loop structure but with different field parameters.
| PARAMETER | ML-KEM | ML-DSA |
|---|---|---|
| Modulus q | 3 329 | 8 380 417 |
| Coefficient type | int16_t | int32_t |
| Montgomery operand | int32_t → int16_t | int64_t → int32_t |
| Montgomery inverse QINV | 62209 (q^-1 mod 2^16) | q^-1 mod 2^32 |
| Inv-NTT scale value | 1441 = 128^-1 * 2^16 mod q | 41978 = mont^2 / 256 |
| Inv-NTT scale placement | Pre-scale pass (before butterflies) | Post-scale pass (after butterflies) |
| Barrett constants | V=20159, shift=26 | reduce32 via arithmetic shift |
Inv-NTT deviation vs. NIST Reference (ML-KEM)
The Ledger and NIST implementations of the inverse NTT differ in where the 1/128 scale factor is applied. The NIST reference folds it into a post-scaling pass after all butterfly layers. The Ledger implementation applies it as a pre-scaling pass before the butterfly loop. Because NTT is a linear transform, f · INTT(x) = INTT(f · x), so both orderings are mathematically equivalent and produce bit-identical output.
The pre-scaling choice simplifies the inner loop: the scale multiplication is isolated in its own pass and the butterfly loop only handles additions and zeta multiplications, without needing to special-case the final layer. Barrett reduction is applied on the addition arm of each butterfly to prevent coefficient overflow. ML-DSA’s inverse NTT follows the NIST Dilithium reference more closely, applying the scale factor f = 41978 as a final post-pass.
Hash Functions & XOF
Rather than bundling an independent Keccak implementation, both algorithms reuse the existing Ledger SDK SHAKE and SHA3 primitives via cx_sha3_256_hash, cx_sha3_512_hash, cx_shake128_hash, and cx_shake256_hash. For multi-part hashing operations (e.g., H(tr || M′)), the streaming API is used to avoid allocating a large concatenated buffer.
Stack Optimizations vs. the NIST Reference
ML-KEM: Row-By-Row Matrix Expansion
The NIST reference generates the entire k×k matrix A before computing the product A·s. The Ledger implementation never materializes A: each row is generated on-the-fly, the inner product for that row is accumulated, and the row is immediately discarded. Only one polyvec (one row) lives on the stack at a time.
// Optimization: Generate matrix A row-by-row
// to avoid storing full k×k polymat in RAM</span>
polyvec a_row = {0}; // ONE row at a time
for (uint32_t i = 0U; i < p->k; i++) {
MLKEM_POLYMAT_gen_matrix_row(&a_row, publicseed, 0, (uint8_t)i, p->k);
MLKEM_POLYVEC_basemul_acc_montgomery(&pkpv.vec[i], &a_row, &skpv, p->k);
}SDK file: cx_mlkem_indcpa.c
ML-KEM decapsulation: Stack Union
Decapsulation requires both a re-encryption buffer ct2 and a temporary buffer tmp, but these are needed in sequential phases. A union overlaps them to avoid double-counting their sizes on the stack:
// ct2 is used first (re-encryption),
// then tmp overwrites it (implicit-reject derivation).
union {
uint8_t ct2[MLKEM1024_CIPHERTEXTBYTES];
uint8_t tmp[MLKEM_SYMBYTES + MLKEM1024_CIPHERTEXTBYTES];
} u;SDK file: cx_mlkem.c
ML-DSA: the HAVE_MLDSA_OPTIMIZATION Flag
ML-DSA signing is significantly more memory-intensive than ML-KEM. When HAVE_MLDSA_OPTIMIZATION is enabled, a dedicated low-RAM layer (cx_mldsa_lowram.c) replaces the reference approach. The optimization techniques are taken from ZKnox.

ML-DSA KeyGen: On-the-Fly s1, s2, A
In the NIST reference, keygen holds full secret vectors s1, s2, and matrix A simultaneously. The Ledger implementation recomputes each polynomial on-the-fly and packs it directly into the output buffers, holding only 3 scratch polynomials at any time:
// Only 3 scratch polys held at once.
// s1[j] is re-expanded each time it's needed,
// then packed once (first row only).
for (uint8_t i = 0; i < p->k; i++) {
for (uint8_t j = 0; j < p->l; j++) {
MLDSA_SAMPLE_eta(&tC, rhoprime, j, p->eta); // expand s1[j]
if (i == 0) MLDSA_PACK_polyeta(&sk[...], &tC, p->eta); // pack once
MLDSA_POLY_ntt(&tC);
MLDSA_SAMPLE_uniform(&tB, rho, nonce); // expand A[i][j]
MLDSA_POLY_pointwise_montgomery(&tA, &tB, &tC, j == 0);
}
// tA holds row i of A*s1 — invert NTT, add s2, split t
}SDK file: cx_mldsa_internal.c
Application Available Memory
The application available SRAM memory includes the bss and the stack.
| PRODUCT | SRAM |
|---|---|
| Ledger Nano X | 28kB |
| Ledger Nano S Plus | 32kB |
| Flex | 36kB |
| Stax | 36kB |
| Ledger Nano Gen5 | 40kB |
Security Considerations
| CONCERN | ML-KEM | ML-DSA |
|---|---|---|
| Constant-time CT compare | cx_constant_time_eq + MLKEM_UTIL_ct_cmov | N/A (sign/verify) |
| Implicit reject oracle | Always return CX_OK; output is J(z,ct) | — |
| Sensitive zeroization | explicit_bzero on sensitive variables | explicit_bzero on sensitive variables |
| Enc key modulus check | MLKEM_UTIL_check_ek: all pk coefficients verified < q (FIPS 203 §7.2) | — |
| Abort bound | — | Up to 814 attempts (p < 2^-256 of failure) |
Side-channel Security: Current State & Roadmap
| ⚠ NO SIDE-CHANNEL COUNTERMEASURES IN V1 The current implementation provides only algorithmic security: constant-time comparison paths and systematic zeroization of sensitive intermediates. It does NOT include hardware countermeasures against fault injection and side-channel attacks. such as arithmetic masking or execution shuffling in addition to random delay. A future v2 will add shuffling (random permutation of polynomial operations) and arithmetic masking (splitting secret coefficients into randomized shares) as countermeasures. |
| DEVIATION FROM NIST REFERENCE The NIST FIPS 203/204 reference implementations keep full polynomial vectors and matrices in memory simultaneously. The Ledger SDK trades compute (re-expanding polynomials on each pass) for stack. This is the correct trade-off for a device with a limited stack budget. The output is bit-for-bit identical to the NIST test vectors. |
SDK API usage
ML-KEM
Include lcx_mlkem.h. Buffer sizes are available as macros or via MLKEM_PARAM[param] at runtime.
Key Generation
#include "lcx_mlkem.h"
uint8_t pk[MLKEM768_PUBLICKEYBYTES];
uint8_t sk[MLKEM768_SECRETKEYBYTES];
cx_err_t err = MLKEM_crypto_kem_keypair(
pk, sizeof(pk),
sk, sizeof(sk),
MLKEM_768
);
Encapsulation (Sender)
uint8_t ct[MLKEM768_CIPHERTEXTBYTES];
uint8_t ss[MLKEM_SSBYTES]; // 32-byte shared secret
cx_err_t err = MLKEM_crypto_kem_enc(
ct, sizeof(ct),
ss, sizeof(ss),
pk, sizeof(pk),
MLKEM_768
);
// ss is now a 32-byte shared secret; use with AES
Decapsulation (Recipient)
uint8_t ss_recv[MLKEM_SSBYTES];
cx_err_t err = MLKEM_crypto_kem_dec(
ss_recv, sizeof(ss_recv),
ct, ct_len,
sk, sizeof(sk),
MLKEM_768
);
// Always returns CX_OK — compare ss_recv with sender's ss
// at the application layer. A mismatch means the ciphertext
// was tampered with; ss_recv will be pseudorandom.| ⚠ DECAPSULATION ALWAYS RETURNS CX_OKPer the FO transform, MLKEM_crypto_kem_dec returns CX_OK even when ciphertext authentication fails. The shared secret in ss will be a pseudorandom value derived from the implicit-reject seed. Always verify the shared secret matches your expected value at the application layer: do not rely on the return code for integrity. |

ML-DSA
Include lcx_mldsa.h. The API adds a ctx/ctx_len context string parameter per FIPS 204 §5.2, and a sig_actual_len output for the actual signature length written.
Key Generation
#include "lcx_mldsa.h"
uint8_t pk[MLDSA65_PUBLICKEYBYTES];
uint8_t sk[MLDSA65_SECRETKEYBYTES];
cx_err_t err = MLDSA_keygen(
pk, sizeof(pk), sk, sizeof(sk), MLDSA_65
);
Signing
uint8_t sig[MLDSA65_SIGBYTES];
size_t sig_len = 0;
cx_err_t err = MLDSA_sign(
sig, sizeof(sig), &sig_len,
msg, msg_len,
ctx, ctx_len, // ctx may be NULL if ctx_len == 0
sk, sizeof(sk),
MLDSA_65
);
// sig_len holds actual bytes written
Verification
cx_err_t err = MLDSA_verify(
sig, sig_len,
msg, msg_len,
ctx, ctx_len,
pk, sizeof(pk),
MLDSA_65
);
if (err == CX_OK) {
// signature is valid
} else {
// CX_INVALID_PARAMETER or CX_SIGNATURE_INVALID
}HashML-DSA (Pre-hash Variant)
For large messages that cannot be hashed on-device in a single pass, use MLDSA_sign_prehash / MLDSA_verify_prehash. The caller is responsible for computing the digest before calling. This is incompatible with some use cases like clear-signing.
uint8_t ph_m[32]; // Pre-hash
cx_sha3_256_hash(msg, msg_len, ph_m); // caller hashes
cx_err_t err = MLDSA_sign_prehash(
sig, sizeof(sig), &sig_len,
ph_m, sizeof(ph_m),
ctx, ctx_len,
sk, sizeof(sk),
MLDSA_PREHASH_SHA3_256,
MLDSA_65
);| CHOOSING A PARAMETER SET For most use cases, ML-KEM-768 (NIST Level 3) and ML-DSA-65 (NIST Level 3) offer a good balance between security and performance on constrained hardware. ML-DSA-87 is only available when HAVE_MLDSA_OPTIMIZATION is enabled. |
Sandra RASOAMIARAMANANA
Engineering Manager & Cryptographer
References
- https://github.com/pq-code-package/mlkem-native
- https://github.com/pq-code-package/mldsa-native
- https://github.com/ZKNoxHQ/ETHDILITHIUM/tree/main/c-lowram
To learn more about the potential threat of quantum computing on hardware wallets read a previous article on side-channel attacks on post quantum cryptography.
Read this in-depth analysis by the Ledger Donjon to understand how blockchain systems and hardware wallets must adapt to new signature algorithms and security constraints.