curve: Hash to curve and field as defined in the standard (#377)

* Implementation of `hash_to_field` as defined in the standard
* Implementation of `hash_to_curve` as defined in the standard, by changing the mechanism over which we chose the sign.
* For the point above, had to change the `elligator_encode` to return whether `eps` is a square or not (required for `hash_to_curve`).
* Included test vectors of the draft.
* Included `FieldElement::from_bytes_wide(bytes: &u8; 64])` to reduce integers encoded in 64 bytes.
This commit is contained in:
Iñigo Querejeta Azurmendi 2025-07-05 22:01:26 +02:00 committed by GitHub
parent 44bb8cb7c1
commit 25a9dbb811
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 501 additions and 50 deletions

View file

@ -6,6 +6,7 @@ major series.
## Unreleased
* Move AVX-512 backend selection logic to a separate CFG flag that requires nightly
* Add Elligator2 hashing methods `EdwardsPoint::hash_to_curve()` and `FieldElement::hash_to_field()`
## 4.x series

View file

@ -59,7 +59,7 @@ zeroize = { version = "1", default-features = false, optional = true }
cpufeatures = "0.2.17"
[target.'cfg(curve25519_dalek_backend = "fiat")'.dependencies]
fiat-crypto = { version = "0.2.1", default-features = false }
fiat-crypto = { version = "0.3.0", default-features = false }
[features]
default = ["alloc", "precomputed-tables", "zeroize"]
@ -68,6 +68,7 @@ precomputed-tables = []
legacy_compatibility = []
group = ["dep:group", "rand_core"]
group-bits = ["group", "ff/bits"]
digest = ["dep:digest", "digest/core-api"]
[target.'cfg(all(not(curve25519_dalek_backend = "fiat"), not(curve25519_dalek_backend = "serial"), target_arch = "x86_64"))'.dependencies]
curve25519-dalek-derive = { version = "0.1", path = "../curve25519-dalek-derive" }

View file

@ -1,10 +1,12 @@
#![allow(non_snake_case)]
use rand::{rngs::OsRng, thread_rng};
use rand::{rngs::OsRng, thread_rng, RngCore};
use criterion::{
criterion_main, measurement::Measurement, BatchSize, BenchmarkGroup, BenchmarkId, Criterion,
};
#[cfg(feature = "digest")]
use sha2::Sha512;
use curve25519_dalek::constants;
use curve25519_dalek::scalar::Scalar;
@ -72,6 +74,21 @@ mod edwards_benches {
});
}
#[cfg(feature = "digest")]
fn hash_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
let mut rng = thread_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 hash to curve (SHA-512, input size 32 bytes)",
|b| b.iter(|| EdwardsPoint::hash_to_curve::<Sha512>(&[&msg], &[&domain_sep])),
);
}
pub(crate) fn edwards_benches() {
let mut c = Criterion::default();
let mut g = c.benchmark_group("edwards benches");
@ -83,6 +100,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);
hash_to_curve(&mut g);
}
}

View file

@ -230,7 +230,7 @@ impl FieldElement2625 {
/// encoding of every field element should decode, re-encode to
/// the canonical encoding, and check that the input was
/// canonical.
pub fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
pub const fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
let mut temp = [0u8; 32];
temp.copy_from_slice(data);
temp[31] &= 127u8;

View file

@ -207,7 +207,7 @@ impl FieldElement51 {
/// the canonical encoding, and check that the input was
/// canonical.
///
pub fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
pub const fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
let mut temp = [0u8; 32];
temp.copy_from_slice(bytes);
temp[31] &= 127u8;

View file

@ -30,6 +30,13 @@ pub(crate) const MINUS_ONE: FieldElement2625 = FieldElement2625::from_limbs([
33554431,
]);
/// sqrt(-486664)
#[cfg(feature = "digest")]
pub(crate) const ED25519_SQRTAM2: FieldElement2625 = FieldElement2625::from_limbs([
54885894, 25242303, 55597453, 9067496, 51808079, 33312638, 25456129, 14121551, 54921728,
3972023,
]);
/// Edwards `d` value, equal to `-121665/121666 mod p`.
pub(crate) const EDWARDS_D: FieldElement2625 = FieldElement2625::from_limbs([
56195235, 13857412, 51736253, 6949390, 114729, 24766616, 60832955, 30306712, 48412415, 21499315,

View file

@ -333,14 +333,14 @@ impl FieldElement2625 {
/// In other words, each coefficient of the result is bounded by
/// either `2^(25 + 0.007)` or `2^(26 + 0.007)`, as appropriate.
#[rustfmt::skip] // keep alignment of carry chain
fn reduce(mut z: [u64; 10]) -> FieldElement2625 {
const fn reduce(mut z: [u64; 10]) -> FieldElement2625 {
const LOW_25_BITS: u64 = (1 << 25) - 1;
const LOW_26_BITS: u64 = (1 << 26) - 1;
/// Carry the value from limb i = 0..8 to limb i+1
#[inline(always)]
fn carry(z: &mut [u64; 10], i: usize) {
const fn carry(z: &mut [u64; 10], i: usize) {
debug_assert!(i < 9);
if i % 2 == 0 {
// Even limbs have 26 bits
@ -401,29 +401,32 @@ impl FieldElement2625 {
/// the canonical encoding, and check that the input was
/// canonical.
#[rustfmt::skip] // keep alignment of h[*] values
pub fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
pub const fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
#[inline]
fn load3(b: &[u8]) -> u64 {
(b[0] as u64) | ((b[1] as u64) << 8) | ((b[2] as u64) << 16)
const fn load3_at(b: &[u8], i: usize) -> u64 {
(b[i] as u64) | ((b[i + 1] as u64) << 8) | ((b[i + 2] as u64) << 16)
}
#[inline]
fn load4(b: &[u8]) -> u64 {
(b[0] as u64) | ((b[1] as u64) << 8) | ((b[2] as u64) << 16) | ((b[3] as u64) << 24)
const fn load4_at(b: &[u8], i: usize) -> u64 {
(b[i] as u64)
| ((b[i + 1] as u64) << 8)
| ((b[i + 2] as u64) << 16)
| ((b[i + 3] as u64) << 24)
}
let mut h = [0u64;10];
const LOW_23_BITS: u64 = (1 << 23) - 1;
h[0] = load4(&data[ 0..]);
h[1] = load3(&data[ 4..]) << 6;
h[2] = load3(&data[ 7..]) << 5;
h[3] = load3(&data[10..]) << 3;
h[4] = load3(&data[13..]) << 2;
h[5] = load4(&data[16..]);
h[6] = load3(&data[20..]) << 7;
h[7] = load3(&data[23..]) << 5;
h[8] = load3(&data[26..]) << 4;
h[9] = (load3(&data[29..]) & LOW_23_BITS) << 2;
h[0] = load4_at(data, 0);
h[1] = load3_at(data, 4) << 6;
h[2] = load3_at(data, 7) << 5;
h[3] = load3_at(data, 10) << 3;
h[4] = load3_at(data, 13) << 2;
h[5] = load4_at(data, 16);
h[6] = load3_at(data, 20) << 7;
h[7] = load3_at(data, 23) << 5;
h[8] = load3_at(data, 26) << 4;
h[9] = (load3_at(data, 29) & LOW_23_BITS) << 2;
FieldElement2625::reduce(h)
}

View file

@ -31,6 +31,16 @@ pub(crate) const MINUS_ONE: FieldElement51 = FieldElement51::from_limbs([
2251799813685247,
]);
/// sqrt(-486664)
#[cfg(feature = "digest")]
pub(crate) const ED25519_SQRTAM2: FieldElement51 = FieldElement51::from_limbs([
1693982333959686,
608509411481997,
2235573344831311,
947681270984193,
266558006233600,
]);
/// Edwards `d` value, equal to `-121665/121666 mod p`.
pub(crate) const EDWARDS_D: FieldElement51 = FieldElement51::from_limbs([
929955233495203,

View file

@ -335,30 +335,30 @@ impl FieldElement51 {
/// canonical.
///
#[rustfmt::skip] // keep alignment of bit shifts
pub fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
let load8 = |input: &[u8]| -> u64 {
(input[0] as u64)
| ((input[1] as u64) << 8)
| ((input[2] as u64) << 16)
| ((input[3] as u64) << 24)
| ((input[4] as u64) << 32)
| ((input[5] as u64) << 40)
| ((input[6] as u64) << 48)
| ((input[7] as u64) << 56)
};
pub const fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
const fn load8_at(input: &[u8], i: usize) -> u64 {
(input[i] as u64)
| ((input[i + 1] as u64) << 8)
| ((input[i + 2] as u64) << 16)
| ((input[i + 3] as u64) << 24)
| ((input[i + 4] as u64) << 32)
| ((input[i + 5] as u64) << 40)
| ((input[i + 6] as u64) << 48)
| ((input[i + 7] as u64) << 56)
}
let low_51_bit_mask = (1u64 << 51) - 1;
FieldElement51(
// load bits [ 0, 64), no shift
[ load8(&bytes[ 0..]) & low_51_bit_mask
[ load8_at(bytes, 0) & low_51_bit_mask
// load bits [ 48,112), shift to [ 51,112)
, (load8(&bytes[ 6..]) >> 3) & low_51_bit_mask
, (load8_at(bytes, 6) >> 3) & low_51_bit_mask
// load bits [ 96,160), shift to [102,160)
, (load8(&bytes[12..]) >> 6) & low_51_bit_mask
, (load8_at(bytes, 12) >> 6) & low_51_bit_mask
// load bits [152,216), shift to [153,216)
, (load8(&bytes[19..]) >> 1) & low_51_bit_mask
, (load8_at(bytes, 19) >> 1) & low_51_bit_mask
// load bits [192,256), shift to [204,112)
, (load8(&bytes[24..]) >> 12) & low_51_bit_mask
, (load8_at(bytes, 24) >> 12) & low_51_bit_mask
])
}

View file

@ -175,4 +175,14 @@ mod test {
let should_be_ad_minus_one = constants::SQRT_AD_MINUS_ONE.square();
assert_eq!(should_be_ad_minus_one, ad_minus_one);
}
/// Test that ED25519_SQRTAM2 squared is MONTGOMERY_A_NEG - 2
#[test]
#[cfg(feature = "digest")]
fn test_sqrt_a_minus_2() {
let one = FieldElement::ONE;
let a_minus_two = &(&constants::MONTGOMERY_A_NEG - &one) - &one;
assert_eq!(constants::ED25519_SQRTAM2.square(), a_minus_two)
}
}

View file

@ -105,7 +105,10 @@ use core::ops::{AddAssign, SubAssign};
use core::ops::{Mul, MulAssign};
#[cfg(feature = "digest")]
use digest::{generic_array::typenum::U64, Digest};
use digest::{
consts::True, crypto_common::BlockSizeUser, generic_array::typenum::U64, typenum::IsGreater,
Digest, FixedOutput, HashMarker,
};
#[cfg(feature = "group")]
use {
@ -261,6 +264,8 @@ impl TryFrom<&[u8]> for CompressedEdwardsY {
// structs containing `EdwardsPoint`s and use Serde's derived
// serializers to serialize those structures.
#[cfg(feature = "digest")]
use constants::ED25519_SQRTAM2;
#[cfg(feature = "serde")]
use serde::de::Visitor;
#[cfg(feature = "serde")]
@ -620,12 +625,62 @@ impl EdwardsPoint {
.collect()
}
#[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
/// 255 bytes.
///
/// # Panics
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
pub fn hash_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>(bytes, domain_sep);
let (M1, is_sq) = crate::montgomery::elligator_encode(&fe);
// 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()
}
#[cfg(feature = "digest")]
/// Maps the digest of the input bytes to the curve. This is NOT a hash-to-curve function, as
/// it produces points with a non-uniform distribution. Rather, it performs something that
/// resembles (but is not) half of the
/// [`hash_to_curve`](https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#section-3-4.2.1)
/// [`hash_to_curve`](https://www.rfc-editor.org/rfc/rfc9380.html#section-3-4.2.1)
/// function from the Elligator2 spec.
///
/// For a hash to curve with uniform distribution and compatible with the spec, see
/// [`Self::hash_to_curve`].
#[deprecated(
since = "4.0.0",
note = "previously named `hash_from_bytes`, this is not a secure hash function"
@ -644,7 +699,7 @@ impl EdwardsPoint {
let fe = FieldElement::from_bytes(&res);
let M1 = crate::montgomery::elligator_encode(&fe);
let (M1, _) = crate::montgomery::elligator_encode(&fe);
let E1_opt = M1.to_edwards(sign_bit);
E1_opt
@ -2359,7 +2414,7 @@ mod test {
////////////////////////////////////////////////////////////
#[cfg(all(feature = "alloc", feature = "digest"))]
fn test_vectors() -> Vec<Vec<&'static str>> {
fn signal_test_vectors() -> Vec<Vec<&'static str>> {
vec![
vec![
"214f306e1576f5a7577636fe303ca2c625b533319f52442b22a9fa3b7ede809f",
@ -2408,7 +2463,7 @@ mod test {
#[allow(deprecated)]
#[cfg(all(feature = "alloc", feature = "digest"))]
fn elligator_signal_test_vectors() {
for vector in test_vectors().iter() {
for vector in signal_test_vectors().iter() {
let input = hex::decode(vector[0]).unwrap();
let output = hex::decode(vector[1]).unwrap();
@ -2416,4 +2471,71 @@ mod test {
assert_eq!(point.compress().to_bytes(), output[..]);
}
}
// Hash-to-curve test vectors from
// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
// 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"",
"1ff2b70ecf862799e11b7ae744e3489aa058ce805dd323a936375a84695e76da",
"222e314d04a4d5725e9f2aff9fb2a6b69ef375a1214eb19021ceab2d687f0f9b",
),
(
b"abc",
"5f13cc69c891d86927eb37bd4afc6672360007c63f68a33ab423a3aa040fd2a8",
"67732d50f9a26f73111dd1ed5dba225614e538599db58ba30aaea1f5c827fa42",
),
(
b"abcdef0123456789",
"1dd2fefce934ecfd7aae6ec998de088d7dd03316aa1847198aecf699ba6613f1",
"2f8a6c24dd1adde73909cada6a4a137577b0f179d336685c4a955a0a8e1a86fb",
),
(
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"35fbdc5143e8a97afd3096f2b843e07df72e15bfca2eaf6879bf97c5d3362f73",
"2af6ff6ef5ebba128b0774f4296cb4c2279a074658b083b8dcca91f57a603450",
),
(
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"6e5e1f37e99345887fc12111575fc1c3e36df4b289b8759d23af14d774b66bff",
"2c90c3d39eb18ff291d33441b35f3262cdd307162cc97c31bfcc7a4245891a37"
)
];
#[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_";
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 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,
}
};
let computed = EdwardsPoint::hash_to_curve::<sha2::Sha512>(&[&input], &[dst]);
assert_eq!(computed, expected_output, "Failed in test {}", index);
}
}
}

View file

@ -35,6 +35,14 @@ use subtle::ConstantTimeEq;
use crate::backend;
use crate::constants;
#[cfg(feature = "digest")]
use digest::{
core_api::BlockSizeUser,
generic_array::{typenum::U64, GenericArray},
typenum::{IsGreater, True},
Digest, FixedOutput, HashMarker,
};
cfg_if! {
if #[cfg(curve25519_dalek_backend = "fiat")] {
/// A `FieldElement` represents an element of the field
@ -91,6 +99,47 @@ impl ConstantTimeEq for FieldElement {
}
impl FieldElement {
/// Load a `FieldElement` from 64 bytes, by reducing modulo q.
#[cfg(feature = "digest")]
pub(crate) fn from_bytes_wide(bytes: &[u8; 64]) -> Self {
let mut fl = [0u8; 32];
let mut gl = [0u8; 32];
fl.copy_from_slice(&bytes[..32]);
gl.copy_from_slice(&bytes[32..]);
// Mask off the top bits of both halves, since from_bytes masks them off anyway. We'll add
// them back in later.
let fl_top_bit = (fl[31] >> 7) as u16;
let gl_top_bit = (gl[31] >> 7) as u16;
fl[31] &= 0x7f;
gl[31] &= 0x7f;
// Interpret both sides as field elements
let mut fe_f = Self::from_bytes(&fl);
let fe_g = Self::from_bytes(&gl);
// The full field elem is now fe_f + 2²⁵⁵ fl_top_bit + 2²⁵⁶ fe_g + 2⁵¹¹ gl_top_bit
// Add the masked off bits back to fe_f. fl_top_bit, if set, is 2^255 ≡ 19 (mod q).
// gl_top_bit, if set, is 2^511 ≡ 722 (mod q)
let top_bits_sum = {
// This only need to be a u16 because the max value is 741
let addend: u16 = fl_top_bit * 19 + gl_top_bit * 722;
let mut addend_bytes = [0u8; 32];
addend_bytes[..2].copy_from_slice(&addend.to_le_bytes());
Self::from_bytes(&addend_bytes)
};
fe_f += &top_bits_sum;
// Now add the high half into fe_f. The RHS is multiplied by 2^256 ≡ 38 (mod q)
const THIRTY_EIGHT: FieldElement = FieldElement::from_bytes(&[
38, 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,
]);
fe_f += &(&THIRTY_EIGHT * &fe_g);
fe_f
}
/// Determine if this `FieldElement` is negative, in the sense
/// used in the ed25519 paper: `x` is negative if the low bit is
/// set.
@ -303,6 +352,62 @@ impl FieldElement {
pub(crate) fn invsqrt(&self) -> (Choice, FieldElement) {
FieldElement::sqrt_ratio_i(&FieldElement::ONE, self)
}
#[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.
///
/// # Panics
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
pub fn hash_to_field<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> Self
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 = GenericArray::<u8, D::BlockSize>::default();
let mut hasher = D::new().chain_update(z_pad);
for slice in bytes {
hasher = hasher.chain_update(slice);
}
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)
}
}
#[cfg(test)]
@ -492,4 +597,178 @@ mod test {
fn batch_invert_empty() {
FieldElement::batch_invert(&mut []);
}
// The following two consts were generated with the following sage script:
//
// import random
//
// F = GF(2**255 - 19)
// # Use a seed to make sure we produce the same test vectors every time
// random.seed("Ozamataz Buckshank")
//
// # Generates test vectors, each of the form (input_bytes, reduced_field_elem_bytes),
// # where input_bytes is length input_bytes_len
// def gen_example(input_bytes_len):
// # Generate random bytes
// input_bytes = [random.randint(0, 255) for _ in range(input_bytes_len)]
//
// # Now convert to a field element and get the reduced byte representation
// elem = F(int.from_bytes(input_bytes, byteorder='little'))
// reduced_bytes = list(int(elem).to_bytes(32, byteorder='little'))
//
// # Format input and output as hex strings
// input_bytes_hex = ''.join(f'{byte:02x}' for byte in input_bytes)
// reduced_bytes_hex = ''.join(f'{byte:02x}' for byte in reduced_bytes)
// return f"(\"{input_bytes_hex}\", \"{reduced_bytes_hex}\")"
//
// print("SET 1: Input bytes are length 64")
// for _ in range(5):
// print(gen_example(64))
//
// print("SET 2: Input bytes are length 48")
// for _ in range(5):
// print(gen_example(48))
/// Test vectors for FieldElement::from_bytes_wide. Elements are of the form (len-64 bytestring,
/// reduced field element)
#[cfg(feature = "digest")]
const FROM_BYTES_WIDE_KAT_BIG: &[(&str, &str)] = &[
(
"77b663085cac0e916f40dbeea5116f201816406e68ccf01b32a97162ae1d5bf95d0d01c2c72fbeeb27a63\
5b85b715d5ce6f74118a60a7aec53c798ad648a482f",
"62b38bd402c4498f5cead14643e54dd649e20a0810610e36a73f1f27a0a81f7e",
),
(
"d437c75ec79886650243a79c62933bb307eb12ff16d05db4a6a8a877f4a91abb6eeb64d2e20519c021799\
3a1dc5639283a06639985a2c892208171503335afb5",
"3d2ec29972783de9043e8b982278beaba9d7c5c3ebef257e7cd38168928f1c33",
),
(
"6daa9e1abe6c604fb6e841c04bf90a6ef88aef6b1eab17dd44f7207ef472cd2d54bac849f703e64f36e56\
77e7e86b82be7d26aa220daf1f208bb36dcc1a12338",
"28546a0e7303852bc6eead8312f06eeb48d9ca87f60bfeec98ba402ebb751703",
),
(
"c3920e326dbf806a50105be78263c1dc9390fb4741587b250cd758c2bfa3ed70faedbbc5f9b1d024e00fe\
7d7daf796866853f42e72d638e6533c5eb5b7caf3c6",
"40eaf38b802a7be1956ba7f3fe2d2ad717f23f40342deb5180cb55ae04bb1d79",
),
(
"23f143c72ead6c0f336b4e746a06921f0eb180002e8ce916d196de16216788617c6aeb90a074a85196f03\
81375011248927c1215e9ec65b382a6ec556fb3f504",
"b1bf354a04fd6d2e8321c24ecb3d3ed2c42e3f21c7b60ab8374effd7a709011e",
),
];
/// Test vectors for FieldElement::from_bytes_wide. Elements are of the form (len-48 bytestring,
/// reduced field element)
#[cfg(feature = "digest")]
const FROM_BYTES_WIDE_KAT_MEDIUM: &[(&str, &str)] = &[
(
"82e9cbe4928e3d0bbf1f91824a91acfb30d929f7a2fa5cbcc967c63ea0f3357c29c19f1bc9dcad69d85c1\
c6265970685",
"989582fe6c540cbbdee7c612570aa7ba44d929f7a2fa5cbcc967c63ea0f3357c",
),
(
"5480494df4fb3a3b19da17e1c8b9192ccb09ec76720321977079300c42c17b9e95b01eb37ffe7048fcd1c\
9e6094da6c4",
"85b6d7e3e8c200fc8b050d234129c95ce809ec76720321977079300c42c17b1e",
),
(
"93ec8a480dde098f74bcd341ef4f248f6440cc6e631d7000784f66975a4fd628438bb1350ba4c1421fec3\
670decced06",
"8598e540b737c87718c9fae9f3b870966540cc6e631d7000784f66975a4fd628",
),
(
"fd0154ff9a5c4c9ee4e8183c23db97018e0e6201a812f6d4faedda50652d51f65c110b9a1a100a3fc3ff1\
c4ea3cf22e4",
"b895f8dc8dc0caf9dfdf66d460adc2deaf0e6201a812f6d4faedda50652d5176",
),
(
"0e829dc955e0a1e0dbda9849cb2022b295275782348bd6308b3d0c5836f3ca0130911a17fd54054c3a0f8\
b2486f8ce85",
"2e0f8f37e77d6c29831d3db6b404db8ea9275782348bd6308b3d0c5836f3ca01",
),
];
#[cfg(feature = "digest")]
#[test]
fn from_bytes_wide() {
// Do the 64-byte input ones first
for (input_bytes, expected_reduced) in FROM_BYTES_WIDE_KAT_BIG {
let reduce_fe = FieldElement::from_bytes_wide(
&hex::decode(input_bytes)
.unwrap()
.as_slice()
.try_into()
.unwrap(),
);
assert_eq!(
&reduce_fe.to_bytes(),
hex::decode(expected_reduced).unwrap().as_slice()
);
}
// Now do the 48-byte inputs
for (input_bytes, expected_reduced) in FROM_BYTES_WIDE_KAT_MEDIUM {
let mut padded_input_bytes = [0u8; 64];
padded_input_bytes[..48].copy_from_slice(&hex::decode(input_bytes).unwrap());
let reduce_fe = FieldElement::from_bytes_wide(&padded_input_bytes);
assert_eq!(
&reduce_fe.to_bytes(),
hex::decode(expected_reduced).unwrap().as_slice()
);
}
}
/// Hash to field test vectors from
/// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
/// These are of the form (input_msg, output_field_elem)
#[cfg(feature = "digest")]
const RFC_HASH_TO_FIELD_KAT: &[(&[u8], &str)] = &[
(
b"",
"7f3e7fb9428103ad7f52db32f9df32505d7b427d894c5093f7a0f0374a30641d"
),
(
b"abc",
"09cfa30ad79bd59456594a0f5d3a76f6b71c6787b04de98be5cd201a556e253b"
),
(
b"abcdef0123456789",
"475ccff99225ef90d78cc9338e9f6a6bb7b17607c0c4428937de75d33edba941",
),
(
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"049a1c8bd51bcb2aec339f387d1ff51428b88d0763a91bcdf6929814ac95d03d"
),
(
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"3cb0178a8137cefa5b79a3a57c858d7eeeaa787b2781be4a362a2f0750d24fa0"
)
];
#[test]
#[cfg(feature = "digest")]
fn hash_to_field() {
use sha2::Sha512;
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())
};
assert_eq!(fe, expected_fe);
}
}
}

View file

@ -252,14 +252,14 @@ impl MontgomeryPoint {
}
}
/// Perform the Elligator2 mapping to a Montgomery point.
/// 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.
///
/// See <https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-10#section-6.7.1>
/// See <https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method>
//
// TODO Determine how much of the hash-to-group API should be exposed after the CFRG
// draft gets into a more polished/accepted state.
#[allow(unused)]
pub(crate) fn elligator_encode(r_0: &FieldElement) -> MontgomeryPoint {
pub(crate) fn elligator_encode(r_0: &FieldElement) -> (MontgomeryPoint, Choice) {
let one = FieldElement::ONE;
let d_1 = &one + &r_0.square2(); /* 2r^2 */
@ -278,7 +278,7 @@ pub(crate) fn elligator_encode(r_0: &FieldElement) -> MontgomeryPoint {
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())
(MontgomeryPoint(u.to_bytes()), eps_is_sq)
}
/// A `ProjectivePoint` holds a point on the projective line
@ -652,7 +652,7 @@ mod test {
let bits_in: [u8; 32] = (&bytes[..]).try_into().expect("Range invariant broken");
let fe = FieldElement::from_bytes(&bits_in);
let eg = elligator_encode(&fe);
let (eg, _) = elligator_encode(&fe);
assert_eq!(eg.to_bytes(), ELLIGATOR_CORRECT_OUTPUT);
}
@ -660,7 +660,7 @@ mod test {
fn montgomery_elligator_zero_zero() {
let zero = [0u8; 32];
let fe = FieldElement::from_bytes(&zero);
let eg = elligator_encode(&fe);
let (eg, _) = elligator_encode(&fe);
assert_eq!(eg.to_bytes(), zero);
}
}