Skip to content

Hash to curve as defined in the standard #377

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

Merged
merged 26 commits into from
Jul 5, 2025
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd7bbf4
Hash to curve as defined in the standard
iquerejeta Nov 11, 2021
3ed563c
Fixing errors from rebase
iquerejeta Apr 25, 2025
3bb26e2
Some issues with imports and no_std
iquerejeta Apr 25, 2025
5f7e8b4
Addressing review comments
iquerejeta May 22, 2025
30e7ed9
Issues with docs
iquerejeta May 23, 2025
97bbb08
Visibility of ed25519_sqrtam2 test
iquerejeta May 23, 2025
695e03d
Address review comments
iquerejeta May 26, 2025
a4ffcc6
Check BlockSize > OutputSize
iquerejeta May 26, 2025
8709d77
Nits
iquerejeta May 26, 2025
62d6a8e
Check non-zero length for DST
iquerejeta May 26, 2025
18cc71c
Rename hash_to_curve function; fix up docs
rozbb May 27, 2025
ab72290
Add inline comments
iquerejeta May 29, 2025
b45e371
Add comment for justifying there is no overflow
iquerejeta May 30, 2025
2d52686
Remove some leftover use of deprecated function as_bytes()
iquerejeta Jun 25, 2025
48aa494
Clean up documentation; make KATs clearer
rozbb Jun 27, 2025
b34c67e
Bad copy/paste
rozbb Jun 27, 2025
58e8090
Clippy
rozbb Jun 27, 2025
71ad99a
Make new from_bytes_wide KATs from sage
rozbb Jun 28, 2025
52f6ca6
Fix docs build
rozbb Jun 28, 2025
f8ebed1
Make from_bytes const for every backend
rozbb Jul 5, 2025
20ecda2
Make from_bytes_wide generic over the underlying field element type
rozbb Jul 5, 2025
b987e9a
Add Edwards hash_to_curve bench
rozbb Jul 5, 2025
158ba55
Update changelog
rozbb Jul 5, 2025
c619ebc
Fix unused
rozbb Jul 5, 2025
5fc63b3
Fix more unused
rozbb Jul 5, 2025
641dbeb
Update changelog
rozbb Jul 5, 2025
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
3 changes: 2 additions & 1 deletion curve25519-dalek/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ zeroize = { version = "1", default-features = false, optional = true }
cpufeatures = "0.2.17"

[target.'cfg(curve25519_dalek_backend = "fiat")'.dependencies]
fiat-crypto = { version = "0.2.1", default-features = false }
fiat-crypto = { version = "0.3.0", default-features = false }

[features]
default = ["alloc", "precomputed-tables", "zeroize"]
Expand All @@ -68,6 +68,7 @@ precomputed-tables = []
legacy_compatibility = []
group = ["dep:group", "rand_core"]
group-bits = ["group", "ff/bits"]
digest = ["dep:digest", "digest/core-api"]

[target.'cfg(all(not(curve25519_dalek_backend = "fiat"), not(curve25519_dalek_backend = "serial"), target_arch = "x86_64"))'.dependencies]
curve25519-dalek-derive = { version = "0.1", path = "../curve25519-dalek-derive" }
Expand Down
20 changes: 19 additions & 1 deletion curve25519-dalek/benches/dalek_benchmarks.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#![allow(non_snake_case)]

use rand::{rngs::OsRng, thread_rng};
use rand::{rngs::OsRng, thread_rng, RngCore};

use criterion::{
criterion_main, measurement::Measurement, BatchSize, BenchmarkGroup, BenchmarkId, Criterion,
};
#[cfg(feature = "digest")]
use sha2::Sha512;

use curve25519_dalek::constants;
use curve25519_dalek::scalar::Scalar;
Expand Down Expand Up @@ -72,6 +74,21 @@ mod edwards_benches {
});
}

#[cfg(feature = "digest")]
fn hash_to_curve<M: Measurement>(c: &mut BenchmarkGroup<M>) {
let mut rng = thread_rng();

let mut msg = [0u8; 32];
let mut domain_sep = [0u8; 32];
rng.fill_bytes(&mut msg);
rng.fill_bytes(&mut domain_sep);

c.bench_function(
"Elligator2 hash to curve (SHA-512, input size 32 bytes)",
|b| b.iter(|| EdwardsPoint::hash_to_curve::<Sha512>(&[&msg], &[&domain_sep])),
);
}

pub(crate) fn edwards_benches() {
let mut c = Criterion::default();
let mut g = c.benchmark_group("edwards benches");
Expand All @@ -83,6 +100,7 @@ mod edwards_benches {
consttime_fixed_base_scalar_mul(&mut g);
consttime_variable_base_scalar_mul(&mut g);
vartime_double_base_scalar_mul(&mut g);
hash_to_curve(&mut g);
}
}

Expand Down
2 changes: 1 addition & 1 deletion curve25519-dalek/src/backend/serial/fiat_u32/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ impl FieldElement2625 {
/// encoding of every field element should decode, re-encode to
/// the canonical encoding, and check that the input was
/// canonical.
pub fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
pub const fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
let mut temp = [0u8; 32];
temp.copy_from_slice(data);
temp[31] &= 127u8;
Expand Down
2 changes: 1 addition & 1 deletion curve25519-dalek/src/backend/serial/fiat_u64/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ impl FieldElement51 {
/// the canonical encoding, and check that the input was
/// canonical.
///
pub fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
pub const fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
let mut temp = [0u8; 32];
temp.copy_from_slice(bytes);
temp[31] &= 127u8;
Expand Down
7 changes: 7 additions & 0 deletions curve25519-dalek/src/backend/serial/u32/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ pub(crate) const MINUS_ONE: FieldElement2625 = FieldElement2625::from_limbs([
33554431,
]);

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

/// Edwards `d` value, equal to `-121665/121666 mod p`.
pub(crate) const EDWARDS_D: FieldElement2625 = FieldElement2625::from_limbs([
56195235, 13857412, 51736253, 6949390, 114729, 24766616, 60832955, 30306712, 48412415, 21499315,
Expand Down
37 changes: 20 additions & 17 deletions curve25519-dalek/src/backend/serial/u32/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,14 @@ impl FieldElement2625 {
/// In other words, each coefficient of the result is bounded by
/// either `2^(25 + 0.007)` or `2^(26 + 0.007)`, as appropriate.
#[rustfmt::skip] // keep alignment of carry chain
fn reduce(mut z: [u64; 10]) -> FieldElement2625 {
const fn reduce(mut z: [u64; 10]) -> FieldElement2625 {

const LOW_25_BITS: u64 = (1 << 25) - 1;
const LOW_26_BITS: u64 = (1 << 26) - 1;

/// Carry the value from limb i = 0..8 to limb i+1
#[inline(always)]
fn carry(z: &mut [u64; 10], i: usize) {
const fn carry(z: &mut [u64; 10], i: usize) {
debug_assert!(i < 9);
if i % 2 == 0 {
// Even limbs have 26 bits
Expand Down Expand Up @@ -401,29 +401,32 @@ impl FieldElement2625 {
/// the canonical encoding, and check that the input was
/// canonical.
#[rustfmt::skip] // keep alignment of h[*] values
pub fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
pub const fn from_bytes(data: &[u8; 32]) -> FieldElement2625 {
#[inline]
fn load3(b: &[u8]) -> u64 {
(b[0] as u64) | ((b[1] as u64) << 8) | ((b[2] as u64) << 16)
const fn load3_at(b: &[u8], i: usize) -> u64 {
(b[i] as u64) | ((b[i + 1] as u64) << 8) | ((b[i + 2] as u64) << 16)
}

#[inline]
fn load4(b: &[u8]) -> u64 {
(b[0] as u64) | ((b[1] as u64) << 8) | ((b[2] as u64) << 16) | ((b[3] as u64) << 24)
const fn load4_at(b: &[u8], i: usize) -> u64 {
(b[i] as u64)
| ((b[i + 1] as u64) << 8)
| ((b[i + 2] as u64) << 16)
| ((b[i + 3] as u64) << 24)
}

let mut h = [0u64;10];
const LOW_23_BITS: u64 = (1 << 23) - 1;
h[0] = load4(&data[ 0..]);
h[1] = load3(&data[ 4..]) << 6;
h[2] = load3(&data[ 7..]) << 5;
h[3] = load3(&data[10..]) << 3;
h[4] = load3(&data[13..]) << 2;
h[5] = load4(&data[16..]);
h[6] = load3(&data[20..]) << 7;
h[7] = load3(&data[23..]) << 5;
h[8] = load3(&data[26..]) << 4;
h[9] = (load3(&data[29..]) & LOW_23_BITS) << 2;
h[0] = load4_at(data, 0);
h[1] = load3_at(data, 4) << 6;
h[2] = load3_at(data, 7) << 5;
h[3] = load3_at(data, 10) << 3;
h[4] = load3_at(data, 13) << 2;
h[5] = load4_at(data, 16);
h[6] = load3_at(data, 20) << 7;
h[7] = load3_at(data, 23) << 5;
h[8] = load3_at(data, 26) << 4;
h[9] = (load3_at(data, 29) & LOW_23_BITS) << 2;

FieldElement2625::reduce(h)
}
Expand Down
10 changes: 10 additions & 0 deletions curve25519-dalek/src/backend/serial/u64/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ pub(crate) const MINUS_ONE: FieldElement51 = FieldElement51::from_limbs([
2251799813685247,
]);

/// sqrt(-486664)
#[cfg(feature = "digest")]
pub(crate) const ED25519_SQRTAM2: FieldElement51 = FieldElement51::from_limbs([
1693982333959686,
608509411481997,
2235573344831311,
947681270984193,
266558006233600,
]);

/// Edwards `d` value, equal to `-121665/121666 mod p`.
pub(crate) const EDWARDS_D: FieldElement51 = FieldElement51::from_limbs([
929955233495203,
Expand Down
32 changes: 16 additions & 16 deletions curve25519-dalek/src/backend/serial/u64/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,30 +335,30 @@ impl FieldElement51 {
/// canonical.
///
#[rustfmt::skip] // keep alignment of bit shifts
pub fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
let load8 = |input: &[u8]| -> u64 {
(input[0] as u64)
| ((input[1] as u64) << 8)
| ((input[2] as u64) << 16)
| ((input[3] as u64) << 24)
| ((input[4] as u64) << 32)
| ((input[5] as u64) << 40)
| ((input[6] as u64) << 48)
| ((input[7] as u64) << 56)
};
pub const fn from_bytes(bytes: &[u8; 32]) -> FieldElement51 {
const fn load8_at(input: &[u8], i: usize) -> u64 {
(input[i] as u64)
| ((input[i + 1] as u64) << 8)
| ((input[i + 2] as u64) << 16)
| ((input[i + 3] as u64) << 24)
| ((input[i + 4] as u64) << 32)
| ((input[i + 5] as u64) << 40)
| ((input[i + 6] as u64) << 48)
| ((input[i + 7] as u64) << 56)
}

let low_51_bit_mask = (1u64 << 51) - 1;
FieldElement51(
// load bits [ 0, 64), no shift
[ load8(&bytes[ 0..]) & low_51_bit_mask
[ load8_at(bytes, 0) & low_51_bit_mask
// load bits [ 48,112), shift to [ 51,112)
, (load8(&bytes[ 6..]) >> 3) & low_51_bit_mask
, (load8_at(bytes, 6) >> 3) & low_51_bit_mask
// load bits [ 96,160), shift to [102,160)
, (load8(&bytes[12..]) >> 6) & low_51_bit_mask
, (load8_at(bytes, 12) >> 6) & low_51_bit_mask
// load bits [152,216), shift to [153,216)
, (load8(&bytes[19..]) >> 1) & low_51_bit_mask
, (load8_at(bytes, 19) >> 1) & low_51_bit_mask
// load bits [192,256), shift to [204,112)
, (load8(&bytes[24..]) >> 12) & low_51_bit_mask
, (load8_at(bytes, 24) >> 12) & low_51_bit_mask
])
}

Expand Down
10 changes: 10 additions & 0 deletions curve25519-dalek/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,14 @@ mod test {
let should_be_ad_minus_one = constants::SQRT_AD_MINUS_ONE.square();
assert_eq!(should_be_ad_minus_one, ad_minus_one);
}

/// Test that ED25519_SQRTAM2 squared is MONTGOMERY_A_NEG - 2
#[test]
#[cfg(feature = "digest")]
fn test_sqrt_a_minus_2() {
let one = FieldElement::ONE;
let a_minus_two = &(&constants::MONTGOMERY_A_NEG - &one) - &one;

assert_eq!(constants::ED25519_SQRTAM2.square(), a_minus_two)
}
}
Loading