diff --git a/curve25519-dalek/CHANGELOG.md b/curve25519-dalek/CHANGELOG.md index 5052a47..eb66b84 100644 --- a/curve25519-dalek/CHANGELOG.md +++ b/curve25519-dalek/CHANGELOG.md @@ -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 diff --git a/curve25519-dalek/Cargo.toml b/curve25519-dalek/Cargo.toml index fe53e4c..f647860 100644 --- a/curve25519-dalek/Cargo.toml +++ b/curve25519-dalek/Cargo.toml @@ -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" } diff --git a/curve25519-dalek/benches/dalek_benchmarks.rs b/curve25519-dalek/benches/dalek_benchmarks.rs index 0b2653e..ef136ff 100644 --- a/curve25519-dalek/benches/dalek_benchmarks.rs +++ b/curve25519-dalek/benches/dalek_benchmarks.rs @@ -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(c: &mut BenchmarkGroup) { + 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::(&[&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); } } diff --git a/curve25519-dalek/src/backend/serial/fiat_u32/field.rs b/curve25519-dalek/src/backend/serial/fiat_u32/field.rs index 4c46781..db05738 100644 --- a/curve25519-dalek/src/backend/serial/fiat_u32/field.rs +++ b/curve25519-dalek/src/backend/serial/fiat_u32/field.rs @@ -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; diff --git a/curve25519-dalek/src/backend/serial/fiat_u64/field.rs b/curve25519-dalek/src/backend/serial/fiat_u64/field.rs index 0d8e62f..29b7060 100644 --- a/curve25519-dalek/src/backend/serial/fiat_u64/field.rs +++ b/curve25519-dalek/src/backend/serial/fiat_u64/field.rs @@ -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; diff --git a/curve25519-dalek/src/backend/serial/u32/constants.rs b/curve25519-dalek/src/backend/serial/u32/constants.rs index 03e094e..191bb13 100644 --- a/curve25519-dalek/src/backend/serial/u32/constants.rs +++ b/curve25519-dalek/src/backend/serial/u32/constants.rs @@ -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, diff --git a/curve25519-dalek/src/backend/serial/u32/field.rs b/curve25519-dalek/src/backend/serial/u32/field.rs index 0515341..32c4587 100644 --- a/curve25519-dalek/src/backend/serial/u32/field.rs +++ b/curve25519-dalek/src/backend/serial/u32/field.rs @@ -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) } diff --git a/curve25519-dalek/src/backend/serial/u64/constants.rs b/curve25519-dalek/src/backend/serial/u64/constants.rs index baeb1dd..22e0ccc 100644 --- a/curve25519-dalek/src/backend/serial/u64/constants.rs +++ b/curve25519-dalek/src/backend/serial/u64/constants.rs @@ -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, diff --git a/curve25519-dalek/src/backend/serial/u64/field.rs b/curve25519-dalek/src/backend/serial/u64/field.rs index ad071f0..631d975 100644 --- a/curve25519-dalek/src/backend/serial/u64/field.rs +++ b/curve25519-dalek/src/backend/serial/u64/field.rs @@ -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 ]) } diff --git a/curve25519-dalek/src/constants.rs b/curve25519-dalek/src/constants.rs index 3428546..e2f7977 100644 --- a/curve25519-dalek/src/constants.rs +++ b/curve25519-dalek/src/constants.rs @@ -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) + } } diff --git a/curve25519-dalek/src/edwards.rs b/curve25519-dalek/src/edwards.rs index 7da4678..dac3a86 100644 --- a/curve25519-dalek/src/edwards.rs +++ b/curve25519-dalek/src/edwards.rs @@ -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(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint + where + D: BlockSizeUser + Default + FixedOutput + HashMarker, + D::BlockSize: IsGreater, + { + // For reference see + // https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2 + + let fe = FieldElement::hash_to_field::(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> { + fn signal_test_vectors() -> Vec> { 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::(&[&input], &[dst]); + assert_eq!(computed, expected_output, "Failed in test {}", index); + } + } } diff --git a/curve25519-dalek/src/field.rs b/curve25519-dalek/src/field.rs index 946bdcf..76fba2d 100644 --- a/curve25519-dalek/src/field.rs +++ b/curve25519-dalek/src/field.rs @@ -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(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> Self + where + D: BlockSizeUser + Default + FixedOutput + HashMarker, + D::BlockSize: IsGreater, + { + let l_i_b_str = 48u16.to_be_bytes(); + let z_pad = GenericArray::::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::(&[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); + } + } } diff --git a/curve25519-dalek/src/montgomery.rs b/curve25519-dalek/src/montgomery.rs index 29f0e49..35ae09a 100644 --- a/curve25519-dalek/src/montgomery.rs +++ b/curve25519-dalek/src/montgomery.rs @@ -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 +/// See // -// 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); } }