dcrypt_algorithms/ec/bls12_381/
hash_to_curve.rs

1// crates/algorithms/src/ec/bls12_381/hash_to_curve.rs
2
3//! Hash-to-Curve implementation for BLS12-381 (G1 and G2)
4//!
5//! Implements the standard suites from RFC 9380:
6//! - BLS12381G1_XMD:SHA-256_SSWU_RO_
7//! - BLS12381G2_XMD:SHA-256_SSWU_RO_
8
9use super::field::fp::Fp;
10use super::field::fp2::Fp2;
11use super::{G1Projective, G2Projective};
12use crate::error::{Error, Result};
13use crate::hash::sha2::Sha256;
14use crate::hash::HashFunction;
15use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
16
17// =============================================================================
18// expand_message_xmd (SHA-256)
19// =============================================================================
20
21fn expand_message_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Result<Vec<u8>> {
22    const B_IN_BYTES: usize = 32;
23    let ell = (len_in_bytes + B_IN_BYTES - 1) / B_IN_BYTES;
24    if ell > 255 {
25        return Err(Error::Parameter {
26            name: "len_in_bytes".into(),
27            reason: "requested output too long for expand_message_xmd".into(),
28        });
29    }
30    let mut dst_prime = Vec::with_capacity(dst.len() + 1);
31    dst_prime.extend_from_slice(dst);
32    dst_prime.push(dst.len() as u8);
33
34    // b_0
35    let mut hasher = Sha256::new();
36    hasher.update(&[0u8; 64])?; // Z_pad
37    hasher.update(msg)?;
38    hasher.update(&(len_in_bytes as u16).to_be_bytes())?;
39    hasher.update(&[0u8])?;
40    hasher.update(&dst_prime)?;
41    let b_0 = hasher.finalize()?;
42
43    let mut b_i = vec![0u8; B_IN_BYTES];
44    let mut uniform_bytes = Vec::with_capacity(len_in_bytes);
45
46    // b_1
47    let mut hasher = Sha256::new();
48    hasher.update(b_0.as_ref())?;
49    hasher.update(&[1u8])?;
50    hasher.update(&dst_prime)?;
51    let digest = hasher.finalize()?;
52    b_i.copy_from_slice(digest.as_ref());
53    uniform_bytes.extend_from_slice(&b_i);
54
55    for i in 2..=ell {
56        let mut xor_input = [0u8; B_IN_BYTES];
57        for j in 0..B_IN_BYTES {
58            xor_input[j] = b_0.as_ref()[j] ^ b_i[j];
59        }
60        let mut hasher = Sha256::new();
61        hasher.update(&xor_input)?;
62        hasher.update(&[i as u8])?;
63        hasher.update(&dst_prime)?;
64        let digest = hasher.finalize()?;
65        b_i.copy_from_slice(digest.as_ref());
66        uniform_bytes.extend_from_slice(&b_i);
67    }
68
69    uniform_bytes.truncate(len_in_bytes);
70    Ok(uniform_bytes)
71}
72
73// =============================================================================
74// Sign functions (RFC 9380 Section 4.1)
75// =============================================================================
76
77fn sgn0_fp(x: &Fp) -> Choice {
78    // sgn0(x) = x mod 2
79    let bytes = x.to_bytes();
80    // Fp is big-endian encoded, so LSB is at the end
81    Choice::from(bytes[47] & 1)
82}
83
84fn sgn0_fp2(x: &Fp2) -> Choice {
85    // sgn0(x0 + i*x1) = sgn0(x0) if x0 != 0 else sgn0(x1)
86    let sign_0 = sgn0_fp(&x.c0);
87    let zero_0 = x.c0.is_zero();
88    let sign_1 = sgn0_fp(&x.c1);
89    sign_0 | (zero_0 & sign_1)
90}
91
92// =============================================================================
93// G1: Simplified SWU with Z = -11
94// =============================================================================
95
96fn map_to_curve_g1(u: &Fp) -> G1Projective {
97    // Z = -11 mod p
98    let z = Fp::from_raw_unchecked([
99        0x3c20_8c16_d87c_f1ff,
100        0x9781_6a91_6871_ca8d,
101        0xb850_45b7_179e_9a4d,
102        0x9d71_20b8_351c_4374,
103        0x1993_4a11_123d_9479,
104        0x1987_2869_eb4b_31b8,
105    ]);
106
107    let tv1 = u.square();
108    let tv3 = z * tv1;
109    let tv2 = tv3.square();
110    let mut x = tv2 + tv3;
111    let x3 = z * x;
112    x = x + Fp::one();
113    let mut gx = x.square() * x;
114    
115    // B = 4
116    let b_coeff = Fp::from_raw_unchecked([
117        0xaa27_0000_000c_fff3,
118        0x53cc_0032_fc34_000a,
119        0x478f_e97a_6b0a_807f,
120        0xb1d3_7ebe_e6ba_24d7,
121        0x8ec9_733b_bf78_ab2f,
122        0x09d6_4551_3d83_de7e,
123    ]);
124    
125    gx = gx + b_coeff; 
126    
127    let y = gx.sqrt();
128    let is_gx_square = y.is_some();
129    let y = y.unwrap_or(Fp::zero());
130    
131    let x2 = x3;
132    let gx2 = (x2.square() * x2) + b_coeff;
133    let y2 = gx2.sqrt().unwrap_or(Fp::zero());
134    
135    let x = Fp::conditional_select(&x2, &x, is_gx_square);
136    let y = Fp::conditional_select(&y2, &y, is_gx_square);
137    
138    // Ensure signs match
139    let flip = !sgn0_fp(u).ct_eq(&sgn0_fp(&y));
140    let y = Fp::conditional_select(&y, &-y, flip);
141    
142    G1Projective { x, y, z: Fp::one() }
143}
144
145/// Hashes a message to a point in G1 using the SSWU map.
146pub fn hash_to_curve_g1(msg: &[u8], dst: &[u8]) -> Result<G1Projective> {
147    let uniform_bytes = expand_message_xmd(msg, dst, 64)?;
148    let mut u0_bytes = [0u8; 64];
149    u0_bytes.copy_from_slice(&uniform_bytes[0..64]);
150    let u0 = Fp::from_bytes_wide(&u0_bytes);
151    let q = map_to_curve_g1(&u0);
152    Ok(q.clear_cofactor())
153}
154
155// =============================================================================
156// G2: Simplified SWU + 3-isogeny
157// =============================================================================
158
159// Coefficients for the 3-isogeny map (x-numerator)
160const ISO3_XNUM: [Fp2; 4] = [
161    Fp2 {
162        c0: Fp::from_raw_unchecked([
163            0x47f6_71c7_1ce0_5e62, 0x06dd_5707_1206_393e, 0x7c80_cd2a_f3fd_71a2, 0x0481_03ea_9e6c_d062, 0xc545_16ac_c8d0_37f6, 0x1380_8f55_0920_ea41,
164        ]),
165        c1: Fp::from_raw_unchecked([
166            0x47f6_71c7_1ce0_5e62, 0x06dd_5707_1206_393e, 0x7c80_cd2a_f3fd_71a2, 0x0481_03ea_9e6c_d062, 0xc545_16ac_c8d0_37f6, 0x1380_8f55_0920_ea41,
167        ]),
168    },
169    Fp2 {
170        c0: Fp::zero(),
171        c1: Fp::from_raw_unchecked([
172            0x5fe5_5555_554c_71d0, 0x873f_ffdd_236a_aaa3, 0x6a6b_4619_b26e_f918, 0x21c2_8884_0887_4945, 0x2836_cda7_028c_abc5, 0x0ac7_3310_a7fd_5abd,
173        ]),
174    },
175    Fp2 {
176        c0: Fp::from_raw_unchecked([
177            0x0a0c_5555_5559_71c3, 0xdb0c_0010_1f9e_aaae, 0xb1fb_2f94_1d79_7997, 0xd396_0742_ef41_6e1c, 0xb700_40e2_c205_56f4, 0x149d_7861_e581_393b,
178        ]),
179        c1: Fp::from_raw_unchecked([
180            0xaff2_aaaa_aaa6_38e8, 0x439f_ffee_91b5_5551, 0xb535_a30c_d937_7c8c, 0x90e1_4442_0443_a4a2, 0x941b_66d3_8146_55e2, 0x0563_9988_53fe_ad5e,
181        ]),
182    },
183    Fp2 {
184        c0: Fp::from_raw_unchecked([
185            0x40aa_c71c_71c7_25ed, 0x1909_5555_7a84_e38e, 0xd817_050a_8f41_abc3, 0xd864_85d4_c87f_6fb1, 0x696e_b479_f885_d059, 0x198e_1a74_3280_02d2,
186        ]),
187        c1: Fp::zero(),
188    },
189];
190
191// Coefficients for the 3-isogeny map (x-denominator)
192const ISO3_XDEN: [Fp2; 3] = [
193    Fp2 {
194        c0: Fp::zero(),
195        c1: Fp::from_raw_unchecked([
196            0x1f3a_ffff_ff13_ab97, 0xf25b_fc61_1da3_ff3e, 0xca37_57cb_3819_b208, 0x3e64_2736_6f8c_ec18, 0x0397_7bc8_6095_b089, 0x04f6_9db1_3f39_a952,
197        ]),
198    },
199    Fp2 {
200        c0: Fp::from_raw_unchecked([
201            0x4476_0000_0027_552e, 0xdcb8_009a_4348_0020, 0x6f7e_e9ce_4a6e_8b59, 0xb103_30b7_c0a9_5bc6, 0x6140_b1fc_fb1e_54b7, 0x0381_be09_7f0b_b4e1,
202        ]),
203        c1: Fp::from_raw_unchecked([
204            0x7588_ffff_ffd8_557d, 0x41f3_ff64_6e0b_ffdf, 0xf7b1_e8d2_ac42_6aca, 0xb374_1acd_32db_b6f8, 0xe9da_f5b9_482d_581f, 0x167f_53e0_ba74_31b8,
205        ]),
206    },
207    Fp2::one(),
208];
209
210// Coefficients for the 3-isogeny map (y-numerator)
211const ISO3_YNUM: [Fp2; 4] = [
212    Fp2 {
213        c0: Fp::from_raw_unchecked([
214            0x96d8_f684_bdfc_77be, 0xb530_e4f4_3b66_d0e2, 0x184a_88ff_3796_52fd, 0x57cb_23ec_fae8_04e1, 0x0fd2_e39e_ada3_eba9, 0x08c8_055e_31c5_d5c3,
215        ]),
216        c1: Fp::from_raw_unchecked([
217            0x96d8_f684_bdfc_77be, 0xb530_e4f4_3b66_d0e2, 0x184a_88ff_3796_52fd, 0x57cb_23ec_fae8_04e1, 0x0fd2_e39e_ada3_eba9, 0x08c8_055e_31c5_d5c3,
218        ]),
219    },
220    Fp2 {
221        c0: Fp::zero(),
222        c1: Fp::from_raw_unchecked([
223            0xbf0a_71c7_1c91_b406, 0x4d6d_55d2_8b76_38fd, 0x9d82_f98e_5f20_5aee, 0xa27a_a27b_1d1a_18d5, 0x02c3_b2b2_d293_8e86, 0x0c7d_1342_0b09_807f,
224        ]),
225    },
226    Fp2 {
227        c0: Fp::from_raw_unchecked([
228            0xd7f9_5555_5553_1c74, 0x21cf_fff7_48da_aaa8, 0x5a9a_d186_6c9b_be46, 0x4870_a221_0221_d251, 0x4a0d_b369_c0a3_2af1, 0x02b1_ccc4_29ff_56af,
229        ]),
230        c1: Fp::from_raw_unchecked([
231            0xe205_aaaa_aaac_8e37, 0xfcdc_0007_6879_5556, 0x0c96_011a_8a15_37dd, 0x1c06_a963_f163_406e, 0x010d_f44c_82a8_81e6, 0x174f_4526_0f80_8feb,
232        ]),
233    },
234    Fp2 {
235        c0: Fp::from_raw_unchecked([
236            0xa470_bda1_2f67_f35c, 0xc0fe_38e2_3327_b425, 0xc9d3_d0f2_c6f0_678d, 0x1c55_c993_5b5a_982e, 0x27f6_c0e2_f074_6764, 0x117c_5e6e_28aa_9054,
237        ]),
238        c1: Fp::zero(),
239    },
240];
241
242// Coefficients for the 3-isogeny map (y-denominator)
243const ISO3_YDEN: [Fp2; 4] = [
244    Fp2 {
245        c0: Fp::from_raw_unchecked([
246            0x0162_ffff_fa76_5adf, 0x8f7b_ea48_0083_fb75, 0x561b_3c22_59e9_3611, 0x11e1_9fc1_a9c8_75d5, 0xca71_3efc_0036_7660, 0x03c6_a03d_41da_1151,
247        ]),
248        c1: Fp::from_raw_unchecked([
249            0x0162_ffff_fa76_5adf, 0x8f7b_ea48_0083_fb75, 0x561b_3c22_59e9_3611, 0x11e1_9fc1_a9c8_75d5, 0xca71_3efc_0036_7660, 0x03c6_a03d_41da_1151,
250        ]),
251    },
252    Fp2 {
253        c0: Fp::zero(),
254        c1: Fp::from_raw_unchecked([
255            0x5db0_ffff_fd3b_02c5, 0xd713_f523_58eb_fdba, 0x5ea6_0761_a84d_161a, 0xbb2c_75a3_4ea6_c44a, 0x0ac6_7359_21c1_119b, 0x0ee3_d913_bdac_fbf6,
256        ]),
257    },
258    Fp2 {
259        c0: Fp::from_raw_unchecked([
260            0x66b1_0000_003a_ffc5, 0xcb14_00e7_64ec_0030, 0xa73e_5eb5_6fa5_d106, 0x8984_c913_a0fe_09a9, 0x11e1_0afb_78ad_7f13, 0x0542_9d0e_3e91_8f52,
261        ]),
262        c1: Fp::from_raw_unchecked([
263            0x534d_ffff_ffc4_aae6, 0x5397_ff17_4c67_ffcf, 0xbff2_73eb_870b_251d, 0xdaf2_8271_5287_0915, 0x393a_9cba_ca9e_2dc3, 0x14be_74db_faee_5748,
264        ]),
265    },
266    Fp2::one(),
267];
268
269const SSWU_ELLP_A: Fp2 = Fp2 {
270    c0: Fp::zero(),
271    c1: Fp::from_raw_unchecked([
272        0xe53a_0000_0313_5242, 0x0108_0c0f_def8_0285, 0xe788_9edb_e340_f6bd, 0x0b51_3751_2631_0601, 0x02d6_9857_17c7_44ab, 0x1220_b4e9_79ea_5467,
273    ]),
274};
275
276const SSWU_ELLP_B: Fp2 = Fp2 {
277    c0: Fp::from_raw_unchecked([
278        0x22ea_0000_0cf8_9db2, 0x6ec8_32df_7138_0aa4, 0x6e1b_9440_3db5_a66e, 0x75bf_3c53_a794_73ba, 0x3dd3_a569_412c_0a34, 0x125c_db5e_74dc_4fd1,
279    ]),
280    c1: Fp::from_raw_unchecked([
281        0x22ea_0000_0cf8_9db2, 0x6ec8_32df_7138_0aa4, 0x6e1b_9440_3db5_a66e, 0x75bf_3c53_a794_73ba, 0x3dd3_a569_412c_0a34, 0x125c_db5e_74dc_4fd1,
282    ]),
283};
284
285const SSWU_XI: Fp2 = Fp2 {
286    c0: Fp::from_raw_unchecked([
287        0x87eb_ffff_fff9_555c, 0x656f_ffe5_da8f_fffa, 0x0fd0_7493_45d3_3ad2, 0xd951_e663_0665_76f4, 0xde29_1a3d_41e9_80d3, 0x0815_664c_7dfe_040d,
288    ]),
289    c1: Fp::from_raw_unchecked([
290        0x43f5_ffff_fffc_aaae, 0x32b7_fff2_ed47_fffd, 0x07e8_3a49_a2e9_9d69, 0xeca8_f331_8332_bb7a, 0xef14_8d1e_a0f4_c069, 0x040a_b326_3eff_0206,
291    ]),
292};
293
294fn iso_map(u: &G2Projective) -> G2Projective {
295    const COEFFS: [&[Fp2]; 4] = [&ISO3_XNUM, &ISO3_XDEN, &ISO3_YNUM, &ISO3_YDEN];
296
297    let G2Projective { x, y, z } = *u;
298    let mut mapvals = [Fp2::zero(); 4];
299
300    // compute powers of z
301    let zsq = z.square();
302    let zpows = [z, zsq, zsq * z];
303
304    // compute map value by Horner's rule
305    for idx in 0..4 {
306        let coeff = COEFFS[idx];
307        let clast = coeff.len() - 1;
308        mapvals[idx] = coeff[clast];
309        for jdx in 0..clast {
310            mapvals[idx] = mapvals[idx] * x + zpows[jdx] * coeff[clast - 1 - jdx];
311        }
312    }
313
314    // x denominator is order 1 less than x numerator, so we need an extra factor of z
315    mapvals[1] *= z;
316
317    // multiply result of Y map by the y-coord, y / z
318    mapvals[2] *= y;
319    mapvals[3] *= z;
320
321    G2Projective {
322        x: mapvals[0] * mapvals[3], // xnum * yden,
323        y: mapvals[2] * mapvals[1], // ynum * xden,
324        z: mapvals[1] * mapvals[3], // xden * yden
325    }
326}
327
328fn map_to_curve_simple_swu(u: &Fp2) -> G2Projective {
329    let usq = u.square();
330    let xi_usq = SSWU_XI * usq;
331    let xisq_u4 = xi_usq.square();
332    let nd_common = xisq_u4 + xi_usq; // XI^2 * u^4 + XI * u^2
333    let x_den = SSWU_ELLP_A * Fp2::conditional_select(&(-nd_common), &SSWU_XI, nd_common.is_zero());
334    let x0_num = SSWU_ELLP_B * (Fp2::one() + nd_common); // B * (1 + (XI^2 * u^4 + XI * u^2))
335
336    // compute g(x0(u))
337    let x_densq = x_den.square();
338    let gx_den = x_densq * x_den;
339    // x0_num^3 + A * x0_num * x_den^2 + B * x_den^3
340    let gx0_num = (x0_num.square() + SSWU_ELLP_A * x_densq) * x0_num + SSWU_ELLP_B * gx_den;
341
342    // We can't use the optimized chain here because we don't have the chain module linked properly
343    // in this context, so we fall back to standard sqrt.
344    // However, since we need to check if gx0(u) is square, we check directly.
345    let gx0_div_den = gx0_num * gx_den.invert().unwrap_or(Fp2::zero());
346    let sqrt_candidate = gx0_div_den.sqrt();
347    let is_square = sqrt_candidate.is_some();
348    
349    // If gx0 is square, x = x0_num/x_den, y = sqrt(gx0)
350    // If not, x = x1 = x0 * XI * u^2, y = sqrt(g(x1))
351    
352    let x0_num_xi_usq = x0_num * xi_usq;
353    let x_num = Fp2::conditional_select(&x0_num_xi_usq, &x0_num, is_square);
354    
355    // Compute y
356    // If gx0 was square, y = sqrt(gx0)
357    // If not, y = sqrt(g(x1)) = sqrt(g(x0) * XI^3 * u^6)
358    // We already computed sqrt(gx0) potentially.
359    
360    let mut y = Fp2::zero();
361    if bool::from(is_square) {
362        y = sqrt_candidate.unwrap();
363    } else {
364        // g(x1) = g(x0) * XI^3 * u^6
365        // g(x0) = gx0_div_den
366        // XI^3 * u^6 = (XI * u^2)^3 = xi_usq^3
367        let gx1 = gx0_div_den * xi_usq.square() * xi_usq;
368        y = gx1.sqrt().unwrap_or(Fp2::zero());
369    }
370
371    // ensure sign of y and sign of u agree
372    let flip = !sgn0_fp2(u).ct_eq(&sgn0_fp2(&y));
373    let y = Fp2::conditional_select(&y, &-y, flip);
374
375    G2Projective {
376        x: x_num,
377        y: y * x_den,
378        z: x_den,
379    }
380}
381
382/// Hashes a message to a point in G2 using the SSWU map and 3-isogeny.
383pub fn hash_to_curve_g2(msg: &[u8], dst: &[u8]) -> Result<G2Projective> {
384    let uniform_bytes = expand_message_xmd(msg, dst, 256)?;
385    let mut u0_bytes = [0u8; 128];
386    let mut u1_bytes = [0u8; 128];
387    u0_bytes.copy_from_slice(&uniform_bytes[0..128]);
388    u1_bytes.copy_from_slice(&uniform_bytes[128..256]);
389    let u0 = Fp2::from_bytes_wide(&u0_bytes);
390    let u1 = Fp2::from_bytes_wide(&u1_bytes);
391    
392    let p0 = map_to_curve_simple_swu(&u0);
393    let q0 = iso_map(&p0);
394    
395    let p1 = map_to_curve_simple_swu(&u1);
396    let q1 = iso_map(&p1);
397    
398    let r = q0 + q1;
399    Ok(r.clear_cofactor())
400}