diff --git a/zip-frost.rst b/zip-frost.rst index 7d9d5d21..c50d3cd5 100644 --- a/zip-frost.rst +++ b/zip-frost.rst @@ -32,9 +32,10 @@ The terms below are to be interpreted as follows: Abstract ======== -This proposal defines the usage of FROST [#FROST]_, a threshold signature scheme, -to generate signatures compatible with spend authorization signatures in the Zcash -protocol, for the Sapling and Orchard network upgrades. +This proposal adapts FROST [#FROST]_, a threshold signature scheme, +to make it unlinkable, which is a requirement for its use in the Zcash protocol. +The adapted scheme generates signatures compatible with spend authorization +signatures in the Zcash protocol, for the Sapling and Orchard network upgrades. Motivation @@ -42,14 +43,17 @@ Motivation In the Zcash protocol, Spend Authorization Signatures are employed to effectively authorize a transaction. The ability to generate these signatures with the user's -private key is what effectively allows the to spend funds. +private key is what effectively allows the user to spend funds. -This is security-critical step, since anyone who obtains access to the private +This is a security-critical step, since anyone who obtains access to the private key will be able to spend the user's funds. For this reason, one interesting possibility is to require multiple entities to allow the transaction to go through. This can be accomplished with threshold signatures, where... (TODO: explain) -FROST is one of such threshold signatures protocols. (TODO: more about FROST) +FROST is one of such threshold signatures protocols. However, it can't be used as-is +since the Zcash protocol also requires re-randomizing public and private keys +to ensure unlinkability between transactions. This ZIP specifies a variant of +FROST with re-randomization support. Requirements @@ -57,14 +61,46 @@ Requirements - All signatures generated by following this ZIP MUST be verified successfully as Sapling or Orchard spend authorization signatures. -- (Must keep same security requirements from the spec?) -- (Randomizer security requirements?) -- The party that generates alpha should also be the party that has access to the witness for the proof. +- The signatures generated by following this ZIP should meet the security criteria + for Signature with Re-Randomizable Keys as specified in the Zcash protocol [#protocol]_ +- Follow the threat model described below. + +Threat Model +------------ + +In normal usage, a Zcash user follows multiple steps in order to generate a +shielded transaction: + +- The transaction is created. +- The transaction is signed with a randomized version of the user's spend + authorization private key. +- The zero-knowledge proof for the transaction is created with the randomizer + as an auxiliary (secret) input, among others. + +When employing re-randomized FROST as specified in this ZIP, the goal is to +split the spend authorization private key among multiple participants. This means +that the proof generation will still be generated be a single participant, likely +the one that created the transaction in the first place and that will actually +submit the transaction to the network. Note that his user already controls the +privacy of the transaction since they are responsible for creating the proof. + +This fits well into the "Coordinator" role from the FROST specification. The +Coordinator is responsible for sending the message to be signed to all participants, +and to aggregate the signature shares. + +With those considerations in mind, the threat model considered in this ZIP is: + +- The Coordinator is trusted with the privacy of the transaction (which includes + the unlinkability property). A rogue Coordinator will be able to break + unlinkability and privacy, but should not be able to create signed transactions + without the approval of `MIN_SIGNERS` participants, as specified in FROST. + Non-requirements ================ -- (Talk about how proof generation is not covered?) +- The control of the transaction privacy is not distributed among participants + and is trusted with the Coordinator. Specification @@ -74,14 +110,58 @@ Re-randomized FROST ------------------- To add re-randomization to FROST, follow the specification [#FROST]_ with the -following modifications: +following modifications. -In Round Two, the Coordinator also generates a random scalar α by following the -RedDSA.GenRandom procedure and sends it to each signer along with the message -and the set of signing commitments. +Randomizer Generation +''''''''''''''''''''' -The `sign` function is changed to receive α and incorporate it into the -computation of `sig_share`. It is specified as the following: :: +A new helper function is defined, which computes RedDSA.GenRandom: + +:: + + randomizer_generate(): + + Inputs: + - None + + Outputs: randomizer, a Scalar + + def randomizer_generate(): + randomizer_input = random_bytes(64) + return H2(randomizer_input) + + +Binding Factor Computation +'''''''''''''''''''''''''' + +The `compute_binding_factor` function is changed to receive the `randomizer` +as follows: :: + + Inputs: + - encoded_commitment_list, an encoded commitment list (as computed + by encode_group_commitment_list) + - msg, the message to be signed. + - randomizer, the randomizer Scalar. + + Outputs: A Scalar representing the binding factor + + def compute_binding_factor(encoded_commitment_list, msg, randomizer): + msg_hash = H3(msg) + rho_input = encoded_commitment_list || msg_hash || G.SerializeScalar(randomizer) + binding_factor = H1(rho_input) + return binding_factor + + +Round Two - Signature Share Generation +'''''''''''''''''''''''''''''''''''''' + +In Round Two, the Coordinator generates a random scalar `randomizer` by calling +`randomizer_generate` and sends it to each signer, over a secret channel, +along with the message and the set of signing commitments. (Note that this differs +from regular FROST which just requires an authenticated channel.) + +The `sign` function is changed to receive `randomizer` and incorporate it into the +computation of the binding factor. It is specified as the following: :: Inputs: - identifier, Identifier i of the signer. Note identifier will never equal 0. @@ -97,16 +177,19 @@ computation of `sig_share`. It is specified as the following: :: Each element in the list indicates the signer identifier j and their two commitment Element values (hiding_nonce_commitment_j, binding_nonce_commitment_j).s This list MUST be sorted in ascending order by signer identifier. - - alpha, the randomizer Scalar. + - randomizer, the randomizer Scalar. Outputs: a Scalar value representing the signature share - def sign(identifier, sk_i, group_public_key, nonce_i, msg, commitment_list, alpha): + def sign(identifier, sk_i, group_public_key, nonce_i, msg, commitment_list, randomizer): + # Compute the randomized group public key + randomized_group_public_key = randomizer * group_public_key + # Encode the commitment list encoded_commitments = encode_group_commitment_list(commitment_list) # Compute the binding factor - binding_factor = compute_binding_factor(encoded_commitments, msg) + binding_factor = compute_binding_factor(encoded_commitments, msg, randomizer) # Compute the group commitment group_commitment = compute_group_commitment(commitment_list, binding_factor) @@ -116,17 +199,94 @@ computation of `sig_share`. It is specified as the following: :: lambda_i = derive_lagrange_coefficient(identifier, participant_list) # Compute the per-message challenge - challenge = compute_challenge(group_commitment, group_public_key, msg) + challenge = compute_challenge(group_commitment, randomized_group_public_key, msg) # Compute the signature share (hiding_nonce, binding_nonce) = nonce_i - sig_share = hiding_nonce + (binding_nonce * binding_factor) + (lambda_i * (sk_i + alpha / len(commitment_list)) * challenge) + sig_share = hiding_nonce + (binding_nonce * binding_factor) + (lambda_i * sk_i * challenge) return sig_share -When the Coordinator outputs the signature, the Coordinator must also compute and -output G.ScalarBaseMult(α). +Signature Share Verification and Aggregation +'''''''''''''''''''''''''''''''''''''''''''' + +The `verify_signature_share` is also changed to incorporate the randomizer, +as follows: :: + + Inputs: + - identifier, Identifier i of the signer. Note identifier will never equal 0. + - PK_i, the public key for the ith signer, where PK_i = G.ScalarBaseMult(sk_i), + an Element in G + - comm_i, pair of Element values in G (hiding_nonce_commitment, binding_nonce_commitment) + generated in round one from the ith signer. + - sig_share_i, a Scalar value indicating the signature share as produced in + round two from the ith signer. + - commitment_list = + [(j, hiding_nonce_commitment_j, binding_nonce_commitment_j), ...], a + list of commitments issued in Round 1 by each signer, where each element + in the list indicates the signer identifier j and their two commitment + Element values (hiding_nonce_commitment_j, binding_nonce_commitment_j).s + This list MUST be sorted in ascending order by signer identifier. + - group_public_key, public key corresponding to the group signing key, + an Element in G. + - msg, the message to be signed. + - randomizer, the randomizer Scalar. + + Outputs: True if the signature share is valid, and False otherwise. + + def verify_signature_share(identifier, PK_i, comm_i, sig_share_i, commitment_list, + group_public_key, msg, randomizer): + # Compute the randomized group public key + randomized_group_public_key = randomizer * group_public_key + + # Encode the commitment list + encoded_commitments = encode_group_commitment_list(commitment_list) + + # Compute the binding factor + binding_factor = compute_binding_factor(encoded_commitments, msg, randomizer) + + # Compute the group commitment + group_commitment = compute_group_commitment(commitment_list, binding_factor) + + # Compute the commitment share + (hiding_nonce_commitment, binding_nonce_commitment) = comm_i + comm_share = hiding_nonce_commitment + (binding_nonce_commitment * binding_factor) + + # Compute the challenge + challenge = compute_challenge(group_commitment, randomized_group_public_key, msg) + + # Compute Lagrange coefficient + participant_list = participants_from_commitment_list(commitment_list) + lambda_i = derive_lagrange_coefficient(identifier, participant_list) + + # Compute relation values + l = G.ScalarBaseMult(sig_share_i) + r = comm_share + ((challenge * lambda_i) * PK_i) + + return l == r + +The `aggregate` function is changed to incorporate the randomizer as follows: :: + + Inputs: + - group_commitment, the group commitment returned by compute_group_commitment, + an Element in G. + - sig_shares, a set of signature shares z_i, Scalar values, for each signer, + of length NUM_SIGNERS, where MIN_SIGNERS <= NUM_SIGNERS <= MAX_SIGNERS. + - group_public_key, public key corresponding to the group signing key, + - challenge, the challenge returned by compute_challenge, a Scalar. + - randomizer, the randomizer Scalar. + + Outputs: + - (R, z), a Schnorr signature consisting of an Element R and Scalar z. + - randomized_group_public_key, the randomized group public key + + def aggregate(group_commitment, sig_shares, group_public_key, challenge, randomizer): + randomized_group_public_key = randomizer * group_public_key + z = 0 + for z_i in sig_shares: + z = z + z_i + return (group_commitment, z + randomizer * challenge), randomized_group_public_key Ciphersuites