In this blogpost, we first review the landscape of Post-Quantum Cryptography implementations across some of the most popular cryptographic libraries. Following that, we present the results of a differential fuzzing campaign on few of the presented implementations.


Introduction

One of the internship offers of the 2025–2026 season aimed to test Post-Quantum Cryptography (PQC) implementations with an internal differential fuzzing tool. As such, a list of targets had to be defined, and a state-of-the-art review of the featured PQC algorithms in popular open-source cryptographic libraries was done. Before presenting these implementations, let's first do a quick reminder on PQC.

Post-Quantum Cryptography

Cryptographic algorithms are based on problems that have historically been hard to solve with classical computers. However, among these problems, integer factorisation and the discrete logarithm problem can be solved efficiently by quantum computers, via Shor's algorithm. Although current quantum computers show progress, they are not yet capable of breaking the intended cryptographic algorithms. The main concern and motive for imminent replacement of quantum vulnerable algorithms is the "Harvest now, decrypt later" threat. An adversary can store sensitive data that was encrypted with quantum vulnerable algorithms, and decrypt it once quantum computers are able to.

As a result, new cryptographic algorithms had to be designed, such that their security relies on problems that are not efficiently solvable by quantum algorithms. These new algorithms form what we call Post-Quantum Cryptography (PQC). In 2016, the US National Institute of Standards and Technology (NIST) launched a competition to standardize PQC algorithms, open to researchers from all over the world. They announced the first round of algorithms in 2022, with three signature schemes and one Key Encapsulation Mechanism (KEM). A new phase resulted in the selection of a second KEM, while an additional process to standardize more signature schemes continues.

Post-Quantum migration timelines have been set by government agencies such as ANSSI and NSA. Major tech companies such as Google are planning for full post-quantum replacement as early as 2029. If you are interested in the PQC transition process, check out this blogpost by Angèle Bossuat which explains Signal's PQC upgraded ratcheting mechanism.

Post-Quantum algorithms

To choose which algorithms to include for the fuzzing campaign, we focused on the ones standardized by NIST, as well as some others that were implemented for practical use.

Key Encapsulation Mechanisms

The Harvest now, Decrypt later threat specifically targets classical key exchange schemes. In consequence, PQC algorithms were designed for a cryptographic scheme called Key Encapsulation Mechanism (KEM). In this asymmetric scheme, Alice possesses a key pair and Bob wants to establish a shared secret with Alice. Bob uses Alice's public key to compute a ciphertext and a shared secret. This first step is called Encapsulation. Bob then sends Alice the ciphertext, and Alice performs the second step called Decapsulation. In this step Alice derives the shared secret from the ciphertext, using her private key.

Diagram of a Key Encapsulation Mechanism, where Alice generates a key pair, Bob uses her encapsulation key to generate a shared secret and ciphertext, and Alice uses her decapsulation key to recover the shared secret from the ciphertext.

Diagram of a KEM from FIPS 203.


The first KEM standardized by NIST is the Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM), specified in FIPS 2031. Its security relies on the Module Learning With Errors (MLWE) problem, a lattice-based problem with a particular polynomial ring setting. Its encryption and decryption mechanism is reminiscent of the ElGamal encryption scheme, utilizing randomness and replacing the discrete logarithm part with an MLWE instance.

Since some of the post-quantum algorithms are relatively new, the possibility of discovering a way to classically break them is non-negligible. As a result, NIST decided to standardize a second KEM based on a different problem. This backup algorithm to ML-KEM is called Hamming Quasi-Cyclic (HQC)2. Its security relies on the Quasi-Cyclic Syndrome Decoding problem, based on error-correcting codes.

There are other PQ KEM algorithms that were not standardized by NIST. Two of them are also lattice-based algorithms. The first one is FrodoKEM3. Its security is based on the standard Learning With Errors (LWE) problem. The main difference with ML-KEM is the absence of the polynomial ring structure. While the polynomial ring structure grants ML-KEM performance optimizations compared to standard LWE schemes, it also leverages additional hardness assumptions. This is the main motive behind the use of FrodoKEM, which is strongly considered by European government agencies.

Part of the N-th degree Truncated polynomial Ring Units (NTRU) Prime project, Streamlined NTRU Prime (SNTRUP)4 is another lattice-based KEM. The NTRU Prime project aims to reduce the attack surface of NTRU lattice-based schemes by using a different polynomial ring structure.

One more code-based algorithm was targeted, Classic McEliece5. It is the oldest code-based cryptosystem (1978). Its security relies on the standard Syndrome Decoding problem, which does not have a quasi-cyclic structure unlike HQC. It was not chosen by NIST for standardization due to its large public and private keys, leading to a costlier key generation, as explained in NIST IR 85456. However, it performs better for encapsulation and decapsulation. This can be advantageous in contexts where a single key pair is reused multiple times. Note however that due to recent advancement in its cryptanalysis, Germany's Office for Information Security (BSI) advises against using Classic McEliece.

Another way to reduce the risk associated with the novelty of PQC algorithms and implementations is to use a hybrid scheme. This type of scheme combines a traditional and mature pre-quantum algorithm with a post-quantum one. It benefits from both the proven security of the former against classical attackers, and the conjectured security of the latter against quantum attackers. For example, in a KEM context, a shared secret key from each algorithm is generated. These two keys are then combined into one by using a Key Derivation Function (KDF). The security of a hybrid scheme also relies on the security of the hybridization technique used. Hybrid schemes are recommended by ANSSI for their PQC migration plan7. I chose to study the hybrid algorithm called X-Wing8. This algorithm combines the X25519 elliptic curve key exchange with ML-KEM, more precisely its ML-KEM-768 parameter set.

Digital Signatures

Module-Lattice-Based Digital Signature (ML-DSA), specified in FIPS 2049, was standardized by NIST along with ML-KEM. Its security also relies on the MLWE problem. Just like its KEM counterpart, its structure is similar to a previous discrete logarithm based scheme. Here ML-DSA is reminiscent of Schnorr signatures. They both follow the Fiat-Shamir paradigm, turning a zero-knowledge proof non-interactive by utilizing randomness and hash functions.

Fast-Fourier Transform over NTRU-Lattice-Based Digital Signature Algorithm (FN-DSA) was one of the three signature schemes selected at the third round of the NIST competition. It is based on the Gentry, Peikert, and Vaikuntanathan (GPV) framework10, instantiated over NTRU lattices and a new technique called Fast Fourier sampling. While the other selected algorithms have already been standardized, FN-DSA's standardization is still ongoing. As such, current implementations of the algorithm may not be compatible with the final standardized version. In consequence this algorithm was not targeted for the fuzzing campaign.

Another set of post-quantum signature algorithms is what we call hash-based signatures. Their security relies on the one-way assumption of hash functions. They are what we call Few-Times Signatures schemes (FTS), meaning they have a limited amount of signatures possible. They use One-Time Signatures (OTS) and Merkle trees' structure to convert them into an FTS scheme.

We distinguish two types of hash-based signature algorithms, the stateless ones and the stateful ones. Stateful algorithms require maintaining a state. They must keep track of which OTS key pairs have been used. This can be a drawback in specific contexts such as distributed systems. However, stateful algorithms tend to have better performance and signature sizes than stateless ones. In NIST SP 800-20811, NIST recommended the use of two stateful algorithms. Leighton-Micali Signature system (LMS), specified in RFC 855412, is the first one. It uses Leighton-Micali One-Time Signatures (LM-OTS), based on Winternitz OTS schemes. The other recommended stateful algorithm is the eXtended Merkle Signature Scheme (XMSS) specified in RFC 839113. Its main difference with LMS is the use of another Winternitz OTS variant called WOTS+, where bitmasks are used.

The third signature algorithm selected in the competition, and later standardized by NIST, is Stateless Hash-Based Digital Signature Algorithm (SLH-DSA), specified in FIPS 20514. It is based on two other few-times signatures, FORS and XMSS. Its use of a very large number of FORS key pairs and a hypertree of XMSS Merkle trees allows it to have a stateless property.

Implementations

OpenSSL

Founded in 1998, the OpenSSL project maintains a library that provides implementations of Transport Layer Security (TLS) and Secure Sockets Layer (SSL) protocols, relying on their own implementations of cryptographic algorithms. It is most notably used by web servers for their secure HTTP connections. Written in C, OpenSSL has been forked to form other widely used open source cryptographic libraries. In 2025, they started featuring the following self-implemented Post-Quantum algorithms.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version.
  • LMS, standardized RFC 8554 version. Only the verification function is implemented. The author specifies that the stateful property is problematic for software implementations, and should rather be used for hardware ones.

BoringSSL

In 2014, Google decided to fork OpenSSL to build a cryptography library that specifically meets their needs. Before 2014, Google were using OpenSSL, but their accumulated number of patches in different copies of the library pushed them to create their own. BoringSSL is the current SSL library for Chrome, Android, and other apps and programs. BoringSSL announced their own post-quantum implementations when the first algorithms were standardized by NIST.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, only two parameter sets out of twelve are available (SLH-DSA-SHA2-128s and SLH-DSA-SHAKE-256f). Other missing features from standardized version are internal vectors, pre-hash vectors, key generation from a seed, and deterministic signing.
  • X-Wing, version 02 of the RFC draft8.

wolfSSL

wolfSSL is a C library that implements SSL/TLS. It is designed for embedded systems which confers its portable, small, and fast attributes. Its cryptography is provided by the wolfCrypt library. Founded in 2004, it has been included in a variety of applications and devices such as smart automobile devices, mobile phones or credit card scanners. Their post-quantum support was originally based on the liboqs library. However, they replaced this integration with their own implementations, aiming for a reduction in code size and easier maintenance.

  • ML-KEM, standardized FIPS 203 version, along with x86_64 and ARM optimizations.
  • FrodoKEM, as specified in the draft15.
  • ML-DSA, standardized FIPS 204 version, along with x86_64 and ARM optimizations.
  • SLH-DSA, standardized FIPS 205 version.
  • LMS, standardized RFC 8554 version.
  • XMSS, standardized RFC 8391 version.

liboqs

liboqs is a collection of implementations of post-quantum algorithms written in C. It was created in 2018 as part of the Open Quantum Safe (OQS) project, which aims to support the PQC transition. It provides a common API for these algorithms as well as a test harness and benchmarking routines.

  • ML-KEM, standardized FIPS 203 version. Imported from PQ Code Package's mlkem-native implementation 16. Liboqs also features a GPU-optimized implementation from NVIDIA cuPQC17.
  • HQC, imported from the reference implementation18.
  • FrodoKEM, imported from PQCrypto-LWEKE, the reference implementation19.
  • Streamlined NTRU Prime, imported from the implementation used in OpenSSH20, itself based on the reference implementation21.
  • Classic McEliece, imported from the PQClean library22.
  • ML-DSA, standardized FIPS 204 version. Imported from PQ Code Package's mldsa-native implementation 23. Liboqs also features a GPU-optimized implementation from NVIDIA cuPQC17.
  • SLH-DSA, standardized FIPS 205 version. Imported from PQ Code Package's slhdsa-c implementation24.
  • LMS, imported from the RFC 8554 reference implementation25, by Cisco Systems.
  • XMSS, imported from the RFC 8391 reference implementation26.

Libsodium

Forked from the NaCl library in 2013 and maintained by Frank Denis, libsodium provides a wide range of cryptographic implementations. Written in C, its API prioritizes ease of integration. It is used in a wide variety of software, from communication platforms such as Discord or Wire, to cryptocurrencies like Zcash and text editors like Vim. Libsodium recently started featuring PQC in 2026 with the following algorithms:

  • ML-KEM, standardized FIPS 203 version, based on the reference implementation. Only the ML-KEM-768 parameter set is featured.
  • X-Wing, as specified in 8.

AWS-LC

AWS-LC is the general purpose cryptography library maintained and used by Amazon Web Services. It started around 2020 as a fork of BoringSSL. They announced fully deployed and production ready post-quantum implementations after NIST's first standardization process. PQC is notably integrated in their TLS implementation.

  • ML-KEM, standardized FIPS 203 version. Implementation imported from PQ Code Package's mlkem-native.
  • ML-DSA, standardized FIPS 204 version. Implementation imported from PQ Code Package's mldsa-native.

OpenSSH

Developed since 1999 by OpenBSD, OpenSSH is the most widely used tool for the SSH remote login protocol. It relies on various cryptographic algorithms implemented in their portable library written in C. OpenSSH features two hybrid key agreement protocols, each one combining an ECDH instance with respectively the following PQC algorithms:

  • ML-KEM, standardized FIPS 203 version. Implementation from Cryspen's Libcrux library written in Rust. The Rust code was translated to C using Eurydice, a Rust to C compiler. Only the ML-KEM-768 parameter set is featured.
  • SNTRUP, reference code is used. It is the same implementation liboqs currently uses.

pqm4

The pqm4 library is a benchmarking and testing framework that started in 2018 from the PQCRYPTO project, funded by the European Commission through the Horizon 2020 program. This library targets the ARM Cortex-M4 family of microcontrollers. It imports implementations from the PQClean library, but it also provides implementations with specific optimizations written in assembly for Cortex-M4. Here are listed the implementations that feature the M4 optimizations.

  • ML-KEM, standardized FIPS 203 version.
  • FrodoKEM, available in the Round3 release.
  • SNTRUP, available in the Round3 release.
  • ML-DSA, standardized FIPS 204 version.

Nettle

Nettle is a cryptographic library created in 2001 and developed by Niels Möller. It provides self-developed low level implementations of cryptographic algorithms, with a simple and general API to fit a variety of applications. Being a GNU package since 2009, it is used in several software such as GnuTLS. Since February 2026, Nettle features post-quantum cryptography with the following algorithm:

  • SLH-DSA, standardized FIPS 205 version.

Lean Crypto

Started in 2022 by Stephan Mueller, the leancrypto library exclusively provides self-developed PQC implementations in C. It targets constrained environments such as embedded systems, bare-metal environments, and Linux kernels. For example, the Shim bootloader, used by Linux, supports post-quantum cryptography for secure boot through LeanCrypto.

  • ML-KEM, standardized FIPS 203 version. Derived in parts from reference implementation.
  • HQC, derived in parts from PQClean's implementation.
  • ML-DSA, standardized FIPS 204 version. Derived in parts from reference implementation.
  • SLH-DSA, standardized FIPS 205 version. Derived in parts from reference implementation.

Libgcrypt

Libgcrypt is the cryptographic library provided for GnuPG, a PGP software. But its usage is independent of GnuPG. It was first released in 2001 and is written in C. It has featured post-quantum cryptography since 2024. Their post-quantum implementations derive from the reference implementations of each algorithm.

  • ML-KEM, standardized FIPS 203 version, based on reference implementation.
  • Classic McEliece, based on reference implementation.
  • Streamlined NTRU Prime, based on reference implementation.
  • ML-DSA, standardized FIPS 204 version, based on reference implementation.

SymCrypt

Started in 2006, SymCrypt is Microsoft's core cryptographic library, written in C. Originally intended for symmetric cryptographic operations, it has since 2015 been the primary cryptography library for all algorithms in Windows and other Microsoft products and services. In 2024 it started incorporating their own PQC implementations.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version. Code is not available in public git repository.
  • LMS, standardized RFC 8554 version.
  • XMSS, standardized RFC 8391 version.

TrustCore SDK

TrustCore SDK is a library that provides cross-platform security tools such as certificate-based authentication, encryption, and secure communications. Founded in the early 2000s, it was acquired in 2022 and made open-source by DigiCert, a certificate authority. Its usage targets IoT applications, embedded software, and safety-critical systems. TrustCore started integrating self-developed post-quantum implementations in 2025.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version.

RustCrypto

The RustCrypto project provides pure Rust implementations of cryptographic algorithms. It is widely used across Rust frameworks. For example, Rustls, the most popular Rust TLS implementation, features a RustCrypto provider. Though not yet audited, RustCrypto has various PQC implementations.

  • ML-KEM, standardized FIPS 203 version.
  • FrodoKEM, as specified in 27.
  • HQC, as specified in 2.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version.
  • LMS, standardized RFC 8554 version.
  • XMSS, standardized RFC 8391 version.
  • X-Wing, version 06 of the RFC draft8.

Libcrux

Libcrux is a cryptographic library developed in Rust by Cryspen (now pereq) since 2023. Many of their implementations use formally verified code from the HACL* project28. As for PQC, they entirely implement algorithms in Rust and formally verify their implementations with their tool called Hax29. Libcrux is most notably used by Signal for their Sparse Post-Quantum Ratchet (SPQR) protocol.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version.
  • X-Wing, version 06 of the RFC draft8.

Crypto (Go)

The Go programming language includes a crypto package which contains self-developed cryptographic implementations in Go. Developed since the creation of Go in 2007 at Google, this standard library is used across many applications based in Go. It first introduced support for PQC in 2024.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version. Internal implementation only.

CIRCL

First released in 2019, CIRCL is a collection of cryptographic primitives written in Go. Developed by Cloudflare, its goal is to experiment on the deployment of PQC and elliptic curve cryptography. It is used across Cloudflare's infrastructure. Their PQC implementations are independent of other Go cryptographic libraries.

  • ML-KEM, standardized FIPS 203 version.
  • FrodoKEM, heavily inspired from PQClean's implementation30. Only the parameter set FrodoKEM-640-SHAKE is featured.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version.
  • X-Wing, version 05 of the RFC draft8.

Trail of Bits

Trail of Bits is a cybersecurity research and consulting firm. Their cryptography team has implemented PQC algorithms in Go. These implementations are also self-audited by Trail of Bits, and aim for constant-time.

  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version.
  • LMS, standardized RFC 8554 version.

Botan

Botan is a C++ cryptography library that began in the early 2000s. Developed by Jack Lloyd and a community of contributors, it aims to be the best option for production cryptography. The German Federal Office for Information Security (BSI) has funded Botan for various projects, aiming to improve documentation, resilience, and features of the library. PQC features were added since 2023.

  • ML-KEM, standardized FIPS 203 version. Based on the reference implementation.
  • FrodoKEM, based on the reference implementation.
  • Classic McEliece, based on reference implementation.
  • ML-DSA, standardized FIPS 204 version. Based on the reference implementation.
  • SLH-DSA, standardized FIPS 205 version. Based on reference implementation.
  • LMS, standardized RFC 8554 version.
  • XMSS, standardized RFC 8391 version.

Bouncy Castle

Founded in 2000 in Australia, Bouncy Castle provides pure implementations in both Java and C# programming languages. Widely used for its Java and C# API, the Android operating system notably forked Bouncy Castle. Having experimented with PQC since 2013, algorithms were integrated after NIST standardization.

  • ML-KEM, standardized FIPS 203 version.
  • HQC, not featured in Bouncy Castle C#.
  • FrodoKEM, as specified in 3.
  • Streamlined NTRU Prime, as specified in 4.
  • Classic McEliece, as specified in specified in 5.
  • ML-DSA, standardized FIPS 204 version.
  • SLH-DSA, standardized FIPS 205 version.
  • LMS, standardized RFC 8554 version.
  • XMSS, standardized RFC 8391 version.
  • X-Wing, not featured in Bouncy Castle C#.

corecrypto

Developed and used by Apple, corecrypto is a library that provides self-developed implementations of cryptographic primitives. Formal verification is performed on their implementations through the use of the formal language Cryptol31 and formal verification tools Isabelle32 and Software Analysis Workbench (SAW)33. CoreCrypto notably features PQC that has been deployed in Apple's messaging app iMessage. - ML-KEM, standardized FIPS 203 version. - ML-DSA, standardized FIPS 204 version.

Intel Cryptographic Primitives

The Intel Cryptographic Primitives library offers self-developed cryptographic implementations that are optimized for different Intel CPUs and instruction set architectures. Post-Quantum algorithm started being featured in 2023.

  • ML-KEM, standardized FIPS 203 version.
  • ML-DSA, standardized FIPS 204 version.
  • LMS, standardized RFC 8554 version.
  • XMSS, standardized RFC 8391 version.

Mbed TLS

The Mbed TLS library provides implementations of cryptographic primitives, X.509 certificate handling as well as the SSL/TLS and DTLS protocols. It was specifically designed to be well suited for embedded systems. Post-Quantum cryptography is not yet fully supported as some algorithms are still in the library's road map. However, one specific PQC algorithm is already featured.

  • LMS, standardized RFC 8554 version. It is self-implemented and only features the LMS_SHA256_M32_H10 parameter set.

Summary of libraries and implemented algorithms

The table summarises the contents of this section: it lists the open source projects studied, along with the PQC algorithms they support or not.

Non-exhaustive table of the feature PQC algorithms in open source libraries.


Differential Fuzzing results

The goal of the internship was to test the listed implementations to find vulnerabilities. There are several approaches to test cryptographic implementations. One is to use test vectors to compare operations results to defined standards. The open-source tool crypto-condor automatically performs compliance testing through test vectors. To learn more, you can refer to this blogpost by Julio Loayza Meneses.

Another approach to test cryptographic implementations is through differential fuzzing, which was the approach used during the internship. To learn more on differential fuzzing and its applications in cryptography, you can refer to this blogpost by Célian Glénaz. Quarkslab has developed an internal closed-source differential fuzzer called DeltAFLy. It allows one to assess in-depth functional correctness (for edge cases not covered with test vectors), memory safety, and constant-time behavior of cryptographic implementations. DeltAFLy had already been used to test PQC implementations, you can find out more in this blogpost. Plenty of new PQC targets from the ones listed above remained to be tested with DeltAFLy.

DeltAFLy's logo features a red and gold delta symbol with a pair of wings.

DeltAFLy's logo.


ML-DSA and ML-KEM key checks

The first implementation discrepancies I found were on the signature algorithm ML-DSA. OpenSSL rejects some private key inputs while liboqs accepts them and successfully returns corresponding signatures. These discrepancies come from additional private key checks that OpenSSL includes in its key parsing function. When an ML-DSA private key is parsed, the following parameters are decoded: (ρ,K,tr,s1,s2,t0)(\rho, K, \mathit{tr}, s_1, s_2, t_0). In particular, s1s_1 and s2s_2 are vectors of polynomials whose coefficients should be in a specific interval. For math enthusiasts, algebraically, s1∈Rqℓs_1 \in R_q^\ell and s2∈Rqks_2 \in R_q^k, with Rq=Zq[X]/(X256+1)R_q = \mathbb{Z}_q[X]/(X^{256}+1). When the fuzzer mutates valid inputs and gives a private key with out-of-range coefficients for the polynomials in s1s_1 or s2s_2, OpenSSL detects the malformation of the key and rejects the input. However, other libraries such as liboqs do not perform any key checks (see the next sub-section for a discussion on the consequences of this). Additionally, OpenSSL also recomputes the vector t0t_0 and the hash value tr\mathit{tr} from the decoded vectors s1s_1 and s2s_2. It then compares the recomputed values to the ones that were decoded.

The specification of ML-DSA does not define any of these key checks, nor does it impose the requirement to check the validity of a key pair before use. As a result, libraries such as OpenSSL, BoringSSL, and SymCrypt choose to add the key checks described above, while others that thoroughly follow the specification do not.

Similarly to ML-DSA, we found discrepancies related to key checks in ML-KEM. In this case, the key checks are defined in Section 7.1 of the specification1. Some of them are mandatory to perform before encapsulation and decapsulation, but one key check in particular is noted as optional. It is the pairwise consistency check (PCT). It consists in encapsulating with the public key, decapsulating the resulting ciphertext with the private key, and then verifying whether the two computed shared secrets correspond. Some libraries include this check in their key parsing function, while others do not. Consequently, the fuzzer generates inputs that do not pass the PCT, resulting in discrepancies.

Illustrates the pair-wise consistency check. First, a call to encapsulate returns a ciphertext and shared secret 1. Decapsulating the ciphertext with the corresponding secret key yields a second shared secret. The check verifies is both shared secrets are equal or not.

Diagram of a pair-wise consistency check.


Another difference between some implementations arises from whether the mandatory key checks are performed in the encapsulation and decapsulation functions or in a separate, dedicated key check function. For example, libcrux's implementation of ML-KEM chooses the latter approach. The function validate_public_key(pk) performs the mandatory Encapsulation key check on the public key, defined in Section 7.2 of the specification. The function validate_private_key(sk, ct) performs the Decapsulation input check on the private key and the ciphertext, defined in Section 7.3 of the specification.

Consequences of using malformed ML-KEM and ML-DSA key pairs

If one were to use a corrupted ML-KEM key pair that is detectable by a PCT, the resulting shared secrets would differ, and symmetric encryption would fail. For ML-DSA, regarding out-of-range s1s_1 and s2s_2 coefficients, the specification states the following: "This can be a concern when running SimpleBitUnpack and BitUnpack on inputs that may come from an untrusted source."9. However, these specific concerns are not mentioned again nor explained further in detail in the specification.

The secret vector s1s_1 is used in the computation of the challenge-response:

z=y+c⋅s1z = y + c \cdot s_1 with c∈Rqc \in R_q the public challenge, y∈Rqℓy \in R_q^\ell the private nonce, and z∈Rqℓz \in R_q^\ell. To avoid a bias towards s1s_1 in the response zz, the coefficients of s1s_1 must fall within the relatively short range [−η,η][-\eta, \eta], and zz is accepted and returned only if its polynomial coefficients fall inside the range:

W1=[−γ1+β+1, γ1−β−1]W_1 = [-\gamma_1 + \beta + 1,\ \gamma_1 - \beta - 1] with γ1\gamma_1 and β\beta depending on the parameter set. This process is called rejection sampling and makes the returned zz independent of s1s_1 if the latter has in-range coefficients. Similarly, the following value is biased towards the secret vector s2s_2:

r=w−c⋅s2r = w - c \cdot s_2 with w∈Rqkw \in R_q^k the commit of yy sent by the signer along with the signature. During signing, another rejection sampling is applied to rr. Its values are to be accepted only if its polynomial coefficients fall inside the range W2=[−γ2+β+1, γ2−β−1]W_2 = [-\gamma_2 + \beta + 1,\ \gamma_2 - \beta - 1] with again γ2\gamma_2 and β\beta depending on the parameter set.

ML-DSA's key generation ensures that the coefficients of s1s_1 and s2s_2 fall inside the required range. However, the private key's encoded format allows for coefficients of s1s_1 and s2s_2 to be outside of the required range if the right bits are flipped. In such cases, some values in the acceptance ranges W1W_1 and W2W_2 can never be reached by respectively zz and rr. As a result, the accepted zz and rr are no longer independent of s1s_1 and s2s_2, which leaks secret information and compromises the security of the algorithm. Moreover, there exists a fault injection attack34 that specifically targets the coefficients of s1s_1, underlining why these coefficients must be in range. A reporting of missing ML-KEM and ML-DSA key checks in liboqs can be found here.

A table summarising which libraries implement ML-KEM pair-wise consistency check and ML-DSA's additional checks. From the projects listed in the table, only OpenSSL, BoringSSL, and SymCrypt implement them.

Summary table of the key checks included in the tested libraries.


XMSS OID parsing bug in RustCrypto

Another discrepancy found was in the implementation of XMSS in RustCrypto. This discrepancy is caused by an actual implementation bug. Some parameter sets of XMSS use a hypertree of XMSS Merkle trees to achieve a larger amount of signatures possible. These specific parameter sets are called Multi-Tree XMSS (XMSSMT). The first four bytes of an XMSS/XMSSMT public key correspond to a parameter set identifier that the specification calls the OID. XMSS and XMSSMT's OIDs are defined separately, meaning they both start from zero and hence have colliding OIDs.

RustCrypto's implementation distinguishes XMSS and XMSSMT's OIDs by adding an offset to XMSSMT's OIDs, in the definition of an enum defined as XmssOid. When verifying signatures using a public key imported as raw bytes, the public key has to be parsed into the library's VerifyingKey<P> type, with P specifying the parameter set used. This is done by using the method VerifyingKey::<P>::try_from(bytes). This method begins by calling the function parse_oid_and_params(bytes), which parses the XmssOid from the first four bytes of the imported public key.

/// Parses the OID and initializes params from the first bytes of a key.
/// Tries XMSS OIDs first, then falls back to XMSSMT.
fn parse_oid_and_params(bytes: &[u8]) -> XmssResult<(XmssOid, XmssParams)> {
    if bytes.len() < XMSS_OID_LEN {
        return Err(Error::InvalidOid(0));
    }
    let mut raw_oid: u32 = 0;
    for i in 0..XMSS_OID_LEN {
        raw_oid |= (bytes[XMSS_OID_LEN - i - 1] as u32) << (i * 8);
    }
    let oid = XmssOid::try_from(raw_oid).or_else(|_| XmssOid::from_xmssmt_raw_oid(raw_oid))?;
    let mut params = XmssParams::default();
    oid.initialize(&mut params)?;
    Ok((oid, params))
}

In the above code snippet, we can see that parse_oid_and_params(bytes) first attempts to parse the OID as an XMSS one, by directly calling the method XmssOid::try_from(value). If this first call fails, it will fall back to XmssOid::from_xmssmt_raw_oid(oid) which attempts to parse the OID as an XMSSMT one. This method will add the XMSSMT OID offset before calling XmssOid::try_from(value) again.

The problem arises when importing an XMSSMT public key whose OID collides with an XMSS one. Since parse_oid_and_params(bytes) attempts to parse the OID as an XMSS one first, it will wrongly return the XmssOid of the colliding XMSS parameter set, instead of the actual XMSSMT one with the added offset. As a result, when VerifyingKey::<P>::try_from(bytes) later compares the parsed OID to the expected one that P specifies, as shown in the code snippet below, the check will wrongly fail.

impl<P: XmssParameter> TryFrom<&[u8]> for VerifyingKey<P> {
    type Error = Error;

    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
        let (oid, params) = parse_oid_and_params(value)?;
        let expected_oid = P::oid();
        if oid != expected_oid {
            return Err(Error::InvalidOid(oid.raw_oid()));
        }
...

XMSSMT parameter sets that have an OID that collides with an XMSS one cannot be used if the public key is imported.

The library's tests only include one XMSSMT parameter set. It has a colliding OID but the test uses a public key generated from the library's key generation function, and not a raw bytes one that has to be parsed. The reporting of this bug can be found here.

Conclusion

With recent PQC migration timelines set by state agencies and major tech companies, open source cryptographic libraries are progressively including PQC algorithms. With more and more implementations being developed, making sure that they can safely be deployed for practical usage is a big part of the post-quantum transition process.

In this blog post, we listed and documented the featured PQC algorithms in some of the most widely used open-source cryptographic libraries. We also showed that our tool DeltAFLy is capable of finding bugs and implementation discrepancies.

While implementation bugs arise from mistakes that must be corrected, implementation discrepancies often come from a permissive algorithm specification. In the latter case, the implementation discrepancy is a distinct choice from the library, which often comes down to good practices like in the case of the ML-DSA and ML-KEM key checks.

Acknowledgements

I want to personally thank Julio Loayza Meneses for supervising my internship, Célian Glénaz for helping me understand and use his tool DeltAFLy, and Quarkslab for the great experience and opportunity I had.


  1. NIST FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard ↩↩

  2. Hamming Quasi-Cyclic (HQC) specification ↩↩

  3. FrodoKEM: Learning With Errors Key Encapsulation, Algorithm Specifications And Supporting Documentation ↩↩

  4. NTRU Prime: round 3 submission ↩↩

  5. Classic McEliece: conservative code-based cryptography, cryptosystem specification ↩↩

  6. Status Report on the Fourth Round of the NIST Post-Quantum Cryptography Standardization Process ↩

  7. Avis de l'ANSSI sur la migration vers la cryptographie post-quantique ↩

  8. X-Wing: General-Purpose Hybrid Post-Quantum KEM ↩↩↩↩↩↩

  9. NIST FIPS 204: Module-Lattice-Based Digital Signature Standard ↩↩

  10. Trapdoors for Hard Lattices and New Cryptographic Constructions ↩

  11. Recommendation for Stateful Hash-Based Signature Schemes ↩

  12. RFC 8554: Leighton-Micali Hash-Based Signatures ↩

  13. RFC 8391: XMSS, eXtended Merkle Signature Scheme ↩

  14. NIST FIPS 205: Stateless Hash-Based Digital Signature Standard ↩

  15. FrodoKEM: Key Encapsulation from Learning with Errors, version 03 ↩

  16. Pq-Code-Package/Mlkem-Native ↩

  17. NVIDIA cuPQC ↩↩

  18. HQC reference implementation ↩

  19. PQCrypto-LWEKE: FrodoKEM implementation ↩

  20. Openssh-Portable/Sntrup761.c ↩

  21. NTRU Prime: Software ↩

  22. PQClean implementation of Classic McEliece ↩

  23. Pq-Code-Package/Mldsa-Native ↩

  24. Pq-Code-Package/Slhdsa-c ↩

  25. Cisco/Hash-Sigs: LMS reference implementation ↩

  26. XMSS reference implementation ↩

  27. FrodoKEM, ISO standard ↩

  28. HACL*: A High-Assurance Cryptographic Library ↩

  29. hax: A high assurance translations tool ↩

  30. PQClean implementation of FrodoKEM ↩

  31. Cryptol: The Language of Cryptography ↩

  32. Isabelle: a generic proof assistant ↩

  33. SAW: The Software Analysis Workbench ↩

  34. Correction Fault Attacks on Randomized CRYSTALS-Dilithium ↩


If you would like to learn more about our security audits and explore how we can help you, get in touch with us!