ml-dsa: fix use_hint when 𝓇₀ = 0 (#1194)

Originally reported as GHSA-h37v-hp6w-2pp8 by @XoifaiI

According to FIPS 204 Algorithm 40:

    3: if h = 1 and r0 > 0  return (r1 + 1) mod m
    4: if h = 1 and r0 <= 0  return (r1 − 1) mod m

The check that `r0 > 0` was missing, and so values were being
miscomputed in the case that `r0 = 0`.

Likewise, while there were comprehensive tests for other cases, this
case was also untested.

This adds the necessary check as well as a test that covers the case.
This commit is contained in:
Tony Arcieri
2026-01-31 12:15:20 -07:00
committed by GitHub
parent f980f8ba86
commit 10f4ff04cb
+27 -11
View File
@@ -7,27 +7,31 @@ use module_lattice::utils::Truncate;
use crate::algebra::{AlgebraExt, BaseField, Decompose, Elem, Field, Polynomial, Vector};
use crate::param::{EncodedHint, SignatureParams};
/// Algorithm 39 `MakeHint`: computes hint bit indicating whether adding `z` to `r` alters the high
/// bits of `r`.
fn make_hint<TwoGamma2: Unsigned>(z: Elem, r: Elem) -> bool {
let r1 = r.high_bits::<TwoGamma2>();
let v1 = (r + z).high_bits::<TwoGamma2>();
r1 != v1
}
// The method only deals with public data, so we don't need to worry that / and % are not
// constant-time.
#[allow(clippy::integer_division_remainder_used)]
/// Algorithm 40 `UseHint`: returns the high bits of `r` adjusted according to hint `h`.
#[allow(clippy::integer_division_remainder_used, reason = "params are public")]
fn use_hint<TwoGamma2: Unsigned>(h: bool, r: Elem) -> Elem {
let m: u32 = (BaseField::Q - 1) / TwoGamma2::U32;
let (r1, r0) = r.decompose::<TwoGamma2>();
let gamma2 = TwoGamma2::U32 / 2;
if h && r0.0 <= gamma2 {
Elem::new((r1.0 + 1) % m)
} else if h && r0.0 >= BaseField::Q - gamma2 {
Elem::new((r1.0 + m - 1) % m)
} else if h {
// We use the Elem encoding even for signed integers. Since r0 is computed
// mod+- 2*gamma2 (possibly minus 1), it is guaranteed to be in [-gamma2, gamma2].
unreachable!();
if h {
if r0.0 > 0 && r0.0 <= gamma2 {
Elem::new((r1.0 + 1) % m)
} else if (r0.0 == 0) || (r0.0 >= BaseField::Q - gamma2) {
Elem::new((r1.0 + m - 1) % m)
} else {
// We use the Elem encoding even for signed integers. Since r0 is computed
// mod+- 2*gamma2 (possibly minus 1), it is guaranteed to be in [-gamma2, gamma2].
unreachable!();
}
} else {
r1
}
@@ -227,6 +231,18 @@ mod test {
panic!("Could not find suitable test value");
}
#[test]
fn use_hint_r0_is_zero() {
type TwoGamma2 = <MlDsa65 as ParameterSet>::TwoGamma2;
let m = (BaseField::Q - 1) / TwoGamma2::U32;
let r = Elem::new(0);
let (r1, r0) = r.decompose::<TwoGamma2>();
assert_eq!(r0.0, 0);
let result = use_hint::<TwoGamma2>(true, r);
assert_eq!(result, Elem::new((r1.0 + m - 1) % m));
}
#[test]
fn use_hint_threshold() {
type TwoGamma2 = <MlDsa65 as ParameterSet>::TwoGamma2;