Skip to content

Commit 29b771d

Browse files
committed
Add RFC 8032 and NIST full validation decompression to EdwardsPoint
1 parent c3a82a8 commit 29b771d

File tree

2 files changed

+132
-1
lines changed

2 files changed

+132
-1
lines changed

curve25519-dalek/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ features = [
4040
sha2 = { version = "0.11.0-rc.2", default-features = false }
4141
bincode = "1"
4242
criterion = { version = "0.5", features = ["html_reports"] }
43+
crypto-bigint = { version = "*"}
4344
hex = "0.4.2"
45+
hex-literal = "0.4"
4446
proptest = "1"
4547
rand = "0.9"
4648
rand_core = { version = "0.9", default-features = false, features = ["os_rng"] }

curve25519-dalek/src/edwards.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,59 @@ impl CompressedEdwardsY {
217217
None
218218
}
219219
}
220+
221+
/// Attempt to decompress to an `EdwardsPoint` according to RFC 8032 rules.
222+
///
223+
/// Returns `None` if the input overflows the field modulus or is not the
224+
/// \\(y\\)-coordinate of a curve point.
225+
pub fn decompress_rfc8032(&self) -> Option<EdwardsPoint> {
226+
if !bool::from(decompress::check_modulus(self)) {
227+
return None;
228+
}
229+
230+
self.decompress()
231+
}
232+
233+
/// Attempt to decompress to an `EdwardsPoint` according to NIST's full
234+
/// validation.
235+
///
236+
/// Returns `None` if the input overflows the field modulus, is not the
237+
/// \\(y\\)-coordinate of a curve point, is the identity point or is not
238+
/// contained in the prime-order subgroup.
239+
pub fn decompress_nist_full(&self) -> Option<EdwardsPoint> {
240+
let point = self.decompress_rfc8032()?;
241+
242+
if !point.is_identity() && point.is_torsion_free() {
243+
Some(point)
244+
} else {
245+
None
246+
}
247+
}
220248
}
221249

222250
mod decompress {
223251
use super::*;
252+
use subtle::ConstantTimeGreater;
253+
254+
pub(super) fn check_modulus(repr: &CompressedEdwardsY) -> Choice {
255+
// The modulus is:
256+
// 0xecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f
257+
258+
// Remove the sign from the MSB.
259+
let high_without_sign = repr.as_bytes()[31] & 0x7f;
260+
261+
// High-byte can never be bigger than 0x7f, point is always valid as
262+
// long as its smaller.
263+
let high = high_without_sign.ct_eq(&0x7f);
264+
// If low-byte is bigger than 0xec and all other bytes are 0xff point
265+
// is invalid.
266+
let low = repr.as_bytes()[0].ct_gt(&0xec);
267+
// If all other bytes are 0xff and the low-byte is bigger than 0xec
268+
// point is invalid.
269+
let mid = repr.as_bytes()[1..31].ct_eq(&[0xff; 30]);
270+
271+
!high | (!mid | !low)
272+
}
224273

225274
#[rustfmt::skip] // keep alignment of explanatory comments
226275
pub(super) fn step_1(
@@ -1484,8 +1533,12 @@ impl GroupEncoding for EdwardsPoint {
14841533

14851534
fn from_bytes(bytes: &Self::Repr) -> CtOption<Self> {
14861535
let repr = CompressedEdwardsY(*bytes);
1536+
let is_in_modulus = decompress::check_modulus(&repr);
14871537
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)
1538+
CtOption::new(
1539+
decompress::step_2(&repr, X, Y, Z),
1540+
is_in_modulus & is_valid_y_coord,
1541+
)
14891542
}
14901543

14911544
fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption<Self> {
@@ -1779,6 +1832,7 @@ impl CofactorGroup for EdwardsPoint {
17791832
mod test {
17801833
use super::*;
17811834

1835+
use hex_literal::hex;
17821836
use rand_core::TryRngCore;
17831837

17841838
#[cfg(feature = "alloc")]
@@ -1872,6 +1926,81 @@ mod test {
18721926
assert_eq!(minus_basepoint.T, -(&constants::ED25519_BASEPOINT_POINT.T));
18731927
}
18741928

1929+
/// Test validated decompression
1930+
#[test]
1931+
fn validated_decompression() {
1932+
const VALID_POINTS: [CompressedEdwardsY; 4] = [
1933+
CompressedEdwardsY(hex!(
1934+
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"
1935+
)),
1936+
CompressedEdwardsY(hex!(
1937+
"faffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6f"
1938+
)),
1939+
CompressedEdwardsY(hex!(
1940+
"faffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80"
1941+
)),
1942+
CompressedEdwardsY(hex!(
1943+
"fdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"
1944+
)),
1945+
];
1946+
1947+
for compressed in VALID_POINTS {
1948+
assert!(compressed.decompress().is_some(), "{:02x?}", compressed.0);
1949+
assert!(
1950+
compressed.decompress_rfc8032().is_some(),
1951+
"{:02x?}",
1952+
compressed.0
1953+
);
1954+
}
1955+
1956+
const OVERFLOWING_POINTS: [(CompressedEdwardsY, [u8; 32]); 4] = [
1957+
(
1958+
CompressedEdwardsY(hex!(
1959+
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"
1960+
)),
1961+
[0; 32],
1962+
),
1963+
(
1964+
CompressedEdwardsY(hex!(
1965+
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"
1966+
)),
1967+
hex!("0100000000000000000000000000000000000000000000000000000000000000"),
1968+
),
1969+
(
1970+
CompressedEdwardsY(hex!(
1971+
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"
1972+
)),
1973+
hex!("1200000000000000000000000000000000000000000000000000000000000000"),
1974+
),
1975+
(
1976+
CompressedEdwardsY(hex!(
1977+
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1978+
)),
1979+
hex!("0000000000000000000000000000000000000000000000000000000000000080"),
1980+
),
1981+
];
1982+
1983+
for (compressed, result) in OVERFLOWING_POINTS {
1984+
let point = compressed.decompress().unwrap();
1985+
assert_eq!(point.compress().0, result);
1986+
assert!(compressed.decompress_rfc8032().is_none());
1987+
}
1988+
1989+
const TORSION_POINT: CompressedEdwardsY = CompressedEdwardsY(hex!(
1990+
"5d78934d0311e9791fb9577f568a47605f347b367db825c4e27c52eb0cbe944d"
1991+
));
1992+
1993+
let point = TORSION_POINT.decompress_rfc8032().unwrap();
1994+
assert!(!point.is_torsion_free());
1995+
assert!(TORSION_POINT.decompress_nist_full().is_none());
1996+
1997+
assert!(
1998+
CompressedEdwardsY::identity()
1999+
.decompress_nist_full()
2000+
.is_none()
2001+
);
2002+
}
2003+
18752004
/// Test that computing 1*basepoint gives the correct basepoint.
18762005
#[cfg(feature = "precomputed-tables")]
18772006
#[test]

0 commit comments

Comments
 (0)