From 8aa1458941b77290cc9bbfb91880494a8d5172db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Wed, 21 Oct 2020 11:02:06 -0400 Subject: [PATCH] Implements an Elligator2 map for Curve25519 This implementation: - is agnostic on the hash used to pick a field element, even though SHA512 is commonly used, - follows https://tools.ietf.org/id/draft-irtf-cfrg-hash-to-curve-10.html closely - tests the outputs of the function using libsignal's implementation. --- Cargo.toml | 1 + src/backend/serial/u32/constants.rs | 4 ++ src/backend/serial/u64/constants.rs | 3 + src/edwards.rs | 87 +++++++++++++++++++++++++++++ src/montgomery.rs | 61 ++++++++++++++++++-- 5 files changed, 152 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8420f4d..6e5e44c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ travis-ci = { repository = "dalek-cryptography/curve25519-dalek", branch = "mast sha2 = { version = "0.9", default-features = false } bincode = "1" criterion = "0.3.0" +hex = "0.4.2" rand = "0.7" [[bench]] diff --git a/src/backend/serial/u32/constants.rs b/src/backend/serial/u32/constants.rs index 73f353f..49eb1b0 100644 --- a/src/backend/serial/u32/constants.rs +++ b/src/backend/serial/u32/constants.rs @@ -63,6 +63,10 @@ pub(crate) const SQRT_M1: FieldElement2625 = FieldElement2625([ pub(crate) const APLUS2_OVER_FOUR: FieldElement2625 = FieldElement2625([121666, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +/// `MONT_A` is a constant of Curve25519. (This is used internally within the Elligator map.) +pub(crate) const MONT_A: FieldElement2625 = + FieldElement2625([486662, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + /// `L` is the order of base point, i.e. 2^252 + /// 27742317777372353535851937790883648493 pub(crate) const L: Scalar29 = Scalar29([ diff --git a/src/backend/serial/u64/constants.rs b/src/backend/serial/u64/constants.rs index fc8c6da..9d0c547 100644 --- a/src/backend/serial/u64/constants.rs +++ b/src/backend/serial/u64/constants.rs @@ -91,6 +91,9 @@ pub(crate) const SQRT_M1: FieldElement51 = FieldElement51([ /// `APLUS2_OVER_FOUR` is (A+2)/4. (This is used internally within the Montgomery ladder.) pub(crate) const APLUS2_OVER_FOUR: FieldElement51 = FieldElement51([121666, 0, 0, 0, 0]); +/// `MONT_A` is a constant of Curve25519. (This is used internally within the Elligator map.) +pub(crate) const MONT_A: FieldElement51 = FieldElement51([486662, 0, 0, 0, 0]); + /// `L` is the order of base point, i.e. 2^252 + 27742317777372353535851937790883648493 pub(crate) const L: Scalar52 = Scalar52([ 0x0002631a5cf5d3ed, diff --git a/src/edwards.rs b/src/edwards.rs index 1524dbd..61c3d91 100644 --- a/src/edwards.rs +++ b/src/edwards.rs @@ -100,6 +100,7 @@ use core::ops::{Add, Neg, Sub}; use core::ops::{AddAssign, SubAssign}; use core::ops::{Mul, MulAssign}; +use digest::{generic_array::typenum::U64, Digest}; use subtle::Choice; use subtle::ConditionallyNegatable; use subtle::ConditionallySelectable; @@ -493,6 +494,31 @@ impl EdwardsPoint { s[31] ^= x.is_negative().unwrap_u8() << 7; CompressedEdwardsY(s) } + + /// Perform hashing to the group using the Elligator2 map + /// + /// See https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-10#section-6.7.1 + pub fn hash_from_bytes(bytes: &[u8]) -> EdwardsPoint + where + D: Digest + Default, + { + let mut hash = D::new(); + hash.update(bytes); + let h = hash.finalize(); + let mut res = [0u8; 32]; + res.copy_from_slice(&h[..32]); + + let sign_bit = (res[31] & 0x80) >> 7; + + let fe = FieldElement::from_bytes(&res); + + let M1 = crate::montgomery::elligator_map(&fe); + let E1_opt = M1.to_edwards(sign_bit); + + E1_opt + .expect("Montgomery conversion to Edwards point in Elligator failed") + .mul_by_cofactor() + } } // ------------------------------------------------------------------------ @@ -1432,4 +1458,65 @@ mod test { let bp: EdwardsPoint = bincode::deserialize(raw_bytes).unwrap(); assert_eq!(bp, constants::ED25519_BASEPOINT_POINT); } + + //////////////////////////////////////////////////////////// + // Signal tests from // + // https://github.com/signalapp/libsignal-protocol-c/ // + //////////////////////////////////////////////////////////// + + fn test_vectors() -> Vec> { + vec![ + vec![ + "214f306e1576f5a7577636fe303ca2c625b533319f52442b22a9fa3b7ede809f", + "c95becf0f93595174633b9d4d6bbbeb88e16fa257176f877ce426e1424626052", + ], + vec![ + "2eb10d432702ea7f79207da95d206f82d5a3b374f5f89f17a199531f78d3bea6", + "d8f8b508edffbb8b6dab0f602f86a9dd759f800fe18f782fdcac47c234883e7f", + ], + vec![ + "84cbe9accdd32b46f4a8ef51c85fd39d028711f77fb00e204a613fc235fd68b9", + "93c73e0289afd1d1fc9e4e78a505d5d1b2642fbdf91a1eff7d281930654b1453", + ], + vec![ + "c85165952490dc1839cb69012a3d9f2cc4b02343613263ab93a26dc89fd58267", + "43cbe8685fd3c90665b91835debb89ff1477f906f5170f38a192f6a199556537", + ], + vec![ + "26e7fc4a78d863b1a4ccb2ce0951fbcd021e106350730ee4157bacb4502e1b76", + "b6fc3d738c2c40719479b2f23818180cdafa72a14254d4016bbed8f0b788a835", + ], + vec![ + "1618c08ef0233f94f0f163f9435ec7457cd7a8cd4bb6b160315d15818c30f7a2", + "da0b703593b29dbcd28ebd6e7baea17b6f61971f3641cae774f6a5137a12294c", + ], + vec![ + "48b73039db6fcdcb6030c4a38e8be80b6390d8ae46890e77e623f87254ef149c", + "ca11b25acbc80566603eabeb9364ebd50e0306424c61049e1ce9385d9f349966", + ], + vec![ + "a744d582b3a34d14d311b7629da06d003045ae77cebceeb4e0e72734d63bd07d", + "fad25a5ea15d4541258af8785acaf697a886c1b872c793790e60a6837b1adbc0", + ], + vec![ + "80a6ff33494c471c5eff7efb9febfbcf30a946fe6535b3451cda79f2154a7095", + "57ac03913309b3f8cd3c3d4c49d878bb21f4d97dc74a1eaccbe5c601f7f06f47", + ], + vec![ + "f06fc939bc10551a0fd415aebf107ef0b9c4ee1ef9a164157bdd089127782617", + "785b2a6a00a5579cc9da1ff997ce8339b6f9fb46c6f10cf7a12ff2986341a6e0", + ], + ] + } + + #[test] + fn elligator_signal_test_vectors() { + for vector in test_vectors().iter() { + let input = hex::decode(vector[0]).unwrap(); + let output = hex::decode(vector[1]).unwrap(); + + let point = EdwardsPoint::hash_from_bytes::(&input); + assert_eq!(point.compress().to_bytes(), output[..]); + } + } } diff --git a/src/montgomery.rs b/src/montgomery.rs index c3676c2..3400ca9 100644 --- a/src/montgomery.rs +++ b/src/montgomery.rs @@ -50,7 +50,7 @@ use core::ops::{Mul, MulAssign}; -use constants::APLUS2_OVER_FOUR; +use constants::{APLUS2_OVER_FOUR, MONT_A}; use edwards::{CompressedEdwardsY, EdwardsPoint}; use field::FieldElement; use scalar::Scalar; @@ -58,8 +58,8 @@ use scalar::Scalar; use traits::Identity; use subtle::Choice; -use subtle::ConditionallySelectable; use subtle::ConstantTimeEq; +use subtle::{ConditionallyNegatable, ConditionallySelectable}; use zeroize::Zeroize; @@ -156,6 +156,33 @@ impl MontgomeryPoint { } } +/// Perform the Elligator2 mapping to a Montgomery point +/// +/// See +/// https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-10#section-6.7.1 +pub(crate) fn elligator_map(r_0: &FieldElement) -> MontgomeryPoint { + let minus_a = -&MONT_A; /* A = 486662 */ + let one = FieldElement::one(); + let d_1 = &one + &r_0.square2(); /* 2r^2 */ + + let d = &minus_a * &(d_1.invert()); /* A/(1+2r^2) */ + + let d_sq = &d.square(); + let au = &MONT_A * &d; + + let inner = &(d_sq + &au) + &one; + let eps = &d * &inner; /* eps = d^3 + Ad^2 + d */ + + let (eps_is_sq, _eps) = FieldElement::sqrt_ratio_i(&eps, &one); + + let zero = FieldElement::zero(); + let Atemp = FieldElement::conditional_select(&MONT_A, &zero, eps_is_sq); /* 0, or A if nonsquare*/ + let mut u = &d + &Atemp; /* d, or d+A if nonsquare */ + u.conditional_negate(!eps_is_sq); /* d, or -d-A if nonsquare */ + + MontgomeryPoint(u.to_bytes()) +} + /// A `ProjectivePoint` holds a point on the projective line /// \\( \mathbb P(\mathbb F\_p) \\), which we identify with the Kummer /// line of the Montgomery curve. @@ -315,8 +342,9 @@ impl<'a, 'b> Mul<&'b MontgomeryPoint> for &'a Scalar { #[cfg(test)] mod test { - use constants; use super::*; + use constants; + use core::convert::TryInto; use rand_core::OsRng; @@ -396,8 +424,33 @@ mod test { let p_montgomery: MontgomeryPoint = p_edwards.to_montgomery(); let expected = s * p_edwards; - let result = s * p_montgomery; + let result = s * p_montgomery; assert_eq!(result, expected.to_montgomery()) } + + const ELLIGATOR_CORRECT_OUTPUT: [u8; 32] = [ + 0x5f, 0x35, 0x20, 0x00, 0x1c, 0x6c, 0x99, 0x36, 0xa3, 0x12, 0x06, 0xaf, 0xe7, 0xc7, 0xac, + 0x22, 0x4e, 0x88, 0x61, 0x61, 0x9b, 0xf9, 0x88, 0x72, 0x44, 0x49, 0x15, 0x89, 0x9d, 0x95, + 0xf4, 0x6e, + ]; + + #[test] + #[cfg(feature = "std")] // Vec + fn montgomery_elligator_correct() { + let bytes: std::vec::Vec = (0u8..32u8).collect(); + let bits_in: [u8; 32] = (&bytes[..]).try_into().expect("Range invariant broken"); + + let fe = FieldElement::from_bytes(&bits_in); + let eg = elligator_map(&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_map(&fe); + assert_eq!(eg.to_bytes(), zero); + } }