Skip to content

Commit 015707a

Browse files
iquerejetaarmfazhMichael Rosenberg
authored
Add proper hash_to_curve. (#786)
* Rename hash_to_curve as encode_to_curve * Implement the inline description of the standard. * Generalise map_to_field to return an arbitrary number of field elements. * Implement hash_to_curve as defined in the standard. * Put elligator behind the "digest" feature. * Add warning on non-uniformity of `encode_to_curve`. * Remove the need of Vec for hash_to_field. * Apply suggestions from code review * Refactor expand_message_xmd out of hash_to_field * Add hash-to-curve to benches * Constraint COUNT to 1 or 2; add note on secure hash function usage * Correct hash function usage in encode- and hash-to-curve --------- Co-authored-by: Armando Faz <[email protected]> Co-authored-by: Michael Rosenberg <[email protected]>
1 parent fc88157 commit 015707a

File tree

6 files changed

+499
-134
lines changed

6 files changed

+499
-134
lines changed

curve25519-dalek/benches/dalek_benchmarks.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ mod edwards_benches {
7474
});
7575
}
7676

77+
#[cfg(feature = "digest")]
78+
fn encode_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
79+
let mut rng = rng();
80+
81+
let mut msg = [0u8; 32];
82+
let mut domain_sep = [0u8; 32];
83+
rng.fill_bytes(&mut msg);
84+
rng.fill_bytes(&mut domain_sep);
85+
86+
c.bench_function(
87+
"Elligator2 encode to curve (SHA-512, input size 32 bytes)",
88+
|b| b.iter(|| EdwardsPoint::encode_to_curve::<Sha512>(&[&msg], &[&domain_sep])),
89+
);
90+
}
91+
7792
#[cfg(feature = "digest")]
7893
fn hash_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
7994
let mut rng = rng();
@@ -100,6 +115,7 @@ mod edwards_benches {
100115
consttime_fixed_base_scalar_mul(&mut g);
101116
consttime_variable_base_scalar_mul(&mut g);
102117
vartime_double_base_scalar_mul(&mut g);
118+
encode_to_curve(&mut g);
103119
hash_to_curve(&mut g);
104120
}
105121
}

curve25519-dalek/src/backend/serial/u32/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ pub(crate) const SQRT_M1: FieldElement2625 = FieldElement2625::from_limbs([
7878
pub(crate) const APLUS2_OVER_FOUR: FieldElement2625 =
7979
FieldElement2625::from_limbs([121666, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
8080

81+
#[cfg(feature = "digest")]
8182
/// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation
8283
/// for Curve25519 in its Montgomery form. (This is used internally within the
8384
/// Elligator map.)
8485
pub(crate) const MONTGOMERY_A: FieldElement2625 =
8586
FieldElement2625::from_limbs([486662, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
8687

88+
#[cfg(feature = "digest")]
8789
/// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the
8890
/// Elligator map.)
8991
pub(crate) const MONTGOMERY_A_NEG: FieldElement2625 = FieldElement2625::from_limbs([

curve25519-dalek/src/backend/serial/u64/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,13 @@ pub(crate) const SQRT_M1: FieldElement51 = FieldElement51::from_limbs([
108108
pub(crate) const APLUS2_OVER_FOUR: FieldElement51 =
109109
FieldElement51::from_limbs([121666, 0, 0, 0, 0]);
110110

111+
#[cfg(feature = "digest")]
111112
/// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation
112113
/// for Curve25519 in its Montgomery form. (This is used internally within the
113114
/// Elligator map.)
114115
pub(crate) const MONTGOMERY_A: FieldElement51 = FieldElement51::from_limbs([486662, 0, 0, 0, 0]);
115116

117+
#[cfg(feature = "digest")]
116118
/// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the
117119
/// Elligator map.)
118120
pub(crate) const MONTGOMERY_A_NEG: FieldElement51 = FieldElement51::from_limbs([

curve25519-dalek/src/edwards.rs

Lines changed: 161 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -635,50 +635,106 @@ impl EdwardsPoint {
635635
}
636636

637637
#[cfg(feature = "digest")]
638-
/// Perform hashing to curve, with explicit hash function and domain separator, `domain_sep`,
639-
/// using the suite `edwards25519_XMD:SHA-512_ELL2_NU_`. The input is the concatenation of the
640-
/// elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least one
641-
/// element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed
642-
/// 255 bytes.
638+
// The function `map_to_curve` calculates an [EdwardsPoint] from a [FieldElement].
639+
fn map_to_curve(fe: FieldElement) -> EdwardsPoint {
640+
let c1 = ED25519_SQRTAM2;
641+
642+
// 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u)
643+
let (xMn, xMd, yMn, yMd) = crate::montgomery::elligator_encode(&fe);
644+
// 2. xn = xMn * yMd
645+
let xn = &xMn * &yMd;
646+
// 3. xn = xn * c1
647+
let xn = &xn * &c1;
648+
// 4. xd = xMd * yMn
649+
let xd = &xMd * &yMn;
650+
// 5. yn = xMn - xMd
651+
let yn = &xMn - &xMd;
652+
// 6. yd = xMn + xMd
653+
let yd = &xMn + &xMd;
654+
// 7. tv1 = xd * yd
655+
let tv1 = &xd * &yd;
656+
// 8. e = tv1 == 0
657+
let e = tv1.ct_eq(&FieldElement::ZERO);
658+
// 9. xn = CMOV(xn, 0, e)
659+
let xn = FieldElement::conditional_select(&xn, &FieldElement::ZERO, e);
660+
// 10. xd = CMOV(xd, 1, e)
661+
let xd = FieldElement::conditional_select(&xd, &FieldElement::ONE, e);
662+
// 11. yn = CMOV(yn, 1, e)
663+
let yn = FieldElement::conditional_select(&yn, &FieldElement::ONE, e);
664+
// 12. yd = CMOV(yd, 1, e)
665+
let yd = FieldElement::conditional_select(&yd, &FieldElement::ONE, e);
666+
// 13. return (xn, xd, yn, yd)
667+
668+
EdwardsPoint {
669+
X: &xn * &yd,
670+
Y: &xd * &yn,
671+
Z: &xd * &yd,
672+
T: &xn * &yn,
673+
}
674+
}
675+
676+
#[cfg(feature = "digest")]
677+
/// Perform encode to curve per RFC 9380, with explicit hash function and domain separator
678+
/// `domain_sep`, using the Twisted Edwards Elligator 2 method. The input is the concatenation
679+
/// of the elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least
680+
/// one element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed 255
681+
/// bytes.
682+
///
683+
/// The specification names SHA-512 as an example of a secure hash to use with this function,
684+
/// but you may use any 512-bit hash within reason (see the
685+
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
686+
///
687+
/// # Warning
688+
/// `encode_to_curve` is a nonuniform encoding from byte strings to points in `G`. That is,
689+
/// the distribution of its output is not uniformly random in `G`: the set of possible outputs
690+
/// of encode_to_curve is only a fraction of the points in `G`, and some points in this set
691+
/// are more likely to be output than others.
692+
///
693+
/// If your application needs the distribution of the output to be statistically close to
694+
/// uniform in `G`, use [Self::hash_to_curve] instead.
643695
///
644696
/// # Panics
645697
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
646-
pub fn hash_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
698+
pub fn encode_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
647699
where
648700
D: BlockSizeUser + Default + FixedOutput<OutputSize = U64> + HashMarker,
649701
D::BlockSize: IsGreater<D::OutputSize, Output = True>,
650702
{
651703
// For reference see
652704
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
653705

654-
let fe = FieldElement::hash_to_field::<D>(bytes, domain_sep);
655-
let (M1, is_sq) = crate::montgomery::elligator_encode(&fe);
706+
let fe = FieldElement::hash_to_field::<D, 1>(bytes, domain_sep);
707+
let Q = Self::map_to_curve(fe[0]);
708+
Q.mul_by_cofactor()
709+
}
656710

657-
// The `to_edwards` conversion we're performing takes as input the sign of the Edwards
658-
// `y` coordinate. However, the specification uses `is_sq` to determine the sign of the
659-
// Montgomery `v` coordinate. Our approach reconciles this mismatch as follows:
660-
//
661-
// * We arbitrarily fix the sign of the Edwards `y` coordinate (we choose 0).
662-
// * Using the Montgomery `u` coordinate and the Edwards `X` coordinate, we recover `v`.
663-
// * We verify that the sign of `v` matches the expected one, i.e., `is_sq == mont_v.is_negative()`.
664-
// * If it does not match, we conditionally negate to correct the sign.
665-
//
666-
// Note: This logic aligns with the RFC draft specification:
667-
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
668-
// followed by the mapping
669-
// https://www.rfc-editor.org/rfc/rfc9380.html#name-mappings-for-twisted-edward
670-
// The only difference is that our `elligator_encode` returns only the Montgomery `u` coordinate,
671-
// so we apply this workaround to reconstruct and validate the sign.
711+
#[cfg(feature = "digest")]
712+
/// Perform a hash to curve per RFC 9380, with explicit hash function and domain separator
713+
/// `domain_sep`, using the Twisted Edwards Elligator 2 method. The input is the concatenation
714+
/// of the elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least
715+
/// one element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed
716+
/// 255 bytes.
717+
///
718+
/// The specification names SHA-512 as an example of a secure hash to use with this function,
719+
/// but you may use any 512-bit hash within reason (see the
720+
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
721+
///
722+
/// # Panics
723+
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
724+
pub fn hash_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
725+
where
726+
D: BlockSizeUser + Default + FixedOutput<OutputSize = U64> + HashMarker,
727+
D::BlockSize: IsGreater<D::OutputSize, Output = True>,
728+
{
729+
// For reference see
730+
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
672731

673-
let mut E1_opt = M1
674-
.to_edwards(0)
675-
.expect("Montgomery conversion to Edwards point in Elligator failed");
732+
let fe = FieldElement::hash_to_field::<D, 2>(bytes, domain_sep);
733+
let Q0 = Self::map_to_curve(fe[0]);
734+
let Q1 = Self::map_to_curve(fe[1]);
676735

677-
// Now we recover v, to ensure that we got the sign right.
678-
let mont_v =
679-
&(&ED25519_SQRTAM2 * &FieldElement::from_bytes(&M1.to_bytes())) * &E1_opt.X.invert();
680-
E1_opt.X.conditional_negate(is_sq ^ mont_v.is_negative());
681-
E1_opt.mul_by_cofactor()
736+
let R = Q0 + Q1;
737+
R.mul_by_cofactor()
682738
}
683739

684740
/// Return an `EdwardsPoint` chosen uniformly at random using a user-provided RNG.
@@ -2387,10 +2443,10 @@ mod test {
23872443
}
23882444

23892445
// Hash-to-curve test vectors from
2390-
// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
2446+
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.2
23912447
// These are of the form (input_msg, output_x, output_y)
23922448
#[cfg(all(feature = "alloc", feature = "digest"))]
2393-
const RFC_HASH_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
2449+
const RFC_ENCODE_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
23942450
(
23952451
b"",
23962452
"1ff2b70ecf862799e11b7ae744e3489aa058ce805dd323a936375a84695e76da",
@@ -2424,32 +2480,88 @@ mod test {
24242480
)
24252481
];
24262482

2483+
#[cfg(all(feature = "alloc", feature = "digest"))]
2484+
fn hex_str_to_fe(hex_str: &str) -> FieldElement {
2485+
let mut bytes = hex::decode(hex_str).unwrap().to_vec();
2486+
bytes.reverse();
2487+
FieldElement::from_bytes(&bytes.try_into().unwrap())
2488+
}
2489+
24272490
#[test]
24282491
#[cfg(all(feature = "alloc", feature = "digest"))]
2429-
fn elligator_hash_to_curve_test_vectors() {
2492+
fn elligator_encode_to_curve_test_vectors() {
24302493
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
2494+
for (index, vector) in RFC_ENCODE_TO_CURVE_KAT.iter().enumerate() {
2495+
let input = vector.0;
2496+
2497+
let expected_output = {
2498+
let x = hex_str_to_fe(vector.1);
2499+
let y = hex_str_to_fe(vector.2);
2500+
AffinePoint { x, y }.to_edwards()
2501+
};
2502+
2503+
let computed = EdwardsPoint::encode_to_curve::<sha2::Sha512>(&[&input], &[dst]);
2504+
assert_eq!(computed, expected_output, "Failed in test {}", index);
2505+
}
2506+
}
2507+
2508+
// Hash-to-curve test vectors from
2509+
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.1
2510+
// These are of the form (input_msg, output_x, output_y)
2511+
#[cfg(all(feature = "alloc", feature = "digest"))]
2512+
const RFC_HASH_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
2513+
(
2514+
b"",
2515+
"3c3da6925a3c3c268448dcabb47ccde5439559d9599646a8260e47b1e4822fc6",
2516+
"09a6c8561a0b22bef63124c588ce4c62ea83a3c899763af26d795302e115dc21",
2517+
),
2518+
2519+
(
2520+
b"abc",
2521+
"608040b42285cc0d72cbb3985c6b04c935370c7361f4b7fbdb1ae7f8c1a8ecad",
2522+
"1a8395b88338f22e435bbd301183e7f20a5f9de643f11882fb237f88268a5531",
2523+
),
2524+
2525+
(
2526+
b"abcdef0123456789",
2527+
"6d7fabf47a2dc03fe7d47f7dddd21082c5fb8f86743cd020f3fb147d57161472",
2528+
"53060a3d140e7fbcda641ed3cf42c88a75411e648a1add71217f70ea8ec561a6",
2529+
),
2530+
(
2531+
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
2532+
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
2533+
"5fb0b92acedd16f3bcb0ef83f5c7b7a9466b5f1e0d8d217421878ea3686f8524",
2534+
"2eca15e355fcfa39d2982f67ddb0eea138e2994f5956ed37b7f72eea5e89d2f7",
2535+
),
2536+
(
2537+
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
2538+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
2539+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
2540+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
2541+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
2542+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2543+
"0efcfde5898a839b00997fbe40d2ebe950bc81181afbd5cd6b9618aa336c1e8c",
2544+
"6dc2fc04f266c5c27f236a80b14f92ccd051ef1ff027f26a07f8c0f327d8f995"
2545+
)
2546+
];
2547+
2548+
#[test]
2549+
#[cfg(all(feature = "alloc", feature = "digest"))]
2550+
fn elligator_hash_to_curve_test_vectors() {
2551+
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_RO_";
24312552
for (index, vector) in RFC_HASH_TO_CURVE_KAT.iter().enumerate() {
24322553
let input = vector.0;
24332554

24342555
let expected_output = {
2435-
let mut x_bytes = hex::decode(vector.1).unwrap();
2436-
x_bytes.reverse();
2437-
let x = FieldElement::from_bytes(&x_bytes.try_into().unwrap());
2438-
2439-
let mut y_bytes = hex::decode(vector.2).unwrap();
2440-
y_bytes.reverse();
2441-
let y = FieldElement::from_bytes(&y_bytes.try_into().unwrap());
2442-
2443-
EdwardsPoint {
2444-
X: x,
2445-
Y: y,
2446-
Z: FieldElement::ONE,
2447-
T: &x * &y,
2448-
}
2556+
let x = hex_str_to_fe(vector.1);
2557+
let y = hex_str_to_fe(vector.2);
2558+
2559+
AffinePoint { x, y }.to_edwards()
24492560
};
24502561

24512562
let computed = EdwardsPoint::hash_to_curve::<sha2::Sha512>(&[&input], &[dst]);
2452-
assert_eq!(computed, expected_output, "Failed in test {}", index);
2563+
2564+
assert_eq!(expected_output, computed, "Failed in test {}", index);
24532565
}
24542566
}
24552567
}

0 commit comments

Comments
 (0)