research: Add Sage FROST implementation for threshold Schnorr signatures.

This commit is contained in:
parazyd
2023-08-11 22:07:34 +02:00
parent 55751abf06
commit 95d0f4713f
4 changed files with 332 additions and 1 deletions

1
script/research/frost/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
frost_util.py

View File

@@ -0,0 +1,274 @@
# Two-Round Threshold Schnorr Signatures with FROST
# https://datatracker.ietf.org/doc/pdf/draft-irtf-cfrg-frost-14
import os
from hashlib import sha256
# Hacky way to import another sage module:
os.system("sage --preparse frost_util.sage")
os.system("mv frost_util.sage.py frost_util.py")
from frost_util import *
MAX_PARTICIPANTS = 10
MIN_PARTICIPANTS = 4
assert MIN_PARTICIPANTS <= MAX_PARTICIPANTS
# Pallas
p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001
q = 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001
Fp = GF(p)
Fq = GF(q)
Ep = EllipticCurve(Fp, (0, 5))
Ep.set_order(q)
# NullifierK Generator: G
nfk_x = 0x25e7aa169ca8198d2e375571faf4c9cf5e7eb192ccb5db9bd36f6aa7e447ca75
nfk_y = 0x155c1f851b1a3384880473442008ff755fe0a49ec1c1b4332db8dce21ae001cc
G = Ep([nfk_x, nfk_y])
# Secret key to share, we assume this is key distribution with trusted dealer.
sk = Fq.random_element()
group_pk = sk * G
alpha = [sk]
for i in range(MIN_PARTICIPANTS-1):
alpha.append(Fq.random_element())
R.<ω> = PolynomialRing(Fq)
poly = R(alpha)
assert poly.degree() == MIN_PARTICIPANTS-1
assert poly.coefficients()[0] == sk
# Secret key shares
sk_i = [poly(i) for i in range(1, MAX_PARTICIPANTS+1)]
# ======================
# Round One - Commitment
# ======================
# Round one involves each participant generating nonces and their
# corresponding public commitments. A nonce is a pair of Scalar
# values, and a commitment is a pair of elliptic curve points.
# Each participant's behaviour in this round is described by the
# commit function below. Note that this function invokes nonce_generate
# twice, once for each type of nonce produced. The output of this
# function is a pair of secret nonces (hiding_nonce, binding_nonce)
# and their corresponding public commitments (hiding_nonce_commitment,
# binding_nonce_commitment).
# Inputs:
# - secret, a Scalar
# Outputs:
# - nonce, a Scalar
def nonce_generate(secret):
random_bytes = os.urandom(32)
return Fq(H3(random_bytes, secret))
# Inputs:
# - sk_i, the secret key share, a Scalar
# Outputs:
# - (nonce, comm), a tuple of nonce and nonce commitment pairs,
# where each value in the nonce pair is a Scalar and each value
# in the nonce commitment pair is an elliptic curve point
def commit(sk_i):
hiding_nonce = nonce_generate(sk_i)
binding_nonce = nonce_generate(sk_i)
hiding_nonce_commit = hiding_nonce * G
binding_nonce_commit = binding_nonce * G
nonces = (hiding_nonce, binding_nonce)
commits = (hiding_nonce_commit, binding_nonce_commit)
return (nonces, commits)
P_nonces = []
P_commits = []
# Either all participants or just the threshold should create nonces.
# It is only important that commit_list has MIN/NUM_PARTICIPANTS.
for i in range(MAX_PARTICIPANTS):
nonces, commits = commit(sk_i[i])
P_nonces.append(nonces)
P_commits.append(commits)
commit_list = []
for (i, (hnc, bnc)) in enumerate(P_commits[:MIN_PARTICIPANTS]):
commit_list.append((Fq(i+1), hnc, bnc))
# The outputs nonce and comm from participant P_i should both be stored
# locally and kept for use in the second round. The nonce value is secret
# and MUST NOT be shared, whereas the public output comm is sent to the
# Coordinator. The nonce values produced by this function MUST NOT be used
# in more than one invocation of `sign()`, and the nonces MUST be generated
# from a source of secure randomness.
# ======================================
# Round Two - Signature Share Generation
# ======================================
# In round two, the Coordinator is responsible for sending the message to
# be signed, and for choosing which participants will participate (of number
# at least MIN_PARTICIPANTS). Signers additionally require locally held data;
# specifically, their secret key and the nonces corresponding to their
# commitment issued in round one.
# The Coordinator begins by sending each participant the message to be
# signed along with the set of signing commitments for all participants
# in the participant list.
# Inputs:
# - group_pk, the public key corresponding to the group signing key
# - commit_list = [(i, hiding_nonce_commit_i, binding_nonce_commit_i), ...],
# a list of commitments issued by each participant, where each element
# indicates a nonzero Scalar identifier i and two commitments which are
# elliptic curve points. This list MUST be sorted in ascending order by
# identifier.
# - msg, the message to be signed.
# Outputs:
# - binding_factor_list, a list of (nonzero Scalar, Scalar) tuples
# representing the binding factors
def compute_binding_factors(group_pk, commit_list, msg):
msg_hash = Fq(H4(msg))
encoded_commitment_hash = Fq(H5(encode_group_commitment_list(commit_list)))
rho_input_prefix = b"".join([
point_to_bytes(group_pk),
scalar_to_bytes(msg_hash),
scalar_to_bytes(encoded_commitment_hash),
])
binding_factor_list = []
for (ident, hiding_nonce_commit, binding_nonce_commit) in commit_list:
rho_input = b"".join([rho_input_prefix, scalar_to_bytes(ident)])
binding_factor = Fq(H1(rho_input))
binding_factor_list.append((ident, binding_factor))
return binding_factor_list
def binding_factor_for_participant(binding_factor_list, ident):
for (i, binding_factor) in binding_factor_list:
if ident == i:
return binding_factor
raise "invalid participant"
def compute_group_commitment(commit_list, binding_factor_list):
group_commitment = Ep(0)
for (ident, hiding_nonce_commit, binding_nonce_commit) in commit_list:
binding_factor = binding_factor_for_participant(binding_factor_list, ident)
binding_nonce = binding_nonce_commit * binding_factor
group_commitment += hiding_nonce_commit + binding_nonce
return group_commitment
def participants_from_commitment_list(commit_list):
identifiers = []
for (identifier, _, _) in commit_list:
identifiers.append(identifier)
return identifiers
def derive_interpolating_value(L, x_i):
if x_i not in L:
raise "invalid parameters"
for x_j in L:
if L.count(x_j) > 1:
raise "invalid parameters"
numerator = Fq(1)
denominator = Fq(1)
for x_j in L:
if x_j == x_i:
continue
numerator *= x_j
denominator *= x_j - x_i
value = numerator / denominator
return value
def compute_challenge(group_commitment, group_pk, msg):
challenge_input = b"".join([
point_to_bytes(group_commitment),
point_to_bytes(group_pk),
msg,
])
challenge = Fq(H2(challenge_input))
return challenge
def sign(ident, sk_i, group_pk, nonce_i, msg, commit_list):
# Compute the binding factor(s)
binding_factor_list = compute_binding_factors(group_pk, commit_list, msg)
binding_factor = binding_factor_for_participant(binding_factor_list, ident)
# Compute the group commitment
group_commit = compute_group_commitment(commit_list, binding_factor_list)
# Compute the interpolating value
participant_list = participants_from_commitment_list(commit_list)
lambda_i = derive_interpolating_value(participant_list, ident)
# Compute the per-message challenge
challenge = compute_challenge(group_commit, group_pk, msg)
# Compute the signature share
(hiding_nonce, binding_nonce) = nonce_i
sig_share = hiding_nonce + (binding_nonce * binding_factor) + \
(lambda_i * sk_i * challenge)
return sig_share
# For demo purposes, we'll just pick the first participants in order.
msg = b"Hello FROST"
sig_shares = []
for i in range(MIN_PARTICIPANTS):
sig_share = sign(Fq(i+1), sk_i[i], group_pk, P_nonces[i], msg, commit_list)
sig_shares.append(sig_share)
# ===========================
# Signature Share Aggregation
# ===========================
def verify_signature_share(ident, PK_i, comm_i, sig_share_i, commit_list,
group_pk, msg):
binding_factor_list = compute_binding_factors(group_pk, commit_list, msg)
binding_factor = binding_factor_for_participant(binding_factor_list, ident)
group_commit = compute_group_commitment(commit_list, binding_factor_list)
(hiding_nonce_comm, binding_nonce_comm) = comm_i
comm_share = hiding_nonce_comm + binding_nonce_comm * binding_factor
challenge = compute_challenge(group_commit, group_pk, msg)
participant_list = participants_from_commitment_list(commit_list)
lambda_i = derive_interpolating_value(participant_list, ident)
l = sig_share_i * G
r = comm_share + PK_i * (challenge * lambda_i)
return l == r
# Verify individual signature shares:
for i in range(MIN_PARTICIPANTS):
assert verify_signature_share(Fq(i+1), sk_i[i] * G, P_commits[i],
sig_shares[i], commit_list, group_pk, msg)
def aggregate(commit_list, msg, group_pk, sig_shares):
binding_factor_list = compute_binding_factors(group_pk, commit_list, msg)
group_commit = compute_group_commitment(commit_list, binding_factor_list)
# Compute aggregated signature
z = Fq(0)
for z_i in sig_shares:
z += z_i
return (group_commit, z)
group_commit, z = aggregate(commit_list, msg, group_pk, sig_shares)
# ============
# Verification
# ============
c = compute_challenge(group_commit, group_pk, msg)
assert G * z == group_commit + group_pk * c

View File

@@ -0,0 +1,55 @@
# Utility functions for the FROST module.
from hashlib import sha256
def scalar_to_bytes(a):
int_repr = ZZ(a)
byte_repr = int(int_repr).to_bytes(32, byteorder="big")
return byte_repr
def point_to_bytes(P):
if P.is_zero():
return b"\x00"
x_bytes = int(ZZ(P[0])).to_bytes(32, byteorder="big")
y_bytes = int(ZZ(P[1])).to_bytes(32, byteorder="big")
return x_bytes + y_bytes
def hash_domain(domain, *args):
concat = domain.encode() + b"".join(str(arg).encode() for arg in args)
return int(sha256(concat).hexdigest(), 16)
def H1(*args):
return hash_domain("H1", *args)
def H2(*args):
return hash_domain("H2", *args)
def H3(*args):
return hash_domain("H3", *args)
def H4(*args):
return hash_domain("H4", *args)
def H5(*args):
return hash_domain("H5", *args)
# Serialize the group commitment list
def encode_group_commitment_list(commit_list):
encoded_group_commitment = b""
for (ident, hiding_nonce_commit, binding_nonce_commit) in commit_list:
enc_commit = scalar_to_bytes(ident) \
+ point_to_bytes(hiding_nonce_commit) \
+ point_to_bytes(binding_nonce_commit)
encoded_group_commitment = encoded_group_commitment + enc_commit
return encoded_group_commitment

View File

@@ -7,6 +7,7 @@ from itertools import chain
t = 3 # Threshold
n = 5 # Participants
assert t <= n
# Pallas
p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001
@@ -14,7 +15,7 @@ q = 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001
Fp = GF(p)
Fq = GF(q)
Ep = EllipticCurve(Fp, (0, 5))
Ep.set_order(q * 0x01)
Ep.set_order(q)
# ValueCommitR Generator: g
vcr_x = 0x07f444550fa409bb4f66235bea8d2048406ed745ee90802f0ec3c668883c5a91