From b129f0352211efd2d4708cdc5942c440b3a100bc Mon Sep 17 00:00:00 2001 From: azuchi Date: Fri, 16 Feb 2024 13:08:04 +0900 Subject: [PATCH] Support DKG --- README.md | 64 +++++++++ lib/frost/dkg.rb | 26 +++- lib/frost/polynomial.rb | 12 ++ spec/fixtures/p256/vectors_dkg.json | 51 +++++++ spec/fixtures/secp256k1/vectors_dkg.json | 51 +++++++ spec/frost/dkg_spec.rb | 173 +++++++++++++++++++++-- 6 files changed, 367 insertions(+), 10 deletions(-) create mode 100644 spec/fixtures/p256/vectors_dkg.json create mode 100644 spec/fixtures/secp256k1/vectors_dkg.json diff --git a/README.md b/README.md index 6113f3c..78823a3 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,67 @@ sig = FROST.aggregate(commitment_list, msg, group_pubkey, [sig_share1, sig_share # verify final signature FROST.verify(sig, group_pubkey, msg) ``` + +### Using DKG + +DKG can be run as below. + +```ruby +max_signer = 5 +min_signer = 3 + +secrets = {} +round1_outputs = {} +# Round 1: +# For each participant, perform the first part of the DKG protocol. +1.upto(max_signer) do |i| + polynomial, package = FROST::DKG.part1(i, min_signer, max_signer, group) + secrets[i] = polynomial + round1_outputs[i] = package +end + +# Each participant sends their commitments and proof to other participants. +received_package = {} +1.upto(max_signer) do |i| + received_package[i] = round1_outputs.select {|k, _| k != i}.values +end + +# Each participant verify knowledge of proof in received package. +received_package.each do |id, packages| + packages.each do |package| + expect(FROST::DKG.verify_proof_of_knowledge(package)).to be true + end +end + +# Round 2: +# Each participant generate share for other participants and send it. +received_shares = {} +1.upto(max_signer) do |i| + polynomial = secrets[i] # own secret + 1.upto(max_signer) do |o| + next if i == o + received_shares[o] ||= [] + received_shares[o] << [i, polynomial.gen_share(o)] + end +end + +# Each participant verify received shares. +1.upto(max_signer) do |i| + received_shares[i].each do |send_by, share| + target_package = received_package[i].find{ |package| package.identifier == send_by } + expect(target_package.verify_share(share)).to be true + end +end + +# Each participant compute signing share. +signing_shares = {} +1.upto(max_signer) do |i| + shares = received_shares[i].map{|_, share| share} + signing_shares[i] = FROST::DKG.compute_signing_share(secrets[i], shares) +end + +# Participant 1 compute group public key. +group_pubkey = FROST::DKG.compute_group_pubkey(secrets[1], received_package[1]) + +# The subsequent signing phase is the same as above with signing_shares as the secret. +``` \ No newline at end of file diff --git a/lib/frost/dkg.rb b/lib/frost/dkg.rb index c61aeb4..041684e 100644 --- a/lib/frost/dkg.rb +++ b/lib/frost/dkg.rb @@ -28,6 +28,9 @@ def part1(identifier, min_signers, max_signers, group) # @param [FROST::Polynomial] polynomial Polynomial containing secret. # @return [FROST::Signature] def gen_proof_of_knowledge(identifier, polynomial) + raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer) + raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial) + k = SecureRandom.random_number(polynomial.group.order - 1) r = polynomial.group.generator * k a0 = polynomial.coefficients.first @@ -43,6 +46,8 @@ def gen_proof_of_knowledge(identifier, polynomial) # @param [FROST::DKG::Package] package Received package. # @return [Boolean] def verify_proof_of_knowledge(package) + raise ArgumentError, "package must be FROST::DKG::Package." unless package.is_a?(FROST::DKG::Package) + verification_key = package.verification_key msg = FROST.encode_identifier(package.identifier, verification_key.group) + [verification_key.to_hex + package.proof.r.to_hex].pack("H*") @@ -50,9 +55,26 @@ def verify_proof_of_knowledge(package) package.proof.r == verification_key.group.generator * package.proof.s + (verification_key * challenge).negate end - # Performs the second part of DKG. - def part2(packages) + # Compute signing share using received shares from other participants + # @param [FROST::Polynomial] polynomial Own polynomial contains own secret. + # @param [Array] received_shares Array of FROST::SecretShare received by other participants. + # @return [FROST::SecretShare] Signing share. + def compute_signing_share(polynomial, received_shares) + raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial) + identifier = received_shares.first.identifier + s_id = received_shares.sum {|share| share.share} + field = ECDSA::PrimeField.new(polynomial.group.order) + FROST::SecretShare.new( + identifier, field.mod(s_id + polynomial.gen_share(identifier).share), polynomial.group) + end + # Compute Group public key. + # @param [FROST::Polynomial] polynomial Own polynomial contains own secret. + # @param [Array] received_packages Array of FROST::DKG::Package received by other participants. + # @return [ECDSA::Point] Group public key. + def compute_group_pubkey(polynomial, received_packages) + raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial) + received_packages.inject(polynomial.verification_point) {|sum, package| sum + package.commitments.first } end end end \ No newline at end of file diff --git a/lib/frost/polynomial.rb b/lib/frost/polynomial.rb index d3296dd..8fa0ea6 100644 --- a/lib/frost/polynomial.rb +++ b/lib/frost/polynomial.rb @@ -62,6 +62,18 @@ def gen_proof_of_knowledge(identifier) FROST::DKG.gen_proof_of_knowledge(identifier, self) end + # Get secret value in this polynomial. + # @return [Integer] secret + def secret + coefficients.first + end + + # Get point to correspond to secret in this polynomial. + # @return [ECDSA::Point] secret point + def verification_point + group.generator * secret + end + # Generates the lagrange coefficient for the i'th participant. # @param [Array] x_coordinates The list of x-coordinates. # @param [Integer] xi an x-coordinate contained in x_coordinates. diff --git a/spec/fixtures/p256/vectors_dkg.json b/spec/fixtures/p256/vectors_dkg.json new file mode 100644 index 0000000..c012f77 --- /dev/null +++ b/spec/fixtures/p256/vectors_dkg.json @@ -0,0 +1,51 @@ +{ + "config": { + "MAX_PARTICIPANTS": 3, + "MIN_PARTICIPANTS": 2, + "name": "FROST(P-256, SHA-256)", + "group": "P-256", + "hash": "SHA-256" + }, + "inputs": { + "verifying_key": "03639d2ad039e39ae2d7f9a28955a55061d76f0e7b518476c5e5ffa33d1bb9fdb3", + "1": { + "identifier": 1, + "signing_key": "32feae119a184fda4be258289dbce37b1811500600d4e8e9497f0fbd136d052e", + "coefficient": "416eb3b9040da27d2ad3dd812982f9f6da30da9309ae2fcb859fda9f7dcde0e6", + "vss_commitments": ["02a67c8f918d275e9d56108e5eeca8de70b21bdfe4d61d9785b0535d69c52d3f6c", "03e9140b2bcf116755a3397cfc2b3b7bd09a54e3b3544cd81f7e62eb3cc872143a"], + "proof_of_knowledge": "03f77e767e8245f49231442f16b3a7198a1140fac2777d38a6a453a9cc3bca82bb4ef8bc7a32d2d80c275862059fe13a29461c4d2abb3f9c4ae5e718eba907372a", + "signing_shares": { + "2": "a3405e99a61c369a98df1107cd4bd26906e2c55997ad060ca7e7a7744585a03a", + "3": "aea6852c4fe45e87c03a5f2d7b29c7872026cd0e3ca919f324d131564eaf303f" + }, + "verifying_share": "036b59b8128e00afa2a334b19d075695a5c79e59d28dfbaa7945b1520f45edfc52", + "signing_share": "c654459194268778cfcfa5df0fb577625c64c25337c19a2fa81df864290c913c" + }, + "2": { + "identifier": 2, + "signing_key": "ace068a672458c645a313b57f0eb33b160281a9d458df65f6da9a83c078dfb7b", + "coefficient": "f65ff5f233d6aa373eadd5afdc609eb763a1a569f936ae322df7c9fb3a5aca10", + "vss_commitments": ["037ef641c6fe1f49b9940d06a453bf7b3c230713e5ffe7a2e2a4bb373ddba074ef", "0384662ca5e0da725028206063e9226b28a535645ca4d7024284813b1dc9d91abb"], + "proof_of_knowledge": "02c1b8317d76016aeed7300b39c945fd6130f1cef4a3c72ca1fb93a60f54f683a93f6593b42facb608c001d760e84f3bef7066a082f3482386a7ee66d441723ada", + "signing_shares": { + "1": "b5dc1583a23394d4a18a132af0c2d768cc73052c1431488054bec4fc0f08c6fa", + "3": "7ef45ddf14f100f50e6e198b118743392078ab353cfea5d4698a38ff33d490d8" + }, + "verifying_share": "02f928ad1fd3679fd5e73b39e6233068451411dfbb52d4f465c18a176f6be49ce1", + "signing_share": "ce70c7f0911776998785136dabf68bc2dda225c993e46589acb4d9e4c9f7777a" + }, + "3": { + "identifier": 3, + "signing_key": "de58ac798ad7bc1a7206a4cfe4cc4bd51fd4eee73c538e11e01829ad6989cfa6", + "coefficient": "d04dd8b1c50ca26e4e33ba5d965d7bb1bd38d8d4a76d2a663872d26be18885ea", + "vss_commitments": ["0363edc1382241228fc6925dc1789737beec5baae430acc6a113581507fac5cf0e", "03156aff5630761ef046d82a52a813b9043653551636774691590bfedab18efef7"], + "proof_of_knowledge": "024c4d52fe40b0b9cea36d842f91fb244a9c09a96954aedb1ffce08a55296d1b4e09225ae0807611f173821e7c15e2ed7c7d0fa26e5fda5d283e2c96f832df906a", + "signing_shares": { + "1": "f74ac93ca6413751cc5df0ac1a45d15fa6a3dfbf1ddf784bda5e9f9b8cd6a7e0", + "2": "90004a800dc98b07163abc67860d0fd854581ad23beb25671c63a5e4c174e9b8" + }, + "verifying_share": "028e818f24bcadbb04f60c352a9cbe25568a737279cc6db3a91857ab7c992a1aad", + "signing_share": "d68d4a4f8e0865ba3f3a80fc4837a0235edf893ff00730e3b14bbb656ae25db8" + } + } + } diff --git a/spec/fixtures/secp256k1/vectors_dkg.json b/spec/fixtures/secp256k1/vectors_dkg.json new file mode 100644 index 0000000..90a8820 --- /dev/null +++ b/spec/fixtures/secp256k1/vectors_dkg.json @@ -0,0 +1,51 @@ +{ + "config": { + "MAX_PARTICIPANTS": 3, + "MIN_PARTICIPANTS": 2, + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "verifying_key": "037b5b0c4b6c91a16fb78499e8a74cc792f9ea79cb94860fcb90f801472930de47", + "1": { + "identifier": 1, + "signing_key": "e7a3cf1fdb1e17d4c3e8a7f663803ef305d03bdfdc930b824b0664c6b853156d", + "coefficient": "819adb51466d687c3944f8dad799a09551af9c083c918a50d9a24a883ae86e2a", + "vss_commitments": ["02dd81b7019efd1d38352b8df26a47d8e6bcb4ce7db71b2f9739b01031105294e2", "03cad1d1bc9d75de15ed0b4cb49dbde670d70988aa96d7982a25ee5484c97d3efc"], + "proof_of_knowledge": "0304df6af7f67b0d5f49ea2116f2d561a0a535c184836779f0f0677ff0838740ce20a0cb076384312f8817e030ca20379bab9247ee56fc3576b0b092f01c005691", + "signing_shares": { + "2": "3c4ae6fe69d55280cb06a0551f8563e526ee6f133a99433addcbb722a4c6f438", + "3": "e2454ec522749fc08388fed9c120b6ada8e1fd1e00026624c95b273f94dbf8a8" + }, + "verifying_share": "02b2597e19a037ba2eef224402a50652be93c1ab5bbd6195fc07ae6f6ecfa1304d", + "signing_share": "87cee034add572924bbd40001bbffa1db1f28a4bf52efebb4c2ad0978c71edf5" + }, + "2": { + "identifier": 2, + "signing_key": "ea163e297661aadf460b3de39a7550bd9b8fb2d07f1e1db5af098720156591a5", + "coefficient": "5234a8d4f373a7a184fb627185101326460d99296ac3c5c0ee948e8f5f97a3d4", + "vss_commitments": ["0280709e1bc38ca14a42f04dde31b33308d5a7ed7ef79a87c0cc14200783b519ac", "03490b38389a84ea57fde7b369962a92c53b367c221d5cd4728a7c6dfddb337c51"], + "proof_of_knowledge": "02afffa1f80fd46f2bac01bf7967649014a3a5236a62f32f98ce11fec20ee7229072c534d89a6b7b4c16129780404e172c3bdb527a77d40d760b80cc6538bcd4c4", + "signing_shares": { + "1": "ead985c267f8e8cd367299ac12b3801eee809709a66d7fe83e789b4a5dedb080", + "3": "39ee690094ac23a2373b35714ae7d3dc0e07e380bf547bf71758903d291a3e0b" + }, + "verifying_share": "03037adc4e0f796b96fc639ac194c1e167ccc5dd57505c813b0533b2bcd6d6ddaa", + "signing_share": "b3477e9659ee0691bdafd1e40230cb07aed5a5e05bd6649f625f12acbb304556" + }, + "3": { + "identifier": 3, + "signing_key": "8a9c3489b03d1bdecfd6c84237599980890d39d49167b016bb8b5fb530677204", + "coefficient": "57a91a3b723783e1b3b2369789c71d2d1fd4c3496e9ab60e0dcfc78a647486a4", + "vss_commitments": ["03f26b76678fe0174196430bb94e4e688044ae7bae2ccd7fef21c354429eb8bd61", "020d7a0d25b4ebed5157daf56aba2b89c3e0522f3bc293cc5e138f10e9c5efa465"], + "proof_of_knowledge": "02ad586ef180cda6bae1d2144ee090d277c77b789c8261349a247073626373cd8723b0ea6a62e8bc37372567ab4ef221d5e0a6c46d57d3746f6e5fde863298a542", + "signing_shares": { + "1": "6c746113ae6651496fb79286ea4d20b58581562b33b669fd58488745c89fdd69", + "2": "e0b438a850bca1c3d4fd653829a58a31b309a1661020cebcbaf4d44163f63be0" + }, + "verifying_share": "02f2198ff3f1e1de2249cdc59eb4ec926936892fa39fc1582861ad2e84681624b3", + "signing_share": "dec01cf806069a912fa263c7e8a19bf1abb8c174c27dca83789354c1e9ee9cb7" + } + } + } diff --git a/spec/frost/dkg_spec.rb b/spec/frost/dkg_spec.rb index fb71b24..49f1b94 100644 --- a/spec/frost/dkg_spec.rb +++ b/spec/frost/dkg_spec.rb @@ -4,7 +4,106 @@ let(:group) { ECDSA::Group::Secp256k1 } - describe "sign with dkg" do + shared_examples "DKG Test Vector" do + it do + min_signers = vectors['config']['MIN_PARTICIPANTS'] + max_signers = vectors['config']['MAX_PARTICIPANTS'] + participant1 = vectors['inputs']['1'] + participant2 = vectors['inputs']['2'] + participant3 = vectors['inputs']['3'] + participants = [participant1, participant2, participant3] + + # Round1 + # generate secret and commitments and proof of knowledge. + received_packages = {} + polynomials = {} + participants.each do |p| + identifier = p['identifier'] + coeffs = [p['signing_key'].hex, p['coefficient'].hex] + polynomial = FROST::Polynomial.new(coeffs, group) + polynomials[identifier] = polynomial + commitments = polynomial.gen_commitments + p['vss_commitments'].each.with_index do |commitment, i| + expect(commitments[i].to_hex).to eq(commitment) + end + proof = FROST::Signature.decode(p['proof_of_knowledge'], group) + package = FROST::DKG::Package.new(identifier, commitments, proof) + [1, 2, 3].select{|v| v != identifier }.each do |target| + received_packages[target] ||= [] + received_packages[target] << package + end + end + + # Each participant verify knowledge of proof in received package. + participants.each do |p| + identifier = p['identifier'] + received_packages[identifier].each do |package| + expect(described_class.verify_proof_of_knowledge(package)).to be true + end + end + + # Round 2: + # Each participant generate share for other participants. + received_shares = {} + participants.each do |participant| + identifier = participant['identifier'] + polynomial = polynomials[identifier] + 1.upto(max_signers).each do |target| + next if identifier == target + received_shares[target] ||= [] + received_shares[target] << [identifier, polynomial.gen_share(target)] + end + end + + participants.each do |participant| + identifier = participant['identifier'] + participant['signing_shares'].each do |from, share| + received_share = received_shares[identifier].find{|send_by, _| send_by == from.to_i}[1] + expect(received_share.share).to eq(share.hex) + end + end + + # Each participant verify received shares. + participants.each do |participant| + identifier = participant['identifier'] + 1.upto(max_signers).each do |target| + next if identifier == target + received_share = received_shares[identifier].find{|send_by, _| send_by == target}[1] + received_package = received_packages[identifier].find{|package| package.identifier == target} + expect(received_package.verify_share(received_share)).to be true + end + end + + # Each participant compute signing share. + secret_shares = {} + participants.each do |participant| + identifier = participant['identifier'] + share = described_class.compute_signing_share(polynomials[identifier], received_shares[identifier].map{|_, share| share}) + secret_shares[identifier] = share + expect(share.share).to eq(participant['signing_share'].hex) + expect(share.to_point.to_hex).to eq(participant['verifying_share']) + end + + # 1 computes group public key. + group_pubkey = described_class.compute_group_pubkey(polynomials[1], received_packages[1]) + expect(group_pubkey.to_hex).to eq(vectors['inputs']['verifying_key']) + end + end + + describe "Test DKG" do + context "secp256k1" do + let(:vectors) { load_fixture("secp256k1/vectors_dkg.json") } + it_behaves_like "DKG Test Vector", "secp256k1" + end + + context "P256" do + let(:group) { ECDSA::Group::Secp256r1 } + let(:vectors) { load_fixture("p256/vectors_dkg.json") } + it_behaves_like "DKG Test Vector", "P256" + end + end + + shared_examples "sign with dkg" do it do max_signer = 5 min_signer = 3 @@ -33,26 +132,84 @@ end # Round 2: - # Each participant generate share for other participants. + # Each participant generate share for other participants and send it. received_shares = {} 1.upto(max_signer) do |i| polynomial = secrets[i] # own secret 1.upto(max_signer) do |o| next if i == o received_shares[o] ||= [] - received_shares[o] << polynomial.gen_share(i) + received_shares[o] << [i, polynomial.gen_share(o)] end end # Each participant verify received shares. 1.upto(max_signer) do |i| - received_shares[i].each do |share| - target_package = received_package[i].find{|package| package.identifier == share.identifier} - (min_signer - 1).times do |degree| - expect(target_package.verify_share(share)).to be true - end + received_shares[i].each do |send_by, share| + target_package = received_package[i].find{ |package| package.identifier == send_by } + expect(target_package.verify_share(share)).to be true end end + + # Each participant compute signing share. + signing_shares = {} + 1.upto(max_signer) do |i| + shares = received_shares[i].map{|_, share| share} + signing_shares[i] = FROST::DKG.compute_signing_share(secrets[i], shares) + end + + # Compute group public key. + compute_pubkeys = 1.upto(max_signer).map do |i| + FROST::DKG.compute_group_pubkey(secrets[i], received_package[i]) + end + # All participants calculate the same group pubkey. + expect(compute_pubkeys.uniq.length).to eq(1) + group_pubkey = compute_pubkeys.first + + # FROST signing process with dkg + # group_pubkey = compute_pubkeys.first + msg = ["74657374"].pack("H*") + + # Round 1: Generate nonce and commitment + share1 = signing_shares[1] + share2 = signing_shares[2] + share4 = signing_shares[4] + hiding_nonce1 = FROST::Nonce.gen_from_secret(share1) + binding_nonce1 = FROST::Nonce.gen_from_secret(share1) + hiding_nonce2 = FROST::Nonce.gen_from_secret(share2) + binding_nonce2 = FROST::Nonce.gen_from_secret(share2) + hiding_nonce4 = FROST::Nonce.gen_from_secret(share4) + binding_nonce4 = FROST::Nonce.gen_from_secret(share4) + + comm1 = FROST::Commitments.new(share1.identifier, hiding_nonce1.to_point, binding_nonce1.to_point) + comm2 = FROST::Commitments.new(share2.identifier, hiding_nonce2.to_point, binding_nonce2.to_point) + comm4 = FROST::Commitments.new(share4.identifier, hiding_nonce4.to_point, binding_nonce4.to_point) + commitment_list = [comm1, comm2, comm4] + + # Round 2: each participant generates their signature share(1 and 2, 4) + sig_share1 = FROST.sign(share1, group_pubkey, [hiding_nonce1, binding_nonce1], msg, commitment_list) + sig_share2 = FROST.sign(share2, group_pubkey, [hiding_nonce2, binding_nonce2], msg, commitment_list) + sig_share4 = FROST.sign(share4, group_pubkey, [hiding_nonce4, binding_nonce4], msg, commitment_list) + + expect(FROST.verify_share(1, share1.to_point, sig_share1, commitment_list, group_pubkey, msg)).to be true + expect(FROST.verify_share(2, share2.to_point, sig_share2, commitment_list, group_pubkey, msg)).to be true + expect(FROST.verify_share(4, share4.to_point, sig_share4, commitment_list, group_pubkey, msg)).to be true + + # Aggregation + sig = FROST.aggregate(commitment_list, msg, group_pubkey, [sig_share1, sig_share2, sig_share4]) + + expect(FROST.verify(sig, group_pubkey, msg)).to be true + end + end + + describe "Test sign with DKG" do + context "secp256k1" do + it_behaves_like "sign with dkg", "secp256k1" + end + + context "P256" do + let(:group) { ECDSA::Group::Secp256r1 } + it_behaves_like "sign with dkg", "P256" end end