diff --git a/curve25519-dalek/Cargo.toml b/curve25519-dalek/Cargo.toml index 01bd3b96..91b32a18 100644 --- a/curve25519-dalek/Cargo.toml +++ b/curve25519-dalek/Cargo.toml @@ -37,6 +37,8 @@ hex = "0.4.2" json = "0.12.4" rand = "0.8" rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +rand_distr = "0.4.3" +kolmogorov_smirnov = "1.1.0" [build-dependencies] rustc_version = "0.4.0" diff --git a/curve25519-dalek/src/edwards.rs b/curve25519-dalek/src/edwards.rs index 7392a571..f1978f98 100644 --- a/curve25519-dalek/src/edwards.rs +++ b/curve25519-dalek/src/edwards.rs @@ -103,8 +103,8 @@ use core::ops::{Mul, MulAssign}; use cfg_if::cfg_if; -#[cfg(feature = "digest")] -use crate::elligator2::map_to_point; +#[cfg(feature = "elligator2")] +use crate::elligator2::{map_fe_to_edwards, MASK_UNSET_BYTE}; #[cfg(feature = "digest")] use digest::{generic_array::typenum::U64, Digest}; @@ -598,7 +598,7 @@ impl EdwardsPoint { let sign_bit = (res[31] & 0x80) >> 7; - let fe1 = map_to_point(&res); + let fe1 = MontgomeryPoint::map_to_point_unbounded(&res); let E1_opt = fe1.to_edwards(sign_bit); E1_opt @@ -606,49 +606,34 @@ impl EdwardsPoint { .mul_by_cofactor() } - #[cfg(elligator2)] - /// Build an [`EdwardsPoint`] using the birational mapping from (the - /// extended `(u, v)` form of) a montgomery point. - pub fn from_uv(u: &[u8; 32], v: &[u8; 32]) -> EdwardsPoint { - let u_fe = FieldElement::from_bytes(u); - let v_fe = FieldElement::from_bytes(v); - let (x, y) = Self::new_edwards_point(&u_fe, &v_fe); - Self::from_xy(x, y) - } - - #[cfg(elligator2)] - fn new_edwards_point(u: &FieldElement, v: &FieldElement) -> (FieldElement, FieldElement) { - // Per RFC 7748: (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1)) - - let two = &FieldElement::ONE + &FieldElement::ONE; - let (_, sqrt_neg_a_plus_two) = - FieldElement::sqrt_ratio_i(&(&MONTGOMERY_A_NEG + &two), &FieldElement::ONE); - - let mut x = &(u * &v.invert()) * &sqrt_neg_a_plus_two; - - let u_plus_one = u + &FieldElement::ONE; - let u_minus_one = u - &FieldElement::ONE; - - let mut y = &u_minus_one * &u_plus_one.invert(); - - // This mapping is undefined when t == 0 or s == -1, i.e., when the - // denominator of either of the above rational functions is zero. - // Implementations MUST detect exceptional cases and return the value - // (v, w) = (0, 1), which is the identity point on all twisted Edwards - // curves. - let result_undefined = v.is_zero() | u_plus_one.is_zero(); - x.conditional_assign(&FieldElement::ZERO, result_undefined); - y.conditional_assign(&FieldElement::ONE, result_undefined); - - // Convert from Edwards (x, y) to extended (x, y, z, t) coordinates. - // new_edwards_from_xy(x, y) - - (x, y) + #[cfg(feature = "elligator2")] + /// Perform the Elligator2 mapping to an [`EdwardsPoint`]. + /// + /// Calculates a point on elliptic curve E (Curve25519) from an element of + /// the finite field F over which E is defined. See section 6.7.1 of the + /// RFC. + /// + /// The input u and output P are elements of the field F. Note that + /// the output P is a point on the edwards curve and as such it's byte + /// representation is distinguishable from uniform random. + /// + /// Input: + /// * u -> an element of field F. + /// + /// Output: + /// * P - a point on the Edwards elliptic curve. + /// + /// See + pub fn map_to_point(r: &[u8; 32]) -> EdwardsPoint { + let mut clamped = *r; + clamped[31] &= MASK_UNSET_BYTE; + let r_0 = FieldElement::from_bytes(&clamped); + let (x, y) = map_fe_to_edwards(&r_0); + Self::from_xy(&x, &y) } - #[cfg(elligator2)] + #[cfg(feature = "elligator2")] fn from_xy(x: &FieldElement, y: &FieldElement) -> EdwardsPoint { - // Yeah yeah yeah, no where better to put this. :( let z = FieldElement::ONE; let t = x * y; @@ -2275,10 +2260,6 @@ mod test { "2eb10d432702ea7f79207da95d206f82d5a3b374f5f89f17a199531f78d3bea6", "d8f8b508edffbb8b6dab0f602f86a9dd759f800fe18f782fdcac47c234883e7f", ], - vec![ - "84cbe9accdd32b46f4a8ef51c85fd39d028711f77fb00e204a613fc235fd68b9", - "93c73e0289afd1d1fc9e4e78a505d5d1b2642fbdf91a1eff7d281930654b1453", - ], vec![ "c85165952490dc1839cb69012a3d9f2cc4b02343613263ab93a26dc89fd58267", "43cbe8685fd3c90665b91835debb89ff1477f906f5170f38a192f6a199556537", @@ -2291,22 +2272,27 @@ mod test { "1618c08ef0233f94f0f163f9435ec7457cd7a8cd4bb6b160315d15818c30f7a2", "da0b703593b29dbcd28ebd6e7baea17b6f61971f3641cae774f6a5137a12294c", ], - vec![ - "48b73039db6fcdcb6030c4a38e8be80b6390d8ae46890e77e623f87254ef149c", - "ca11b25acbc80566603eabeb9364ebd50e0306424c61049e1ce9385d9f349966", - ], vec![ "a744d582b3a34d14d311b7629da06d003045ae77cebceeb4e0e72734d63bd07d", "fad25a5ea15d4541258af8785acaf697a886c1b872c793790e60a6837b1adbc0", ], - vec![ - "80a6ff33494c471c5eff7efb9febfbcf30a946fe6535b3451cda79f2154a7095", - "57ac03913309b3f8cd3c3d4c49d878bb21f4d97dc74a1eaccbe5c601f7f06f47", - ], vec![ "f06fc939bc10551a0fd415aebf107ef0b9c4ee1ef9a164157bdd089127782617", "785b2a6a00a5579cc9da1ff997ce8339b6f9fb46c6f10cf7a12ff2986341a6e0", ], + // Non Least-Square-Root representative values. (i.e. representative > 2^254-10 ) + vec![ + "84cbe9accdd32b46f4a8ef51c85fd39d028711f77fb00e204a613fc235fd68b9", + "93c73e0289afd1d1fc9e4e78a505d5d1b2642fbdf91a1eff7d281930654b1453", + ], + vec![ + "48b73039db6fcdcb6030c4a38e8be80b6390d8ae46890e77e623f87254ef149c", + "ca11b25acbc80566603eabeb9364ebd50e0306424c61049e1ce9385d9f349966", + ], + vec![ + "80a6ff33494c471c5eff7efb9febfbcf30a946fe6535b3451cda79f2154a7095", + "57ac03913309b3f8cd3c3d4c49d878bb21f4d97dc74a1eaccbe5c601f7f06f47", + ], ] } @@ -2314,12 +2300,16 @@ mod test { #[allow(deprecated)] #[cfg(all(feature = "alloc", feature = "digest"))] 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(); + for (n, vector) in test_vectors().iter().enumerate() { + let input = hex::decode(vector[0]).expect("failed to decode hex input"); + let output = hex::decode(vector[1]).expect("failed to decode hex output"); let point = EdwardsPoint::nonspec_map_to_curve::(&input); - assert_eq!(point.compress().to_bytes(), output[..]); + assert_eq!( + hex::encode(point.compress().to_bytes()), + hex::encode(&output[..]), + "signal map_to_curve failed for test {n}" + ); } } } diff --git a/curve25519-dalek/src/elligator2.rs b/curve25519-dalek/src/elligator2.rs index 085cdbfa..18b3b926 100644 --- a/curve25519-dalek/src/elligator2.rs +++ b/curve25519-dalek/src/elligator2.rs @@ -12,17 +12,53 @@ use subtle::{ CtOption, }; +/// bitmask for a single byte when clearing the high order two bits of a representative +pub(crate) const MASK_UNSET_BYTE: u8 = 0x3f; +/// bitmask for a single byte when setting the high order two bits of a representative +pub(crate) const MASK_SET_BYTE: u8 = 0xc0; + /// (p - 1) / 2 = 2^254 - 10 pub(crate) const DIVIDE_MINUS_P_1_2_BYTES: [u8; 32] = [ 0xf6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, ]; -/// Gets the public representative for a key pair using the private key -pub fn representative_from_privkey(privkey: &[u8; 32]) -> Option<[u8; 32]> { +/// Gets a public representative for a key pair using the private key. +/// +/// The `tweak` parameter is used to adjust the computed representative making +/// it computationally indistinguishable from uniform random. If this property +/// is not required then the provided tweak value does not matter. +/// +/// The tweak allows us to overcome three limitations: +/// - Representatives are not always canonical. +/// - Bit 255 (the most significant bit) was always zero. +/// - Only points from the large prime-order subgroup are represented. +/// +/// In order for the returned representative to be canonical a tweak to the +/// high order two bits must be applied. +/// ```txt +/// [An adversary could] observe a representative, interpret it as a field +/// element, square it, then take the square root using the same +/// non-canonical square root algorithm. With representatives produced by +/// an affected version of [the elligator2 implementation], the output of +/// the square-then-root operation would always match the input. With +/// random strings, the output would match only half the time. +/// ``` +/// +/// For a more in-depth explanation see: +/// https://github.com/agl/ed25519/issues/27 +/// https://www.bamsoftware.com/papers/fep-flaws/ +pub fn representative_from_privkey(privkey: &[u8; 32], tweak: u8) -> Option<[u8; 32]> { let pubkey = EdwardsPoint::mul_base_clamped(*privkey).to_montgomery(); let v_in_sqrt = v_in_sqrt(privkey); - point_to_representative(&pubkey, v_in_sqrt.into()).into() + let p: Option<[u8; 32]> = point_to_representative(&pubkey, v_in_sqrt.into()).into(); + match p { + None => None, + Some(mut a) => { + a[31] |= MASK_SET_BYTE & tweak; + Some(a) + } + } } /// This function is used to map a curve point (i.e. an x25519 public key) @@ -116,7 +152,7 @@ fn is_encodable(u: &FieldElement) -> Choice { #[inline] pub(crate) fn high_y(d: &FieldElement) -> Choice { let d_sq = &d.square(); - let au = &MONTGOMERY_A * &d; + let au = &MONTGOMERY_A * d; let inner = &(d_sq + &au) + &FieldElement::ONE; let eps = d * &inner; /* eps = d^3 + Ad^2 + d */ @@ -184,32 +220,9 @@ pub fn v_in_sqrt_pubkey_edwards(pubkey: &EdwardsPoint) -> Choice { // ---------------------------------------------------------------------------- // ---------------------------------------------------------------------------- -#[allow(unused, non_snake_case)] -/// Perform the Elligator2 mapping to Curve25519 in accordance with RFC9380. -/// -/// Calculates a point on elliptic curve E (Curve25519) from an element of -/// the finite field F over which E is defined. See section 6.7.1 of the -/// RFC. -/// -/// The input r and outputs u and v are elements of the field F. The -/// affine coordinates (u, v) specify a point on an elliptic curve -/// defined over F. Note, however, that the point (u, v) is not a -/// uniformly random point. -/// -/// Input: -/// * r -> an element of field F. -/// -/// Output: -/// * Q - a point in `(u,v)` for on the Montgomery elliptic curve. -/// -/// See -pub fn map_to_curve(r: &[u8; 32]) -> ([u8; 32], [u8; 32]) { - let fe = FieldElement::from_bytes(r); - let (x, y) = map_fe_to_curve(&fe); - (x.as_bytes(), y.as_bytes()) -} - -pub(crate) fn map_fe_to_curve(r: &FieldElement) -> (FieldElement, FieldElement) { +fn map_to_curve_parts( + r: &FieldElement, +) -> (FieldElement, FieldElement, FieldElement, FieldElement) { let zero = FieldElement::ZERO; let one = FieldElement::ONE; let mut minus_one = FieldElement::ONE; @@ -239,30 +252,49 @@ pub(crate) fn map_fe_to_curve(r: &FieldElement) -> (FieldElement, FieldElement) let (_, mut y) = FieldElement::sqrt_ratio_i(&y2, &one); y.conditional_negate(eps_is_sq ^ y.is_negative()); - (x, y) + (&x * &d_1, d_1, y, one) } -#[allow(unused, non_snake_case)] -/// Perform the Elligator2 mapping to a [`MontgomeryPoint`]. -/// -/// Calculates a point on elliptic curve E (Curve25519) from an element of -/// the finite field F over which E is defined. See section 6.7.1 of the -/// RFC. -/// -/// The input u and output P are elements of the field F. Note, however, that -/// the output point P is not a uniformly random point. -/// -/// Input: -/// * u -> an element of field F. -/// -/// Output: -/// * P - a point on the Montgomery elliptic curve. -/// -/// See -pub fn map_to_point(r: &[u8; 32]) -> MontgomeryPoint { - let r_0 = FieldElement::from_bytes(r); - let (p, _) = map_fe_to_curve(&r_0); - MontgomeryPoint(p.as_bytes()) +pub(crate) fn map_fe_to_montgomery(r: &FieldElement) -> (FieldElement, FieldElement) { + let (xmn, xmd, y, _) = map_to_curve_parts(r); + (&xmn * &(xmd.invert()), y) +} + +pub(crate) fn map_fe_to_edwards(r: &FieldElement) -> (FieldElement, FieldElement) { + // 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u) + let (xmn, xmd, ymn, ymd) = map_to_curve_parts(r); + // c1 = sqrt(-486664) + // this cannot fail as it computes a constant + let c1 = &(&MONTGOMERY_A_NEG - &FieldElement::ONE) - &FieldElement::ONE; + let (_, c1) = FieldElement::sqrt_ratio_i(&c1, &FieldElement::ONE); + + // 2. xn = xMn * yMd + // 3. xn = xn * c1 + let mut xn = &(&xmn * &ymd) * &c1; + + // 4. xd = xMd * yMn # xn / xd = c1 * xM / yM + let mut xd = &xmd * &ymn; + + // 5. yn = xMn - xMd + let mut yn = &xmn - &xmd; + // 6. yd = xMn + xMd # (n / d - 1) / (n / d + 1) = (n - d) / (n + d) + let mut yd = &xmn + &xmd; + + // 7. tv1 = xd * yd + // 8. e = tv1 == 0 + let cond = (&xd * &yd).is_zero(); + + // 9. xn = CMOV(xn, 0, e) + // 10. xd = CMOV(xd, 1, e) + // 11. yn = CMOV(yn, 1, e) + // 12. yd = CMOV(yd, 1, e) + xn = FieldElement::conditional_select(&xn, &FieldElement::ZERO, cond); + xd = FieldElement::conditional_select(&xd, &FieldElement::ONE, cond); + yn = FieldElement::conditional_select(&yn, &FieldElement::ONE, cond); + yd = FieldElement::conditional_select(&yd, &FieldElement::ONE, cond); + + // 13. return (xn, xd, yn, yd) + (&xn * &(xd.invert()), &yn * &(yd.invert())) } // ------------------------------------------------------------------------ @@ -275,7 +307,7 @@ pub fn map_to_point(r: &[u8; 32]) -> MontgomeryPoint { mod test { use super::*; - const MASK_UNSET_BYTE: u8 = 0x3f; + use hex::FromHex; //////////////////////////////////////////////////////////// // Ntor tests // @@ -286,26 +318,26 @@ mod test { fn repres_from_pubkey_kleshni() { // testcases from kleshni for (i, testcase) in encoding_testcases().iter().enumerate() { - let point: [u8; 32] = hex::decode(testcase.point).unwrap().try_into().unwrap(); + let point = <[u8; 32]>::from_hex(testcase.point).expect("failed to decode hex point"); let edw_point = MontgomeryPoint(point) .to_edwards(testcase.high_y as u8) - .unwrap(); + .expect("failed to convert point to edwards"); let v_in_sqrt = v_in_sqrt_pubkey_edwards(&edw_point); let repres: Option<[u8; 32]> = point_to_representative(&MontgomeryPoint(point), v_in_sqrt.into()).into(); if testcase.representative.is_some() { assert_eq!( - testcase.representative.unwrap(), - hex::encode(repres.unwrap()), + testcase.representative.expect("checked, is some"), + hex::encode(repres.expect("failed to get representative from point")), "[good case] kleshni ({i}) bad pubkey from true representative" ); } else { assert!( repres.is_none(), "[good case] kleshni ({i}) expected none got repres {}", - hex::encode(repres.unwrap()) + hex::encode(repres.expect("this should not fail")) ); } } @@ -317,17 +349,16 @@ mod test { /// are generated from agl/ed25519 to ensure compatibility. fn repres_from_privkey_agl() { for (i, vector) in ntor_valid_test_vectors().iter().enumerate() { - let privkey: [u8; 32] = hex::decode(vector[0]).unwrap().try_into().unwrap(); + let privkey = <[u8; 32]>::from_hex(vector[0]).expect("failed to decode hex privatekey"); let true_repres = vector[2]; - let repres_res = representative_from_privkey(&privkey); + let repres_res = representative_from_privkey(&privkey, 0u8); assert!( Into::::into(repres_res.is_some()), "failed to get representative when we should have gotten one :(" ); - let mut repres = repres_res.unwrap(); + let repres = repres_res.expect("failed to get representative from pubkey"); - repres[31] &= MASK_UNSET_BYTE; assert_eq!( true_repres, hex::encode(repres), @@ -341,27 +372,33 @@ mod test { fn pubkey_from_repres() { // testcases from kleshni for (i, testcase) in decoding_testcases().iter().enumerate() { - let repres: [u8; 32] = hex::decode(testcase.representative) - .unwrap() - .try_into() - .unwrap(); + let repres = <[u8; 32]>::from_hex(testcase.representative) + .expect("failed to decode hex representative"); - let point = MontgomeryPoint::from_representative(&MontgomeryPoint(repres)); + let point = MontgomeryPoint::map_to_point(&repres); assert_eq!( testcase.point, hex::encode(point.to_bytes()), "[good case] kleshni ({i}) bad representative from point" ); + + let point_from_unbounded = MontgomeryPoint::map_to_point_unbounded(&repres); + assert_eq!( + testcase.non_lsr_point, + hex::encode(point_from_unbounded.to_bytes()), + "[good case] kleshni ({i}) bad representative from point" + ); } // testcases from golang agl/ed25519 for (i, vector) in ntor_valid_test_vectors().iter().enumerate() { let true_pubkey = vector[1]; - let repres: [u8; 32] = hex::decode(vector[2]).unwrap().try_into().unwrap(); + let repres = + <[u8; 32]>::from_hex(vector[2]).expect("failed to decode hex representative"); // ensure that the representative can be reversed to recover the // original public key. - let pubkey = MontgomeryPoint::from_representative(&MontgomeryPoint(repres)); + let pubkey = MontgomeryPoint::map_to_point(&repres); assert_eq!( true_pubkey, hex::encode(pubkey.to_bytes()), @@ -374,15 +411,15 @@ mod test { #[cfg(feature = "elligator2")] fn non_representable_points() { for (i, key) in ntor_invalid_keys().iter().enumerate() { - let privkey: [u8; 32] = hex::decode(key).unwrap().try_into().unwrap(); + let privkey = <[u8; 32]>::from_hex(key).expect("failed to decode hex privkey"); // ensure that the representative can be reversed to recover the // original public key. - let res: Option<[u8; 32]> = representative_from_privkey(&privkey).into(); + let res: Option<[u8; 32]> = representative_from_privkey(&privkey, 0u8); assert!( res.is_none(), "[bad case] agl/ed25519 ({i}) expected None, got Some({})", - hex::encode(res.unwrap()) + hex::encode(res.expect("this shouldn't happen")) ); } } @@ -534,7 +571,7 @@ mod test { ] } - const ENCODING_TESTS_COUNT: usize = 6; + const ENCODING_TESTS_COUNT: usize = 10; struct EncodingTestCase { high_y: bool, point: &'static str, @@ -585,13 +622,50 @@ mod test { "0000000000000000000000000000000000000000000000000000000000000000", ), }, + // A not encodable point with both "high_y" values + EncodingTestCase { + point: "10745497d35c6ede6ea6b330546a6fcbf15c903a7be28ae69b1ca14e0bf09b60", + high_y: false, + representative: Some( + "d660db8cf212d31ce8c6f7139e69b9ac47fd81c7c0bfcb93e364b2d424e24813", + ), + }, + EncodingTestCase { + point: "10745497d35c6ede6ea6b330546a6fcbf15c903a7be28ae69b1ca14e0bf09b60", + high_y: true, + representative: Some( + "489a2e0f6955e08f1ae6eb8dcdbc0f867a87a96a02d2dfd2aca21d8b536f0f1b", + ), + }, + // A not encodable point with both "high_y" values + EncodingTestCase { + point: "6d3187192afc3bcc05a497928816e3e2336dc539aa7fc296a9ee013f560db843", + high_y: false, + representative: Some( + "63d0d79e7f3c279cf4a0a5c3833fd85aa1f2c004c4e466f3a3844b3c2e06e410", + ), + }, + EncodingTestCase { + point: "6d3187192afc3bcc05a497928816e3e2336dc539aa7fc296a9ee013f560db843", + high_y: true, + representative: Some( + "0f03b41c86aeb49acf2f76b39cc90a55a0b140b7290f1c9e032591ddcb074537", + ), + }, ] } const DECODING_TESTS_COUNT: usize = 7; struct DecodingTestCase { representative: &'static str, + /// if we only allow least-square-root values as the representative and + /// clear the high order two bits (effectively) ensuring that the + /// representative value is less than `2^254 - 10`, this is the point + /// that we should receive. point: &'static str, + /// if we allow unbounded values to be used directly as representatives, + /// not only least-square-root values, this is the point we should receive. + non_lsr_point: &'static str, } fn decoding_testcases() -> [DecodingTestCase; DECODING_TESTS_COUNT] { @@ -600,36 +674,45 @@ mod test { DecodingTestCase { representative: "e73507d38bae63992b3f57aac48c0abc14509589288457995a2b4ca3490aa207", point: "1e8afffed6bf53fe271ad572473262ded8faec68e5e67ef45ebb82eeba52604f", + non_lsr_point: "1e8afffed6bf53fe271ad572473262ded8faec68e5e67ef45ebb82eeba52604f", }, // A small representative with true "high_y" property DecodingTestCase { representative: "95a16019041dbefed9832048ede11928d90365f24a38aa7aef1b97e23954101b", point: "794f05ba3e3a72958022468c88981e0be5782be1e1145ce2c3c6fde16ded5363", + non_lsr_point: "794f05ba3e3a72958022468c88981e0be5782be1e1145ce2c3c6fde16ded5363", }, // The last representative returning true: (p - 1) / 2 DecodingTestCase { representative: "f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", point: "9cdb525555555555555555555555555555555555555555555555555555555555", + non_lsr_point: "9cdb525555555555555555555555555555555555555555555555555555555555", }, // The first representative returning false: (p + 1) / 2 DecodingTestCase { representative: "f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", point: "9cdb525555555555555555555555555555555555555555555555555555555555", + non_lsr_point: "9cdb525555555555555555555555555555555555555555555555555555555555", }, + // 0 + DecodingTestCase { + representative: "0000000000000000000000000000000000000000000000000000000000000000", + point: "0000000000000000000000000000000000000000000000000000000000000000", + non_lsr_point: "0000000000000000000000000000000000000000000000000000000000000000", + }, + // These two tests are not least-square-root representations. + // A large representative with false "high_y" property DecodingTestCase { representative: "179f24730ded2ce3173908ec61964653b8027e383f40346c1c9b4d2bdb1db76c", - point: "10745497d35c6ede6ea6b330546a6fcbf15c903a7be28ae69b1ca14e0bf09b60", + point: "e6e5355e0482e952cc951f13db26316ab111ae9edb58c45428a984ce7042d349", + non_lsr_point: "10745497d35c6ede6ea6b330546a6fcbf15c903a7be28ae69b1ca14e0bf09b60", }, // A large representative with true "high_y" property DecodingTestCase { representative: "8a2f286180c3d8630b5f5a3c7cc027a55e0d3ffb3b1b990c5c7bb4c3d1f91b6f", - point: "6d3187192afc3bcc05a497928816e3e2336dc539aa7fc296a9ee013f560db843", - }, - // 0 - DecodingTestCase { - representative: "0000000000000000000000000000000000000000000000000000000000000000", - point: "0000000000000000000000000000000000000000000000000000000000000000", + point: "27e222fec324b0293842a59a63b8201b0f97b1dd599ebcd478a896b7261aff3e", + non_lsr_point: "6d3187192afc3bcc05a497928816e3e2336dc539aa7fc296a9ee013f560db843", }, ] } @@ -645,15 +728,13 @@ mod rfc9380 { #[test] fn map_to_curve_test_go_ed25519_extra() { - for i in 0..CURVE25519_ELL2.len() { - let testcase = &CURVE25519_ELL2[i]; - + for (i, testcase) in CURVE25519_ELL2.iter().enumerate() { let u = testcase[0].must_from_be(); - let mut clamped = u.clone(); + let mut clamped = u; clamped[31] &= 63; // map point to curve - let (q_x, _) = map_fe_to_curve(&FieldElement::from_bytes(&clamped)); + let (q_x, _) = map_fe_to_montgomery(&FieldElement::from_bytes(&clamped)); // check resulting point assert_eq!( @@ -666,12 +747,11 @@ mod rfc9380 { #[test] fn map_to_curve_test_curve25519() { - for i in 0..curve25519_XMD_SHA512_ELL2_NU.len() { - let testcase = &curve25519_XMD_SHA512_ELL2_NU[i]; + for (i, testcase) in curve25519_XMD_SHA512_ELL2_NU.iter().enumerate() { let u = FieldElement::from_bytes(&testcase.u_0.must_from_le()); // map point to curve - let (q_x, q_y) = map_fe_to_curve(&u); + let (q_x, q_y) = map_fe_to_montgomery(&u); // check resulting point assert_eq!( @@ -687,14 +767,13 @@ mod rfc9380 { testcase ); } - for i in 0..curve25519_XMD_SHA512_ELL2_RO.len() { - let testcase = &curve25519_XMD_SHA512_ELL2_RO[i]; + for (i, testcase) in curve25519_XMD_SHA512_ELL2_RO.iter().enumerate() { let u0 = FieldElement::from_bytes(&testcase.u_0.must_from_le()); let u1 = FieldElement::from_bytes(&testcase.u_1.must_from_le()); // map points to curve - let (q0_x, q0_y) = map_fe_to_curve(&u0); - let (q1_x, q1_y) = map_fe_to_curve(&u1); + let (q0_x, q0_y) = map_fe_to_montgomery(&u0); + let (q1_x, q1_y) = map_fe_to_montgomery(&u1); // check resulting points assert_eq!( @@ -726,59 +805,55 @@ mod rfc9380 { #[test] fn map_to_curve_test_edwards25519() { - for i in 0..edwards25519_XMD_SHA512_ELL2_NU.len() { - let testcase = &curve25519_XMD_SHA512_ELL2_NU[i]; + for (i, testcase) in edwards25519_XMD_SHA512_ELL2_NU.iter().enumerate() { let u = FieldElement::from_bytes(&testcase.u_0.must_from_le()); - - // map point to curve - let (q_x, q_y) = map_fe_to_curve(&u); + let (q_x, q_y) = map_fe_to_edwards(&u); // check resulting point assert_eq!( q_x.encode_le(), testcase.Q_x, - "({i}) incorrect Q0_x curve25519 NU\n{:?}", + "({i}) incorrect Q0_x edwards25519 NU\n{:?}", testcase ); assert_eq!( q_y.encode_le(), testcase.Q_y, - "({i}) incorrect Q0_y curve25519 NU\n{:?}", + "({i}) incorrect Q0_y edwards25519 NU\n{:?}", testcase ); } - for i in 0..edwards25519_XMD_SHA512_ELL2_RO.len() { - let testcase = &curve25519_XMD_SHA512_ELL2_RO[i]; + for (i, testcase) in edwards25519_XMD_SHA512_ELL2_RO.iter().enumerate() { let u0 = FieldElement::from_bytes(&testcase.u_0.must_from_le()); let u1 = FieldElement::from_bytes(&testcase.u_1.must_from_le()); // map points to curve - let (q0_x, q0_y) = map_fe_to_curve(&u0); - let (q1_x, q1_y) = map_fe_to_curve(&u1); + let (q0_x, q0_y) = map_fe_to_edwards(&u0); + let (q1_x, q1_y) = map_fe_to_edwards(&u1); // check resulting points assert_eq!( q0_x.encode_le(), testcase.Q0_x, - "({i}) incorrect Q0_x curve25519 RO\n{:?}", + "({i}) incorrect Q0_x edwards25519 RO\n{:?}", testcase ); assert_eq!( q0_y.encode_le(), testcase.Q0_y, - "({i}) incorrect Q0_y curve25519 RO\n{:?}", + "({i}) incorrect Q0_y edwards25519 RO\n{:?}", testcase ); assert_eq!( q1_x.encode_le(), testcase.Q1_x, - "({i}) incorrect Q1_x curve25519 RO\n{:?}", + "({i}) incorrect Q1_x edwards25519 RO\n{:?}", testcase ); assert_eq!( q1_y.encode_le(), testcase.Q1_y, - "({i}) incorrect Q1_y curve25519 RO\n{:?}", + "({i}) incorrect Q1_y edwards25519 RO\n{:?}", testcase ); } @@ -790,7 +865,7 @@ mod rfc9380 { /// 2. associated point /// /// These test cases need the upper two bits cleared to be properly mapped. - const CURVE25519_ELL2: [[&'static str; 2]; 14] = [ + const CURVE25519_ELL2: [[&str; 2]; 14] = [ [ "0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000000", @@ -1051,12 +1126,12 @@ mod rfc9380 { impl<'a> FromByteString for &'a str { fn must_from_le(&self) -> [u8; 32] { - let mut u = <[u8; 32]>::from_hex(self).unwrap(); + let mut u = <[u8; 32]>::from_hex(self).expect("failed to unhex"); u.reverse(); u } fn must_from_be(&self) -> [u8; 32] { - <[u8; 32]>::from_hex(self).unwrap() + <[u8; 32]>::from_hex(self).expect("failed to unhex from be") } } @@ -1069,7 +1144,7 @@ mod rfc9380 { fn encode_le(&self) -> String { let mut b = self.as_bytes(); b.reverse(); - hex::encode(&b) + hex::encode(b) } fn encode_be(&self) -> String { @@ -1079,9 +1154,9 @@ mod rfc9380 { impl ToByteString for [u8; 32] { fn encode_le(&self) -> String { - let mut b = self.clone(); + let mut b = *self; b.reverse(); - hex::encode(&b) + hex::encode(b) } fn encode_be(&self) -> String { @@ -1089,3 +1164,186 @@ mod rfc9380 { } } } + +#[cfg(test)] +#[cfg(feature = "elligator2")] +mod randomness { + use super::*; + + use kolmogorov_smirnov as ks; + use rand::{thread_rng, RngCore}; + use rand_distr::{Binomial, Distribution}; + use std::vec::Vec; + + struct BitCounts { + counts: [[u64; 100]; 32 * 8], + entries: usize, + } + + impl BitCounts { + fn new() -> Self { + BitCounts { + counts: [[0u64; 100]; 256], + entries: 0, + } + } + fn entry(&mut self, arr: &[u8; 32]) { + for (i, arr_byte) in arr.iter().enumerate() { + for j in 0..8 { + self.counts[i * 8 + j][self.entries % 100] += ((arr_byte >> j) & 0x01) as u64; + } + } + self.entries += 1; + } + fn outliers(&self) -> Vec { + let mut rng = thread_rng(); + let binomial_100 = + Binomial::new(100, 0.5).expect("failed to build binomial distribution"); + let expected_dist: [u64; 100] = binomial_100 + .sample_iter(&mut rng) + .take(100) + .collect::>() + .try_into() + .expect("failed to build example binomial expected distribution"); + // this is a high confidence, but we want to avoid this test failing + // due to statistical variability on repeat runs. + let confidence = 0.95; + let mut outlier_indices = vec![]; + + for (n, bit_trial) in self.counts.iter().enumerate() { + let result = ks::test(bit_trial, &expected_dist, confidence); + // require reject_probability == 1.0 to avoid statistical variability on re-runs + if result.is_rejected && result.reject_probability == 1.0 { + // samples definitely not from same distribution + outlier_indices.push(n); + println!( + "{n}, {} {} {} {}", + result.statistic, + result.reject_probability, + result.critical_value, + result.confidence + ); + if n == 255 { + println!("{:?}\n{:?}", bit_trial, expected_dist); + } + } + } + outlier_indices + } + } + + #[test] + /// If we use a "random" value for the tweak the high order bits will be + /// set such that the representative is: + /// 1) unchanged w.r.t. the public key value derived from the representative + /// i.e) it still derives the same public key value from the (clamped) + /// representative that we get from the private key. + /// 2) statistically indistinguishable from uniform random. + /// 4) computationally indistinguishable from random strings for the `a ?= sqrt(a^2)` test. + /// + /// (To see this test fail change `rng.next_u32() as u8` to `0u8`) + fn bitwise_entropy() { + const ITERATIONS: usize = 10000; + // number of iterations + let mut i = 0usize; + let mut rng = thread_rng(); + let mut privkey = [0u8; 32]; + + // count of occurences of a 1 per bit in the representative + let mut bitcounts = BitCounts::new(); + + while i < ITERATIONS { + rng.fill_bytes(&mut privkey); + let alice_representative = + match representative_from_privkey(&privkey, rng.next_u32() as u8) { + None => continue, + Some(r) => r, + }; + + bitcounts.entry(&alice_representative); + + let pub_from_repr = MontgomeryPoint::map_to_point(&alice_representative); + let pub_from_priv = EdwardsPoint::mul_base_clamped(privkey).to_montgomery(); + assert_eq!( + hex::encode(pub_from_priv.as_bytes()), + hex::encode(pub_from_repr.as_bytes()), + "failed pubkey match at iteration {i}" + ); + + i += 1; + } + + let outliers = bitcounts.outliers(); + assert!(outliers.is_empty(), "bad bits: {:?}", outliers); + } + + /// TLDR: The sqrt_ratio_i function is canonical so this library does not + /// suffer from the describbed computational distinguisher. + /// + /// The specific issue that this is testing for can be described as: + /// ```txt + /// An instantiation of Elligator is parameterized by what might be called + /// a “canonical” square root function, one with the property that + /// `√a2 = √(−a)2` for all field elements `a`. That is, we designate just + /// over half the field elements as “non-negative,” and the image of the + /// square root function consists of exactly those elements. A convenient + /// definition of “non-negative” for Curve25519, suggested by its authors, + /// is the lower half of the field, the elements `{0, 1, …, (q − 1) / 2}`. + /// When there are two options for a square root, take the smaller of the two. + /// ``` + /// + /// Any Elligator implementation that does not do this canonicalization of + /// the final square root, and instead it maps a given input systematically + /// to either its negative or non-negative root is vulnerable to the + /// following computational distinguisher. + /// + /// ```txt + /// [An adversary could] observe a representative, interpret it as a field + /// element, square it, then take the square root using the same + /// non-canonical square root algorithm. With representatives produced by + /// an affected version of [the elligator2 implementation], the output of + /// the square-then-root operation would always match the input. With + /// random strings, the output would match only half the time. + /// ``` + /// + /// For a more in-depth explanation see: + /// https://github.com/agl/ed25519/issues/27 + /// https://www.bamsoftware.com/papers/fep-flaws/ + #[test] + fn test_canonical() { + const ITERATIONS: usize = 10000; + // number of iterations + let mut i = 0usize; + let mut rng = thread_rng(); + let mut privkey = [0u8; 32]; + + // number of times the representative (interpreted as a point) squared, then square_rooted, + // equals the original representative. Should happen w/ 50% probability. + let mut squares_equal = 0usize; + + while i < ITERATIONS { + rng.fill_bytes(&mut privkey); + let alice_representative = match representative_from_privkey(&privkey, 0u8) { + None => continue, + Some(r) => r, + }; + + if is_canonical(&alice_representative) { + squares_equal += 1; + } + i += 1; + } + + let expected_range = 4500..5500; // if truly binomial n=10000, p=0.5 then this should "always" pass (> 10x std dev) + assert!( + expected_range.contains(&squares_equal), + "squares_equal: {squares_equal} is not in [4500:5500]" + ); + } + + fn is_canonical(repres: &[u8; 32]) -> bool { + let r_fe = FieldElement::from_bytes(repres); + let (ok, r_fe_prime) = FieldElement::sqrt_ratio_i(&r_fe.square(), &FieldElement::ONE); + (r_fe.ct_eq(&r_fe_prime) & ok).into() + } +} diff --git a/curve25519-dalek/src/montgomery.rs b/curve25519-dalek/src/montgomery.rs index 1f235274..d0a3087e 100644 --- a/curve25519-dalek/src/montgomery.rs +++ b/curve25519-dalek/src/montgomery.rs @@ -248,9 +248,61 @@ impl MontgomeryPoint { } #[cfg(feature = "elligator2")] - /// This decodes an elligator2 hidden point to a curve point on Curve25519. - pub fn from_representative(&self) -> MontgomeryPoint { - elligator2::map_to_point(&self.0) + /// Perform the Elligator2 mapping to a [`MontgomeryPoint`]. + /// + /// Calculates a point on elliptic curve E (Curve25519) from an element of + /// the finite field F over which E is defined. See section 6.7.1 of the + /// RFC. The unbounded variant does NOT assume that input values are always + /// going to be the least-square-root representation of the field element. + /// This is divergent from both the elligator2 specification and RFC9380, + /// however, some implementations miss this detail. This allows us to be + /// compatible with those alternate implementations if necessary, since the + /// resulting point will be different for inputs with either of the + /// high-order two bits set. + /// + /// The input u and output P are elements of the field F. Note that + /// the output P is a point on the Montgomery curve and as such it's byte + /// representation is distinguishable from uniform random. + /// + /// Input: + /// * u -> an element of field F. + /// + /// Output: + /// * P - a point on the Montgomery elliptic curve. + /// + /// See + pub fn map_to_point_unbounded(r: &[u8; 32]) -> MontgomeryPoint { + let r_0 = FieldElement::from_bytes(r); + let (p, _) = elligator2::map_fe_to_montgomery(&r_0); + MontgomeryPoint(p.as_bytes()) + } + + #[cfg(feature = "elligator2")] + /// Perform the Elligator2 mapping to a [`MontgomeryPoint`]. + /// + /// Calculates a point on elliptic curve E (Curve25519) from an element of + /// the finite field F over which E is defined. See section 6.7.1 of the + /// RFC. It is assumed that input values are always going to be the + /// least-square-root representation of the field element in allignment + /// with both the elligator2 specification and RFC9380. + /// + /// The input u and output P are elements of the field F. Note that + /// the output P is a point on the Montgomery curve and as such it's byte + /// representation is distinguishable from uniform random. + /// + /// Input: + /// * u -> an element of field F. + /// + /// Output: + /// * P - a point on the Montgomery elliptic curve. + /// + /// See + pub fn map_to_point(r: &[u8; 32]) -> MontgomeryPoint { + let mut clamped = *r; + clamped[31] &= elligator2::MASK_UNSET_BYTE; + let r_0 = FieldElement::from_bytes(&clamped); + let (p, _) = elligator2::map_fe_to_montgomery(&r_0); + MontgomeryPoint(p.as_bytes()) } } @@ -408,6 +460,7 @@ mod test { use crate::constants; #[cfg(feature = "alloc")] + #[cfg(feature = "elligator2")] use alloc::vec::Vec; use rand_core::{CryptoRng, RngCore}; @@ -625,7 +678,7 @@ mod test { let bytes: Vec = (0u8..32u8).collect(); let bits_in: [u8; 32] = (&bytes[..]).try_into().expect("Range invariant broken"); - let eg = elligator2::map_to_point(&bits_in); + let eg = MontgomeryPoint::map_to_point(&bits_in); assert_eq!(eg.0, ELLIGATOR_CORRECT_OUTPUT); } @@ -633,7 +686,7 @@ mod test { #[cfg(feature = "elligator2")] fn montgomery_elligator_zero_zero() { let zero = [0u8; 32]; - let eg = elligator2::map_to_point(&zero); + let eg = MontgomeryPoint::map_to_point(&zero); assert_eq!(eg.0, zero); } } diff --git a/x25519-dalek/src/x25519.rs b/x25519-dalek/src/x25519.rs index cb18c1b0..244889e2 100644 --- a/x25519-dalek/src/x25519.rs +++ b/x25519-dalek/src/x25519.rs @@ -72,7 +72,7 @@ impl AsRef<[u8]> for PublicKey { /// secret is used at most once. #[cfg_attr(feature = "zeroize", derive(Zeroize))] #[cfg_attr(feature = "zeroize", zeroize(drop))] -pub struct EphemeralSecret(pub(crate) [u8; 32]); +pub struct EphemeralSecret(pub(crate) [u8; 32], pub(crate) u8); impl EphemeralSecret { /// Perform a Diffie-Hellman key agreement between `self` and @@ -94,8 +94,10 @@ impl EphemeralSecret { pub fn random_from_rng(mut csprng: T) -> Self { // The secret key is random bytes. Clamping is done later. let mut bytes = [0u8; 32]; + let mut tweak = [0u8; 1]; csprng.fill_bytes(&mut bytes); - EphemeralSecret(bytes) + csprng.fill_bytes(&mut tweak); + EphemeralSecret(bytes, tweak[0]) } /// Generate a new [`EphemeralSecret`]. @@ -134,7 +136,7 @@ impl<'a> From<&'a EphemeralSecret> for PublicKey { #[cfg_attr(feature = "zeroize", derive(Zeroize))] #[cfg_attr(feature = "zeroize", zeroize(drop))] #[derive(Clone)] -pub struct ReusableSecret(pub(crate) [u8; 32]); +pub struct ReusableSecret(pub(crate) [u8; 32], pub(crate) u8); #[cfg(feature = "reusable_secrets")] impl ReusableSecret { @@ -157,8 +159,10 @@ impl ReusableSecret { pub fn random_from_rng(mut csprng: T) -> Self { // The secret key is random bytes. Clamping is done later. let mut bytes = [0u8; 32]; + let mut tweak = [0u8; 1]; csprng.fill_bytes(&mut bytes); - ReusableSecret(bytes) + csprng.fill_bytes(&mut tweak); + ReusableSecret(bytes, tweak[0]) } /// Generate a new [`ReusableSecret`]. @@ -195,7 +199,7 @@ impl<'a> From<&'a ReusableSecret> for PublicKey { #[cfg_attr(feature = "zeroize", derive(Zeroize))] #[cfg_attr(feature = "zeroize", zeroize(drop))] #[derive(Clone)] -pub struct StaticSecret([u8; 32]); +pub struct StaticSecret([u8; 32], u8); #[cfg(feature = "static_secrets")] impl StaticSecret { @@ -218,8 +222,10 @@ impl StaticSecret { pub fn random_from_rng(mut csprng: T) -> Self { // The secret key is random bytes. Clamping is done later. let mut bytes = [0u8; 32]; + let mut tweak = [0u8; 1]; csprng.fill_bytes(&mut bytes); - StaticSecret(bytes) + csprng.fill_bytes(&mut tweak); + StaticSecret(bytes, tweak[0]) } /// Generate a new [`StaticSecret`]. @@ -245,7 +251,7 @@ impl StaticSecret { impl From<[u8; 32]> for StaticSecret { /// Load a secret key from a byte array. fn from(bytes: [u8; 32]) -> StaticSecret { - StaticSecret(bytes) + StaticSecret(bytes, 0u8) } } @@ -473,7 +479,7 @@ impl<'a> From<&'a [u8; 32]> for PublicRepresentative { impl<'a> From<&'a EphemeralSecret> for Option { /// Given an x25519 [`EphemeralSecret`] key, compute its corresponding [`PublicRepresentative`]. fn from(secret: &'a EphemeralSecret) -> Option { - let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0); + let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0, secret.1); let res: Option<[u8; 32]> = repres; Some(PublicRepresentative(res?)) } @@ -484,7 +490,7 @@ impl<'a> From<&'a EphemeralSecret> for Option { impl<'a> From<&'a ReusableSecret> for Option { /// Given an x25519 [`ReusableSecret`] key, compute its corresponding [`PublicRepresentative`]. fn from(secret: &'a ReusableSecret) -> Option { - let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0); + let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0, secret.1); let res: Option<[u8; 32]> = repres; Some(PublicRepresentative(res?)) } @@ -495,7 +501,7 @@ impl<'a> From<&'a ReusableSecret> for Option { impl<'a> From<&'a StaticSecret> for Option { /// Given an x25519 [`StaticSecret`] key, compute its corresponding [`PublicRepresentative`]. fn from(secret: &'a StaticSecret) -> Option { - let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0); + let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0, secret.1); let res: Option<[u8; 32]> = repres; Some(PublicRepresentative(res?)) } @@ -505,7 +511,8 @@ impl<'a> From<&'a StaticSecret> for Option { impl<'a> From<&'a PublicRepresentative> for PublicKey { /// Given an elligator2 [`PublicRepresentative`], compute its corresponding [`PublicKey`]. fn from(representative: &'a PublicRepresentative) -> PublicKey { - let point = curve25519_dalek::elligator2::map_to_point(&representative.0); + let point = MontgomeryPoint::map_to_point(&representative.0); PublicKey(point) } } +