# State of Post-Quantum Cryptography implementations

RustCrypto's XMSS parser tries XMSS OIDs before XMSSMT, so colliding XMSSMT public keys fail import; plus liboqs skips ML-DSA/ML-KEM key checks.

- Payout: Research
- Severity: low
- Program: RustCrypto
- Write-up by: Joshua Setia
- Disclosed: 2026-10-21
- Write-up quality: 6/10
- Original: http://blog.quarkslab.com/state-of-post-quantum-cryptography-implementations.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-quarkslab-com-state-of-post-quantum-cryptography-implementations-html
- Tags: area:oss domain:oss kind:research platform:research sev:low tech:rust technique:fuzzing technique:parser-differential technique:source-review vuln:crypto vuln:misconfiguration

**Target:** XMSS/XMSSMT public key parsing (VerifyingKey::try_from) and ML-DSA/ML-KEM key parsing
**Root cause:** parse_oid_and_params tries XmssOid::try_from(raw_oid) first and only falls back to from_xmssmt_raw_oid, so an XMSSMT OID colliding with an XMSS OID resolves to the wrong parameter set and the expected-OID check fails. Separately, liboqs omits ML-DSA s1/s2 range and t0/tr recomputation checks and the optional ML-KEM PCT that OpenSSL/BoringSSL/SymCrypt enforce.
**Steps:**
1. Differential-fuzz ML-DSA/ML-KEM private keys with DeltAFLy, mutating valid keys to out-of-range s1/s2 coefficients and failing PCTs.
2. Observe OpenSSL rejects mutated keys while liboqs accepts and signs/encapsulates.
3. For XMSS: import an XMSSMT public key as raw bytes via `VerifyingKey::<P>::try_from(bytes)` where its OID collides with an XMSS OID.
4. `parse_oid_and_params` returns the colliding XMSS OID; `oid != expected_oid` triggers `Error::InvalidOid`.
5. Confirm library tests miss this because they use keygen output, not raw-byte import.
**How found:** Internship differential-fuzzing campaign with Quarkslab's DeltAFLy across PQC libraries; discrepancies in key-check behavior led to code review of RustCrypto's OID parsing, where the XMSS-first fallback order looked suspicious.
**Exploitation:** No direct attack demonstrated; malformed ML-DSA keys with out-of-range s1/s2 bias rejection sampling and can leak secret info (also a fault-injection target), and corrupted ML-KEM key pairs fail PCT causing mismatched shared secrets.
**Insight:** When two schemes share an OID namespace, check parser fallback order: try the offset/namespaced variant first, and test raw-byte key import paths rather than only keygen-produced keys.

## Proof of concept (copied exactly from the original report)

**RustCrypto XMSS OID parsing function**

```
/// 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))
}
```

**VerifyingKey try_from OID comparison**

```
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()));
 }
...
```

