# CVE-2026-22705: RustCrypto: Signatures has timing side-channel in ML-DSA decomposition

Source: https://junglewise.ai/threats/cve-2026-22705-rustcrypto-signatures-has-timing-side-channel-in-ml-dsa  
Publisher: Junglewise Threat Intelligence, https://junglewise.ai/threats  
Published: 2026-01-13. Updated: 2026-05-11  
License: CC BY 4.0 with a link back

CVE-2026-22705 · Severity: medium · CVSS 6.4 · Published 2026-01-13

### Summary

A timing side-channel was discovered in the Decompose algorithm which is used during ML-DSA signing to generate hints for the signature.

### Details

The analysis was performed using a constant-time analyzer that examines compiled assembly code for instructions with data-dependent timing behavior. The analyzer flags:

- **UDIV/SDIV instructions**: Hardware division instructions have early termination optimizations where execution time depends on operand values.

The `decompose` function used a hardware division instruction to compute `r1.0 / TwoGamma2::U32`. This function is called during signing through `high_bits()` and `low_bits()`, which process values derived from secret key components:

- `(&w - &cs2).low_bits()` where `cs2` is derived from secret key component `s2`
- `Hint::new()` calls `high_bits()` on values derived from secret key component `t0`

**Original Code**:
```rust
fn decompose<TwoGamma2: Unsigned>(self) -> (Elem, Elem) {
    // ...
    let mut r1 = r_plus - r0;
    r1.0 /= TwoGamma2::U32;  // Variable-time division on secret-derived data
    (r1, r0)
}
```

### PoC

I do not have an exploit written for this, currently.

### Impact

The dividend (`r1.0`) is derived from secret key material. An attacker with precise timing measurements could extract information about the signing key by observing timing variations in the division operation.

### Mitigation

Replacing division with constant-time Barrett reduction mitigates this risk. Since `TwoGamma2` is a compile-time constant, we precompute the multiplicative inverse:

```patch
diff --git a/ml-dsa/src/algebra.rs b/ml-dsa/src/algebra.rs
index 559b68a..bb126ce 100644
--- a/ml-dsa/src/algebra.rs
+++ b/ml-dsa/src/algebra.rs
@@ -54,8 +54,50 @@ pub(crate) trait Decompose {
     fn decompose<TwoGamma2: Unsigned>(self) -> (Elem, Elem);
 }
 
+/// Constant-time division by a compile-time constant divisor.
+///
+/// This trait provides a constant-time alternative to the hardware division
+/// instruction, which has variable timing based on operand values.
+/// Uses Barrett reduction to compute `x / M` where M is a compile-time constant.
+pub(crate) trait ConstantTimeDiv: Unsigned {
+    /// Bit shift for Barrett reduction, chosen to provide sufficient precision
+    const CT_DIV_SHIFT: usize;
+    /// Precomputed multiplier: ceil(2^SHIFT / M)
+    const CT_DIV_MULTIPLIER: u64;
+
+    /// Perform constant-time division of x by Self::U32
+    /// Requires: x < Q (the field modulus, ~2^23)
+    #[inline(always)]
+    fn ct_div(x: u32) -> u32 {
+        // Barrett reduction: q = (x * MULTIPLIER) >> SHIFT
+        // This gives us floor(x / M) for x < 2^SHIFT / MULTIPLIER * M
+        let x64 = u64::from(x);
+        let quotient = (x64 * Self::CT_DIV_MULTIPLIER) >> Self::CT_DIV_SHIFT;
+        quotient as u32
+    }
+}
+
+impl<M> ConstantTimeDiv for M
+where
+    M: Unsigned,
+{
+    // Use a shift that provides enough precision for the ML-DSA field (Q ~ 2^23)
+    // We need SHIFT > log2(Q) + log2(M) to ensure accuracy
+    // With Q < 2^24 and M < 2^20, SHIFT = 48 is sufficient
+    const CT_DIV_SHIFT: usize = 48;
+
+    // Precompute the multiplier at compile time
+    // We add (M-1) before dividing to get ceiling division, ensuring we never underestimate
+    #[allow(clippy::integer_division_remainder_used)]
+    const CT_DIV_MULTIPLIER: u64 = ((1u64 << Self::CT_DIV_SHIFT) + M::U64 - 1) / M::U64;
+}
+
 impl Decompose for Elem {
     // Algorithm 36 Decompose
+    //
+    // This implementation uses constant-time division to avoid timing side-channels.
+    // The original algorithm used hardware division which has variable timing based
+    // on operand values, potentially leaking secret information during signing.
     fn decompose<TwoGamma2: Unsigned>(self) -> (Elem, Elem) {
         let r_plus = self.clone();
         let r0 = r_plus.mod_plus_minus::<TwoGamma2>();
@@ -63,8 +105,9 @@ impl Decompose for Elem {
         if r_plus - r0 == Elem::new(BaseField::Q - 1) {
             (Elem::new(0), r0 - Elem::new(1))
         } else {
-            let mut r1 = r_plus - r0;
-            r1.0 /= TwoGamma2::U32;
+            let diff = r_plus - r0;
+            // Use constant-time division instead of hardware division
+            let r1 = Elem::new(TwoGamma2::ct_div(diff.0));
             (r1, r0)
         }
     }
```

See our blog post on [how we avoided side-channels in our Go implementation of ML-DSA](https://blog.trailofbits.com/2025/11/14/how-we-avoided-side-channels-in-our-new-post-quantum-go-cryptography-libraries/) for more information.

Technologies: [ml-dsa (crates.io)](https://junglewise.ai/threats/technologies/ml-dsa). Vendors: [crates.io](https://junglewise.ai/threats/vendors/crates-io).

## Quick answers

**Is CVE-2026-22705 exploited in the wild?** No. Junglewise Threat Intelligence has no report of CVE-2026-22705 being exploited in the wild.

**How severe is CVE-2026-22705?** Medium severity, with a CVSS score of 6.4, published 13 January 2026.

**Which products and versions are affected?** ml-dsa (crates.io) <= 0.1.0-rc.2, SEMVER:introduced=0,fixed=0.1.0-rc.3, SEMVER:introduced=0.0.0-0,fixed=0.1.0-rc.3.

**Is there a fix for CVE-2026-22705?** Yes. ml-dsa (crates.io): upgrade to 0.1.0-rc.3 or later. Junglewise finds every place you run it, drafts the upgrade and applies it once you approve.

**Am I affected by CVE-2026-22705?** Check whether you run ml-dsa (crates.io) at an affected version. Junglewise does that check for you around the clock: connect your code, cloud and domains, and it matches new vulnerabilities like this one against what you actually run and tells you when one reaches you.

## How to fix

- ml-dsa (crates.io): upgrade to 0.1.0-rc.3 or later

  ```sh
  cargo update -p ml-dsa
  ```


## Affected products

- ml-dsa (crates.io) <= 0.1.0-rc.2, SEMVER:introduced=0,fixed=0.1.0-rc.3, SEMVER:introduced=0.0.0-0,fixed=0.1.0-rc.3

## Identifiers

- CVE-2026-22705
- GHSA-hcp2-x6j4-29j7
- RUSTSEC-2025-0144

## References

- [https://api.github.com/users/tob-scott-a](https://api.github.com/users/tob-scott-a)
- [https://github.com/tob-scott-a](https://github.com/tob-scott-a)
- [https://api.github.com/users/tob-scott-a/gists%7B/gist_id%7D](https://api.github.com/users/tob-scott-a/gists%7B/gist_id%7D)
- [https://api.github.com/users/tob-scott-a/repos](https://api.github.com/users/tob-scott-a/repos)
- [https://avatars.githubusercontent.com/u/147527775?v=4](https://avatars.githubusercontent.com/u/147527775?v=4)
- [https://api.github.com/users/tob-scott-a/events%7B/privacy%7D](https://api.github.com/users/tob-scott-a/events%7B/privacy%7D)

## Related threats

- [ml-dsa's UseHint function has off by two error when r0 equals zero](https://junglewise.ai/threats/ml-dsa-s-usehint-function-has-off-by-two-error-when-r0-equals-zero-a878a22d)
- [ML-DSA Signature Verification Accepts Signatures with Repeated Hint Indices](https://junglewise.ai/threats/cve-2026-24850-ml-dsa-signature-verification-accepts-signatures-with-repeated)
