Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hash to curve as defined in the standard #377

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/backend/serial/u32/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ pub(crate) const EDWARDS_D: FieldElement2625 = FieldElement2625([
56195235, 13857412, 51736253, 6949390, 114729, 24766616, 60832955, 30306712, 48412415, 21499315,
]);

/// sqrt(-486664)
pub(crate) const ED25519_SQRTAM2: FieldElement2625 = FieldElement2625([
54885894, 25242303, 55597453, 9067496, 51808079, 33312638, 25456129, 14121551, 54921728, 3972023,
]);

/// Edwards `2*d` value, equal to `2*(-121665/121666) mod p`.
pub(crate) const EDWARDS_D2: FieldElement2625 = FieldElement2625([
45281625, 27714825, 36363642, 13898781, 229458, 15978800, 54557047, 27058993, 29715967, 9444199,
Expand Down
20 changes: 20 additions & 0 deletions src/backend/serial/u32/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,26 @@ impl FieldElement2625 {
FieldElement2625::reduce(h)
}

/// Load a `FieldElement51` from 64 bytes, by reducing modulo q.
pub fn from_bytes_wide(hash: &[u8; 64]) -> FieldElement2625 {
let mut fl = [0u8; 32];
let mut gl = [0u8; 32];
fl.copy_from_slice(&hash[..32]);
gl.copy_from_slice(&hash[32..]);
fl[31] &= 0x7f;
gl[31] &= 0x7f;

let fe_f = Self::from_bytes(&fl);
let fe_g = Self::from_bytes(&gl);
let mut fe_f64 = fe_f.0.map(|i| i as u64);
let fe_g64 = fe_g.0.map(|i| i as u64);
fe_f64[0] = fe_f64[0] + (hash[31] >> 7) as u64 * 19 + (hash[63] >> 7) as u64 * 722;
for i in 0..10 {
fe_f64[i] += 38 * fe_g64[i];
}
Self::reduce(fe_f64)
}

/// Serialize this `FieldElement51` to a 32-byte array. The
/// encoding is canonical.
pub fn to_bytes(&self) -> [u8; 32] {
Expand Down
9 changes: 9 additions & 0 deletions src/backend/serial/u64/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ pub(crate) const EDWARDS_D: FieldElement51 = FieldElement51([
1442794654840575,
]);

/// sqrt(-486664)
pub(crate) const ED25519_SQRTAM2: FieldElement51 = FieldElement51([
1693982333959686,
608509411481997,
2235573344831311,
947681270984193,
266558006233600
]);

/// Edwards `2*d` value, equal to `2*(-121665/121666) mod p`.
pub(crate) const EDWARDS_D2: FieldElement51 = FieldElement51([
1859910466990425,
Expand Down
18 changes: 18 additions & 0 deletions src/backend/serial/u64/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,24 @@ impl FieldElement51 {
])
}

/// Load a `FieldElement51` from 64 bytes, by reducing modulo q.
pub fn from_bytes_wide(hash: &[u8; 64]) -> FieldElement51 {
let mut fl = [0u8; 32];
let mut gl = [0u8; 32];
fl.copy_from_slice(&hash[..32]);
gl.copy_from_slice(&hash[32..]);
fl[31] &= 0x7f;
gl[31] &= 0x7f;

let mut fe_f = Self::from_bytes(&fl);
let fe_g = Self::from_bytes(&gl);
fe_f.0[0] = fe_f.0[0] + (hash[31] >> 7) as u64 * 19 + (hash[63] >> 7) as u64 * 722;
for i in 0..5 {
fe_f.0[i] += 38 * fe_g.0[i];
}
Self::reduce(fe_f.0)
}

/// Serialize this `FieldElement51` to a 32-byte array. The
/// encoding is canonical.
pub fn to_bytes(&self) -> [u8; 32] {
Expand Down
65 changes: 62 additions & 3 deletions src/edwards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ impl CompressedEdwardsY {
use serde::{self, Serialize, Deserialize, Serializer, Deserializer};
#[cfg(feature = "serde")]
use serde::de::Visitor;
use constants::ED25519_SQRTAM2;

#[cfg(feature = "serde")]
impl Serialize for EdwardsPoint {
Expand Down Expand Up @@ -526,9 +527,26 @@ impl EdwardsPoint {
CompressedEdwardsY(s)
}

/// Perform hashing to the group using the Elligator2 map
/// Perform hashing to curve, with explicit hash function and DST using the suite
/// edwards25519_XMD:SHA-512_ELL2_NU_
///
/// See https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-10#section-6.7.1
/// See https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-6.8.2
pub fn hash_to_curve_dst<D>(bytes: &[u8], dst: &[u8]) -> EdwardsPoint
where
D: Digest<OutputSize = U64> + Default,
{
let fe = FieldElement::hash_to_field::<D>(bytes, dst);
let (M1, is_sq) = crate::montgomery::elligator_encode(&fe);
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()
}

/// Hash from bytes, following the elligator2 version implemented in signal
pub fn hash_from_bytes<D>(bytes: &[u8]) -> EdwardsPoint
where
D: Digest<OutputSize = U64> + Default,
Expand All @@ -543,7 +561,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
Expand Down Expand Up @@ -1208,6 +1226,7 @@ mod test {
use subtle::ConditionallySelectable;
use constants;
use super::*;
use sha2::Sha512;

/// X coordinate of the basepoint.
/// = 15112221349535400772501151409588531511454012693041857206046113283949847762202
Expand Down Expand Up @@ -1807,4 +1826,44 @@ mod test {
assert_eq!(point.compress().to_bytes(), output[..]);
}
}

// Hash-to-curve test vectors from
// https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/blob/master/draft-irtf-cfrg-hash-to-curve.md
fn test_vectors_h2c() -> Vec<Vec<&'static str>> {
vec![
vec![
"",
"222e314d04a4d5725e9f2aff9fb2a6b69ef375a1214eb19021ceab2d687f0f9b",
],
vec![
"abc",
"67732d50f9a26f73111dd1ed5dba225614e538599db58ba30aaea1f5c827fa42",
],
vec![
"abcdef0123456789",
"af8a6c24dd1adde73909cada6a4a137577b0f179d336685c4a955a0a8e1a86fb",
],
vec![
"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"aaf6ff6ef5ebba128b0774f4296cb4c2279a074658b083b8dcca91f57a603450",
],
vec![
"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ac90c3d39eb18ff291d33441b35f3262cdd307162cc97c31bfcc7a4245891a37",
],
]
}

#[test]
fn elligator_hash_to_curve_test_vectors(){
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
for (index, vector) in test_vectors_h2c().iter().enumerate() {
let input = vector[0].as_bytes();
let mut output = hex::decode(vector[1]).unwrap();
output.reverse();

let point = EdwardsPoint::hash_to_curve_dst::<Sha512>(&input, dst).compress().to_bytes();
assert!(!(point != output[..]), "Failed in test {}", index);
}
}
}
81 changes: 81 additions & 0 deletions src/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub type FieldElement = backend::serial::u64::field::FieldElement51;

#[cfg(feature = "u32_backend")]
pub use backend::serial::u32::field::*;
use digest::{generic_array::typenum::U64, Digest};

/// A `FieldElement` represents an element of the field
/// \\( \mathbb Z / (2\^{255} - 19)\\).
///
Expand Down Expand Up @@ -289,12 +291,44 @@ impl FieldElement {
pub fn invsqrt(&self) -> (Choice, FieldElement) {
FieldElement::sqrt_ratio_i(&FieldElement::one(), self)
}

/// Hash_to_field as described in hash_to_curve standard
pub fn hash_to_field<D>(bytes: &[u8], dst: &[u8]) -> Self
where
D: Digest<OutputSize = U64> + Default,
{
let len_in_bytes = 48;
let l_i_b_str = [0u8, len_in_bytes];
let z_pad = [0u8; 128];
let dst_prime = [dst, &[dst.len() as u8]].concat();

let b_0 = D::new()
.chain(z_pad)
.chain(bytes)
.chain(l_i_b_str)
.chain([0u8])
.chain(&dst_prime)
.finalize();

let b_1 = D::new()
.chain(b_0.as_slice())
.chain([1u8])
.chain(&dst_prime)
.finalize();

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)]
mod test {
use field::*;
use subtle::ConditionallyNegatable;
use sha2::Sha512;

/// Random element a of GF(2^255-19), from Sage
/// a = 1070314506888354081329385823235218444233221\
Expand Down Expand Up @@ -473,4 +507,51 @@ mod test {
fn batch_invert_empty() {
FieldElement::batch_invert(&mut []);
}

#[test]
fn from_hash_wide() {
let mut test_vec= [0x6d, 0x2f, 0x2f, 0xfa, 0x94, 0x12, 0xb4, 0x15, 0x2f, 0x6a, 0xb6, 0x28, 0x41, 0xb8, 0x25, 0x92, 0x4a, 0x44, 0x90, 0x65, 0x15, 0x2b, 0x95, 0x47, 0x6f, 0x12, 0x1d, 0xe8, 0x99, 0xbb, 0x77, 0xbd, 0x48, 0x24, 0x6a, 0x37, 0x8e, 0x31, 0x33, 0xfb, 0x30, 0x23, 0x2a, 0xad, 0xa9, 0x20, 0xae, 0x04];
let mut hash_wide = [0u8; 64];
hash_wide[..48].copy_from_slice(&test_vec);

let mut reduce_fe = FieldElement::from_bytes_wide(&hash_wide);
let mut expected_reduced = [0x30, 0x92, 0xf0, 0x33, 0xb1, 0x6d, 0x4d, 0x5f, 0x74, 0xa3, 0xf7, 0xdc, 0x70, 0x91, 0xfe, 0x43, 0x4b, 0x44, 0x90, 0x65, 0x15, 0x2b, 0x95, 0x47, 0x6f, 0x12, 0x1d, 0xe8, 0x99, 0xbb, 0x77, 0x3d];

assert_eq!(reduce_fe.to_bytes(), expected_reduced);

test_vec= [0xae, 0x69, 0x22, 0xd7, 0x28, 0xc1, 0x21, 0xf6, 0x90, 0x48, 0x61, 0xbd, 0x67, 0x49, 0x67, 0xb3, 0x19, 0xd4, 0x6d, 0xee, 0x9d, 0x04, 0x7f, 0x86, 0xc4, 0x27, 0xc5, 0x3f, 0x8b, 0x29, 0xa5, 0x5c, 0xdb, 0xe1, 0x5e, 0xae, 0x23, 0x4e, 0xb7, 0x84, 0xe5, 0x9d, 0x6d, 0x20, 0x2a, 0x78, 0x20, 0xd6];
hash_wide = [0u8; 64];
hash_wide[..48].copy_from_slice(&test_vec);

reduce_fe = FieldElement::from_bytes_wide(&hash_wide);
expected_reduced = [0x30, 0xf0, 0x37, 0xb9, 0x74, 0x5a, 0x57, 0xa9, 0xa2, 0xb8, 0xa6, 0x8d, 0xa8, 0x1f, 0x39, 0x7c, 0x39, 0xd4, 0x6d, 0xee, 0x9d, 0x04, 0x7f, 0x86, 0xc4, 0x27, 0xc5, 0x3f, 0x8b, 0x29, 0xa5, 0x5c];

assert_eq!(reduce_fe.to_bytes(), expected_reduced);

test_vec= [0x3a, 0x7a, 0x2b, 0x29, 0x83, 0xe8, 0x88, 0x61, 0x25, 0x20, 0xcf, 0x6a, 0xfe, 0xbb, 0xea, 0x6b, 0x21, 0x8b, 0x58, 0x15, 0xf2, 0x38, 0x80, 0x09, 0x2a, 0x92, 0x5a, 0xf9, 0x4c, 0xd6, 0xfa, 0x24, 0x6a, 0x35, 0xb7, 0xdb, 0xed, 0x1e, 0x8f, 0xdf, 0xfd, 0xcd, 0x36, 0x6e, 0x55, 0xed, 0x0a, 0xbe];
hash_wide = [0u8; 64];
hash_wide[..48].copy_from_slice(&test_vec);

reduce_fe = FieldElement::from_bytes_wide(&hash_wide);
expected_reduced = [0xf6, 0x67, 0x5d, 0xc6, 0xd1, 0x7f, 0xc7, 0x90, 0xd4, 0xb3, 0xf1, 0xc6, 0xac, 0xf6, 0x89, 0xa1, 0x3d, 0x8b, 0x58, 0x15, 0xf2, 0x38, 0x80, 0x09, 0x2a, 0x92, 0x5a, 0xf9, 0x4c, 0xd6, 0xfa, 0x24];

assert_eq!(reduce_fe.to_bytes(), expected_reduced);
}

#[test]
fn hash_to_field() {
let message = [0xfc, 0x51, 0xcd, 0x8e, 0x62, 0x18, 0xa1, 0xa3, 0x8d, 0xa4, 0x7e, 0xd0, 0x02, 0x30, 0xf0, 0x58, 0x08, 0x16, 0xed, 0x13, 0xba, 0x33, 0x03, 0xac, 0x5d, 0xeb, 0x91, 0x15, 0x48, 0x90, 0x80, 0x25, 0xaf, 0x82];
let dst = b"ECVRF_edwards25519_XMD:SHA-512_ELL2_NU_\x04";
let fe = FieldElement::hash_to_field::<Sha512>(&message, dst);
let expected_fe = FieldElement::from_bytes(&[0xf6, 0x67, 0x5d, 0xc6, 0xd1, 0x7f, 0xc7, 0x90, 0xd4, 0xb3, 0xf1, 0xc6, 0xac, 0xf6, 0x89, 0xa1, 0x3d, 0x8b, 0x58, 0x15, 0xf2, 0x38, 0x80, 0x09, 0x2a, 0x92, 0x5a, 0xf9, 0x4c, 0xd6, 0xfa, 0x24]);
assert_eq!(fe, expected_fe);

let message = "";
let dst = "QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
let fe = FieldElement::hash_to_field::<Sha512>(message.as_bytes(), dst.as_bytes());
let mut expected_fe_bytes = [0x7f, 0x3e, 0x7f, 0xb9, 0x42, 0x81, 0x03, 0xad, 0x7f, 0x52, 0xdb, 0x32, 0xf9, 0xdf, 0x32, 0x50, 0x5d, 0x7b, 0x42, 0x7d, 0x89, 0x4c, 0x50, 0x93, 0xf7, 0xa0, 0xf0, 0x37, 0x4a, 0x30, 0x64, 0x1d];
expected_fe_bytes.reverse();
let expected_fe = FieldElement::from_bytes(&expected_fe_bytes);
assert_eq!(fe, expected_fe);
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ extern crate fiat_crypto;
// Used for traits related to constant-time code.
extern crate subtle;

#[cfg(test)]
extern crate sha2;
#[cfg(all(test, feature = "serde"))]
extern crate bincode;
#[cfg(feature = "serde")]
Expand Down
12 changes: 7 additions & 5 deletions src/montgomery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,16 @@ 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>
//
// 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 */

Expand All @@ -190,7 +192,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
Expand Down Expand Up @@ -465,15 +467,15 @@ 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);
}

#[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);
}
}