Skip to content

Commit aa58b98

Browse files
committed
Add CompressedEdwardsY::validated_decompress()
1 parent c3a82a8 commit aa58b98

File tree

2 files changed

+84
-11
lines changed

2 files changed

+84
-11
lines changed

curve25519-dalek/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ sha2 = { version = "0.11.0-rc.2", default-features = false }
4141
bincode = "1"
4242
criterion = { version = "0.5", features = ["html_reports"] }
4343
hex = "0.4.2"
44+
hex-literal = "0.4"
4445
proptest = "1"
4546
rand = "0.9"
4647
rand_core = { version = "0.9", default-features = false, features = ["os_rng"] }

curve25519-dalek/src/edwards.rs

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -209,43 +209,81 @@ impl CompressedEdwardsY {
209209
/// Returns `None` if the input is not the \\(y\\)-coordinate of a
210210
/// curve point.
211211
pub fn decompress(&self) -> Option<EdwardsPoint> {
212-
let (is_valid_y_coord, X, Y, Z) = decompress::step_1(self);
212+
let Y = FieldElement::from_bytes(self.as_bytes());
213+
let (is_valid_y_coord, X, Z) = decompress::step_1(&Y);
213214

214215
if is_valid_y_coord.into() {
215-
Some(decompress::step_2(self, X, Y, Z))
216+
Some(decompress::step_2(self.as_bytes(), X, Y, Z))
216217
} else {
217218
None
218219
}
219220
}
221+
222+
/// Attempt to decompress to an `EdwardsPoint` according to NIST rules.
223+
///
224+
/// Returns `None` if the input overflows the field modulus or is not the
225+
/// \\(y\\)-coordinate of a curve point. With `PointValidation::Full` it
226+
/// will also return `None` if the point is not contained in the
227+
/// prime-order subgroup.
228+
pub fn validated_decompress(&self, validation: PointValidation) -> Option<EdwardsPoint> {
229+
let Y = FieldElement::from_bytes(self.as_bytes());
230+
231+
if &Y.to_bytes() != self.as_bytes() {
232+
return None;
233+
}
234+
235+
let (is_valid_y_coord, X, Z) = decompress::step_1(&Y);
236+
237+
if !bool::from(is_valid_y_coord) {
238+
return None;
239+
}
240+
241+
let point = decompress::step_2(self.as_bytes(), X, Y, Z);
242+
243+
if let PointValidation::Full = validation {
244+
point.is_torsion_free().then_some(point)
245+
} else {
246+
Some(point)
247+
}
248+
}
249+
}
250+
251+
/// NIST validation rules.
252+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
253+
pub enum PointValidation {
254+
/// Partial validation. Only checks for overflowing the field modulus.
255+
Partial,
256+
/// Full validation. Checks for overflowing the field modulus and
257+
/// prime-order subgroup.
258+
Full,
220259
}
221260

222261
mod decompress {
223262
use super::*;
224263

225264
#[rustfmt::skip] // keep alignment of explanatory comments
226265
pub(super) fn step_1(
227-
repr: &CompressedEdwardsY,
228-
) -> (Choice, FieldElement, FieldElement, FieldElement) {
229-
let Y = FieldElement::from_bytes(repr.as_bytes());
266+
Y: &FieldElement,
267+
) -> (Choice, FieldElement, FieldElement) {
230268
let Z = FieldElement::ONE;
231269
let YY = Y.square();
232270
let u = &YY - &Z; // u = y²-1
233271
let v = &(&YY * &constants::EDWARDS_D) + &Z; // v = dy²+1
234272
let (is_valid_y_coord, X) = FieldElement::sqrt_ratio_i(&u, &v);
235273

236-
(is_valid_y_coord, X, Y, Z)
274+
(is_valid_y_coord, X, Z)
237275
}
238276

239277
#[rustfmt::skip]
240278
pub(super) fn step_2(
241-
repr: &CompressedEdwardsY,
279+
repr: &[u8; 32],
242280
mut X: FieldElement,
243281
Y: FieldElement,
244282
Z: FieldElement,
245283
) -> EdwardsPoint {
246284
// FieldElement::sqrt_ratio_i always returns the nonnegative square root,
247285
// so we negate according to the supplied sign bit.
248-
let compressed_sign_bit = Choice::from(repr.as_bytes()[31] >> 7);
286+
let compressed_sign_bit: Choice = Choice::from(repr[31] >> 7);
249287
X.conditional_negate(compressed_sign_bit);
250288

251289
EdwardsPoint {
@@ -1483,9 +1521,13 @@ impl GroupEncoding for EdwardsPoint {
14831521
type Repr = [u8; 32];
14841522

14851523
fn from_bytes(bytes: &Self::Repr) -> CtOption<Self> {
1486-
let repr = CompressedEdwardsY(*bytes);
1487-
let (is_valid_y_coord, X, Y, Z) = decompress::step_1(&repr);
1488-
CtOption::new(decompress::step_2(&repr, X, Y, Z), is_valid_y_coord)
1524+
let Y = FieldElement::from_bytes(bytes);
1525+
let y_encoding_is_canonical = Y.to_bytes().ct_eq(bytes);
1526+
let (is_valid_y_coord, X, Z) = decompress::step_1(&Y);
1527+
CtOption::new(
1528+
decompress::step_2(bytes, X, Y, Z),
1529+
y_encoding_is_canonical & is_valid_y_coord,
1530+
)
14891531
}
14901532

14911533
fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption<Self> {
@@ -1779,6 +1821,7 @@ impl CofactorGroup for EdwardsPoint {
17791821
mod test {
17801822
use super::*;
17811823

1824+
use hex_literal::hex;
17821825
use rand_core::TryRngCore;
17831826

17841827
#[cfg(feature = "alloc")]
@@ -1872,6 +1915,35 @@ mod test {
18721915
assert_eq!(minus_basepoint.T, -(&constants::ED25519_BASEPOINT_POINT.T));
18731916
}
18741917

1918+
/// Test validated decompression
1919+
#[test]
1920+
fn validated_decompression() {
1921+
const OVERFLOWING_POINT: CompressedEdwardsY = CompressedEdwardsY(hex!(
1922+
"e184c7ba7f37cd33bd357f70dc0dfb537fb397761a3fd0a75eb3314b34cc78b5"
1923+
));
1924+
1925+
assert!(OVERFLOWING_POINT.decompress().is_some());
1926+
assert!(
1927+
OVERFLOWING_POINT
1928+
.validated_decompress(PointValidation::Partial)
1929+
.is_none()
1930+
);
1931+
1932+
const TORSION_POINT: CompressedEdwardsY = CompressedEdwardsY(hex!(
1933+
"5d78934d0311e9791fb9577f568a47605f347b367db825c4e27c52eb0cbe944d"
1934+
));
1935+
1936+
let point = TORSION_POINT
1937+
.validated_decompress(PointValidation::Partial)
1938+
.unwrap();
1939+
assert!(!point.is_torsion_free());
1940+
assert!(
1941+
OVERFLOWING_POINT
1942+
.validated_decompress(PointValidation::Full)
1943+
.is_none()
1944+
);
1945+
}
1946+
18751947
/// Test that computing 1*basepoint gives the correct basepoint.
18761948
#[cfg(feature = "precomputed-tables")]
18771949
#[test]

0 commit comments

Comments
 (0)