mirror of
https://github.com/saymrwulf/curve25519-dalek-source.git
synced 2026-09-03 20:13:48 +00:00
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:
parent
44bb8cb7c1
commit
25a9dbb811
13 changed files with 501 additions and 50 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue