From 643f825ba20dfb904025b74a80a2f91222e9c1e5 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 28 Jan 2026 16:24:57 -0700 Subject: [PATCH] ml-kem: define `FieldElement` using `module_lattice::algebra::Elem` Replaces the `FieldElement` defined in the `ml-kem` crate using one defined with the `Elem` type from the `module-lattice` crate, namely as a type alias thereof. This is the first step towards actually using the generic implementation shared with the `ml-dsa` crate. This necessitated adding a `subtle` feature to `module-lattice` so we can define `ConstantTimeEq` on `Elem`. Uses the `module_lattice::define_field!` macro to define a new `BaseField` type for `ml-kem`, using the modulus 3329, which replaces several inherent constants previously defined on `Field`. The previous `FieldElement::base_case_multiply` method was turned into a private free function within `algebra`, since we can't define an inherent method on an external type. --- Cargo.lock | 1 + ml-kem/Cargo.toml | 4 +- ml-kem/src/algebra.rs | 164 ++++++++++++---------------------- ml-kem/src/compress.rs | 31 +++---- ml-kem/src/encode.rs | 57 ++++++------ ml-kem/src/param.rs | 25 +++--- module-lattice/Cargo.toml | 2 + module-lattice/src/algebra.rs | 12 +++ 8 files changed, 133 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d372aa4..14d4374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -793,6 +793,7 @@ version = "0.1.0-pre.0" dependencies = [ "hybrid-array", "num-traits", + "subtle", "zeroize", ] diff --git a/ml-kem/Cargo.toml b/ml-kem/Cargo.toml index de4a1fd..99a5bd7 100644 --- a/ml-kem/Cargo.toml +++ b/ml-kem/Cargo.toml @@ -21,12 +21,12 @@ alloc = ["pkcs8?/alloc"] getrandom = ["kem/getrandom"] pem = ["pkcs8/pem"] pkcs8 = ["dep:const-oid", "dep:pkcs8"] -zeroize = ["dep:zeroize"] +zeroize = ["module-lattice/zeroize", "dep:zeroize"] hazmat = [] [dependencies] array = { package = "hybrid-array", version = "0.4.4", features = ["extra-sizes", "subtle"] } -module-lattice = "0.1.0-pre.0" +module-lattice = { version = "0.1.0-pre.0", features = ["subtle"] } kem = "0.3.0-rc.2" rand_core = "0.10.0-rc-6" sha3 = { version = "0.11.0-rc.3", default-features = false } diff --git a/ml-kem/src/algebra.rs b/ml-kem/src/algebra.rs index 6df777d..72d3898 100644 --- a/ml-kem/src/algebra.rs +++ b/ml-kem/src/algebra.rs @@ -1,6 +1,9 @@ use array::{Array, typenum::U256}; use core::ops::{Add, Mul, Sub}; -use module_lattice::util::Truncate; +use module_lattice::{ + algebra::{Elem, Field}, + util::Truncate, +}; use sha3::digest::XofReader; use subtle::{Choice, ConstantTimeEq}; @@ -14,88 +17,33 @@ use zeroize::Zeroize; pub type Integer = u16; -/// An element of GF(q). Although `q` is only 16 bits wide, we use a wider uint type to so that we -/// can defer modular reductions. -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct FieldElement(pub Integer); +module_lattice::define_field!(BaseField, Integer, u32, u64, 3329); -impl ConstantTimeEq for FieldElement { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} +/// An element of GF(q). +pub type FieldElement = Elem; -#[cfg(feature = "zeroize")] -impl Zeroize for FieldElement { - fn zeroize(&mut self) { - self.0.zeroize(); - } -} - -impl FieldElement { - pub const Q: Integer = 3329; - pub const Q32: u32 = Self::Q as u32; - pub const Q64: u64 = Self::Q as u64; - const BARRETT_SHIFT: usize = 24; - #[allow(clippy::integer_division_remainder_used)] - const BARRETT_MULTIPLIER: u64 = (1 << Self::BARRETT_SHIFT) / Self::Q64; - - // A fast modular reduction for small numbers `x < 2*q` - fn small_reduce(x: u16) -> u16 { - if x < Self::Q { x } else { x - Self::Q } - } - - fn barrett_reduce(x: u32) -> u16 { - let product = u64::from(x) * Self::BARRETT_MULTIPLIER; - let quotient: u32 = Truncate::truncate(product >> Self::BARRETT_SHIFT); - let remainder = x - quotient * Self::Q32; - Self::small_reduce(Truncate::truncate(remainder)) - } - - // Algorithm 11. BaseCaseMultiply - // - // This is a hot loop. We promote to u64 so that we can do the absolute minimum number of - // modular reductions, since these are the expensive operation. - fn base_case_multiply(a0: Self, a1: Self, b0: Self, b1: Self, i: usize) -> (Self, Self) { - let a0 = u32::from(a0.0); - let a1 = u32::from(a1.0); - let b0 = u32::from(b0.0); - let b1 = u32::from(b1.0); - let g = u32::from(GAMMA[i].0); - - let b1g = u32::from(Self::barrett_reduce(b1 * g)); - - let c0 = Self::barrett_reduce(a0 * b0 + a1 * b1g); - let c1 = Self::barrett_reduce(a0 * b1 + a1 * b0); - (Self(c0), Self(c1)) - } -} - -impl Add for FieldElement { - type Output = Self; - - fn add(self, rhs: Self) -> Self { - Self(Self::small_reduce(self.0 + rhs.0)) - } -} - -impl Sub for FieldElement { - type Output = Self; - - fn sub(self, rhs: Self) -> Self { - // Guard against underflow if `rhs` is too large - Self(Self::small_reduce(self.0 + Self::Q - rhs.0)) - } -} - -impl Mul for FieldElement { - type Output = FieldElement; - - fn mul(self, rhs: FieldElement) -> FieldElement { - let x = u32::from(self.0); - let y = u32::from(rhs.0); - Self(Self::barrett_reduce(x * y)) - } +// Algorithm 11. BaseCaseMultiply +// +// This is a hot loop. We promote to u64 so that we can do the absolute minimum number of +// modular reductions, since these are the expensive operation. +fn base_case_multiply( + a0: FieldElement, + a1: FieldElement, + b0: FieldElement, + b1: FieldElement, + i: usize, +) -> (FieldElement, FieldElement) { + let a0 = u32::from(a0.0); + let a1 = u32::from(a1.0); + let b0 = u32::from(b0.0); + let b1 = u32::from(b1.0); + let g = u32::from(GAMMA[i].0); + + let b1g = u32::from(BaseField::barrett_reduce(b1 * g)); + + let c0 = BaseField::barrett_reduce(a0 * b0 + a1 * b1g); + let c1 = BaseField::barrett_reduce(a0 * b1 + a1 * b0); + (Elem(c0), Elem(c1)) } /// An element of the ring `R_q`, i.e., a polynomial over `Z_q` of degree 255 @@ -243,7 +191,7 @@ impl<'a> FieldElementReader<'a> { fn next(&mut self) -> FieldElement { if let Some(val) = self.next { self.next = None; - return FieldElement(val); + return Elem(val); } loop { @@ -259,15 +207,15 @@ impl<'a> FieldElementReader<'a> { let d1 = Integer::from(b[0]) + ((Integer::from(b[1]) & 0xf) << 8); let d2 = (Integer::from(b[1]) >> 4) + ((Integer::from(b[2]) as Integer) << 4); - if d1 < FieldElement::Q { - if d2 < FieldElement::Q { + if d1 < BaseField::Q { + if d2 < BaseField::Q { self.next = Some(d2); } - return FieldElement(d1); + return Elem(d1); } - if d2 < FieldElement::Q { - return FieldElement(d2); + if d2 < BaseField::Q { + return Elem(d2); } } } @@ -308,18 +256,18 @@ const ZETA_POW_BITREV: [FieldElement; 128] = { } // Compute the powers of zeta - let mut pow = [FieldElement(0); 128]; + let mut pow = [Elem(0); 128]; let mut i = 0; let mut curr = 1u64; #[allow(clippy::integer_division_remainder_used)] while i < 128 { - pow[i] = FieldElement(curr as u16); + pow[i] = Elem(curr as u16); i += 1; - curr = (curr * ZETA) % FieldElement::Q64; + curr = (curr * ZETA) % BaseField::QLL; } // Reorder the powers according to bitrev7 - let mut pow_bitrev = [FieldElement(0); 128]; + let mut pow_bitrev = [Elem(0); 128]; let mut i = 0; while i < 128 { pow_bitrev[i] = pow[bitrev7(i)]; @@ -331,13 +279,13 @@ const ZETA_POW_BITREV: [FieldElement; 128] = { #[allow(clippy::cast_possible_truncation)] const GAMMA: [FieldElement; 128] = { const ZETA: u64 = 17; - let mut gamma = [FieldElement(0); 128]; + let mut gamma = [Elem(0); 128]; let mut i = 0; while i < 128 { let zpr = ZETA_POW_BITREV[i].0 as u64; #[allow(clippy::integer_division_remainder_used)] - let g = (zpr * zpr * ZETA) % FieldElement::Q64; - gamma[i] = FieldElement(g as u16); + let g = (zpr * zpr * ZETA) % BaseField::QLL; + gamma[i] = Elem(g as u16); i += 1; } gamma @@ -351,7 +299,7 @@ impl Mul<&NttPolynomial> for &NttPolynomial { let mut out = NttPolynomial(Array::default()); for i in 0..128 { - let (c0, c1) = FieldElement::base_case_multiply( + let (c0, c1) = base_case_multiply( self.0[2 * i], self.0[2 * i + 1], rhs.0[2 * i], @@ -421,7 +369,7 @@ impl NttPolynomial { } } - FieldElement(3303) * &Polynomial(f) + Elem(3303) * &Polynomial(f) } } @@ -545,9 +493,9 @@ mod test { for (i, x) in self.0.iter().enumerate() { for (j, y) in rhs.0.iter().enumerate() { let (sign, index) = if i + j < 256 { - (FieldElement(1), i + j) + (Elem(1), i + j) } else { - (FieldElement(FieldElement::Q - 1), i + j - 256) + (Elem(BaseField::Q - 1), i + j - 256) }; out.0[index] = out.0[index] + (sign * *x * *y); @@ -560,26 +508,26 @@ mod test { // A polynomial with only a scalar component, to make simple test cases fn const_ntt(x: Integer) -> NttPolynomial { let mut p = Polynomial::default(); - p.0[0] = FieldElement(x); + p.0[0] = Elem(x); p.ntt() } #[test] #[allow(clippy::cast_possible_truncation)] fn polynomial_ops() { - let f = Polynomial(Array::from_fn(|i| FieldElement(i as Integer))); - let g = Polynomial(Array::from_fn(|i| FieldElement(2 * i as Integer))); - let sum = Polynomial(Array::from_fn(|i| FieldElement(3 * i as Integer))); + let f = Polynomial(Array::from_fn(|i| Elem(i as Integer))); + let g = Polynomial(Array::from_fn(|i| Elem(2 * i as Integer))); + let sum = Polynomial(Array::from_fn(|i| Elem(3 * i as Integer))); assert_eq!((&f + &g), sum); assert_eq!((&sum - &g), f); - assert_eq!(FieldElement(3) * &f, sum); + assert_eq!(Elem(3) * &f, sum); } #[test] #[allow(clippy::cast_possible_truncation, clippy::similar_names)] fn ntt() { - let f = Polynomial(Array::from_fn(|i| FieldElement(i as Integer))); - let g = Polynomial(Array::from_fn(|i| FieldElement(2 * i as Integer))); + let f = Polynomial(Array::from_fn(|i| Elem(i as Integer))); + let g = Polynomial(Array::from_fn(|i| Elem(2 * i as Integer))); let f_hat = f.ntt(); let g_hat = g.ntt(); @@ -668,7 +616,7 @@ mod test { // // for k in $-\eta, \ldots, \eta$. The cases of interest here are \eta = 2, 3. type Distribution = [f64; Q_SIZE]; - const Q_SIZE: usize = FieldElement::Q as usize; + const Q_SIZE: usize = BaseField::Q as usize; static CBD2: Distribution = { let mut dist = [0.0; Q_SIZE]; dist[Q_SIZE - 2] = 1.0 / 16.0; @@ -689,7 +637,7 @@ mod test { dist[3] = 1.0 / 64.0; dist }; - static UNIFORM: Distribution = [1.0 / (FieldElement::Q as f64); Q_SIZE]; + static UNIFORM: Distribution = [1.0 / (BaseField::Q as f64); Q_SIZE]; fn kl_divergence(p: &Distribution, q: &Distribution) -> f64 { p.iter() @@ -704,7 +652,7 @@ mod test { let mut sample_dist: Distribution = [0.0; Q_SIZE]; let bump: f64 = 1.0 / (sample.len() as f64); for x in sample { - assert!(x.0 < FieldElement::Q); + assert!(x.0 < BaseField::Q); assert!(ref_dist[x.0 as usize] > 0.0); sample_dist[x.0 as usize] += bump; diff --git a/ml-kem/src/compress.rs b/ml-kem/src/compress.rs index 4716bbe..9ce20ac 100644 --- a/ml-kem/src/compress.rs +++ b/ml-kem/src/compress.rs @@ -1,6 +1,6 @@ -use crate::algebra::{FieldElement, Integer, Polynomial, PolynomialVector}; +use crate::algebra::{BaseField, FieldElement, Integer, Polynomial, PolynomialVector}; use crate::param::{ArraySize, EncodingSize}; -use module_lattice::util::Truncate; +use module_lattice::{algebra::Field, util::Truncate}; // A convenience trait to allow us to associate some constants with a typenum pub trait CompressionFactor: EncodingSize { @@ -18,7 +18,7 @@ where const MASK: Integer = ((1 as Integer) << T::USIZE) - 1; const DIV_SHIFT: usize = 34; #[allow(clippy::integer_division_remainder_used)] - const DIV_MUL: u64 = (1 << T::DIV_SHIFT) / FieldElement::Q64; + const DIV_MUL: u64 = (1 << T::DIV_SHIFT) / BaseField::QLL; } // Traits for objects that allow compression / decompression @@ -35,7 +35,7 @@ impl Compress for FieldElement { // round(a / b) = floor((a + b/2) / b) // a / q ~= (a * x) >> s where x >> s ~= 1/q fn compress(&mut self) -> &Self { - const Q_HALF: u64 = (FieldElement::Q64 + 1) >> 1; + const Q_HALF: u64 = (BaseField::QLL + 1) >> 1; let x = u64::from(self.0); let y = (((x << D::USIZE) + Q_HALF) * D::DIV_MUL) >> D::DIV_SHIFT; self.0 = u16::truncate(y) & D::MASK; @@ -45,7 +45,7 @@ impl Compress for FieldElement { // Equation 4.6: Decompress_d(x) = round((q / 2^d) x) fn decompress(&mut self) -> &Self { let x = u32::from(self.0); - let y = ((x * FieldElement::Q32) + D::POW2_HALF) >> D::USIZE; + let y = ((x * BaseField::QL) + D::POW2_HALF) >> D::USIZE; self.0 = Truncate::truncate(y); self } @@ -90,28 +90,29 @@ impl Compress for PolynomialVector { pub(crate) mod test { use super::*; use array::typenum::{U1, U4, U5, U6, U10, U11, U12}; + use module_lattice::algebra::Elem; use num_rational::Ratio; #[allow(clippy::cast_possible_truncation)] fn rational_compress(input: u16) -> u16 { - let fraction = Ratio::new(u32::from(input) * (1 << D::USIZE), FieldElement::Q32); + let fraction = Ratio::new(u32::from(input) * (1 << D::USIZE), BaseField::QL); (fraction.round().to_integer() as u16) & D::MASK } #[allow(clippy::cast_possible_truncation)] fn rational_decompress(input: u16) -> u16 { - let fraction = Ratio::new(u32::from(input) * FieldElement::Q32, 1 << D::USIZE); + let fraction = Ratio::new(u32::from(input) * BaseField::QL, 1 << D::USIZE); fraction.round().to_integer() as u16 } // Verify against inequality 4.7 #[allow(clippy::integer_division_remainder_used)] fn compression_decompression_inequality() { - const QI32: i32 = FieldElement::Q as i32; - let error_threshold = i32::from(Ratio::new(FieldElement::Q, 1 << D::USIZE).to_integer()); + const QI32: i32 = BaseField::Q as i32; + let error_threshold = i32::from(Ratio::new(BaseField::Q, 1 << D::USIZE).to_integer()); - for x in 0..FieldElement::Q { - let mut y = FieldElement(x); + for x in 0..BaseField::Q { + let mut y = Elem(x); y.compress::(); y.decompress::(); @@ -131,7 +132,7 @@ pub(crate) mod test { fn decompression_compression_equality() { for x in 0..(1 << D::USIZE) { - let mut y = FieldElement(x); + let mut y = Elem(x); y.decompress::(); y.compress::(); @@ -142,7 +143,7 @@ pub(crate) mod test { fn decompress_KAT() { for y in 0..(1 << D::USIZE) { let x_expected = rational_decompress::(y); - let mut x_actual = FieldElement(y); + let mut x_actual = Elem(y); x_actual.decompress::(); assert_eq!(x_expected, x_actual.0); @@ -150,9 +151,9 @@ pub(crate) mod test { } fn compress_KAT() { - for x in 0..FieldElement::Q { + for x in 0..BaseField::Q { let y_expected = rational_compress::(x); - let mut y_actual = FieldElement(x); + let mut y_actual = Elem(x); y_actual.compress::(); assert_eq!(y_expected, y_actual.0, "for x: {}, D: {}", x, D::USIZE); diff --git a/ml-kem/src/encode.rs b/ml-kem/src/encode.rs index 26769ed..97c078f 100644 --- a/ml-kem/src/encode.rs +++ b/ml-kem/src/encode.rs @@ -1,13 +1,14 @@ +use crate::{ + algebra::{ + BaseField, FieldElement, Integer, NttPolynomial, NttVector, Polynomial, PolynomialVector, + }, + param::{ArraySize, EncodedPolynomial, EncodingSize, VectorEncodingSize}, +}; use array::{ Array, typenum::{U256, Unsigned}, }; -use module_lattice::util::Truncate; - -use crate::algebra::{ - FieldElement, Integer, NttPolynomial, NttVector, Polynomial, PolynomialVector, -}; -use crate::param::{ArraySize, EncodedPolynomial, EncodingSize, VectorEncodingSize}; +use module_lattice::{algebra::Field, util::Truncate}; type DecodedValue = Array; @@ -57,7 +58,7 @@ fn byte_decode(bytes: &EncodedPolynomial) -> DecodedValue { vj.0 = val & mask; if D::USIZE == 12 { - vj.0 %= FieldElement::Q; + vj.0 %= BaseField::Q; } } } @@ -150,6 +151,8 @@ pub(crate) mod test { }; use core::{fmt::Debug, ops::Rem}; use getrandom::SysRng; + use module_lattice::algebra::Elem; + use module_lattice::algebra::Field; use rand_core::{Rng, UnwrapErr}; // A helper trait to construct larger arrays by repeating smaller ones @@ -186,10 +189,10 @@ pub(crate) mod test { let mut rng = UnwrapErr(SysRng); let decoded = Array::::from_fn(|_| (rng.next_u32() & 0xFFFF) as Integer); let m = match D::USIZE { - 12 => FieldElement::Q, + 12 => BaseField::Q, d => (1 as Integer) << d, }; - let decoded = decoded.iter().map(|x| FieldElement(x % m)).collect(); + let decoded = decoded.iter().map(|x| Elem(x % m)).collect(); let actual_encoded = byte_encode::(&decoded); let actual_decoded = byte_decode::(&actual_encoded); @@ -202,20 +205,20 @@ pub(crate) mod test { #[test] fn byte_codec() { // The 1-bit can only represent decoded values equal to 0 or 1. - let decoded: DecodedValue = Array::<_, U2>([FieldElement(0), FieldElement(1)]).repeat(); + let decoded: DecodedValue = Array::<_, U2>([Elem(0), Elem(1)]).repeat(); let encoded: EncodedPolynomial = Array([0xaa; 32]); byte_codec_test::(&decoded, &encoded); // For other codec widths, we use a standard sequence let decoded: DecodedValue = Array::<_, U8>([ - FieldElement(0), - FieldElement(1), - FieldElement(2), - FieldElement(3), - FieldElement(4), - FieldElement(5), - FieldElement(6), - FieldElement(7), + Elem(0), + Elem(1), + Elem(2), + Elem(3), + Elem(4), + Elem(5), + Elem(6), + Elem(7), ]) .repeat(); @@ -252,7 +255,7 @@ pub(crate) mod test { fn byte_codec_12_mod() { // DecodeBytes_12 is required to reduce mod q let encoded: EncodedPolynomial = Array([0xff; 384]); - let decoded: DecodedValue = Array([FieldElement(0xfff % FieldElement::Q); 256]); + let decoded: DecodedValue = Array([Elem(0xfff % BaseField::Q); 256]); let actual_decoded = byte_decode::(&encoded); assert_eq!(actual_decoded, decoded); @@ -274,14 +277,14 @@ pub(crate) mod test { fn vector_codec() { let poly = Polynomial( Array::<_, U8>([ - FieldElement(0), - FieldElement(1), - FieldElement(2), - FieldElement(3), - FieldElement(4), - FieldElement(5), - FieldElement(6), - FieldElement(7), + Elem(0), + Elem(1), + Elem(2), + Elem(3), + Elem(4), + Elem(5), + Elem(6), + Elem(7), ]) .repeat(), ); diff --git a/ml-kem/src/param.rs b/ml-kem/src/param.rs index a0ffef8..7c1b814 100644 --- a/ml-kem/src/param.rs +++ b/ml-kem/src/param.rs @@ -10,9 +10,11 @@ //! know any details about object sizes. For example, `VectorEncodingSize::flatten` needs to know //! that the size of an encoded vector is `K` times the size of an encoded polynomial. -use core::fmt::Debug; -use core::ops::{Add, Div, Mul, Rem, Sub}; - +use crate::{ + B32, + algebra::{BaseField, FieldElement, NttVector}, + encode::Encode, +}; use array::{ Array, typenum::{ @@ -21,13 +23,14 @@ use array::{ type_operators::Gcd, }, }; - -use crate::{ - B32, - algebra::{FieldElement, NttVector}, - encode::Encode, +use core::{ + fmt::Debug, + ops::{Add, Div, Mul, Rem, Sub}, +}; +use module_lattice::{ + algebra::{Elem, Field}, + util::{Flatten, Unflatten}, }; -use module_lattice::util::{Flatten, Unflatten}; #[cfg(doc)] use crate::Seed; @@ -119,7 +122,7 @@ where Const: ToUInt, { let max = 1 << B; - let mut out = [FieldElement(0); N]; + let mut out = [Elem(0); N]; let mut x = 0usize; while x < max { let mut y = 0usize; @@ -128,7 +131,7 @@ where let x_ones = x.count_ones() as u16; let y_ones = y.count_ones() as u16; let i = x + (y << B); - out[i] = FieldElement((x_ones + FieldElement::Q - y_ones) % FieldElement::Q); + out[i] = Elem((x_ones + BaseField::Q - y_ones) % BaseField::Q); y += 1; } diff --git a/module-lattice/Cargo.toml b/module-lattice/Cargo.toml index 36cb7a1..d224a4c 100644 --- a/module-lattice/Cargo.toml +++ b/module-lattice/Cargo.toml @@ -20,7 +20,9 @@ hybrid-array = { version = "0.4", features = ["extra-sizes"] } num-traits = { version = "0.2", default-features = false } # optional dependencies +subtle = { version = "2", optional = true, default-features = false } zeroize = { version = "1.8.1", optional = true, default-features = false } [features] +subtle = ["dep:subtle"] zeroize = ["hybrid-array/zeroize", "dep:zeroize"] diff --git a/module-lattice/src/algebra.rs b/module-lattice/src/algebra.rs index 32aaa63..3887321 100644 --- a/module-lattice/src/algebra.rs +++ b/module-lattice/src/algebra.rs @@ -5,6 +5,8 @@ use core::ops::{Add, Mul, Neg, Sub}; use hybrid_array::{Array, ArraySize, typenum::U256}; use num_traits::PrimInt; +#[cfg(feature = "subtle")] +use subtle::{Choice, ConstantTimeEq}; #[cfg(feature = "zeroize")] use zeroize::Zeroize; @@ -83,6 +85,16 @@ impl Elem { } } +#[cfg(feature = "subtle")] +impl ConstantTimeEq for Elem +where + F::Int: ConstantTimeEq, +{ + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + #[cfg(feature = "zeroize")] impl Zeroize for Elem where