constantine/sage/derive_hash_to_curve.sage

574 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/sage
# vim: syntax=python
# vim: set ts=2 sw=2 et:
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
# ############################################################
#
# Frobenius constants
#
# ############################################################
# Imports
# ---------------------------------------------------------
import os
import inspect, textwrap
import sage.schemes.elliptic_curves.isogeny_small_degree as isd
# Working directory
# ---------------------------------------------------------
os.chdir(os.path.dirname(__file__))
# Sage imports
# ---------------------------------------------------------
# Accelerate arithmetic by accepting probabilistic proofs
from sage.structure.proof.all import arithmetic
arithmetic(False)
load('curves.sage')
# Utilities
# ---------------------------------------------------------
def fp2_to_hex(a):
v = vector(a)
return '0x' + Integer(v[0]).hex() + ' + β * ' + '0x' + Integer(v[1]).hex()
def field_to_nim(value, field, curve, prefix = "", comment_above = "", comment_right = ""):
result = '# ' + comment_above + '\n' if comment_above else ''
comment_right = ' # ' + comment_right if comment_right else ''
if field == 'Fp2':
v = vector(value)
result += inspect.cleandoc(f"""
{prefix}Fp2[{curve}].fromHex( {comment_right}
"0x{Integer(v[0]).hex()}",
"0x{Integer(v[1]).hex()}"
)""")
elif field == 'Fp':
result += inspect.cleandoc(f"""
{prefix}Fp[{curve}].fromHex( {comment_right}
"0x{Integer(value).hex()}")
""")
else:
raise NotImplementedError()
return result
def dump_poly(name, poly, field, curve):
result = f'const {name}* = [\n'
result += ' # Polynomial k₀ + k₁ x + k₂ x² + k₃ x³ + ... + kₙ xⁿ\n'
result += ' # The polynomial is stored as an array of coefficients ordered from k₀ to kₙ\n'
result += '\n'
poly = list(poly)
lastRow = len(poly) - 1
for rowID, val in enumerate(reversed(poly)):
(coef, power) = val
result += textwrap.indent(
field_to_nim(
coef, field, curve,
comment_above = str(power)
),
' ')
result += ',\n' if rowID != lastRow else '\n'
result += ']'
return result
# Isogenies
# ---------------------------------------------------------
def find_iso(E):
"""
Find an isogenous curve with j-invariant not in {0, 1728} so that
Simplified Shallue-van de Woestijne method is directly applicable
(i.e the Elliptic Curve coefficient y² = x³ + A*x + B have AB != 0)
"""
for p_test in primes(30):
isos = [i for i in isd.isogenies_prime_degree(E, p_test)
if i.codomain().j_invariant() not in (0, 1728) ]
if len(isos) > 0:
print(f'Found {len(isos)} isogenous curves of degree {p_test}')
return isos[0].dual()
print(f'Found no isogenies')
return None
def find_z_sswu(F, A, B):
"""
https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#ref-SAGE
Arguments:
- F, a field object, e.g., F = GF(2^521 - 1)
- A and B, the coefficients of the curve equation y² = x³ + A * x + B
"""
R.<xx> = F[] # Polynomial ring over F
g = xx^3 + F(A) * xx + F(B) # y² = g(x) = x³ + A * x + B
ctr = F.gen()
while True:
for Z_cand in (F(ctr), F(-ctr)):
if Z_cand.is_square():
# Criterion 1: Z is non-square in F.
continue
if Z_cand == F(-1):
# Criterion 2: Z != -1 in F.
continue
if not (g - Z_cand).is_irreducible():
# Criterion 3: g(x) - Z is irreducible over F.
continue
if g(B / (Z_cand * A)).is_square():
# Criterion 4: g(B / (Z * A)) is square in F.
return Z_cand
ctr += 1
def search_isogeny(curve_name, curve_config):
p = curve_config[curve_name]['field']['modulus']
Fp = GF(p)
# Base constants - E1
A = curve_config[curve_name]['curve']['a']
B = curve_config[curve_name]['curve']['b']
E1 = EllipticCurve(Fp, [A, B])
# Base constants - E2
embedding_degree = curve_config[curve_name]['tower']['embedding_degree']
twist_degree = curve_config[curve_name]['tower']['twist_degree']
twist = curve_config[curve_name]['tower']['twist']
G2_field_degree = embedding_degree // twist_degree
G2_field = f'Fp{G2_field_degree}' if G2_field_degree > 1 else 'Fp'
if G2_field_degree == 2:
non_residue_fp = curve_config[curve_name]['tower']['QNR_Fp']
elif G2_field_degree == 1:
if twist_degree == 6:
# Only for complete serialization
non_residue_fp = curve_config[curve_name]['tower']['SNR_Fp']
else:
raise NotImplementedError()
else:
raise NotImplementedError()
Fp = GF(p)
K.<u> = PolynomialRing(Fp)
if G2_field == 'Fp2':
Fp2.<beta> = Fp.extension(u^2 - non_residue_fp)
G2F = Fp2
if twist_degree == 6:
non_residue_twist = curve_config[curve_name]['tower']['SNR_Fp2']
else:
raise NotImplementedError()
elif G2_field == 'Fp':
G2F = Fp
if twist_degree == 6:
non_residue_twist = curve_config[curve_name]['tower']['SNR_Fp']
else:
raise NotImplementedError()
else:
raise NotImplementedError()
if twist == 'D_Twist':
G2B = B/G2F(non_residue_twist)
E2 = EllipticCurve(G2F, [0, G2B])
elif twist == 'M_Twist':
G2B = B*G2F(non_residue_twist)
E2 = EllipticCurve(G2F, [0, G2B])
else:
raise ValueError('E2 must be a D_Twist or M_Twist but found ' + twist)
# Isogenies:
iso_G1 = find_iso(E1)
a_G1 = iso_G1.domain().a4()
b_G1 = iso_G1.domain().a6()
iso_G2 = find_iso(E2)
a_G2 = iso_G2.domain().a4()
b_G2 = iso_G2.domain().a6()
# Z
Z_G1 = find_z_sswu(Fp, a_G1, b_G1)
Z_G2 = find_z_sswu(Fp2, a_G2, b_G2)
print(f"{curve_name} G1 - isogeny of degree {iso_G1.degree()} with eq y² = x³ + A'x + B':")
print(f" A': 0x{Integer(a_G1).hex()}")
print(f" B': 0x{Integer(b_G1).hex()}")
print(f" Z: {Z_G1}")
print(f"{curve_name} G2 - isogeny of degree {iso_G2.degree()} with eq y² = x³ + A'x + B':")
print(f" A': {fp2_to_hex(a_G2)}")
print(f" B': {fp2_to_hex(b_G2)}")
print(f" Z: {fp2_to_hex(Z_G2)}")
# BLS12-381 G1
# ---------------------------------------------------------
# Hardcoding from spec:
# - https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-8.8.1
# - https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/blob/f7dd3761/poc/sswu_opt_3mod4.sage#L126-L132
def genBLS12381G1_H2C_constants(curve_config):
curve_name = 'BLS12_381'
# ------------------------------------------
p = curve_config[curve_name]['field']['modulus']
Fp = GF(p)
K.<u> = PolynomialRing(Fp)
# ------------------------------------------
# Hash to curve isogenous curve parameters
# y² = x³ + A'*x + B'
print('\n----> Hash-to-Curve map to isogenous BLS12-381 E\'1 <----\n')
buf = inspect.cleandoc(f"""
# Hash-to-Curve map to isogenous BLS12-381 E'1 constants
# -----------------------------------------------------------------
#
# y² = x³ + A'*x + B' with p ≡ 3 (mod 4) the BLS12-381 characteristic (base modulus)
#
# Hardcoding from spec:
# - https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-8.8.1
# - https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/blob/f7dd3761/poc/sswu_opt_3mod4.sage#L126-L132
""")
buf += '\n\n'
# Base constants
Aprime_E1 = Fp('0x144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac98936f8da0e0f97f5cf428082d584c1d')
Bprime_E1 = Fp('0x12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23215a316ceaa5d1cc48e98e172be0')
Z = Fp(11)
# Extra
minus_A = -Aprime_E1
ZmulA = Z * Aprime_E1
sqrt_minus_Z3 = sqrt(-Z^3)
buf += f'const {curve_name}_h2c_G1_Aprime_E1* = '
buf += field_to_nim(Aprime_E1, 'Fp', curve_name)
buf += '\n'
buf += f'const {curve_name}_h2c_G1_Bprime_E1* = '
buf += field_to_nim(Bprime_E1, 'Fp', curve_name)
buf += '\n'
buf += f'const {curve_name}_h2c_G1_Z* = '
buf += field_to_nim(Z, 'Fp', curve_name)
buf += '\n'
buf += f'const {curve_name}_h2c_G1_minus_A* = '
buf += field_to_nim(minus_A, 'Fp', curve_name)
buf += '\n'
buf += f'const {curve_name}_h2c_G1_ZmulA* = '
buf += field_to_nim(ZmulA, 'Fp', curve_name)
buf += '\n'
buf += f'const {curve_name}_h2c_G1_sqrt_minus_Z3* = '
buf += field_to_nim(sqrt_minus_Z3, 'Fp', curve_name)
buf += '\n'
return buf
def genBLS12381G1_H2C_isogeny_map(curve_config):
curve_name = 'BLS12_381'
# Hash to curve isogenous curve parameters
# y² = x³ + A'*x + B'
print('\n----> Hash-to-Curve 3-isogeny map BLS12-381 E\'1 constants <----\n')
buf = inspect.cleandoc(f"""
# Hash-to-Curve 11-isogeny map BLS12-381 E'1 constants
# -----------------------------------------------------------------
#
# The polynomials map a point (x', y') on the isogenous curve E'1
# to (x, y) on E1, represented as (xnum/xden, y' * ynum/yden)
""")
buf += '\n\n'
p = curve_config[curve_name]['field']['modulus']
Fp = GF(p)
# Base constants - E1
A = curve_config[curve_name]['curve']['a']
B = curve_config[curve_name]['curve']['b']
E1 = EllipticCurve(Fp, [A, B])
# Base constants - Isogenous curve E'1, degree 11
Aprime_E1 = Fp('0x144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac98936f8da0e0f97f5cf428082d584c1d')
Bprime_E1 = Fp('0x12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23215a316ceaa5d1cc48e98e172be0')
Eprime1 = EllipticCurve(Fp, [Aprime_E1, Bprime_E1])
iso = EllipticCurveIsogeny(E=E1, kernel=None, codomain=Eprime1, degree=11).dual()
if (- iso.rational_maps()[1])(1, 1) > iso.rational_maps()[1](1, 1):
iso.switch_sign()
(xm, ym) = iso.rational_maps()
maps = (xm.numerator(), xm.denominator(), ym.numerator(), ym.denominator())
buf += dump_poly(
'BLS12_381_h2c_G1_11_isogeny_map_xnum',
xm.numerator(), 'Fp', curve_name)
buf += '\n'
buf += dump_poly(
'BLS12_381_h2c_G1_11_isogeny_map_xden',
xm.denominator(), 'Fp', curve_name)
buf += '\n'
buf += dump_poly(
'BLS12_381_h2c_G1_11_isogeny_map_ynum',
ym.numerator(), 'Fp', curve_name)
buf += '\n'
buf += dump_poly(
'BLS12_381_h2c_G1_11_isogeny_map_yden',
ym.denominator(), 'Fp', curve_name)
return buf
# BLS12-381 G2
# ---------------------------------------------------------
# Hardcoding from spec:
# - https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-8.8.2
# - https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/blob/f7dd3761/poc/sswu_opt_9mod16.sage#L142-L148
def genBLS12381G2_H2C_constants(curve_config):
curve_name = 'BLS12_381'
# ------------------------------------------
embdeg = curve_config[curve_name]['tower']['embedding_degree']
twdeg = curve_config[curve_name]['tower']['twist_degree']
g2field = f'Fp{embdeg//twdeg}' if (embdeg//twdeg) > 1 else 'Fp'
p = curve_config[curve_name]['field']['modulus']
Fp = GF(p)
K.<u> = PolynomialRing(Fp)
if g2field == 'Fp2':
QNR_Fp = curve_config[curve_name]['tower']['QNR_Fp']
Fp2.<beta> = Fp.extension(u^2 - QNR_Fp)
else:
SNR_Fp = curve_config[curve_name]['tower']['SNR_Fp']
Fp2.<beta> = Fp.extension(u^2 - SNR_Fp)
# ------------------------------------------
# Hash to curve isogenous curve parameters
# y² = x³ + A'*x + B'
print('\n----> Hash-to-Curve map to isogenous BLS12-381 E\'2 <----\n')
buf = inspect.cleandoc(f"""
# Hash-to-Curve map to isogenous BLS12-381 E'2 constants
# -----------------------------------------------------------------
#
# y² = x³ + A'*x + B' with p² = q ≡ 9 (mod 16), p the BLS12-381 characteristic (base modulus)
#
# Hardcoding from spec:
# - https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-8.8.2
# - https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/blob/f7dd3761/poc/sswu_opt_9mod16.sage#L142-L148
""")
buf += '\n\n'
# Base constants
Aprime_E2 = Fp2([0, 240])
Bprime_E2 = Fp2([1012, 1012])
Z = Fp2([-2, -1])
# Extra
minus_A = -Aprime_E2
ZmulA = Z * Aprime_E2
inv_Z3 = (Z^3)^-1 # modular inverse of Z³
(a, b) = vector(inv_Z3)
squared_norm_inv_Z3 = a^2 + b^2 # ||1/Z³||²
# x^((p-3)/4)) ≡ 1/√x (mod p) if p ≡ 3 (mod 4)
inv_norm_inv_Z3 = squared_norm_inv_Z3^((p-3)/4) # 1/||1/Z³||
buf += f'const {curve_name}_h2c_G2_Aprime_E2* = '
buf += field_to_nim(Aprime_E2, 'Fp2', curve_name, comment_right = "240𝑖")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_Bprime_E2* = '
buf += field_to_nim(Bprime_E2, 'Fp2', curve_name, comment_right = "1012 * (1 + 𝑖)")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_Z* = '
buf += field_to_nim(Z, 'Fp2', curve_name, comment_right = "-(2 + 𝑖)")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_minus_A* = '
buf += field_to_nim(minus_A, 'Fp2', curve_name, comment_right = "-240𝑖")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_ZmulA* = '
buf += field_to_nim(ZmulA, 'Fp2', curve_name, comment_right = "Z*A = 240-480𝑖")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_inv_Z3* = '
buf += field_to_nim(inv_Z3, 'Fp2', curve_name, comment_right = "1/Z³")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_squared_norm_inv_Z3* = '
buf += field_to_nim(squared_norm_inv_Z3, 'Fp', curve_name, comment_right = "||1/Z³||²")
buf += '\n'
buf += f'const {curve_name}_h2c_G2_inv_norm_inv_Z3* = '
buf += field_to_nim(inv_norm_inv_Z3, 'Fp', curve_name, comment_right = "1/||1/Z³||")
buf += '\n'
return buf
def genBLS12381G2_H2C_isogeny_map(curve_config):
curve_name = 'BLS12_381'
# ------------------------------------------
p = curve_config[curve_name]['field']['modulus']
# This extension field construction
# does not work with isogenies :/
#
# embdeg = curve_config[curve_name]['tower']['embedding_degree']
# twdeg = curve_config[curve_name]['tower']['twist_degree']
# g2field = f'Fp{embdeg//twdeg}' if (embdeg//twdeg) > 1 else 'Fp'
#
# Fp = GF(p)
# K.<u> = PolynomialRing(Fp)
# if g2field == 'Fp2':
# QNR_Fp = curve_config[curve_name]['tower']['QNR_Fp']
# Fp2.<beta> = Fp.extension(u^2 - QNR_Fp)
# else:
# SNR_Fp = curve_config[curve_name]['tower']['SNR_Fp']
# Fp2.<beta> = Fp.extension(u^2 - SNR_Fp)
# ------------------------------------------
QNR_Fp = curve_config[curve_name]['tower']['QNR_Fp']
Fp2.<beta> = GF(p^2, modulus=(x^2-QNR_Fp))
# Hash to curve isogenous curve parameters
# y² = x³ + A'*x + B'
print('\n----> Hash-to-Curve 3-isogeny map BLS12-381 E\'2 constants <----\n')
buf = inspect.cleandoc(f"""
# Hash-to-Curve 3-isogeny map BLS12-381 E'2 constants
# -----------------------------------------------------------------
#
# The polynomials map a point (x', y') on the isogenous curve E'2
# to (x, y) on E2, represented as (xnum/xden, y' * ynum/yden)
""")
buf += '\n\n'
# Base constants - E2
A = curve_config[curve_name]['curve']['a']
B = curve_config[curve_name]['curve']['b']
twist = curve_config[curve_name]['tower']['twist']
SNR_Fp2 = curve_config[curve_name]['tower']['SNR_Fp2']
if twist == 'M_twist':
Btwist = B * Fp2(SNR_Fp2)
else:
Btwist = B / Fp2(SNR_Fp2)
E2 = EllipticCurve(Fp2, [A, Btwist])
# Base constants - Isogenous curve E'2, degree 3
Aprime_E2 = Fp2([0, 240])
Bprime_E2 = Fp2([1012, 1012])
Eprime2 = EllipticCurve(Fp2, [Aprime_E2, Bprime_E2])
iso_kernel = [6 * (1 - beta), 1]
iso = EllipticCurveIsogeny(E=Eprime2, kernel=iso_kernel, codomain=E2, degree=3)
if (- iso.rational_maps()[1])(1, 1) > iso.rational_maps()[1](1, 1):
iso.switch_sign()
(xm, ym) = iso.rational_maps()
maps = (xm.numerator(), xm.denominator(), ym.numerator(), ym.denominator())
buf += dump_poly(
'BLS12_381_h2c_G2_3_isogeny_map_xnum',
xm.numerator(), 'Fp2', curve_name)
buf += '\n'
buf += dump_poly(
'BLS12_381_h2c_G2_3_isogeny_map_xden',
xm.denominator(), 'Fp2', curve_name)
buf += '\n'
buf += dump_poly(
'BLS12_381_h2c_G2_3_isogeny_map_ynum',
ym.numerator(), 'Fp2', curve_name)
buf += '\n'
buf += dump_poly(
'BLS12_381_h2c_G2_3_isogeny_map_yden',
ym.denominator(), 'Fp2', curve_name)
return buf
# CLI
# ---------------------------------------------------------
if __name__ == "__main__":
# Usage
# BLS12-381
# sage sage/derive_hash_to_curve.sage BLS12_381 G2
# for Hash-to-Curve
# or
# sage sage/derive_hash_to_curve.sage BLS12_381 iso
# to search for a suitable isogeny
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("curve",nargs="+")
args = parser.parse_args()
curve = args.curve[0]
group_or_iso = args.curve[1]
if group_or_iso == 'iso':
search_isogeny(curve, Curves)
elif curve == 'BLS12_381' and group_or_iso == 'G1':
h2c = genBLS12381G1_H2C_constants(Curves)
h2c += '\n\n'
h2c += genBLS12381G1_H2C_isogeny_map(Curves)
with open(f'{curve.lower()}_hash_to_curve_g1.nim', 'w') as f:
f.write(copyright())
f.write('\n\n')
f.write(inspect.cleandoc("""
import
../config/curves,
../io/io_fields
"""))
f.write('\n\n')
f.write(h2c)
print(f'Successfully created {curve.lower()}_hash_to_curve_g1.nim')
elif curve == 'BLS12_381' and group_or_iso == 'G2':
h2c = genBLS12381G2_H2C_constants(Curves)
h2c += '\n\n'
h2c += genBLS12381G2_H2C_isogeny_map(Curves)
with open(f'{curve.lower()}_hash_to_curve_g2.nim', 'w') as f:
f.write(copyright())
f.write('\n\n')
f.write(inspect.cleandoc("""
import
../config/curves,
../io/[io_fields, io_extfields]
"""))
f.write('\n\n')
f.write(h2c)
print(f'Successfully created {curve.lower()}_hash_to_curve_g2.nim')
else:
raise ValueError(
curve + group_or_iso +
' is not configured '
)