mirror of
https://github.com/saymrwulf/curve25519-dalek-source.git
synced 2026-09-04 20:24:10 +00:00
Add proper hash_to_curve. (#786)
* Rename hash_to_curve as encode_to_curve * Implement the inline description of the standard. * Generalise map_to_field to return an arbitrary number of field elements. * Implement hash_to_curve as defined in the standard. * Put elligator behind the "digest" feature. * Add warning on non-uniformity of `encode_to_curve`. * Remove the need of Vec for hash_to_field. * Apply suggestions from code review * Refactor expand_message_xmd out of hash_to_field * Add hash-to-curve to benches * Constraint COUNT to 1 or 2; add note on secure hash function usage * Correct hash function usage in encode- and hash-to-curve --------- Co-authored-by: Armando Faz <armfazh@users.noreply.github.com> Co-authored-by: Michael Rosenberg <mrosenberg@cloudflare.com>
This commit is contained in:
parent
fc8815721c
commit
015707ab4e
6 changed files with 495 additions and 130 deletions
|
|
@ -74,6 +74,21 @@ mod edwards_benches {
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
fn encode_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
|
||||
let mut rng = 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 encode to curve (SHA-512, input size 32 bytes)",
|
||||
|b| b.iter(|| EdwardsPoint::encode_to_curve::<Sha512>(&[&msg], &[&domain_sep])),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
fn hash_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
|
||||
let mut rng = rng();
|
||||
|
|
@ -100,6 +115,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);
|
||||
encode_to_curve(&mut g);
|
||||
hash_to_curve(&mut g);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,12 +78,14 @@ pub(crate) const SQRT_M1: FieldElement2625 = FieldElement2625::from_limbs([
|
|||
pub(crate) const APLUS2_OVER_FOUR: FieldElement2625 =
|
||||
FieldElement2625::from_limbs([121666, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
/// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation
|
||||
/// for Curve25519 in its Montgomery form. (This is used internally within the
|
||||
/// Elligator map.)
|
||||
pub(crate) const MONTGOMERY_A: FieldElement2625 =
|
||||
FieldElement2625::from_limbs([486662, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
/// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the
|
||||
/// Elligator map.)
|
||||
pub(crate) const MONTGOMERY_A_NEG: FieldElement2625 = FieldElement2625::from_limbs([
|
||||
|
|
|
|||
|
|
@ -108,11 +108,13 @@ pub(crate) const SQRT_M1: FieldElement51 = FieldElement51::from_limbs([
|
|||
pub(crate) const APLUS2_OVER_FOUR: FieldElement51 =
|
||||
FieldElement51::from_limbs([121666, 0, 0, 0, 0]);
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
/// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation
|
||||
/// for Curve25519 in its Montgomery form. (This is used internally within the
|
||||
/// Elligator map.)
|
||||
pub(crate) const MONTGOMERY_A: FieldElement51 = FieldElement51::from_limbs([486662, 0, 0, 0, 0]);
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
/// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the
|
||||
/// Elligator map.)
|
||||
pub(crate) const MONTGOMERY_A_NEG: FieldElement51 = FieldElement51::from_limbs([
|
||||
|
|
|
|||
|
|
@ -635,12 +635,90 @@ impl EdwardsPoint {
|
|||
}
|
||||
|
||||
#[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
|
||||
// The function `map_to_curve` calculates an [EdwardsPoint] from a [FieldElement].
|
||||
fn map_to_curve(fe: FieldElement) -> EdwardsPoint {
|
||||
let c1 = ED25519_SQRTAM2;
|
||||
|
||||
// 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u)
|
||||
let (xMn, xMd, yMn, yMd) = crate::montgomery::elligator_encode(&fe);
|
||||
// 2. xn = xMn * yMd
|
||||
let xn = &xMn * &yMd;
|
||||
// 3. xn = xn * c1
|
||||
let xn = &xn * &c1;
|
||||
// 4. xd = xMd * yMn
|
||||
let xd = &xMd * &yMn;
|
||||
// 5. yn = xMn - xMd
|
||||
let yn = &xMn - &xMd;
|
||||
// 6. yd = xMn + xMd
|
||||
let yd = &xMn + &xMd;
|
||||
// 7. tv1 = xd * yd
|
||||
let tv1 = &xd * &yd;
|
||||
// 8. e = tv1 == 0
|
||||
let e = tv1.ct_eq(&FieldElement::ZERO);
|
||||
// 9. xn = CMOV(xn, 0, e)
|
||||
let xn = FieldElement::conditional_select(&xn, &FieldElement::ZERO, e);
|
||||
// 10. xd = CMOV(xd, 1, e)
|
||||
let xd = FieldElement::conditional_select(&xd, &FieldElement::ONE, e);
|
||||
// 11. yn = CMOV(yn, 1, e)
|
||||
let yn = FieldElement::conditional_select(&yn, &FieldElement::ONE, e);
|
||||
// 12. yd = CMOV(yd, 1, e)
|
||||
let yd = FieldElement::conditional_select(&yd, &FieldElement::ONE, e);
|
||||
// 13. return (xn, xd, yn, yd)
|
||||
|
||||
EdwardsPoint {
|
||||
X: &xn * &yd,
|
||||
Y: &xd * &yn,
|
||||
Z: &xd * &yd,
|
||||
T: &xn * &yn,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
/// Perform encode to curve per RFC 9380, with explicit hash function and domain separator
|
||||
/// `domain_sep`, using the Twisted Edwards Elligator 2 method. 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.
|
||||
///
|
||||
/// The specification names SHA-512 as an example of a secure hash to use with this function,
|
||||
/// but you may use any 512-bit hash within reason (see the
|
||||
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
|
||||
///
|
||||
/// # Warning
|
||||
/// `encode_to_curve` is a nonuniform encoding from byte strings to points in `G`. That is,
|
||||
/// the distribution of its output is not uniformly random in `G`: the set of possible outputs
|
||||
/// of encode_to_curve is only a fraction of the points in `G`, and some points in this set
|
||||
/// are more likely to be output than others.
|
||||
///
|
||||
/// If your application needs the distribution of the output to be statistically close to
|
||||
/// uniform in `G`, use [Self::hash_to_curve] instead.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
|
||||
pub fn encode_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
|
||||
where
|
||||
D: BlockSizeUser + Default + FixedOutput<OutputSize = U64> + HashMarker,
|
||||
D::BlockSize: IsGreater<D::OutputSize, Output = True>,
|
||||
{
|
||||
// For reference see
|
||||
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
|
||||
|
||||
let fe = FieldElement::hash_to_field::<D, 1>(bytes, domain_sep);
|
||||
let Q = Self::map_to_curve(fe[0]);
|
||||
Q.mul_by_cofactor()
|
||||
}
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
/// Perform a hash to curve per RFC 9380, with explicit hash function and domain separator
|
||||
/// `domain_sep`, using the Twisted Edwards Elligator 2 method. 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.
|
||||
///
|
||||
/// The specification names SHA-512 as an example of a secure hash to use with this function,
|
||||
/// but you may use any 512-bit hash within reason (see the
|
||||
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
|
||||
pub fn hash_to_curve<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> EdwardsPoint
|
||||
|
|
@ -651,34 +729,12 @@ impl EdwardsPoint {
|
|||
// For reference see
|
||||
// https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method-2
|
||||
|
||||
let fe = FieldElement::hash_to_field::<D>(bytes, domain_sep);
|
||||
let (M1, is_sq) = crate::montgomery::elligator_encode(&fe);
|
||||
let fe = FieldElement::hash_to_field::<D, 2>(bytes, domain_sep);
|
||||
let Q0 = Self::map_to_curve(fe[0]);
|
||||
let Q1 = Self::map_to_curve(fe[1]);
|
||||
|
||||
// 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()
|
||||
let R = Q0 + Q1;
|
||||
R.mul_by_cofactor()
|
||||
}
|
||||
|
||||
/// Return an `EdwardsPoint` chosen uniformly at random using a user-provided RNG.
|
||||
|
|
@ -2387,10 +2443,10 @@ mod test {
|
|||
}
|
||||
|
||||
// Hash-to-curve test vectors from
|
||||
// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
|
||||
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.2
|
||||
// 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)] = &[
|
||||
const RFC_ENCODE_TO_CURVE_KAT: &[(&[u8], &str, &str)] = &[
|
||||
(
|
||||
b"",
|
||||
"1ff2b70ecf862799e11b7ae744e3489aa058ce805dd323a936375a84695e76da",
|
||||
|
|
@ -2424,32 +2480,88 @@ mod test {
|
|||
)
|
||||
];
|
||||
|
||||
#[cfg(all(feature = "alloc", feature = "digest"))]
|
||||
fn hex_str_to_fe(hex_str: &str) -> FieldElement {
|
||||
let mut bytes = hex::decode(hex_str).unwrap().to_vec();
|
||||
bytes.reverse();
|
||||
FieldElement::from_bytes(&bytes.try_into().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(feature = "alloc", feature = "digest"))]
|
||||
fn elligator_encode_to_curve_test_vectors() {
|
||||
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
|
||||
for (index, vector) in RFC_ENCODE_TO_CURVE_KAT.iter().enumerate() {
|
||||
let input = vector.0;
|
||||
|
||||
let expected_output = {
|
||||
let x = hex_str_to_fe(vector.1);
|
||||
let y = hex_str_to_fe(vector.2);
|
||||
AffinePoint { x, y }.to_edwards()
|
||||
};
|
||||
|
||||
let computed = EdwardsPoint::encode_to_curve::<sha2::Sha512>(&[&input], &[dst]);
|
||||
assert_eq!(computed, expected_output, "Failed in test {}", index);
|
||||
}
|
||||
}
|
||||
|
||||
// Hash-to-curve test vectors from
|
||||
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.1
|
||||
// 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"",
|
||||
"3c3da6925a3c3c268448dcabb47ccde5439559d9599646a8260e47b1e4822fc6",
|
||||
"09a6c8561a0b22bef63124c588ce4c62ea83a3c899763af26d795302e115dc21",
|
||||
),
|
||||
|
||||
(
|
||||
b"abc",
|
||||
"608040b42285cc0d72cbb3985c6b04c935370c7361f4b7fbdb1ae7f8c1a8ecad",
|
||||
"1a8395b88338f22e435bbd301183e7f20a5f9de643f11882fb237f88268a5531",
|
||||
),
|
||||
|
||||
(
|
||||
b"abcdef0123456789",
|
||||
"6d7fabf47a2dc03fe7d47f7dddd21082c5fb8f86743cd020f3fb147d57161472",
|
||||
"53060a3d140e7fbcda641ed3cf42c88a75411e648a1add71217f70ea8ec561a6",
|
||||
),
|
||||
(
|
||||
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
|
||||
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||
"5fb0b92acedd16f3bcb0ef83f5c7b7a9466b5f1e0d8d217421878ea3686f8524",
|
||||
"2eca15e355fcfa39d2982f67ddb0eea138e2994f5956ed37b7f72eea5e89d2f7",
|
||||
),
|
||||
(
|
||||
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"0efcfde5898a839b00997fbe40d2ebe950bc81181afbd5cd6b9618aa336c1e8c",
|
||||
"6dc2fc04f266c5c27f236a80b14f92ccd051ef1ff027f26a07f8c0f327d8f995"
|
||||
)
|
||||
];
|
||||
|
||||
#[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_";
|
||||
let dst = b"QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_RO_";
|
||||
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 x = hex_str_to_fe(vector.1);
|
||||
let y = hex_str_to_fe(vector.2);
|
||||
|
||||
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,
|
||||
}
|
||||
AffinePoint { x, y }.to_edwards()
|
||||
};
|
||||
|
||||
let computed = EdwardsPoint::hash_to_curve::<sha2::Sha512>(&[&input], &[dst]);
|
||||
assert_eq!(computed, expected_output, "Failed in test {}", index);
|
||||
|
||||
assert_eq!(expected_output, computed, "Failed in test {}", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ impl FieldElement {
|
|||
/// Raise this field element to the power (p-5)/8 = 2^252 -3.
|
||||
#[rustfmt::skip] // keep alignment of explanatory comments
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn pow_p58(&self) -> FieldElement {
|
||||
pub(crate) fn pow_p58(&self) -> FieldElement {
|
||||
// The bits of (p-5)/8 are 101111.....11.
|
||||
//
|
||||
// nonzero bits of exponent
|
||||
|
|
@ -354,62 +354,139 @@ impl FieldElement {
|
|||
}
|
||||
|
||||
#[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.
|
||||
/// Hashes the given message and domain separator to produce `COUNT` [`FieldElement`]s, per RFC
|
||||
/// 9380. The hash's input is the concatenation of the elements of `msg`. Likewise for the
|
||||
/// domain separator with `domain_sep`. At least one element of `domain_sep`, MUST be nonempty,
|
||||
/// its concatenation MUST NOT exceed 255 bytes, and `COUNT` MUST be 1 or 2.
|
||||
///
|
||||
/// The specification names SHA-512 as an example of a secure hash to use with this function,
|
||||
/// but you may use any 512-bit hash within reason (see the
|
||||
/// [`spec`](https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2) for details).
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `domain_sep.collect().len() == 0` or `> 255`
|
||||
pub fn hash_to_field<D>(bytes: &[&[u8]], domain_sep: &[&[u8]]) -> Self
|
||||
/// Panics if `domain_sep.collect().len() == 0` or `> 255`. Also panics if `COUNT > 2` or
|
||||
/// `COUNT == 0`.
|
||||
pub fn hash_to_field<D, const COUNT: usize>(
|
||||
msg: &[&[u8]],
|
||||
domain_sep: &[&[u8]],
|
||||
) -> [Self; COUNT]
|
||||
where
|
||||
D: BlockSizeUser + Default + FixedOutput<OutputSize = U64> + HashMarker,
|
||||
D::BlockSize: IsGreater<D::OutputSize, Output = True>,
|
||||
{
|
||||
let l_i_b_str = 48u16.to_be_bytes();
|
||||
let z_pad = Array::<u8, D::BlockSize>::default();
|
||||
// We only use `hash_to_field` for Elligator2, which uses 0 < COUNT <= 2
|
||||
assert!(COUNT == 1 || COUNT == 2);
|
||||
|
||||
let mut hasher = D::new().chain_update(z_pad);
|
||||
// §5.2, we generate count * m * L = COUNT * 1 * (256 + 128)/8 bytes
|
||||
let len_in_bytes = COUNT * 48;
|
||||
// Buffer should hold however many digests we need to produce len_in_bytes bytes.
|
||||
// len_in_bytes is at most 2*48=96, and the digest is 64B. So that's 2 digests, or 128B
|
||||
let mut buf = [0u8; 128];
|
||||
// We can call this without worrying about panics: len_in_bytes is at most 96, and buf is
|
||||
// bigger than it. Further, if `domain_sep` is too large, that's also a panic condition of
|
||||
// this function so it's fine
|
||||
let uniform_bytes = expand_msg_xmd::<D>(msg, domain_sep, &mut buf, len_in_bytes);
|
||||
|
||||
for slice in bytes {
|
||||
hasher = hasher.chain_update(slice);
|
||||
let mut result = [FieldElement::ONE; COUNT];
|
||||
for i in 0..COUNT {
|
||||
let mut bytes_wide = [0u8; 64];
|
||||
bytes_wide[..48].copy_from_slice(&uniform_bytes[48 * i..48 * (i + 1)]);
|
||||
bytes_wide[..48].reverse();
|
||||
|
||||
result[i] = FieldElement::from_bytes_wide(&bytes_wide);
|
||||
}
|
||||
|
||||
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)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes the concatenation of the elements of `msg` with domain separator equal to the
|
||||
/// concatenation of `domain_sep`. The output is an `outlen`-length slice into `buf`. Follows
|
||||
/// https://www.rfc-editor.org/rfc/rfc9380.html#section-5.3.1
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the domain separator is empty, if the total length of the domain separator is more
|
||||
/// than 255 bytes, if `outlen` is more than `255 * D::output_size()`, if `outlen is more than
|
||||
/// 65535, or if `dst.len() < outlen`.
|
||||
#[cfg(feature = "digest")]
|
||||
fn expand_msg_xmd<'a, D>(
|
||||
msg: &[&[u8]],
|
||||
domain_sep: &[&[u8]],
|
||||
buf: &'a mut [u8],
|
||||
outlen: usize,
|
||||
) -> &'a [u8]
|
||||
where
|
||||
D: BlockSizeUser + Default + FixedOutput + HashMarker,
|
||||
{
|
||||
use core::iter::once;
|
||||
|
||||
// The notation we use in this function is the same as in the spec
|
||||
let len_in_bytes = u16::try_from(outlen).expect("outlen must not exceed 65535");
|
||||
|
||||
assert!(buf.len() >= outlen);
|
||||
let ell = u8::try_from((len_in_bytes as usize).div_ceil(D::output_size()))
|
||||
.expect("output length cannot exceed 255 times digest size");
|
||||
let domain_sep_len = u8::try_from(domain_sep.iter().map(|c| c.len()).sum::<usize>())
|
||||
.expect("unexpected overflow from domain separator's size.");
|
||||
assert_ne!(
|
||||
domain_sep_len, 0,
|
||||
"domain separator MUST have nonzero length."
|
||||
);
|
||||
|
||||
let domain_sep_len_slice = &[domain_sep_len][..];
|
||||
let dst_prime = domain_sep.iter().copied().chain(once(domain_sep_len_slice));
|
||||
let z_pad = Array::<u8, D::BlockSize>::default();
|
||||
let l_i_b_str = len_in_bytes.to_be_bytes();
|
||||
|
||||
// Collect the components of msg_prime
|
||||
let msg_prime = once(z_pad.as_slice())
|
||||
.chain(msg.iter().copied())
|
||||
.chain(once(l_i_b_str.as_slice()))
|
||||
.chain(once(&[0u8][..]))
|
||||
.chain(dst_prime.clone());
|
||||
// Hash all of msg_prime
|
||||
let b_0 = msg_prime
|
||||
.fold(D::new(), |h, slice| h.chain_update(slice))
|
||||
.finalize();
|
||||
|
||||
// Collect the input components for the b_1 hash
|
||||
let b_1_input = once(b_0.as_slice())
|
||||
.chain(once(&[1u8][..]))
|
||||
.chain(dst_prime.clone());
|
||||
let b_1 = b_1_input
|
||||
.fold(D::new(), |h, slice| h.chain_update(slice))
|
||||
.finalize();
|
||||
// Write b_1 to the output buffer
|
||||
buf[..D::output_size()].copy_from_slice(&b_1);
|
||||
|
||||
for i in 2..=ell {
|
||||
// Get the last digest we produced
|
||||
let i_ = i as usize;
|
||||
let last_b = &buf[(i_ - 2) * D::output_size()..(i_ - 1) * D::output_size()];
|
||||
// XOR it with b_0
|
||||
let mut xor_bs = b_0.clone();
|
||||
xor_bs
|
||||
.as_mut_slice()
|
||||
.iter_mut()
|
||||
.zip(last_b)
|
||||
.for_each(|(l, r)| *l ^= *r);
|
||||
|
||||
// Hash (b_0 ^ b_{i-1}) || i || dst_prime
|
||||
let i_slice = &[i][..];
|
||||
let b_i_input = once(xor_bs.as_slice())
|
||||
.chain(once(i_slice))
|
||||
.chain(dst_prime.clone());
|
||||
let b_i = b_i_input
|
||||
.fold(D::new(), |h, slice| h.chain_update(slice))
|
||||
.finalize();
|
||||
|
||||
// Write b_i to the output buffer
|
||||
buf[(i_ - 1) * D::output_size()..i_ * D::output_size()].copy_from_slice(&b_i);
|
||||
}
|
||||
|
||||
&buf[..outlen]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::field::*;
|
||||
|
|
@ -721,8 +798,15 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
fn fe_from_test_vector(expected_hex: &str) -> FieldElement {
|
||||
let mut expected_hash = hex::decode(expected_hex).unwrap();
|
||||
expected_hash.reverse();
|
||||
FieldElement::from_bytes(&expected_hash.try_into().unwrap())
|
||||
}
|
||||
|
||||
/// Hash to field test vectors from
|
||||
/// https://www.rfc-editor.org/rfc/rfc9380.html#name-edwards25519_xmdsha-512_ell2
|
||||
/// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.2
|
||||
/// These are of the form (input_msg, output_field_elem)
|
||||
#[cfg(feature = "digest")]
|
||||
const RFC_HASH_TO_FIELD_KAT: &[(&[u8], &str)] = &[
|
||||
|
|
@ -761,14 +845,67 @@ mod test {
|
|||
let dst = "QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_";
|
||||
|
||||
for (msg, expected_hash_hex) in RFC_HASH_TO_FIELD_KAT {
|
||||
let fe = FieldElement::hash_to_field::<Sha512>(&[msg], &[dst.as_bytes()]);
|
||||
let expected_fe = {
|
||||
let mut expected_hash = hex::decode(expected_hash_hex).unwrap();
|
||||
expected_hash.reverse();
|
||||
FieldElement::from_bytes(&expected_hash.try_into().unwrap())
|
||||
};
|
||||
let fe = FieldElement::hash_to_field::<Sha512, 1>(&[msg], &[dst.as_bytes()])[0];
|
||||
let expected_fe = fe_from_test_vector(expected_hash_hex);
|
||||
|
||||
assert_eq!(fe, expected_fe);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash to field test vectors from
|
||||
/// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-J.5.1
|
||||
/// These are of the form (input_msg, output_field_elem, output_field_elem)
|
||||
#[cfg(feature = "digest")]
|
||||
const RFC_HASH_TO_FIELD_KAT_2: &[(&[u8], &str, &str)] = &[
|
||||
(
|
||||
b"",
|
||||
"03fef4813c8cb5f98c6eef88fae174e6e7d5380de2b007799ac7ee712d203f3a",
|
||||
"780bdddd137290c8f589dc687795aafae35f6b674668d92bf92ae793e6a60c75"
|
||||
),
|
||||
(
|
||||
b"abc",
|
||||
"5081955c4141e4e7d02ec0e36becffaa1934df4d7a270f70679c78f9bd57c227",
|
||||
"005bdc17a9b378b6272573a31b04361f21c371b256252ae5463119aa0b925b76"
|
||||
),
|
||||
(
|
||||
b"abcdef0123456789",
|
||||
"285ebaa3be701b79871bcb6e225ecc9b0b32dff2d60424b4c50642636a78d5b3",
|
||||
"2e253e6a0ef658fedb8e4bd6a62d1544fd6547922acb3598ec6b369760b81b31"
|
||||
),
|
||||
(
|
||||
b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\
|
||||
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||
"4fedd25431c41f2a606952e2945ef5e3ac905a42cf64b8b4d4a83c533bf321af",
|
||||
"02f20716a5801b843987097a8276b6d869295b2e11253751ca72c109d37485a9"
|
||||
),
|
||||
(
|
||||
b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"6e34e04a5106e9bd59f64aba49601bf09d23b27f7b594e56d5de06df4a4ea33b",
|
||||
"1c1c2cb59fc053f44b86c5d5eb8c1954b64976d0302d3729ff66e84068f5fd96"
|
||||
)
|
||||
];
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "digest")]
|
||||
fn hash_to_field_2() {
|
||||
use sha2::Sha512;
|
||||
let dst = "QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_RO_";
|
||||
|
||||
for (msg, expected_hash_hex_1, expected_hash_hex_2) in
|
||||
crate::field::test::RFC_HASH_TO_FIELD_KAT_2
|
||||
{
|
||||
let fe = FieldElement::hash_to_field::<Sha512, 2>(&[msg], &[dst.as_bytes()]);
|
||||
|
||||
let expected_fe_1 = fe_from_test_vector(expected_hash_hex_1);
|
||||
assert_eq!(fe[0], expected_fe_1);
|
||||
|
||||
let expected_fe_2 = fe_from_test_vector(expected_hash_hex_2);
|
||||
assert_eq!(fe[1], expected_fe_2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,9 @@ use core::{
|
|||
ops::{Mul, MulAssign},
|
||||
};
|
||||
|
||||
use crate::constants::{APLUS2_OVER_FOUR, MONTGOMERY_A, MONTGOMERY_A_NEG};
|
||||
use crate::constants::APLUS2_OVER_FOUR;
|
||||
#[cfg(feature = "digest")]
|
||||
use crate::constants::{MONTGOMERY_A, MONTGOMERY_A_NEG, SQRT_M1};
|
||||
use crate::edwards::{CompressedEdwardsY, EdwardsPoint};
|
||||
use crate::field::FieldElement;
|
||||
use crate::scalar::{Scalar, clamp_integer};
|
||||
|
|
@ -62,12 +64,26 @@ use crate::scalar::{Scalar, clamp_integer};
|
|||
use crate::traits::Identity;
|
||||
|
||||
use subtle::Choice;
|
||||
use subtle::ConditionallySelectable;
|
||||
use subtle::ConstantTimeEq;
|
||||
use subtle::{ConditionallyNegatable, ConditionallySelectable};
|
||||
|
||||
#[cfg(feature = "zeroize")]
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// We need the const 2^((p+3)/8) for elligator_encode. These defs are checked in tests::consts()
|
||||
#[cfg(all(curve25519_dalek_bits = "32", feature = "digest"))]
|
||||
const FE_C2: FieldElement = FieldElement::from_limbs([
|
||||
34513073, 25610706, 9377949, 3500415, 12389472, 33281959, 41962654, 31548777, 326685, 11406482,
|
||||
]);
|
||||
#[cfg(all(curve25519_dalek_bits = "64", feature = "digest"))]
|
||||
const FE_C2: FieldElement = FieldElement::from_limbs([
|
||||
1718705420411057,
|
||||
234908883556509,
|
||||
2233514472574048,
|
||||
2117202627021982,
|
||||
765476049583133,
|
||||
]);
|
||||
|
||||
/// Holds the \\(u\\)-coordinate of a point on the Montgomery form of
|
||||
/// Curve25519 or its twist.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
|
|
@ -252,33 +268,98 @@ impl MontgomeryPoint {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[cfg(feature = "digest")]
|
||||
/// Perform the Elligator2 mapping to a tuple `(xn, xd, yn, yd)` such that
|
||||
/// `(xn / xd, yn / yd)` is a point on curve25519.
|
||||
///
|
||||
/// See <https://www.rfc-editor.org/rfc/rfc9380.html#name-elligator-2-method>
|
||||
//
|
||||
#[allow(unused)]
|
||||
pub(crate) fn elligator_encode(r_0: &FieldElement) -> (MontgomeryPoint, Choice) {
|
||||
pub(crate) fn elligator_encode(
|
||||
u: &FieldElement,
|
||||
) -> (FieldElement, FieldElement, FieldElement, FieldElement) {
|
||||
// We follow https://www.rfc-editor.org/rfc/rfc9380.html#appendix-G.2.1
|
||||
|
||||
use core::ops::Neg;
|
||||
let one = FieldElement::ONE;
|
||||
let d_1 = &one + &r_0.square2(); /* 2r^2 */
|
||||
let c2 = FE_C2;
|
||||
|
||||
let d = &MONTGOMERY_A_NEG * &(d_1.invert()); /* A/(1+2r^2) */
|
||||
// 1. tv1 = u^2
|
||||
// 2. tv1 = 2 * tv1
|
||||
let tv1 = u.square2();
|
||||
// 3. xd = tv1 + 1
|
||||
let xd = &one + &tv1;
|
||||
// 4. x1n = -J
|
||||
let x1n = MONTGOMERY_A_NEG;
|
||||
// 5. tv2 = xd^2
|
||||
let tv2 = xd.square();
|
||||
// 6. gxd = tv2 * xd
|
||||
let gxd = &tv2 * &xd;
|
||||
// 7. gx1 = J * tv1
|
||||
let gx1 = &MONTGOMERY_A * &tv1;
|
||||
// 8. gx1 = gx1 * x1n
|
||||
let gx1 = &gx1 * &x1n;
|
||||
// 9. gx1 = gx1 + tv2
|
||||
let gx1 = &gx1 + &tv2;
|
||||
// 10. gx1 = gx1 * x1n
|
||||
let gx1 = &gx1 * &x1n;
|
||||
// 11. tv3 = gxd^2
|
||||
let tv3 = gxd.square();
|
||||
// 12. tv2 = tv3^2
|
||||
let tv2 = tv3.square();
|
||||
// 13. tv3 = tv3 * gxd
|
||||
let tv3 = &tv3 * &gxd;
|
||||
// 14. tv3 = tv3 * gx1
|
||||
let tv3 = &tv3 * &gx1;
|
||||
// 15. tv2 = tv2 * tv3
|
||||
let tv2 = &tv2 * &tv3;
|
||||
// 16. y11 = tv2^c4
|
||||
let y11 = tv2.pow_p58();
|
||||
// 17. y11 = y11 * tv3
|
||||
let y11 = &y11 * &tv3;
|
||||
// 18. y12 = y11 * c3
|
||||
let y12 = &y11 * &SQRT_M1;
|
||||
// 19. tv2 = y11^2
|
||||
let tv2 = y11.square();
|
||||
// 20. tv2 = tv2 * gxd
|
||||
let tv2 = &tv2 * &gxd;
|
||||
// 21. e1 = tv2 == gx1
|
||||
let e1 = tv2.ct_eq(&gx1);
|
||||
// 22. y1 = CMOV(y12, y11, e1)
|
||||
let y1 = FieldElement::conditional_select(&y12, &y11, e1);
|
||||
// 23. x2n = x1n * tv1
|
||||
let x2n = &x1n * &tv1;
|
||||
// 24. y21 = y11 * u
|
||||
let y21 = &y11 * u;
|
||||
// 25. y21 = y21 * c2
|
||||
let y21 = &y21 * &c2;
|
||||
// 26. y22 = y21 * c3
|
||||
let y22 = &y21 * &SQRT_M1;
|
||||
// 27. gx2 = gx1 * tv1
|
||||
let gx2 = &gx1 * &tv1;
|
||||
// 28. tv2 = y21^2
|
||||
let tv2 = y21.square();
|
||||
// 29. tv2 = tv2 * gxd
|
||||
let tv2 = &tv2 * &gxd;
|
||||
// 30. e2 = tv2 == gx2
|
||||
let e2 = tv2.ct_eq(&gx2);
|
||||
// 31. y2 = CMOV(y22, y21, e2)
|
||||
let y2 = FieldElement::conditional_select(&y22, &y21, e2);
|
||||
// 32. tv2 = y1^2
|
||||
let tv2 = y1.square();
|
||||
// 33. tv2 = tv2 * gxd
|
||||
let tv2 = &tv2 * &gxd;
|
||||
// 34. e3 = tv2 == gx1
|
||||
let e3 = tv2.ct_eq(&gx1);
|
||||
// 35. xn = CMOV(x2n, x1n, e3)
|
||||
let xn = FieldElement::conditional_select(&x2n, &x1n, e3);
|
||||
// 36. y = CMOV(y2, y1, e3)
|
||||
let y = FieldElement::conditional_select(&y2, &y1, e3);
|
||||
// 37. e4 = sgn0(y) == 1
|
||||
let e4 = y.is_negative();
|
||||
// 38. y = CMOV(y, -y, e3 XOR e4)
|
||||
let y = FieldElement::conditional_select(&y, &y.neg(), e3 ^ e4);
|
||||
// 39. return (xn, xd, y, 1)
|
||||
|
||||
let d_sq = &d.square();
|
||||
let au = &MONTGOMERY_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(&MONTGOMERY_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()), eps_is_sq)
|
||||
(xn, xd, y, one)
|
||||
}
|
||||
|
||||
/// A `ProjectivePoint` holds a point on the projective line
|
||||
|
|
@ -434,9 +515,6 @@ mod test {
|
|||
use super::*;
|
||||
use crate::constants;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use rand_core::{CryptoRng, RngCore, TryRngCore};
|
||||
|
||||
#[test]
|
||||
|
|
@ -639,6 +717,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg(feature = "digest")]
|
||||
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,
|
||||
|
|
@ -647,20 +726,37 @@ mod test {
|
|||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg(feature = "digest")]
|
||||
fn montgomery_elligator_correct() {
|
||||
use alloc::vec::Vec;
|
||||
let bytes: Vec<u8> = (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_encode(&fe);
|
||||
assert_eq!(eg.to_bytes(), ELLIGATOR_CORRECT_OUTPUT);
|
||||
let (un, ud, ..) = elligator_encode(&fe);
|
||||
let u = &un * &ud.invert();
|
||||
assert_eq!(u.to_bytes(), ELLIGATOR_CORRECT_OUTPUT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "digest")]
|
||||
fn montgomery_elligator_zero_zero() {
|
||||
let zero = [0u8; 32];
|
||||
let fe = FieldElement::from_bytes(&zero);
|
||||
let (eg, _) = elligator_encode(&fe);
|
||||
assert_eq!(eg.to_bytes(), zero);
|
||||
let (un, ud, ..) = elligator_encode(&fe);
|
||||
let u = &un * &ud.invert();
|
||||
assert_eq!(u.to_bytes(), zero);
|
||||
}
|
||||
|
||||
// Check that FE_C2 is correctly defined
|
||||
#[test]
|
||||
#[cfg(feature = "digest")]
|
||||
fn c2() {
|
||||
let one = FieldElement::ONE;
|
||||
let two = &one + &one;
|
||||
// c2 = 2^((p+3)/8) = 2^((p-5)/8 + 8/8) = 2*2^((p-5)/8)
|
||||
let c2 = &two * &(two.pow_p58());
|
||||
|
||||
assert_eq!(c2, FE_C2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue