Skip to content

Commit

Permalink
research: Add Sage FROST implementation for threshold Schnorr signatu…
Browse files Browse the repository at this point in the history
…res.
  • Loading branch information
parazyd committed Aug 11, 2023
1 parent 55751ab commit 95d0f47
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 1 deletion.
1 change: 1 addition & 0 deletions script/research/frost/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
frost_util.py
274 changes: 274 additions & 0 deletions script/research/frost/frost.sage
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions script/research/frost/frost_util.sage
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion script/research/pvss/pvss.sage
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ from itertools import chain

t = 3 # Threshold
n = 5 # Participants
assert t <= n

# Pallas
p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001
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
Expand Down

0 comments on commit 95d0f47

Please sign in to comment.