Jelajahi Sumber

research: Add Sage FROST implementation for threshold Schnorr signatures.

parazyd 3 tahun lalu
induk
melakukan
95d0f4713f

+ 1 - 0
script/research/frost/.gitignore

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

+ 274 - 0
script/research/frost/frost.sage

@@ -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 - 0
script/research/frost/frost_util.sage

@@ -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

+ 2 - 1
script/research/pvss/pvss.sage

@@ -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