curve: Bring back ff and group (#909)

* Revert "curve: Remove ff/group features for now (#907)"

This reverts commit 13ac5e66a7.

* Fix build

* Remove group-bits features for soundness concerns

* Update changelog
This commit is contained in:
Michael Rosenberg 2026-06-13 16:13:18 -04:00 committed by GitHub
parent 58b331cde9
commit 1c14d54c60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 702 additions and 4 deletions

View file

@ -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

23
Cargo.lock generated
View file

@ -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"

View file

@ -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))

View file

@ -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"]

View file

@ -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,

View file

@ -304,7 +304,7 @@ mod ristretto_benches {
|b, &&size| {
let mut rng = SysRng;
let points: Vec<RistrettoPoint> = (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));
},

View file

@ -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<R: TryRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
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<Self> {
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<Self> {
// 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<SubgroupPoint> 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<T> Sum<T> for SubgroupPoint
where
T: Borrow<SubgroupPoint>,
{
fn sum<I>(iter: I) -> Self
where
I: Iterator<Item = T>,
{
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<R: TryRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
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 = <EdwardsPoint as GroupEncoding>::Repr;
fn from_bytes(bytes: &Self::Repr) -> CtOption<Self> {
EdwardsPoint::from_bytes(bytes).and_then(|p| p.into_subgroup())
}
fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption<Self> {
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<Self::Subgroup> {
CtOption::new(SubgroupPoint(self), CofactorGroup::is_torsion_free(&self))
}
fn is_torsion_free(&self) -> Choice {
(self * constants::BASEPOINT_ORDER).ct_eq(&Self::identity())
}
}
// ------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------

View file

@ -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<R: CryptoRng + ?Sized>(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<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
pub fn try_random<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
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<R: TryRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
// 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<Self> {
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<Self> {
// 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<Self::Subgroup> {
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<RistrettoPoint> = (0..1024)
.map(|_| RistrettoPoint::try_random(&mut rng).unwrap())
.collect();
points[500] = <RistrettoPoint as Group>::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),
<RistrettoPoint as Group>::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() {

View file

@ -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<R: TryRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
// 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<Self> {
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<Self> {
#[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> {
Self::from_canonical_bytes(repr)
}
fn from_repr_vartime(repr: Self::Repr) -> Option<Self> {
// 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() {

View file

@ -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))

View file

@ -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