|
1 | 1 | use crate::{ |
2 | 2 | Scalar, |
3 | 3 | backend::{self, BackendPoint, TimeSensitive}, |
4 | | - hash::HashInto, |
| 4 | + hash::{Hash32, HashInto}, |
5 | 5 | marker::*, |
6 | 6 | op, |
7 | 7 | }; |
@@ -114,6 +114,174 @@ impl Point<Normal, Public, NonZero> { |
114 | 114 | y.copy_from_slice(&bytes[33..65]); |
115 | 115 | backend::Point::norm_from_coordinates(x, y).map(|p| Point::from_inner(p, Normal)) |
116 | 116 | } |
| 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 | + } |
117 | 285 | } |
118 | 286 |
|
119 | 287 | impl<Z: ZeroChoice, S> Point<Normal, S, Z> { |
|
0 commit comments