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 <armfazh@users.noreply.github.com>
Co-authored-by: Michael Rosenberg <mrosenberg@cloudflare.com>
This commit is contained in:
Iñigo Querejeta Azurmendi 2025-08-26 20:51:39 +02:00 committed by GitHub
parent fc8815721c
commit 015707ab4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 495 additions and 130 deletions

View file

@ -74,6 +74,21 @@ mod edwards_benches {
});
}
#[cfg(feature = "digest")]
fn encode_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
let mut rng = rng();
let mut msg = [0u8; 32];
let mut domain_sep = [0u8; 32];
rng.fill_bytes(&mut msg);
rng.fill_bytes(&mut domain_sep);
c.bench_function(
"Elligator2 encode to curve (SHA-512, input size 32 bytes)",
|b| b.iter(|| EdwardsPoint::encode_to_curve::<Sha512>(&[&msg], &[&domain_sep])),
);
}
#[cfg(feature = "digest")]
fn hash_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
let mut rng = rng();
@ -100,6 +115,7 @@ mod edwards_benches {
consttime_fixed_base_scalar_mul(&mut g);
consttime_variable_base_scalar_mul(&mut g);
vartime_double_base_scalar_mul(&mut g);
encode_to_curve(&mut g);
hash_to_curve(&mut g);
}
}

View file

@ -78,12 +78,14 @@ pub(crate) const SQRT_M1: FieldElement2625 = FieldElement2625::from_limbs([
pub(crate) const APLUS2_OVER_FOUR: FieldElement2625 =
FieldElement2625::from_limbs([121666, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
#[cfg(feature = "digest")]
/// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation
/// for Curve25519 in its Montgomery form. (This is used internally within the
/// Elligator map.)
pub(crate) const MONTGOMERY_A: FieldElement2625 =
FieldElement2625::from_limbs([486662, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
#[cfg(feature = "digest")]
/// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the
/// Elligator map.)
pub(crate) const MONTGOMERY_A_NEG: FieldElement2625 = FieldElement2625::from_limbs([

View file

@ -108,11 +108,13 @@ pub(crate) const SQRT_M1: FieldElement51 = FieldElement51::from_limbs([
pub(crate) const APLUS2_OVER_FOUR: FieldElement51 =
FieldElement51::from_limbs([121666, 0, 0, 0, 0]);
#[cfg(feature = "digest")]
/// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation
/// for Curve25519 in its Montgomery form. (This is used internally within the
/// Elligator map.)
pub(crate) const MONTGOMERY_A: FieldElement51 = FieldElement51::from_limbs([486662, 0, 0, 0, 0]);
#[cfg(feature = "digest")]
/// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the
/// Elligator map.)
pub(crate) const MONTGOMERY_A_NEG: FieldElement51 = FieldElement51::from_limbs([

View file

@ -635,12 +635,90 @@ impl EdwardsPoint {
}
#[cfg(feature = "digest")]
/// Perform hashing to curve, with explicit hash function and domain separator, `domain_sep`,
/// using the suite `edwards25519_XMD:SHA-512_ELL2_NU_`. The input is the concatenation of the
/// elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least one
/// element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed
// The function `map_to_curve` calculates an [EdwardsPoint] from a [FieldElement].
fn map_to_curve(fe: FieldElement) -> EdwardsPoint {
let c1 = ED25519_SQRTAM2;
// 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u)
let (xMn, xMd, yMn, yMd) = crate::montgomery::elligator_encode(&fe);
// 2. xn = xMn * yMd
let xn = &xMn * &yMd;
// 3. xn = xn * c1
let xn = &xn * &c1;
// 4. xd = xMd * yMn
let xd = &xMd * &yMn;
// 5. yn = xMn - xMd
let yn = &xMn - &xMd;
// 6. yd = xMn + xMd
let yd = &xMn + &xMd;
// 7. tv1 = xd * yd
let tv1 = &xd * &yd;
// 8. e = tv1 == 0
let e = tv1.ct_eq(&FieldElement::ZERO);
// 9. xn = CMOV(xn, 0, e)
let xn = FieldElement::conditional_select(&xn, &FieldElement::ZERO, e);
// 10. xd = CMOV(xd, 1, e)
let xd = FieldElement::conditional_select(&xd, &FieldElement::ONE, e);
// 11. yn = CMOV(yn, 1, e)
let yn = FieldElement::conditional_select(&yn, &FieldElement::ONE, e);
// 12. yd = CMOV(yd, 1, e)
let yd = FieldElement::conditional_select(&yd, &FieldElement::ONE, e);
// 13. return (xn, xd, yn, yd)
EdwardsPoint {
X: &xn * &yd,
Y: &xd * &yn,
Z: &xd * &yd,
T: &xn * &yn,
}
}
#[cfg(feature = "digest")]
/// Perform encode to curve per RFC 9380, with explicit hash function and domain separator
/// `domain_sep`, using the Twisted Edwards Elligator 2 method. The input is the concatenation
/// of the elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least
/// one element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed 255
/// bytes.
///
/// The specification names SHA-512 as an example of a secure hash to use with this function,
/// but you may use any 512-bit hash within reason (see the
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
///
/// # Warning
/// `encode_to_curve` is a nonuniform encoding from byte strings to points in `G`. That is,
/// the distribution of its output is not uniformly random in `G`: the set of possible outputs
/// of encode_to_curve is only a fraction of the points in `G`, and some points in this set
/// are more likely to be output than others.
///
/// If your application needs the distribution of the output to be statistically close to
/// uniform in `G`, use [Self::hash_to_curve] instead.
///
/// # Panics
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
pub fn encode_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
where
D: BlockSizeUser + Default + FixedOutput<OutputSize = U64> + HashMarker,
D::BlockSize: IsGreater<D::OutputSize, Output = True>,
{
// For reference see
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
let fe = FieldElement::hash_to_field::<D, 1>(bytes, domain_sep);
let Q = Self::map_to_curve(fe[0]);
Q.mul_by_cofactor()
}
#[cfg(feature = "digest")]
/// Perform a hash to curve per RFC 9380, with explicit hash function and domain separator
/// `domain_sep`, using the Twisted Edwards Elligator 2 method. The input is the concatenation
/// of the elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least
/// one element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed
/// 255 bytes.
///
/// The specification names SHA-512 as an example of a secure hash to use with this function,
/// but you may use any 512-bit hash within reason (see the
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
///
/// # Panics
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
pub fn hash_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
@ -651,34 +729,12 @@ impl EdwardsPoint {
// For reference see
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
let fe = FieldElement::hash_to_field::<D>(bytes, domain_sep);
let (M1, is_sq) = crate::montgomery::elligator_encode(&fe);
let fe = FieldElement::hash_to_field::<D, 2>(bytes, domain_sep);
let Q0 = Self::map_to_curve(fe[0]);
let Q1 = Self::map_to_curve(fe[1]);
// The `to_edwards` conversion we're performing takes as input the sign of the Edwards
// `y` coordinate. However, the specification uses `is_sq` to determine the sign of the
// Montgomery `v` coordinate. Our approach reconciles this mismatch as follows:
//
// * We arbitrarily fix the sign of the Edwards `y` coordinate (we choose 0).
// * Using the Montgomery `u` coordinate and the Edwards `X` coordinate, we recover `v`.
// * We verify that the sign of `v` matches the expected one, i.e., `is_sq == mont_v.is_negative()`.
// * If it does not match, we conditionally negate to correct the sign.
//
// Note: This logic aligns with the RFC draft specification:
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
// followed by the mapping
// https://www.rfc-editor.org/rfc/rfc9380.html#name-mappings-for-twisted-edward
// The only difference is that our `elligator_encode` returns only the Montgomery `u` coordinate,
// so we apply this workaround to reconstruct and validate the sign.
let mut E1_opt = M1
.to_edwards(0)
.expect("Montgomery conversion to Edwards point in Elligator failed");
// Now we recover v, to ensure that we got the sign right.
let mont_v =
&(&ED25519_SQRTAM2 * &FieldElement::from_bytes(&M1.to_bytes())) * &E1_opt.X.invert();
E1_opt.X.conditional_negate(is_sq ^ mont_v.is_negative());
E1_opt.mul_by_cofactor()
let R = Q0 + Q1;
R.mul_by_cofactor()
}
/// Return an `EdwardsPoint` chosen uniformly at random using a user-provided RNG.
@ -2387,10 +2443,10 @@ mod test {
}
// Hash-to-curve test vectors from
// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.2
// These are of the form (input_msg, output_x, output_y)
#[cfg(all(feature = "alloc", feature = "digest"))]
const RFC_HASH_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
const RFC_ENCODE_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
(
b"",
"1ff2b70ecf862799e11b7ae744e3489aa058ce805dd323a936375a84695e76da",
@ -2424,32 +2480,88 @@ mod test {
)
];
#[cfg(all(feature = "alloc", feature = "digest"))]
fn hex_str_to_fe(hex_str: &str) -> FieldElement {
let mut bytes = hex::decode(hex_str).unwrap().to_vec();
bytes.reverse();
FieldElement::from_bytes(&bytes.try_into().unwrap())
}
#[test]
#[cfg(all(feature = "alloc", feature = "digest"))]
fn elligator_encode_to_curve_test_vectors() {
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
for (index, vector) in RFC_ENCODE_TO_CURVE_KAT.iter().enumerate() {
let input = vector.0;
let expected_output = {
let x = hex_str_to_fe(vector.1);
let y = hex_str_to_fe(vector.2);
AffinePoint { x, y }.to_edwards()
};
let computed = EdwardsPoint::encode_to_curve::<sha2::Sha512>(&[&input], &[dst]);
assert_eq!(computed, expected_output, "Failed in test {}", index);
}
}
// Hash-to-curve test vectors from
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.1
// These are of the form (input_msg, output_x, output_y)
#[cfg(all(feature = "alloc", feature = "digest"))]
const RFC_HASH_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
(
b"",
"3c3da6925a3c3c268448dcabb47ccde5439559d9599646a8260e47b1e4822fc6",
"09a6c8561a0b22bef63124c588ce4c62ea83a3c899763af26d795302e115dc21",
),
(
b"abc",
"608040b42285cc0d72cbb3985c6b04c935370c7361f4b7fbdb1ae7f8c1a8ecad",
"1a8395b88338f22e435bbd301183e7f20a5f9de643f11882fb237f88268a5531",
),
(
b"abcdef0123456789",
"6d7fabf47a2dc03fe7d47f7dddd21082c5fb8f86743cd020f3fb147d57161472",
"53060a3d140e7fbcda641ed3cf42c88a75411e648a1add71217f70ea8ec561a6",
),
(
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"5fb0b92acedd16f3bcb0ef83f5c7b7a9466b5f1e0d8d217421878ea3686f8524",
"2eca15e355fcfa39d2982f67ddb0eea138e2994f5956ed37b7f72eea5e89d2f7",
),
(
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"0efcfde5898a839b00997fbe40d2ebe950bc81181afbd5cd6b9618aa336c1e8c",
"6dc2fc04f266c5c27f236a80b14f92ccd051ef1ff027f26a07f8c0f327d8f995"
)
];
#[test]
#[cfg(all(feature = "alloc", feature = "digest"))]
fn elligator_hash_to_curve_test_vectors() {
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_RO_";
for (index, vector) in RFC_HASH_TO_CURVE_KAT.iter().enumerate() {
let input = vector.0;
let expected_output = {
let mut x_bytes = hex::decode(vector.1).unwrap();
x_bytes.reverse();
let x = FieldElement::from_bytes(&x_bytes.try_into().unwrap());
let x = hex_str_to_fe(vector.1);
let y = hex_str_to_fe(vector.2);
let mut y_bytes = hex::decode(vector.2).unwrap();
y_bytes.reverse();
let y = FieldElement::from_bytes(&y_bytes.try_into().unwrap());
EdwardsPoint {
X: x,
Y: y,
Z: FieldElement::ONE,
T: &x * &y,
}
AffinePoint { x, y }.to_edwards()
};
let computed = EdwardsPoint::hash_to_curve::<sha2::Sha512>(&[&input], &[dst]);
assert_eq!(computed, expected_output, "Failed in test {}", index);
assert_eq!(expected_output, computed, "Failed in test {}", index);
}
}
}

View file

@ -266,7 +266,7 @@ impl FieldElement {
/// Raise this field element to the power (p-5)/8 = 2^252 -3.
#[rustfmt::skip] // keep alignment of explanatory comments
#[allow(clippy::let_and_return)]
fn pow_p58(&self) -> FieldElement {
pub(crate) fn pow_p58(&self) -> FieldElement {
// The bits of (p-5)/8 are 101111.....11.
//
// nonzero bits of exponent
@ -354,62 +354,139 @@ impl FieldElement {
}
#[cfg(feature = "digest")]
/// Perform hashing to a [`FieldElement`], per the
/// [`hash_to_curve`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) specification.
/// Uses the suite `edwards25519_XMD:SHA-512_ELL2_NU_`. The input is the concatenation of the
/// elements of `bytes`. Likewise for the domain separator with `domain_sep`. At least one
/// element of `domain_sep`, MUST be nonempty, and the concatenation MUST NOT exceed 255 bytes.
/// Hashes the given message and domain separator to produce `COUNT` [`FieldElement`]s, per RFC
/// 9380. The hash's input is the concatenation of the elements of `msg`. Likewise for the
/// domain separator with `domain_sep`. At least one element of `domain_sep`, MUST be nonempty,
/// its concatenation MUST NOT exceed 255 bytes, and `COUNT` MUST be 1 or 2.
///
/// The specification names SHA-512 as an example of a secure hash to use with this function,
/// but you may use any 512-bit hash within reason (see the
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
///
/// # Panics
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
pub fn hash_to_field<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> Self
/// Panics if `domain_sep.collect().len() == 0` or `> 255`. Also panics if `COUNT > 2` or
/// `COUNT == 0`.
pub fn hash_to_field<D, const COUNT: usize>(
msg: &[&[u8]],
domain_sep: &[&[u8]],
) -> [Self; COUNT]
where
D: BlockSizeUser + Default + FixedOutput<OutputSize = U64> + HashMarker,
D::BlockSize: IsGreater<D::OutputSize, Output = True>,
{
let l_i_b_str = 48u16.to_be_bytes();
let z_pad = Array::<u8, D::BlockSize>::default();
// We only use `hash_to_field` for Elligator2, which uses 0 < COUNT <= 2
assert!(COUNT == 1 || COUNT == 2);
let mut hasher = D::new().chain_update(z_pad);
// §5.2, we generate count * m * L = COUNT * 1 * (256 + 128)/8 bytes
let len_in_bytes = COUNT * 48;
// Buffer should hold however many digests we need to produce len_in_bytes bytes.
// len_in_bytes is at most 2*48=96, and the digest is 64B. So that's 2 digests, or 128B
let mut buf = [0u8; 128];
// We can call this without worrying about panics: len_in_bytes is at most 96, and buf is
// bigger than it. Further, if `domain_sep` is too large, that's also a panic condition of
// this function so it's fine
let uniform_bytes = expand_msg_xmd::<D>(msg, domain_sep, &mut buf, len_in_bytes);
for slice in bytes {
hasher = hasher.chain_update(slice);
let mut result = [FieldElement::ONE; COUNT];
for i in 0..COUNT {
let mut bytes_wide = [0u8; 64];
bytes_wide[..48].copy_from_slice(&uniform_bytes[48 * i..48 * (i + 1)]);
bytes_wide[..48].reverse();
result[i] = FieldElement::from_bytes_wide(&bytes_wide);
}
hasher = hasher.chain_update(l_i_b_str).chain_update([0u8]);
let mut domain_sep_len = 0usize;
for slice in domain_sep {
hasher = hasher.chain_update(slice);
domain_sep_len += slice.len();
}
let domain_sep_len = u8::try_from(domain_sep_len)
.expect("Unexpected overflow from domain separator's size.");
assert_ne!(
domain_sep_len, 0,
"Domain separator MUST have nonzero length."
);
let b_0 = hasher.chain_update([domain_sep_len]).finalize();
let mut hasher = D::new().chain_update(b_0.as_slice()).chain_update([1u8]);
for slice in domain_sep {
hasher = hasher.chain_update(slice)
}
let b_1 = hasher.chain_update([domain_sep_len]).finalize();
// §5.2, we only generate count * m * L = 1 * 1 * (256 + 128)/8 = 48 bytes
let mut bytes_wide = [0u8; 64];
bytes_wide[..48].copy_from_slice(&b_1.as_slice()[..48]);
bytes_wide[..48].reverse();
FieldElement::from_bytes_wide(&bytes_wide)
result
}
}
/// Hashes the concatenation of the elements of `msg` with domain separator equal to the
/// concatenation of `domain_sep`. The output is an `outlen`-length slice into `buf`. Follows
/// https://www.rfc-editor.org/rfc/rfc9380.html#section-5.3.1
///
/// # Panics
/// Panics if the domain separator is empty, if the total length of the domain separator is more
/// than 255 bytes, if `outlen` is more than `255 * D::output_size()`, if `outlen is more than
/// 65535, or if `dst.len() < outlen`.
#[cfg(feature = "digest")]
fn expand_msg_xmd<'a, D>(
msg: &[&[u8]],
domain_sep: &[&[u8]],
buf: &'a mut [u8],
outlen: usize,
) -> &'a [u8]
where
D: BlockSizeUser + Default + FixedOutput + HashMarker,
{
use core::iter::once;
// The notation we use in this function is the same as in the spec
let len_in_bytes = u16::try_from(outlen).expect("outlen must not exceed 65535");
assert!(buf.len() >= outlen);
let ell = u8::try_from((len_in_bytes as usize).div_ceil(D::output_size()))
.expect("output length cannot exceed 255 times digest size");
let domain_sep_len = u8::try_from(domain_sep.iter().map(|c| c.len()).sum::<usize>())
.expect("unexpected overflow from domain separator's size.");
assert_ne!(
domain_sep_len, 0,
"domain separator MUST have nonzero length."
);
let domain_sep_len_slice = &[domain_sep_len][..];
let dst_prime = domain_sep.iter().copied().chain(once(domain_sep_len_slice));
let z_pad = Array::<u8, D::BlockSize>::default();
let l_i_b_str = len_in_bytes.to_be_bytes();
// Collect the components of msg_prime
let msg_prime = once(z_pad.as_slice())
.chain(msg.iter().copied())
.chain(once(l_i_b_str.as_slice()))
.chain(once(&[0u8][..]))
.chain(dst_prime.clone());
// Hash all of msg_prime
let b_0 = msg_prime
.fold(D::new(), |h, slice| h.chain_update(slice))
.finalize();
// Collect the input components for the b_1 hash
let b_1_input = once(b_0.as_slice())
.chain(once(&[1u8][..]))
.chain(dst_prime.clone());
let b_1 = b_1_input
.fold(D::new(), |h, slice| h.chain_update(slice))
.finalize();
// Write b_1 to the output buffer
buf[..D::output_size()].copy_from_slice(&b_1);
for i in 2..=ell {
// Get the last digest we produced
let i_ = i as usize;
let last_b = &buf[(i_ - 2) * D::output_size()..(i_ - 1) * D::output_size()];
// XOR it with b_0
let mut xor_bs = b_0.clone();
xor_bs
.as_mut_slice()
.iter_mut()
.zip(last_b)
.for_each(|(l, r)| *l ^= *r);
// Hash (b_0 ^ b_{i-1}) || i || dst_prime
let i_slice = &[i][..];
let b_i_input = once(xor_bs.as_slice())
.chain(once(i_slice))
.chain(dst_prime.clone());
let b_i = b_i_input
.fold(D::new(), |h, slice| h.chain_update(slice))
.finalize();
// Write b_i to the output buffer
buf[(i_ - 1) * D::output_size()..i_ * D::output_size()].copy_from_slice(&b_i);
}
&buf[..outlen]
}
#[cfg(test)]
mod test {
use crate::field::*;
@ -721,8 +798,15 @@ mod test {
}
}
#[cfg(feature = "digest")]
fn fe_from_test_vector(expected_hex: &str) -> FieldElement {
let mut expected_hash = hex::decode(expected_hex).unwrap();
expected_hash.reverse();
FieldElement::from_bytes(&expected_hash.try_into().unwrap())
}
/// Hash to field test vectors from
/// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
/// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.2
/// These are of the form (input_msg, output_field_elem)
#[cfg(feature = "digest")]
const RFC_HASH_TO_FIELD_KAT: &[(&[u8], &str)] = &[
@ -761,14 +845,67 @@ mod test {
let dst = "QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
for (msg, expected_hash_hex) in RFC_HASH_TO_FIELD_KAT {
let fe = FieldElement::hash_to_field::<Sha512>(&[msg], &[dst.as_bytes()]);
let expected_fe = {
let mut expected_hash = hex::decode(expected_hash_hex).unwrap();
expected_hash.reverse();
FieldElement::from_bytes(&expected_hash.try_into().unwrap())
};
let fe = FieldElement::hash_to_field::<Sha512, 1>(&[msg], &[dst.as_bytes()])[0];
let expected_fe = fe_from_test_vector(expected_hash_hex);
assert_eq!(fe, expected_fe);
}
}
/// Hash to field test vectors from
/// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.1
/// These are of the form (input_msg, output_field_elem, output_field_elem)
#[cfg(feature = "digest")]
const RFC_HASH_TO_FIELD_KAT_2: &[(&[u8], &str, &str)] = &[
(
b"",
"03fef4813c8cb5f98c6eef88fae174e6e7d5380de2b007799ac7ee712d203f3a",
"780bdddd137290c8f589dc687795aafae35f6b674668d92bf92ae793e6a60c75"
),
(
b"abc",
"5081955c4141e4e7d02ec0e36becffaa1934df4d7a270f70679c78f9bd57c227",
"005bdc17a9b378b6272573a31b04361f21c371b256252ae5463119aa0b925b76"
),
(
b"abcdef0123456789",
"285ebaa3be701b79871bcb6e225ecc9b0b32dff2d60424b4c50642636a78d5b3",
"2e253e6a0ef658fedb8e4bd6a62d1544fd6547922acb3598ec6b369760b81b31"
),
(
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"4fedd25431c41f2a606952e2945ef5e3ac905a42cf64b8b4d4a83c533bf321af",
"02f20716a5801b843987097a8276b6d869295b2e11253751ca72c109d37485a9"
),
(
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"6e34e04a5106e9bd59f64aba49601bf09d23b27f7b594e56d5de06df4a4ea33b",
"1c1c2cb59fc053f44b86c5d5eb8c1954b64976d0302d3729ff66e84068f5fd96"
)
];
#[test]
#[cfg(feature = "digest")]
fn hash_to_field_2() {
use sha2::Sha512;
let dst = "QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_RO_";
for (msg, expected_hash_hex_1, expected_hash_hex_2) in
crate::field::test::RFC_HASH_TO_FIELD_KAT_2
{
let fe = FieldElement::hash_to_field::<Sha512, 2>(&[msg], &[dst.as_bytes()]);
let expected_fe_1 = fe_from_test_vector(expected_hash_hex_1);
assert_eq!(fe[0], expected_fe_1);
let expected_fe_2 = fe_from_test_vector(expected_hash_hex_2);
assert_eq!(fe[1], expected_fe_2);
}
}
}

View file

@ -54,7 +54,9 @@ use core::{
ops::{Mul, MulAssign},
};
use crate::constants::{APLUS2_OVER_FOUR, MONTGOMERY_A, MONTGOMERY_A_NEG};
use crate::constants::APLUS2_OVER_FOUR;
#[cfg(feature = "digest")]
use crate::constants::{MONTGOMERY_A, MONTGOMERY_A_NEG, SQRT_M1};
use crate::edwards::{CompressedEdwardsY, EdwardsPoint};
use crate::field::FieldElement;
use crate::scalar::{Scalar, clamp_integer};
@ -62,12 +64,26 @@ use crate::scalar::{Scalar, clamp_integer};
use crate::traits::Identity;
use subtle::Choice;
use subtle::ConditionallySelectable;
use subtle::ConstantTimeEq;
use subtle::{ConditionallyNegatable, ConditionallySelectable};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
// We need the const 2^((p+3)/8) for elligator_encode. These defs are checked in tests::consts()
#[cfg(all(curve25519_dalek_bits = "32", feature = "digest"))]
const FE_C2: FieldElement = FieldElement::from_limbs([
34513073, 25610706, 9377949, 3500415, 12389472, 33281959, 41962654, 31548777, 326685, 11406482,
]);
#[cfg(all(curve25519_dalek_bits = "64", feature = "digest"))]
const FE_C2: FieldElement = FieldElement::from_limbs([
1718705420411057,
234908883556509,
2233514472574048,
2117202627021982,
765476049583133,
]);
/// Holds the \\(u\\)-coordinate of a point on the Montgomery form of
/// Curve25519 or its twist.
#[derive(Copy, Clone, Debug, Default)]
@ -252,33 +268,98 @@ impl MontgomeryPoint {
}
}
/// Perform the Elligator2 mapping to a Montgomery point. Returns a Montgomery point and a `Choice`
/// determining whether eps is a square. This is required by the standard to determine the
/// sign of the v coordinate.
#[cfg(feature = "digest")]
/// Perform the Elligator2 mapping to a tuple `(xn, xd, yn, yd)` such that
/// `(xn / xd, yn / yd)` is a point on curve25519.
///
/// See <https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method>
//
#[allow(unused)]
pub(crate) fn elligator_encode(r_0: &FieldElement) -> (MontgomeryPoint, Choice) {
pub(crate) fn elligator_encode(
u: &FieldElement,
) -> (FieldElement, FieldElement, FieldElement, FieldElement) {
// We follow https://www.rfc-editor.org/rfc/rfc9380.html#appendix-G.2.1
use core::ops::Neg;
let one = FieldElement::ONE;
let d_1 = &one + &r_0.square2(); /* 2r^2 */
let c2 = FE_C2;
let d = &MONTGOMERY_A_NEG * &(d_1.invert()); /* A/(1+2r^2) */
// 1. tv1 = u^2
// 2. tv1 = 2 * tv1
let tv1 = u.square2();
// 3. xd = tv1 + 1
let xd = &one + &tv1;
// 4. x1n = -J
let x1n = MONTGOMERY_A_NEG;
// 5. tv2 = xd^2
let tv2 = xd.square();
// 6. gxd = tv2 * xd
let gxd = &tv2 * &xd;
// 7. gx1 = J * tv1
let gx1 = &MONTGOMERY_A * &tv1;
// 8. gx1 = gx1 * x1n
let gx1 = &gx1 * &x1n;
// 9. gx1 = gx1 + tv2
let gx1 = &gx1 + &tv2;
// 10. gx1 = gx1 * x1n
let gx1 = &gx1 * &x1n;
// 11. tv3 = gxd^2
let tv3 = gxd.square();
// 12. tv2 = tv3^2
let tv2 = tv3.square();
// 13. tv3 = tv3 * gxd
let tv3 = &tv3 * &gxd;
// 14. tv3 = tv3 * gx1
let tv3 = &tv3 * &gx1;
// 15. tv2 = tv2 * tv3
let tv2 = &tv2 * &tv3;
// 16. y11 = tv2^c4
let y11 = tv2.pow_p58();
// 17. y11 = y11 * tv3
let y11 = &y11 * &tv3;
// 18. y12 = y11 * c3
let y12 = &y11 * &SQRT_M1;
// 19. tv2 = y11^2
let tv2 = y11.square();
// 20. tv2 = tv2 * gxd
let tv2 = &tv2 * &gxd;
// 21. e1 = tv2 == gx1
let e1 = tv2.ct_eq(&gx1);
// 22. y1 = CMOV(y12, y11, e1)
let y1 = FieldElement::conditional_select(&y12, &y11, e1);
// 23. x2n = x1n * tv1
let x2n = &x1n * &tv1;
// 24. y21 = y11 * u
let y21 = &y11 * u;
// 25. y21 = y21 * c2
let y21 = &y21 * &c2;
// 26. y22 = y21 * c3
let y22 = &y21 * &SQRT_M1;
// 27. gx2 = gx1 * tv1
let gx2 = &gx1 * &tv1;
// 28. tv2 = y21^2
let tv2 = y21.square();
// 29. tv2 = tv2 * gxd
let tv2 = &tv2 * &gxd;
// 30. e2 = tv2 == gx2
let e2 = tv2.ct_eq(&gx2);
// 31. y2 = CMOV(y22, y21, e2)
let y2 = FieldElement::conditional_select(&y22, &y21, e2);
// 32. tv2 = y1^2
let tv2 = y1.square();
// 33. tv2 = tv2 * gxd
let tv2 = &tv2 * &gxd;
// 34. e3 = tv2 == gx1
let e3 = tv2.ct_eq(&gx1);
// 35. xn = CMOV(x2n, x1n, e3)
let xn = FieldElement::conditional_select(&x2n, &x1n, e3);
// 36. y = CMOV(y2, y1, e3)
let y = FieldElement::conditional_select(&y2, &y1, e3);
// 37. e4 = sgn0(y) == 1
let e4 = y.is_negative();
// 38. y = CMOV(y, -y, e3 XOR e4)
let y = FieldElement::conditional_select(&y, &y.neg(), e3 ^ e4);
// 39. return (xn, xd, y, 1)
let d_sq = &d.square();
let au = &MONTGOMERY_A * &d;
let inner = &(d_sq + &au) + &one;
let eps = &d * &inner; /* eps = d^3 + Ad^2 + d */
let (eps_is_sq, _eps) = FieldElement::sqrt_ratio_i(&eps, &one);
let zero = FieldElement::ZERO;
let Atemp = FieldElement::conditional_select(&MONTGOMERY_A, &zero, eps_is_sq); /* 0, or A if nonsquare*/
let mut u = &d + &Atemp; /* d, or d+A if nonsquare */
u.conditional_negate(!eps_is_sq); /* d, or -d-A if nonsquare */
(MontgomeryPoint(u.to_bytes()), eps_is_sq)
(xn, xd, y, one)
}
/// A `ProjectivePoint` holds a point on the projective line
@ -434,9 +515,6 @@ mod test {
use super::*;
use crate::constants;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use rand_core::{CryptoRng, RngCore, TryRngCore};
#[test]
@ -639,6 +717,7 @@ mod test {
}
#[cfg(feature = "alloc")]
#[cfg(feature = "digest")]
const ELLIGATOR_CORRECT_OUTPUT: [u8; 32] = [
0x5f, 0x35, 0x20, 0x00, 0x1c, 0x6c, 0x99, 0x36, 0xa3, 0x12, 0x06, 0xaf, 0xe7, 0xc7, 0xac,
0x22, 0x4e, 0x88, 0x61, 0x61, 0x9b, 0xf9, 0x88, 0x72, 0x44, 0x49, 0x15, 0x89, 0x9d, 0x95,
@ -647,20 +726,37 @@ mod test {
#[test]
#[cfg(feature = "alloc")]
#[cfg(feature = "digest")]
fn montgomery_elligator_correct() {
use alloc::vec::Vec;
let bytes: Vec<u8> = (0u8..32u8).collect();
let bits_in: [u8; 32] = (&bytes[..]).try_into().expect("Range invariant broken");
let fe = FieldElement::from_bytes(&bits_in);
let (eg, _) = elligator_encode(&fe);
assert_eq!(eg.to_bytes(), ELLIGATOR_CORRECT_OUTPUT);
let (un, ud, ..) = elligator_encode(&fe);
let u = &un * &ud.invert();
assert_eq!(u.to_bytes(), ELLIGATOR_CORRECT_OUTPUT);
}
#[test]
#[cfg(feature = "digest")]
fn montgomery_elligator_zero_zero() {
let zero = [0u8; 32];
let fe = FieldElement::from_bytes(&zero);
let (eg, _) = elligator_encode(&fe);
assert_eq!(eg.to_bytes(), zero);
let (un, ud, ..) = elligator_encode(&fe);
let u = &un * &ud.invert();
assert_eq!(u.to_bytes(), zero);
}
// Check that FE_C2 is correctly defined
#[test]
#[cfg(feature = "digest")]
fn c2() {
let one = FieldElement::ONE;
let two = &one + &one;
// c2 = 2^((p+3)/8) = 2^((p-5)/8 + 8/8) = 2*2^((p-5)/8)
let c2 = &two * &(two.pow_p58());
assert_eq!(c2, FE_C2);
}
}