Skip to content

Commit d7ad294

Browse files
authored
Merge pull request #217 from LLFourn/hash-to-curve
Implement hash_to_curve for secp256k1
2 parents 4dac029 + 44a7290 commit d7ad294

File tree

8 files changed

+991
-2
lines changed

8 files changed

+991
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
- Upgrade to bincode v2
77
- MSRV 1.63 -> 1.85
88
- **BREAKING**: Refactor `CompactProof` in `sigma_fun` to use two type parameters `CompactProof<R, L>` instead of `CompactProof<S: Sigma>` to enable serde support
9+
- Add hash-to-curve methods to `Point`:
10+
- `hash_to_curve` - Simple try-and-increment with uniform distribution (recommended)
11+
- `hash_to_curve_sswu` - RFC 9380 compliant constant-time hashing
12+
- `hash_to_curve_rfc9381_tai` - RFC 9381 VRF try-and-increment format
913

1014
## v0.11.0
1115

secp256kfun/src/backend/k256_impl.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ impl BackendPoint for Point {
6767
}
6868
})
6969
}
70+
71+
fn hash_to_curve<
72+
H: crate::hash::Hash32 + crate::digest::Update + crate::digest::crypto_common::BlockSizeUser,
73+
>(
74+
msg: &[u8],
75+
dst: &[u8],
76+
) -> Point {
77+
crate::vendor::hash_to_curve::hash_to_curve::<H>(msg, dst)
78+
}
7079
}
7180

7281
pub struct ConstantTime;

secp256kfun/src/backend/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ pub trait BackendPoint {
1717
fn norm_to_coordinates(&self) -> ([u8; 32], [u8; 32]);
1818
fn norm_from_bytes_y_oddness(x_bytes: [u8; 32], y_odd: bool) -> Option<Point>;
1919
fn norm_from_coordinates(x: [u8; 32], y: [u8; 32]) -> Option<Point>;
20+
21+
/// Hash to curve implementation following draft-irtf-cfrg-hash-to-curve
22+
/// See: <https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/>
23+
fn hash_to_curve<
24+
H: crate::hash::Hash32 + crate::digest::Update + crate::digest::crypto_common::BlockSizeUser,
25+
>(
26+
msg: &[u8],
27+
dst: &[u8],
28+
) -> Point;
2029
}
2130

2231
#[allow(dead_code)]

secp256kfun/src/point.rs

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
Scalar,
33
backend::{self, BackendPoint, TimeSensitive},
4-
hash::HashInto,
4+
hash::{Hash32, HashInto},
55
marker::*,
66
op,
77
};
@@ -114,6 +114,174 @@ impl Point<Normal, Public, NonZero> {
114114
y.copy_from_slice(&bytes[33..65]);
115115
backend::Point::norm_from_coordinates(x, y).map(|p| Point::from_inner(p, Normal))
116116
}
117+
118+
/// Hash to curve implementation following [RFC 9380]
119+
///
120+
/// Maps arbitrary byte strings to points on the secp256k1 curve in a way that is
121+
/// indifferentiable from a random oracle. This implementation uses the
122+
/// simplified SWU method with a 3-isogeny mapping as specified in
123+
/// [RFC 9380](https://datatracker.ietf.org/doc/rfc9380/).
124+
///
125+
/// ## When to use this method
126+
///
127+
/// The [RFC 9380] method provides constant-time hashing regardless of input, which
128+
/// can be important for denial of service resistance. With try-and-increment
129+
/// methods (like [`hash_to_curve`] and [`hash_to_curve_rfc9381_tai`]), an
130+
/// attacker can craft inputs that require more iterations (up to ~30x in practice),
131+
/// potentially creating a DoS vector. See [this paper](https://eprint.iacr.org/2019/383)
132+
/// for analysis.
133+
///
134+
/// However, in most applications this is not a practical concern because:
135+
/// - Hash-to-curve typically represents a small fraction of total computation
136+
/// - The maximum slowdown is bounded and relatively modest
137+
/// - Creating adversarial inputs requires significant computational resources
138+
///
139+
/// **For most use cases, prefer [`hash_to_curve`]** which is simpler and faster.
140+
/// Only use this method if you have specific DoS concerns and hash-to-curve
141+
/// represents a significant portion of your protocol's computation.
142+
///
143+
/// **HAZMAT WARNING**: It is this author's opinion that [RFC 9380] is overwrought for
144+
/// secp256k1. While this implementation passes test vectors from the
145+
/// [`k256`](https://github.com/RustCrypto/elliptic-curves/tree/master/k256) crate (see their [test vectors](https://github.com/RustCrypto/elliptic-curves/blob/3381a99b6412ef9fa556e32a834e401d569007e3/k256/src/arithmetic/hash2curve.rs#L296)),
146+
/// the complexity of the SSWU algorithm makes me hesitant to recommend its use.
147+
/// The simpler try-and-increment method in [`hash_to_curve`] is preferred.
148+
///
149+
/// # Parameters
150+
/// - `msg`: The message to hash
151+
/// - `dst`: Domain separation tag (DST), should be unique per application
152+
///
153+
/// # Example
154+
/// ```
155+
/// # use secp256kfun::{Point, hash};
156+
/// # use sha2::Sha256;
157+
/// let point = Point::hash_to_curve_sswu::<Sha256>(b"hello world", b"myapp-v1");
158+
/// ```
159+
///
160+
/// [`hash_to_curve`]: Self::hash_to_curve
161+
/// [`hash_to_curve_rfc9381_tai`]: Self::hash_to_curve_rfc9381_tai
162+
/// [RFC 9380]: https://datatracker.ietf.org/doc/html/rfc9380
163+
pub fn hash_to_curve_sswu<H>(msg: &[u8], dst: &[u8]) -> Point<NonNormal, Public, NonZero>
164+
where
165+
H: crate::hash::Hash32 + crate::digest::crypto_common::BlockSizeUser,
166+
{
167+
let backend_point = backend::Point::hash_to_curve::<H>(msg, dst);
168+
Point::from_inner(backend_point, NonNormal)
169+
}
170+
171+
/// Hash to curve using try-and-increment method
172+
///
173+
/// This is a simple and efficient method to hash arbitrary byte strings to curve points
174+
/// with uniform distribution. It works by hashing the input with an incrementing counter
175+
/// until a valid curve point is found.
176+
///
177+
/// **This is the recommended method for most applications.** While it has variable
178+
/// runtime based on input (see [`hash_to_curve_sswu`] for details), this is rarely
179+
/// a practical concern.
180+
///
181+
/// ## Why not the [RFC 9381] try-and-increment?
182+
///
183+
/// The VRF specification ([RFC 9381 §5.4.1.1](https://datatracker.ietf.org/doc/html/rfc9381#section-5.4.1.1))
184+
/// includes a try-and-increment method (see [`hash_to_curve_rfc9381_tai`]) that always
185+
/// uses a fixed y-coordinate parity (0x02). This results in a non-uniform distribution
186+
/// that only includes points with even y-coordinates. Our implementation achieves
187+
/// uniform distribution with a simple modification.
188+
///
189+
/// [`hash_to_curve_rfc9381_tai`]: Self::hash_to_curve_rfc9381_tai
190+
///
191+
/// # Example
192+
/// ```
193+
/// # use secp256kfun::{Point, hash::{Hash32, HashAdd}};
194+
/// # use sha2::Sha256;
195+
/// let hasher = Sha256::default().add(b"hello world");
196+
/// let point = Point::hash_to_curve(hasher);
197+
/// ```
198+
///
199+
/// [`hash_to_curve_sswu`]: Self::hash_to_curve_sswu
200+
/// [RFC 9381]: https://datatracker.ietf.org/doc/html/rfc9381
201+
pub fn hash_to_curve<H: Hash32>(hasher: H) -> Point<Normal, Public, NonZero> {
202+
use crate::hash::HashAdd;
203+
204+
// Try up to 255 times (probability of failure is negligible)
205+
for counter in 0u8..u8::MAX {
206+
let hash_bytes = hasher.clone().add(counter).finalize_fixed();
207+
208+
// Use 0x02 (even y) when counter==0, 0x03 (odd y) when counter>0
209+
// This ensures uniform distribution over all curve points because there is
210+
// a roughly 50% chance that counter will be 0 when we succeed, and this
211+
// probability is independent of the x coordinate distribution
212+
let mut bytes = [0u8; 33];
213+
bytes[0] = 0x02 + (counter > 0) as u8;
214+
bytes[1..].copy_from_slice(&hash_bytes);
215+
216+
if let Some(point) = Point::<Normal, Public, NonZero>::from_bytes(bytes) {
217+
return point;
218+
}
219+
}
220+
221+
// This should never happen (probability ~ 2^-128)
222+
unreachable!("Failed to find valid point after 128 attempts")
223+
}
224+
225+
/// Hash to curve using [RFC 9381] try-and-increment format
226+
///
227+
/// This implements a hash-to-curve method following [RFC 9381]'s try-and-increment
228+
/// algorithm as used in SECP256K1_SHA256_TAI. Note that SECP256K1_SHA256_TAI is not
229+
/// defined in the RFC itself, but is a ciphersuite adopted by various VRF implementations.
230+
///
231+
/// This method always produces points with even y-coordinates (0x02 prefix) which means it's
232+
/// not quite uniform (but this is not a security problem in any reasonable protocol)
233+
///
234+
/// Like other try-and-increment methods, this has variable runtime based on input.
235+
/// See [`hash_to_curve_sswu`] for discussion of DoS considerations.
236+
///
237+
/// [RFC 9381]: https://datatracker.ietf.org/doc/html/rfc9381#section-5.4.1.1
238+
///
239+
/// # Example
240+
/// ```
241+
/// # use secp256kfun::Point;
242+
/// # use sha2::Sha256;
243+
/// let point = Point::hash_to_curve_rfc9381_tai::<Sha256>(b"hello world", b"my-salt");
244+
/// // Use empty bytes if no salt is needed
245+
/// let point2 = Point::hash_to_curve_rfc9381_tai::<Sha256>(b"hello world", b"");
246+
/// ```
247+
///
248+
/// [`hash_to_curve_sswu`]: Self::hash_to_curve_sswu
249+
pub fn hash_to_curve_rfc9381_tai<H: Hash32>(
250+
msg: &[u8],
251+
salt: &[u8],
252+
) -> Point<EvenY, Public, NonZero> {
253+
use crate::hash::HashAdd;
254+
255+
const SUITE_BYTE: u8 = 0xFE; // SECP256K1_SHA256_TAI suite
256+
const DOMAIN_SEP_FRONT: u8 = 0x01;
257+
const DOMAIN_SEP_BACK: u8 = 0x00;
258+
259+
// Pre-compute the invariant part of the hash
260+
let base_hasher = H::default()
261+
.add(SUITE_BYTE)
262+
.add(DOMAIN_SEP_FRONT)
263+
.add(salt)
264+
.add(msg);
265+
266+
// Try up to 255 times (using u8 counter)
267+
for counter in 0u8..u8::MAX {
268+
let hash_bytes = base_hasher
269+
.clone()
270+
.add(counter)
271+
.add(DOMAIN_SEP_BACK)
272+
.finalize_fixed();
273+
274+
// RFC 9381 try-and-increment always produces even y-coordinates
275+
if let Some(point) =
276+
Point::<EvenY, Public, NonZero>::from_xonly_bytes(hash_bytes.into())
277+
{
278+
return point;
279+
}
280+
}
281+
282+
// This should never happen (probability ~ 2^-256)
283+
unreachable!("Failed to find valid point after 256 attempts")
284+
}
117285
}
118286

119287
impl<Z: ZeroChoice, S> Point<Normal, S, Z> {

0 commit comments

Comments
 (0)