# SM2-PKE has 32-bit Biased Nonce Vulnerability

SM2 PKE crate passed byte-length (32) as bit-length to nonce generator, giving 32-bit k; BSGS recovers k and decrypts any ciphertext from public key alone.

- Payout: Research
- Severity: high
- Program: RustCrypto sm2
- Found by: XlabAITeam, keenanwgn, tl2cents, A7um
- Disclosed: 2026-01-09
- CVE: CVE-2026-22698
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w3g8-fp6j-wvqw
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-w3g8-fp6j-wvqw
- Tags: area:oss domain:oss impact:data-leak kind:research platform:ghsa sev:high tech:rust technique:reverse-engineering technique:source-review vuln:crypto

**Target:** SM2 PKE encrypt function in Rust sm2 crate
**Root cause:** N_BYTES (32 bytes) was passed to next_k as a bit_length, so U256::try_random_bits generated only 32 bits of entropy with upper 224 bits zero.
**Steps:**
1. Read sm2/src/pke/encrypting.rs: `const N_BYTES: u32 = Sm2::ORDER.as_ref().bits().div_ceil(8);` yields 32 (bytes).
2. Observe `let k = Scalar::from_uint(&next_k(rng, N_BYTES)?).unwrap();` passes 32 as bit_length.
3. next_k calls `U256::try_random_bits(rng, bit_length)` -> only 32 random bits.
4. Encrypt a message with EncryptingKey::encrypt, extract C1 = [k]G from ciphertext.
5. Run BSGS with m=2^16 baby steps j*G and giant steps C1 - i*m*G to recover k in O(2^16).
6. Compute shared point [k]PB from recipient public key, run SM3 KDF, verify C3, recover plaintext without secret key.
**How found:** Code review of the encrypt path spotted the unit mismatch: N_BYTES computed in bytes was fed to a function expecting a bit length, collapsing nonce entropy to 32 bits.
**Exploitation:** Attacker with only the public key and ciphertext extracts C1, runs BSGS over the 2^32 nonce space (~16s on Apple M3), recovers k, derives [k]PB, applies SM3 KDF to C2 and verifies C3 to obtain plaintext.
**Insight:** Audit unit conversions at crypto API boundaries: byte-length constants passed to bit-length parameters silently shrink keyspace; verify nonce/random generation actually requests full curve-order bits and check upper bits are non-zero.

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

**Command running the PoC**

```
$ time cargo run --example bsgs_recover 
Trying to recover k and decrypt...
recovered k = 0x00000000000000000000000000000000000000000000000000000000ca4f2d79
recovered plaintext: attack-demo-sm2-bsgs-recover-example
cargo run --example bsgs_recover  14.44s user 0.13s system 89% cpu 16.266 total
```

