diff --git a/.github/workflows/curve25519-dalek.yml b/.github/workflows/curve25519-dalek.yml index 3903c0e..ea9acff 100644 --- a/.github/workflows/curve25519-dalek.yml +++ b/.github/workflows/curve25519-dalek.yml @@ -127,7 +127,7 @@ jobs: # This should automatically pick up the simd backend in a x86_64 runner # It should pick AVX2 due to stable toolchain used since AVX512 requires nigthly RUSTFLAGS: '-C target_feature=+avx2' - run: cargo test --no-default-features --features alloc,precomputed-tables,zeroize --target x86_64-unknown-linux-gnu + run: cargo test --no-default-features --features alloc,precomputed-tables,zeroize,group --target x86_64-unknown-linux-gnu msrv: name: Current MSRV is 1.85.0 diff --git a/Cargo.lock b/Cargo.lock index 623331e..e0697e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,8 +312,10 @@ dependencies = [ "criterion", "curve25519-dalek-derive", "digest", + "ff", "fiat-crypto", "getrandom 0.4.0", + "group", "hex", "postcard", "proptest", @@ -435,6 +437,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.0", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -479,6 +491,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.0", + "subtle", +] + [[package]] name = "half" version = "2.7.1" diff --git a/curve25519-dalek/CHANGELOG.md b/curve25519-dalek/CHANGELOG.md index 5ddbc6a..e38e4b5 100644 --- a/curve25519-dalek/CHANGELOG.md +++ b/curve25519-dalek/CHANGELOG.md @@ -5,6 +5,12 @@ major series. ## 5.x series + +# Unreleased + +* Remove `group-bits` feature due to soundness issues with underlying trait ([#909](https://github.com/dalek-cryptography/curve25519-dalek/pull/909)) +* Re-export `rand_core` ([#908](https://github.com/dalek-cryptography/curve25519-dalek/pull/908)) + ### 5.0.0-rc.0 - 2026-05-28 * Remove `ff` and `group` features until they have a release ([#907](https://github.com/dalek-cryptography/curve25519-dalek/pull/907)) diff --git a/curve25519-dalek/Cargo.toml b/curve25519-dalek/Cargo.toml index 7b8f268..81ec34f 100644 --- a/curve25519-dalek/Cargo.toml +++ b/curve25519-dalek/Cargo.toml @@ -47,6 +47,8 @@ required-features = ["alloc", "rand_core"] [dependencies] cfg-if = "1" +ff = { version = "0.14", default-features = false, optional = true } +group = { version = "0.14", default-features = false, optional = true } rand_core = { version = "0.10", default-features = false, optional = true } digest = { version = "0.11", default-features = false, optional = true, features = ["block-api"] } subtle = { version = "2.6.0", default-features = false, features = [ @@ -68,6 +70,7 @@ default = ["alloc", "precomputed-tables", "zeroize"] alloc = ["zeroize?/alloc"] precomputed-tables = [] legacy_compatibility = [] +group = ["dep:group", "rand_core"] digest = ["dep:digest"] lizard = ["digest"] diff --git a/curve25519-dalek/README.md b/curve25519-dalek/README.md index 9180683..2a9a11f 100644 --- a/curve25519-dalek/README.md +++ b/curve25519-dalek/README.md @@ -53,6 +53,7 @@ curve25519-dalek = ">= 5.0, < 5.2" | `digest` | | Enables `RistrettoPoint::{from_hash, hash_from_bytes}` and `Scalar::{from_hash, hash_from_bytes}`. Also enables hash-to-curve methods `EdwardsPoint::{encode_to_curve, hash_to_curve}`. This is an optional dependency whose version is not subject to SemVer. See [below](#public-api-semver-exemptions) for more details. | | `serde` | | Enables `serde` serialization/deserialization for all the point and scalar types. | | `legacy_compatibility`| | Enables `Scalar::from_bits`, which allows the user to build unreduced scalars whose arithmetic is broken. Do not use this unless you know what you're doing. | +| `group` | | Enables external `group` and `ff` crate traits. | | `lizard` | | Enables the [Lizard](src/lizard/README.md) bytestring-to-point injection for `RistrettoPoint`. Specifically enables the methods `lizard_encode` and `lizard_decode`. | To disable the default features when using `curve25519-dalek` as a dependency, diff --git a/curve25519-dalek/benches/dalek_benchmarks.rs b/curve25519-dalek/benches/dalek_benchmarks.rs index b47da35..a35b0da 100644 --- a/curve25519-dalek/benches/dalek_benchmarks.rs +++ b/curve25519-dalek/benches/dalek_benchmarks.rs @@ -304,7 +304,7 @@ mod ristretto_benches { |b, &&size| { let mut rng = SysRng; let points: Vec = (0..size) - .map(|_| RistrettoPoint::try_from_rng(&mut rng).unwrap()) + .map(|_| RistrettoPoint::try_random(&mut rng).unwrap()) .collect(); b.iter(|| RistrettoPoint::double_and_compress_batch(&points)); }, diff --git a/curve25519-dalek/src/edwards.rs b/curve25519-dalek/src/edwards.rs index 5d68da7..2a75012 100644 --- a/curve25519-dalek/src/edwards.rs +++ b/curve25519-dalek/src/edwards.rs @@ -110,6 +110,13 @@ use digest::{ typenum::IsGreater, }; +#[cfg(feature = "group")] +use { + group::{GroupEncoding, cofactor::CofactorGroup, prime::PrimeGroup}, + rand_core::TryRng, + subtle::CtOption, +}; + #[cfg(feature = "rand_core")] use rand_core::Rng; @@ -1444,6 +1451,338 @@ impl Debug for EdwardsPoint { } } +// ------------------------------------------------------------------------ +// group traits +// ------------------------------------------------------------------------ + +// Use the full trait path to avoid Group::identity overlapping Identity::identity in the +// rest of the module (e.g. tests). +#[cfg(feature = "group")] +impl group::Group for EdwardsPoint { + type Scalar = Scalar; + + fn try_random(rng: &mut R) -> Result { + let mut repr = CompressedEdwardsY([0u8; 32]); + loop { + rng.try_fill_bytes(&mut repr.0)?; + if let Some(p) = repr.decompress() { + if !IsIdentity::is_identity(&p) { + break Ok(p); + } + } + } + } + + fn identity() -> Self { + Identity::identity() + } + + fn generator() -> Self { + constants::ED25519_BASEPOINT_POINT + } + + fn is_identity(&self) -> Choice { + self.ct_eq(&Identity::identity()) + } + + fn double(&self) -> Self { + self.double() + } +} + +#[cfg(feature = "group")] +impl GroupEncoding for EdwardsPoint { + type Repr = [u8; 32]; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + let repr = CompressedEdwardsY(*bytes); + let (is_valid_y_coord, X, Y, Z) = decompress::step_1(&repr); + CtOption::new(decompress::step_2(&repr, X, Y, Z), is_valid_y_coord) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + // Just use the checked API; there are no checks we can skip. + Self::from_bytes(bytes) + } + + fn to_bytes(&self) -> Self::Repr { + self.compress().to_bytes() + } +} + +/// A `SubgroupPoint` represents a point on the Edwards form of Curve25519, that is +/// guaranteed to be in the prime-order subgroup. +#[cfg(feature = "group")] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SubgroupPoint(EdwardsPoint); + +#[cfg(feature = "group")] +impl From for EdwardsPoint { + fn from(p: SubgroupPoint) -> Self { + p.0 + } +} + +#[cfg(feature = "group")] +impl Neg for SubgroupPoint { + type Output = Self; + + fn neg(self) -> Self::Output { + SubgroupPoint(-self.0) + } +} + +#[cfg(feature = "group")] +impl Add<&SubgroupPoint> for &SubgroupPoint { + type Output = SubgroupPoint; + fn add(self, other: &SubgroupPoint) -> SubgroupPoint { + SubgroupPoint(self.0 + other.0) + } +} + +#[cfg(feature = "group")] +define_add_variants!( + LHS = SubgroupPoint, + RHS = SubgroupPoint, + Output = SubgroupPoint +); + +#[cfg(feature = "group")] +impl Add<&SubgroupPoint> for &EdwardsPoint { + type Output = EdwardsPoint; + fn add(self, other: &SubgroupPoint) -> EdwardsPoint { + self + other.0 + } +} + +#[cfg(feature = "group")] +define_add_variants!( + LHS = EdwardsPoint, + RHS = SubgroupPoint, + Output = EdwardsPoint +); + +#[cfg(feature = "group")] +impl AddAssign<&SubgroupPoint> for SubgroupPoint { + fn add_assign(&mut self, rhs: &SubgroupPoint) { + self.0 += rhs.0 + } +} + +#[cfg(feature = "group")] +define_add_assign_variants!(LHS = SubgroupPoint, RHS = SubgroupPoint); + +#[cfg(feature = "group")] +impl AddAssign<&SubgroupPoint> for EdwardsPoint { + fn add_assign(&mut self, rhs: &SubgroupPoint) { + *self += rhs.0 + } +} + +#[cfg(feature = "group")] +define_add_assign_variants!(LHS = EdwardsPoint, RHS = SubgroupPoint); + +#[cfg(feature = "group")] +impl Sub<&SubgroupPoint> for &SubgroupPoint { + type Output = SubgroupPoint; + fn sub(self, other: &SubgroupPoint) -> SubgroupPoint { + SubgroupPoint(self.0 - other.0) + } +} + +#[cfg(feature = "group")] +define_sub_variants!( + LHS = SubgroupPoint, + RHS = SubgroupPoint, + Output = SubgroupPoint +); + +#[cfg(feature = "group")] +impl Sub<&SubgroupPoint> for &EdwardsPoint { + type Output = EdwardsPoint; + fn sub(self, other: &SubgroupPoint) -> EdwardsPoint { + self - other.0 + } +} + +#[cfg(feature = "group")] +define_sub_variants!( + LHS = EdwardsPoint, + RHS = SubgroupPoint, + Output = EdwardsPoint +); + +#[cfg(feature = "group")] +impl SubAssign<&SubgroupPoint> for SubgroupPoint { + fn sub_assign(&mut self, rhs: &SubgroupPoint) { + self.0 -= rhs.0; + } +} + +#[cfg(feature = "group")] +define_sub_assign_variants!(LHS = SubgroupPoint, RHS = SubgroupPoint); + +#[cfg(feature = "group")] +impl SubAssign<&SubgroupPoint> for EdwardsPoint { + fn sub_assign(&mut self, rhs: &SubgroupPoint) { + *self -= rhs.0; + } +} + +#[cfg(feature = "group")] +define_sub_assign_variants!(LHS = EdwardsPoint, RHS = SubgroupPoint); + +#[cfg(feature = "group")] +impl Sum for SubgroupPoint +where + T: Borrow, +{ + fn sum(iter: I) -> Self + where + I: Iterator, + { + use group::Group; + iter.fold(SubgroupPoint::identity(), |acc, item| acc + item.borrow()) + } +} + +#[cfg(feature = "group")] +impl Mul<&Scalar> for &SubgroupPoint { + type Output = SubgroupPoint; + + /// Scalar multiplication: compute `scalar * self`. + /// + /// For scalar multiplication of a basepoint, + /// `EdwardsBasepointTable` is approximately 4x faster. + fn mul(self, scalar: &Scalar) -> SubgroupPoint { + SubgroupPoint(self.0 * scalar) + } +} + +#[cfg(feature = "group")] +define_mul_variants!(LHS = Scalar, RHS = SubgroupPoint, Output = SubgroupPoint); + +#[cfg(feature = "group")] +impl Mul<&SubgroupPoint> for &Scalar { + type Output = SubgroupPoint; + + /// Scalar multiplication: compute `scalar * self`. + /// + /// For scalar multiplication of a basepoint, + /// `EdwardsBasepointTable` is approximately 4x faster. + fn mul(self, point: &SubgroupPoint) -> SubgroupPoint { + point * self + } +} + +#[cfg(feature = "group")] +define_mul_variants!(LHS = SubgroupPoint, RHS = Scalar, Output = SubgroupPoint); + +#[cfg(feature = "group")] +impl MulAssign<&Scalar> for SubgroupPoint { + fn mul_assign(&mut self, scalar: &Scalar) { + self.0 *= scalar; + } +} + +#[cfg(feature = "group")] +define_mul_assign_variants!(LHS = SubgroupPoint, RHS = Scalar); + +#[cfg(feature = "group")] +impl ConstantTimeEq for SubgroupPoint { + fn ct_eq(&self, other: &SubgroupPoint) -> Choice { + self.0.ct_eq(&other.0) + } +} + +#[cfg(feature = "group")] +impl ConditionallySelectable for SubgroupPoint { + fn conditional_select(a: &SubgroupPoint, b: &SubgroupPoint, choice: Choice) -> SubgroupPoint { + SubgroupPoint(EdwardsPoint::conditional_select(&a.0, &b.0, choice)) + } +} + +#[cfg(all(feature = "group", feature = "zeroize"))] +impl Zeroize for SubgroupPoint { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +#[cfg(feature = "group")] +impl group::Group for SubgroupPoint { + type Scalar = Scalar; + + fn try_random(rng: &mut R) -> Result { + use group::ff::Field; + + // This will almost never loop, but `Group::random` is documented as returning a + // non-identity element. + let s = loop { + let s: Scalar = Field::try_random(rng)?; + if !s.is_zero_vartime() { + break s; + } + }; + + // This gives an element of the prime-order subgroup. + Ok(Self::generator() * s) + } + + fn identity() -> Self { + SubgroupPoint(Identity::identity()) + } + + fn generator() -> Self { + SubgroupPoint(EdwardsPoint::generator()) + } + + fn is_identity(&self) -> Choice { + self.0.ct_eq(&Identity::identity()) + } + + fn double(&self) -> Self { + SubgroupPoint(self.0.double()) + } +} + +#[cfg(feature = "group")] +impl GroupEncoding for SubgroupPoint { + type Repr = ::Repr; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + EdwardsPoint::from_bytes(bytes).and_then(|p| p.into_subgroup()) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + EdwardsPoint::from_bytes_unchecked(bytes).and_then(|p| p.into_subgroup()) + } + + fn to_bytes(&self) -> Self::Repr { + self.0.compress().to_bytes() + } +} + +#[cfg(feature = "group")] +impl PrimeGroup for SubgroupPoint {} + +#[cfg(feature = "group")] +impl CofactorGroup for EdwardsPoint { + type Subgroup = SubgroupPoint; + + fn clear_cofactor(&self) -> Self::Subgroup { + SubgroupPoint(self.mul_by_cofactor()) + } + + fn into_subgroup(self) -> CtOption { + CtOption::new(SubgroupPoint(self), CofactorGroup::is_torsion_free(&self)) + } + + fn is_torsion_free(&self) -> Choice { + (self * constants::BASEPOINT_ORDER).ct_eq(&Self::identity()) + } +} + // ------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------ diff --git a/curve25519-dalek/src/ristretto.rs b/curve25519-dalek/src/ristretto.rs index a1a90a6..269662d 100644 --- a/curve25519-dalek/src/ristretto.rs +++ b/curve25519-dalek/src/ristretto.rs @@ -177,6 +177,13 @@ use digest::array::typenum::U64; use crate::constants; use crate::field::FieldElement; +#[cfg(feature = "group")] +use { + group::{GroupEncoding, cofactor::CofactorGroup, prime::PrimeGroup}, + rand_core::TryRng, + subtle::CtOption, +}; + #[cfg(feature = "rand_core")] use { core::convert::Infallible, @@ -667,7 +674,7 @@ impl RistrettoPoint { /// results are added, to ensure a uniform distribution. #[cfg(feature = "rand_core")] pub fn random(rng: &mut R) -> Self { - Self::try_from_rng(rng) + Self::try_random(rng) .map_err(|_: Infallible| {}) .expect("[bug] unfallible rng failed") } @@ -689,7 +696,7 @@ impl RistrettoPoint { /// point should be unknown. The map is applied twice and the /// results are added, to ensure a uniform distribution. #[cfg(feature = "rand_core")] - pub fn try_from_rng(rng: &mut R) -> Result { + pub fn try_random(rng: &mut R) -> Result { let mut uniform_bytes = [0u8; 64]; rng.try_fill_bytes(&mut uniform_bytes)?; @@ -1162,6 +1169,86 @@ impl Debug for RistrettoPoint { } } +// ------------------------------------------------------------------------ +// group traits +// ------------------------------------------------------------------------ + +// Use the full trait path to avoid Group::identity overlapping Identity::identity in the +// rest of the module (e.g. tests). +#[cfg(feature = "group")] +impl group::Group for RistrettoPoint { + type Scalar = Scalar; + + fn try_random(rng: &mut R) -> Result { + // NOTE: this is duplicated due to different `rng` bounds + let mut uniform_bytes = [0u8; 64]; + rng.try_fill_bytes(&mut uniform_bytes)?; + Ok(RistrettoPoint::from_uniform_bytes(&uniform_bytes)) + } + + fn identity() -> Self { + Identity::identity() + } + + fn generator() -> Self { + constants::RISTRETTO_BASEPOINT_POINT + } + + fn is_identity(&self) -> Choice { + self.ct_eq(&Identity::identity()) + } + + fn double(&self) -> Self { + self + self + } +} + +#[cfg(feature = "group")] +impl GroupEncoding for RistrettoPoint { + type Repr = [u8; 32]; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + let (s_encoding_is_canonical, s_is_negative, s) = + decompress::step_1(&CompressedRistretto(*bytes)); + + let s_is_valid = s_encoding_is_canonical & !s_is_negative; + + let (ok, t_is_negative, y_is_zero, res) = decompress::step_2(s); + + CtOption::new(res, s_is_valid & ok & !t_is_negative & !y_is_zero) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + // Just use the checked API; the checks we could skip aren't expensive. + Self::from_bytes(bytes) + } + + fn to_bytes(&self) -> Self::Repr { + self.compress().to_bytes() + } +} + +#[cfg(feature = "group")] +impl PrimeGroup for RistrettoPoint {} + +/// Ristretto has a cofactor of 1. +#[cfg(feature = "group")] +impl CofactorGroup for RistrettoPoint { + type Subgroup = Self; + + fn clear_cofactor(&self) -> Self::Subgroup { + *self + } + + fn into_subgroup(self) -> CtOption { + CtOption::new(self, Choice::from(1)) + } + + fn is_torsion_free(&self) -> Choice { + Choice::from(1) + } +} + // ------------------------------------------------------------------------ // Zeroize traits // ------------------------------------------------------------------------ @@ -1190,6 +1277,8 @@ mod test { use crate::edwards::CompressedEdwardsY; #[cfg(feature = "rand_core")] use getrandom::{SysRng, rand_core::UnwrapErr}; + #[cfg(feature = "group")] + use proptest::prelude::*; #[test] #[cfg(feature = "serde")] @@ -1403,6 +1492,57 @@ mod test { } } + #[test] + #[cfg(all(feature = "alloc", feature = "rand_core", feature = "group"))] + fn double_and_compress_1024_random_points() { + use group::Group; + let mut rng = SysRng; + + let mut points: Vec = (0..1024) + .map(|_| RistrettoPoint::try_random(&mut rng).unwrap()) + .collect(); + points[500] = ::identity(); + + let compressed = RistrettoPoint::double_and_compress_batch(&points); + + for (P, P2_compressed) in points.iter().zip(compressed.iter()) { + assert_eq!(*P2_compressed, (P + P).compress()); + } + } + + #[cfg(feature = "group")] + proptest! { + #[test] + fn multiply_double_and_compress_random_points( + p1 in any::<[u8; 64]>(), + p2 in any::<[u8; 64]>(), + s1 in any::<[u8; 32]>(), + s2 in any::<[u8; 32]>(), + ) { + use group::Group; + + let scalars = [ + Scalar::from_bytes_mod_order(s1), + Scalar::ZERO, + Scalar::from_bytes_mod_order(s2), + ]; + + let points = [ + RistrettoPoint::from_uniform_bytes(&p1), + ::identity(), + RistrettoPoint::from_uniform_bytes(&p2), + ]; + + let multiplied_points: [_; 3] = + core::array::from_fn(|i| scalars[i].div_by_2() * points[i]); + let compressed = RistrettoPoint::double_and_compress_batch(&multiplied_points); + + for ((s, P), P2_compressed) in scalars.iter().zip(points).zip(compressed) { + prop_assert_eq!(P2_compressed, (s * P).compress()); + } + } + } + #[test] #[cfg(all(feature = "alloc", feature = "rand_core"))] fn vartime_precomputed_vs_nonprecomputed_multiscalar() { diff --git a/curve25519-dalek/src/scalar.rs b/curve25519-dalek/src/scalar.rs index f27cf9c..7fc7f3c 100644 --- a/curve25519-dalek/src/scalar.rs +++ b/curve25519-dalek/src/scalar.rs @@ -122,6 +122,12 @@ use core::ops::{Sub, SubAssign}; use cfg_if::cfg_if; +#[cfg(feature = "group")] +use group::ff::{Field, FromUniformBytes, PrimeField}; + +#[cfg(feature = "group")] +use rand_core::TryRng; + #[cfg(feature = "rand_core")] use rand_core::CryptoRng; @@ -1236,6 +1242,128 @@ impl UnpackedScalar { } } +#[cfg(feature = "group")] +impl Field for Scalar { + const ZERO: Self = Self::ZERO; + const ONE: Self = Self::ONE; + + fn try_random(rng: &mut R) -> Result { + // NOTE: this is duplicated due to different `rng` bounds + let mut scalar_bytes = [0u8; 64]; + rng.try_fill_bytes(&mut scalar_bytes)?; + Ok(Self::from_bytes_mod_order_wide(&scalar_bytes)) + } + + fn square(&self) -> Self { + self * self + } + + fn double(&self) -> Self { + self + self + } + + fn invert(&self) -> CtOption { + CtOption::new(self.invert(), !self.is_zero()) + } + + fn sqrt_ratio(num: &Self, div: &Self) -> (Choice, Self) { + #[allow(unused_qualifications)] + group::ff::helpers::sqrt_ratio_generic(num, div) + } + + fn sqrt(&self) -> CtOption { + #[allow(unused_qualifications)] + group::ff::helpers::sqrt_tonelli_shanks( + self, + [ + 0xcb02_4c63_4b9e_ba7d, + 0x029b_df3b_d45e_f39a, + 0x0000_0000_0000_0000, + 0x0200_0000_0000_0000, + ], + ) + } +} + +#[cfg(feature = "group")] +impl PrimeField for Scalar { + type Repr = [u8; 32]; + + fn from_repr(repr: Self::Repr) -> CtOption { + Self::from_canonical_bytes(repr) + } + + fn from_repr_vartime(repr: Self::Repr) -> Option { + // Check that the high bit is not set + if (repr[31] >> 7) != 0u8 { + return None; + } + + let candidate = Scalar { bytes: repr }; + + if candidate == candidate.reduce() { + Some(candidate) + } else { + None + } + } + + fn to_repr(&self) -> Self::Repr { + self.to_bytes() + } + + fn is_odd(&self) -> Choice { + Choice::from(self.as_bytes()[0] & 1) + } + + const MODULUS: &'static str = + "0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed"; + const NUM_BITS: u32 = 253; + const CAPACITY: u32 = 252; + + const TWO_INV: Self = Self { + bytes: [ + 0xf7, 0xe9, 0x7a, 0x2e, 0x8d, 0x31, 0x09, 0x2c, 0x6b, 0xce, 0x7b, 0x51, 0xef, 0x7c, + 0x6f, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, + ], + }; + const MULTIPLICATIVE_GENERATOR: Self = Self { + bytes: [ + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ], + }; + const S: u32 = 2; + const ROOT_OF_UNITY: Self = Self { + bytes: [ + 0xd4, 0x07, 0xbe, 0xeb, 0xdf, 0x75, 0x87, 0xbe, 0xfe, 0x83, 0xce, 0x42, 0x53, 0x56, + 0xf0, 0x0e, 0x7a, 0xc2, 0xc1, 0xab, 0x60, 0x6d, 0x3d, 0x7d, 0xe7, 0x81, 0x79, 0xe0, + 0x10, 0x73, 0x4a, 0x09, + ], + }; + const ROOT_OF_UNITY_INV: Self = Self { + bytes: [ + 0x19, 0xcc, 0x37, 0x71, 0x3a, 0xed, 0x8a, 0x99, 0xd7, 0x18, 0x29, 0x60, 0x8b, 0xa3, + 0xee, 0x05, 0x86, 0x3d, 0x3e, 0x54, 0x9f, 0x92, 0xc2, 0x82, 0x18, 0x7e, 0x86, 0x1f, + 0xef, 0x8c, 0xb5, 0x06, + ], + }; + const DELTA: Self = Self { + bytes: [ + 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ], + }; +} + +#[cfg(feature = "group")] +impl FromUniformBytes<64> for Scalar { + fn from_uniform_bytes(bytes: &[u8; 64]) -> Self { + Scalar::from_bytes_mod_order_wide(bytes) + } +} + /// Read one or more u64s stored as little endian bytes. /// /// ## Panics @@ -1888,6 +2016,56 @@ pub(crate) mod test { assert_eq!(sx + s1, Scalar::from(x + 1)); } + #[cfg(feature = "group")] + #[test] + fn ff_constants() { + assert_eq!(Scalar::from(2u64) * Scalar::TWO_INV, Scalar::ONE); + + assert_eq!( + Scalar::ROOT_OF_UNITY * Scalar::ROOT_OF_UNITY_INV, + Scalar::ONE, + ); + + // ROOT_OF_UNITY^{2^s} mod m == 1 + assert_eq!( + Scalar::ROOT_OF_UNITY.pow(&[1u64 << Scalar::S, 0, 0, 0]), + Scalar::ONE, + ); + + // DELTA^{t} mod m == 1 + assert_eq!( + Scalar::DELTA.pow(&[ + 0x9604_98c6_973d_74fb, + 0x0537_be77_a8bd_e735, + 0x0000_0000_0000_0000, + 0x0400_0000_0000_0000, + ]), + Scalar::ONE, + ); + } + + #[cfg(feature = "group")] + #[test] + fn ff_impls() { + assert!(bool::from(Scalar::ZERO.is_even())); + assert!(bool::from(Scalar::ONE.is_odd())); + assert!(bool::from(Scalar::from(2u64).is_even())); + assert!(bool::from(Scalar::DELTA.is_even())); + + assert!(bool::from(Field::invert(&Scalar::ZERO).is_none())); + assert_eq!(Field::invert(&X).unwrap(), XINV); + + let x_sq = X.square(); + // We should get back either the positive or negative root. + assert!([X, -X].contains(&x_sq.sqrt().unwrap())); + + assert_eq!(Scalar::from_repr_vartime(X.to_repr()), Some(X)); + assert_eq!(Scalar::from_repr_vartime([0xff; 32]), None); + + assert_eq!(Scalar::from_repr(X.to_repr()).unwrap(), X); + assert!(bool::from(Scalar::from_repr([0xff; 32]).is_none())); + } + #[test] #[should_panic] fn test_read_le_u64_into_should_panic_on_bad_input() { diff --git a/ed25519-dalek/CHANGELOG.md b/ed25519-dalek/CHANGELOG.md index 4d1fd9a..88b43b2 100644 --- a/ed25519-dalek/CHANGELOG.md +++ b/ed25519-dalek/CHANGELOG.md @@ -8,6 +8,10 @@ Entries are listed in reverse chronological order per undeprecated major series. # 3.x series +## Unreleased + +* Re-export `rand_core` ([#908](https://github.com/dalek-cryptography/curve25519-dalek/pull/908)) + ## 3.0.0-rc.0 - 2026-05-28 * Add allocation-free `EdwardsPoint::compress_batch` ([#832](https://github.com/dalek-cryptography/curve25519-dalek/pull/832)) diff --git a/x25519-dalek/CHANGELOG.md b/x25519-dalek/CHANGELOG.md index 32de1d2..7f82f94 100644 --- a/x25519-dalek/CHANGELOG.md +++ b/x25519-dalek/CHANGELOG.md @@ -4,6 +4,10 @@ Entries are listed in reverse chronological order. # 3.x Series +## Unreleased + +* Re-export `rand_core` ([#908](https://github.com/dalek-cryptography/curve25519-dalek/pull/908)) + ## 3.0.0-rc.0 - 2026-05-28 * Remove `alloc` feature flag, which was doing nothing