frostrb 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0660ec6fbd998b152bdb359276284fd64e00d89ed19e783dbd72e04cac3fad67
4
- data.tar.gz: c01bd20b7f10d10e0362d6ee4c3722c8849f748ed8899c06efd2cfcaee2dcefa
3
+ metadata.gz: 6ec10bec048f4bbf00419a36f27e901afb7ca0b12ae20140d827e1e2eaba107e
4
+ data.tar.gz: 13bce5e0d48b971619c698224e0051162b57b0e08dcc9dabad7eef8a770ecda5
5
5
  SHA512:
6
- metadata.gz: 5cadf1d9b254ac672aad430dae0c27ec7f45878886bbf600cd250a8c4a405f5d89201de96b5bd145ff3fc59ae2384d227a5fe7e73c3abcf002b36a6fbd2d5098
7
- data.tar.gz: 71c14b38913bf8471436979fb8d69a99ad7ad763a79d28f1f738435999ad5b485ba7e09d5e43083bbd2edec5ade02c189076e53678c44a4512a71c86e0883a42
6
+ metadata.gz: 21d11540bbb90c3f3e0e4e96b17204140b40ee52b783da59d12de0898e64d11ed7a4feefc937fd08dff87c001592c14eed39f4d06757211198581047045aa98f
7
+ data.tar.gz: ca60c7fd06432c8c8d6b72fd2a5fb77b42e6184f892af8496c1d540d4215c36ca074fef9c5b495ea8bbcd1e675649a166d41a07b8ab64257f520490ae4ffd869
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.3.0
1
+ ruby-3.4.1
data/README.md CHANGED
@@ -7,7 +7,11 @@ Note: This library has not been security audited and tested widely, so should no
7
7
  The cipher suites currently supported by this library are:
8
8
 
9
9
  * [secp256k1, SHA-256](https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-frostsecp256k1-sha-256)
10
- * [P-256, SHA-256](https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-frostp-256-sha-256)
10
+ * [P-256, SHA-256](https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-frostp-256-sha-256)
11
+ * secp256k1, Taproot
12
+
13
+ Note: The implementation for Taproot is based on [frost-secp256k1-tr](https://github.com/ZcashFoundation/frost/tree/main/frost-secp256k1-tr),
14
+ but since it is not an official BIP, it may change in the future.
11
15
 
12
16
  ## Installation
13
17
 
@@ -30,10 +34,11 @@ Or install it yourself as:
30
34
  ```ruby
31
35
  require 'frost'
32
36
 
33
- group = ECDSA::Group::Secp256k1
37
+ # Setup context.
38
+ ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::RFC9591)
34
39
 
35
40
  # Dealer generate secret.
36
- secret = FROST::SigningKey.generate(group)
41
+ secret = FROST::SigningKey.generate(ctx)
37
42
  group_pubkey = secret.to_point
38
43
 
39
44
  # Generate polynomial(f(x) = ax + b)
@@ -58,48 +63,60 @@ commitment_list = [comm1, comm3]
58
63
  msg = ["74657374"].pack("H*")
59
64
 
60
65
  # Round 2: each participant generates their signature share(1 and 3)
61
- sig_share1 = FROST.sign(share1, group_pubkey, [hiding_nonce1, binding_nonce1], msg, commitment_list)
62
- sig_share3 = FROST.sign(share3, group_pubkey, [hiding_nonce3, binding_nonce3], msg, commitment_list)
66
+ sig_share1 = FROST.sign(ctx, share1, group_pubkey, [hiding_nonce1, binding_nonce1], msg, commitment_list)
67
+ sig_share3 = FROST.sign(ctx, share3, group_pubkey, [hiding_nonce3, binding_nonce3], msg, commitment_list)
63
68
 
64
69
  # verify signature share
65
70
  FROST.verify_share(1, share1.to_point, sig_share1, commitment_list, group_pubkey, msg)
66
71
  FROST.verify_share(3, share3.to_point, sig_share3, commitment_list, group_pubkey, msg)
67
72
 
68
73
  # Aggregation
69
- sig = FROST.aggregate(commitment_list, msg, group_pubkey, [sig_share1, sig_share3])
74
+ sig = FROST.aggregate(ctx, commitment_list, msg, group_pubkey, [sig_share1, sig_share3])
70
75
 
71
76
  # verify final signature
72
77
  FROST.verify(sig, group_pubkey, msg)
73
78
  ```
74
79
 
80
+ ### Bitcoin support
81
+
82
+ When using Bitcoin(taproot), the context type must be `FROST::Type::Taproot` instead of `FROST::Type::RFC9591`.
83
+
84
+ ```ruby
85
+ ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::TAPROOT)
86
+ ```
87
+
75
88
  ### Using DKG
76
89
 
77
90
  DKG can be run as below.
78
91
 
79
92
  ```ruby
93
+ # Setup context.
94
+ ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::RFC9591)
95
+
80
96
  max_signer = 5
81
97
  min_signer = 3
82
98
 
83
- secrets = {}
99
+ secret_packages = {}
84
100
  round1_outputs = {}
85
101
  # Round 1:
86
102
  # For each participant, perform the first part of the DKG protocol.
87
103
  1.upto(max_signer) do |i|
88
- polynomial, package = FROST::DKG.generate_secret(i, min_signer, max_signer, group)
89
- secrets[i] = polynomial
90
- round1_outputs[i] = package
104
+ secret_package = FROST::DKG.generate_secret(ctx, i, min_signer, max_signer)
105
+ secret_packages[i] = secret_package
106
+ round1_outputs[i] = secret_package.public_package
91
107
  end
92
108
 
93
- # Each participant sends their commitments and proof to other participants.
109
+ # Each participant send their commitments and proof to other participants.
94
110
  received_package = {}
95
111
  1.upto(max_signer) do |i|
96
- received_package[i] = round1_outputs.select { |k, _| k != i }.values
112
+ received_package[i] = round1_outputs.select {|k, _| k != i}.values
97
113
  end
98
114
 
99
115
  # Each participant verify knowledge of proof in received package.
100
116
  received_package.each do |id, packages|
117
+ secret_package = secret_packages[id]
101
118
  packages.each do |package|
102
- expect(FROST::DKG.verify_proof_of_knowledge(package)).to be true
119
+ expect(FROST::DKG.verify_proof_of_knowledge(secret_package, package)).to be true
103
120
  end
104
121
  end
105
122
 
@@ -107,31 +124,31 @@ end
107
124
  # Each participant generate share for other participants and send it.
108
125
  received_shares = {}
109
126
  1.upto(max_signer) do |i|
110
- polynomial = secrets[i] # own secret
127
+ secret_package = secret_packages[i] # own secret
111
128
  1.upto(max_signer) do |o|
112
129
  next if i == o
113
130
  received_shares[o] ||= []
114
- received_shares[o] << [i, polynomial.gen_share(o)]
131
+ received_shares[o] << [i, secret_package.gen_share(o)]
115
132
  end
116
133
  end
117
134
 
118
135
  # Each participant verify received shares.
119
136
  1.upto(max_signer) do |i|
120
137
  received_shares[i].each do |send_by, share|
121
- target_package = received_package[i].find { |package| package.identifier == send_by }
138
+ target_package = received_package[i].find{ |package| package.identifier == send_by }
122
139
  expect(target_package.verify_share(share)).to be true
123
140
  end
124
141
  end
125
142
 
126
- # Each participant compute signing share.
143
+ # Each participant computes signing share.
127
144
  signing_shares = {}
128
145
  1.upto(max_signer) do |i|
129
- shares = received_shares[i].map { |_, share| share }
130
- signing_shares[i] = FROST::DKG.compute_signing_share(secrets[i], shares)
146
+ shares = received_shares[i].map{|_, share| share}
147
+ signing_shares[i] = FROST::DKG.compute_signing_share(secret_packages[i], received_package[i], shares)
131
148
  end
132
149
 
133
150
  # Participant 1 compute group public key.
134
- group_pubkey = FROST::DKG.compute_group_pubkey(secrets[1], received_package[1])
151
+ group_pubkey = FROST::DKG.compute_group_pubkey(secret_packages[1], received_package[1])
135
152
 
136
153
  # The subsequent signing phase is the same as above with signing_shares as the secret.
137
154
  ```
@@ -141,8 +158,10 @@ group_pubkey = FROST::DKG.compute_group_pubkey(secrets[1], received_package[1])
141
158
  Using `FROST::Repairable` module, you can repair existing (or new) participant's share with the cooperation of T participants.
142
159
 
143
160
  ```ruby
161
+ ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::RFC9591)
162
+ dealer = FROST::SigningKey.generate(ctx)
163
+
144
164
  # Dealer generate shares.
145
- FROST::SigningKey.generate(ECDSA::Group::Secp256k1)
146
165
  polynomial = dealer.gen_poly(min_signers - 1)
147
166
  shares = 1.upto(max_signers).map {|identifier| polynomial.gen_share(identifier) }
148
167
 
@@ -168,9 +187,9 @@ end
168
187
  # Each helper send sum value to participant.
169
188
  participant_received_values = []
170
189
  received_values.each do |_, values|
171
- participant_received_values << FROST::Repairable.step2(values, ECDSA::Group::Secp256k1)
190
+ participant_received_values << FROST::Repairable.step2(ctx, values)
172
191
  end
173
192
 
174
- # Participant can obtain his share.
175
- repair_share = FROST::Repairable.step3(2, participant_received_values, ECDSA::Group::Secp256k1)
193
+ # Participant can get his share.
194
+ repair_share = FROST::Repairable.step3(ctx, 2, participant_received_values)
176
195
  ```
@@ -0,0 +1,87 @@
1
+ module FROST
2
+ class Context
3
+
4
+ CTX_STRING_SECP256K1 = "FROST-secp256k1-SHA256-v1"
5
+ CTX_STRING_SECP256K1_TR = "FROST-secp256k1-SHA256-TR-v1"
6
+ CTX_STRING_P256 = "FROST-P256-SHA256-v1"
7
+
8
+ attr_reader :group
9
+ attr_reader :type
10
+
11
+ # Constructor
12
+ # @param [ECDSA::Group] group The elliptic curve group.
13
+ # @param [Symbol] type FROST::Type
14
+ # @raise [ArgumentError]
15
+ def initialize(group, type)
16
+ raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
17
+ FROST::Type.validate!(type)
18
+ @group = group
19
+ @type = type
20
+ end
21
+
22
+ # Check this context is taproot or not?
23
+ # @return [Boolean]
24
+ def taproot?
25
+ type == FROST::Type::TAPROOT
26
+ end
27
+
28
+ # Get context string.
29
+ # @return [String] context string.
30
+ # @raise [ArgumentError]
31
+ def ctx_string
32
+ case group
33
+ when ECDSA::Group::Secp256k1
34
+ type == FROST::Type::RFC9591 ? CTX_STRING_SECP256K1 : CTX_STRING_SECP256K1_TR
35
+ when ECDSA::Group::Secp256r1
36
+ CTX_STRING_P256
37
+ else
38
+ # TODO support other suite.
39
+ raise RuntimeError, "group #{group} dose not supported."
40
+ end
41
+ end
42
+
43
+ # Normalize elliptic curve point.
44
+ # If type is BIP-340, return as x-only public key.
45
+ # @param [ECDSA::Point] point
46
+ # @return [String] Normalized point string with binary format.
47
+ def normalize(point)
48
+ if taproot?
49
+ ECDSA::Format::FieldElementOctetString.encode(point.x, group.field)
50
+ else
51
+ [point.to_hex].pack("H*")
52
+ end
53
+ end
54
+
55
+ # Negate nonces depending on context.
56
+ # @param [ECDSA::Point] group_commitment
57
+ # @param [Array] nonces Pair of nonce values (hiding_nonce, binding_nonce) for signer_i.
58
+ # @return [Array] Converted nonces.
59
+ def convert_signer_nocnes(group_commitment, nonces)
60
+ return nonces unless taproot?
61
+ group_commitment.y.even? ? nonces : nonces.map(&:to_negate)
62
+ end
63
+
64
+ # Convert commitment share depending on context.
65
+ # @param [ECDSA::Point] group_commitment
66
+ # @param [ECDSA::Point] commitment_share
67
+ # @return [ECDSA::Point] Converted commitment share.
68
+ def convert_commitment_share(group_commitment, commitment_share)
69
+ return commitment_share unless taproot?
70
+ group_commitment.y.even? ? commitment_share : commitment_share.negate
71
+ end
72
+
73
+ # Preprocess verify inputs, negating the VerifyingKey and `signature.R` if required by BIP-340.
74
+ # @param [ECDSA::Point] public_key
75
+ # @param [FROST::Signature] signature
76
+ # @return [Array] An array of public_key and signature.
77
+ def pre_verify(public_key, signature)
78
+ if taproot?
79
+ public_key = public_key.y.even? ? public_key : public_key.negate
80
+ r = signature.r.y.even? ? signature.r : signature.r.negate
81
+ [public_key, FROST::Signature.new(self, r, signature.s)]
82
+ else
83
+ [public_key, signature]
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,6 +1,6 @@
1
1
  module FROST
2
2
  module DKG
3
- class Package
3
+ class PublicPackage
4
4
  attr_reader :identifier
5
5
  attr_reader :commitments
6
6
  attr_reader :proof
@@ -0,0 +1,59 @@
1
+ module FROST
2
+ module DKG
3
+ # Package to hold participants' secret share.
4
+ class SecretPackage
5
+
6
+ attr_reader :identifier
7
+ attr_reader :polynomial
8
+ attr_reader :public_package
9
+ attr_reader :min_signers
10
+ attr_reader :max_signers
11
+
12
+ # Constructor.
13
+ # @param [Integer] identifier The identifier of this owner.
14
+ # @param [Integer] min_signers Minimum number of signers.
15
+ # @param [Integer] max_signers Maximum number of signers.
16
+ # @param [FROST::Polynomial] polynomial Polynomial with secret share.
17
+ def initialize(identifier, min_signers, max_signers, polynomial)
18
+ raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
19
+ raise ArgumentError, "identifier must be greater than 0." if identifier < 1
20
+ raise ArgumentError, "min_signers must be Integer." unless min_signers.is_a?(Integer)
21
+ raise ArgumentError, "max_signers must be Integer." unless max_signers.is_a?(Integer)
22
+ raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial)
23
+ raise ArgumentError, "max_signers must be greater than or equal to min_signers." if max_signers < min_signers
24
+ raise ArgumentError, "Number of coefficients of polynomial and min_signers do not match." unless min_signers == polynomial.coefficients.length
25
+
26
+ @identifier = identifier
27
+ @min_signers = min_signers
28
+ @max_signers = max_signers
29
+ @polynomial = polynomial
30
+ @public_package = PublicPackage.new(identifier, polynomial.gen_commitments, polynomial.gen_proof_of_knowledge(identifier))
31
+ end
32
+
33
+ # Generate secret share for identifier.
34
+ # @param [Integer] identifier
35
+ # @return [FROST::SecretShare] Generate share.
36
+ def gen_share(identifier)
37
+ polynomial.gen_share(identifier)
38
+ end
39
+
40
+ # Get group.
41
+ # @return [ECDSA::Group]
42
+ def group
43
+ polynomial.group
44
+ end
45
+
46
+ # Get FROST context.
47
+ # @return [FROST::Context]
48
+ def context
49
+ polynomial.context
50
+ end
51
+
52
+ # Get verification point.
53
+ # @return [ECDSA::Point]
54
+ def verification_point
55
+ polynomial.verification_point
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/frost/dkg.rb CHANGED
@@ -2,25 +2,28 @@ module FROST
2
2
  # Distributed Key Generation feature.
3
3
  module DKG
4
4
 
5
- autoload :Package, "frost/dkg/package"
5
+ autoload :SecretPackage, "frost/dkg/secret_package"
6
+ autoload :PublicPackage, "frost/dkg/public_package"
6
7
 
7
8
  module_function
8
9
 
9
10
  # Performs the first part of the DKG.
10
11
  # Participant generate key and commitments, proof of knowledge for secret.
11
- # @param [Integer] identifier
12
- # @param [ECDSA::Group] group Group of elliptic curve.
13
- # @return [Array] The triple of polynomial and public package(FROST::DKG::Package)
14
- def generate_secret(identifier, min_signers, max_signers, group)
12
+ # @param [FROST::Context] context
13
+ # @param [Integer] identifier Party's identifier.
14
+ # @param [Integer] min_signers The number of min signers.
15
+ # @return [FROST::DKG::SecretPackage] Secret received_package for owner.
16
+ def generate_secret(context, identifier, min_signers, max_signers)
15
17
  raise ArgumentError, "identifier must be Integer" unless identifier.is_a?(Integer)
16
18
  raise ArgumentError, "identifier must be greater than 0." if identifier < 1
17
- raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
19
+ raise ArgumentError, "context must be FROST::Context." unless context.is_a?(FROST::Context)
20
+ raise ArgumentError, "min_signers must be Integer." unless min_signers.is_a?(Integer)
21
+ raise ArgumentError, "max_singers must be Integer." unless max_signers.is_a?(Integer)
18
22
  raise ArgumentError, "max_signers must be greater than or equal to min_signers." if max_signers < min_signers
19
23
 
20
- secret = FROST::SigningKey.generate(group)
21
- # Every participant P_i samples t random values (a_{i0}, ..., a_{i(t−1)}) ← Z_q
24
+ secret = FROST::SigningKey.generate(context)
22
25
  polynomial = secret.gen_poly(min_signers - 1)
23
- [polynomial, Package.new(identifier, polynomial.gen_commitments, polynomial.gen_proof_of_knowledge(identifier))]
26
+ SecretPackage.new(identifier, min_signers, max_signers, polynomial)
24
27
  end
25
28
 
26
29
  # Generate proof of knowledge for secret.
@@ -36,45 +39,86 @@ module FROST
36
39
  a0 = polynomial.coefficients.first
37
40
  a0_g = polynomial.group.generator * a0
38
41
  msg = FROST.encode_identifier(identifier, polynomial.group) + [a0_g.to_hex + r.to_hex].pack("H*")
39
- challenge = Hash.hdkg(msg, polynomial.group)
42
+ challenge = Hash.hdkg(msg, polynomial.context)
40
43
  field = ECDSA::PrimeField.new(polynomial.group.order)
41
44
  s = field.mod(k + a0 * challenge)
42
- FROST::Signature.new(r, s)
45
+ FROST::Signature.new(polynomial.context, r, s)
43
46
  end
44
47
 
45
48
  # Verify proof of knowledge for received commitment.
46
- # @param [FROST::DKG::Package] package Received package.
49
+ # @param [FROST::DKG::SecretPackage] secret_package Verifier's secret package.
50
+ # @param [FROST::DKG::PublicPackage] received_package Received received_package.
47
51
  # @return [Boolean]
48
- def verify_proof_of_knowledge(package)
49
- raise ArgumentError, "package must be FROST::DKG::Package." unless package.is_a?(FROST::DKG::Package)
52
+ def verify_proof_of_knowledge(secret_package, received_package)
53
+ raise ArgumentError, "secret_package must be FROST::DKG::SecretPackage." unless secret_package.is_a?(FROST::DKG::SecretPackage)
54
+ raise ArgumentError, "received_package must be FROST::DKG::Package." unless received_package.is_a?(FROST::DKG::PublicPackage)
55
+ raise FROST::Error, "Invalid number of commitments in package." unless secret_package.min_signers == received_package.commitments.length
50
56
 
51
- verification_key = package.verification_key
52
- msg = FROST.encode_identifier(package.identifier, verification_key.group) +
53
- [verification_key.to_hex + package.proof.r.to_hex].pack("H*")
54
- challenge = Hash.hdkg(msg, verification_key.group)
55
- package.proof.r == verification_key.group.generator * package.proof.s + (verification_key * challenge).negate
57
+ verification_key = received_package.verification_key
58
+ msg = FROST.encode_identifier(received_package.identifier, verification_key.group) +
59
+ [verification_key.to_hex + received_package.proof.r.to_hex].pack("H*")
60
+ challenge = Hash.hdkg(msg, secret_package.polynomial.context)
61
+ received_package.proof.r == verification_key.group.generator * received_package.proof.s + (verification_key * challenge).negate
56
62
  end
57
63
 
58
- # Compute signing share using received shares from other participants
59
- # @param [FROST::Polynomial] polynomial Own polynomial contains own secret.
64
+ # Compute signing share using received shares from other participants.
65
+ #
66
+ # @param [FROST::DKG::SecretPackage] secret_package Own secret received_package.
67
+ # @param [Array] received_packages Array of FROST::DKG::Package received by other participants.
60
68
  # @param [Array] received_shares Array of FROST::SecretShare received by other participants.
61
69
  # @return [FROST::SecretShare] Signing share.
62
- def compute_signing_share(polynomial, received_shares)
63
- raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial)
70
+ # @raise [ArgumentError]
71
+ def compute_signing_share(secret_package, received_packages, received_shares)
72
+ raise ArgumentError, "polynomial must be FROST::DKG::SecretPackage." unless secret_package.is_a?(FROST::DKG::SecretPackage)
73
+ raise FROST::Error, "Invalid number of received_shares." unless secret_package.max_signers - 1 == received_shares.length
74
+ raise ArgumentError, "The number of received_packages and received_shares does not match." unless received_packages.length == received_shares.length
75
+
64
76
  identifier = received_shares.first.identifier
65
- s_id = received_shares.sum {|share| share.share}
66
- field = ECDSA::PrimeField.new(polynomial.group.order)
67
- FROST::SecretShare.new(
68
- identifier, field.mod(s_id + polynomial.gen_share(identifier).share), polynomial.group)
77
+ signing_share = received_shares.sum {|share| share.share}
78
+ field = ECDSA::PrimeField.new(secret_package.group.order)
79
+ signing_share = field.mod(signing_share + secret_package.gen_share(identifier).share)
80
+ ctx = secret_package.polynomial.context
81
+ if ctx.taproot?
82
+ # Post-process the DKG output. Add an unusable taproot tweak to the group key computed by a DKG run,
83
+ # to prevent peers from inserting rogue tapscript tweaks into the group's joint public key.
84
+ # From BIP-341:
85
+ # > If the spending conditions do not require a script path, the output key should commit to
86
+ # > an unspendable script path instead of having no script path.
87
+ # > This can be achieved by computing the output key point as Q = P + int(hashTapTweak(bytes(P)))G.
88
+ verification_key = compute_group_pubkey(secret_package, received_packages, apply_even: false)
89
+ has_even = verification_key.y.even?
90
+ unless has_even
91
+ verification_key = verification_key.negate
92
+ signing_share = field.mod(-signing_share)
93
+ end
94
+ signing_share = field.mod(signing_share + tap_tweak(verification_key))
95
+ end
96
+ FROST::SecretShare.new(secret_package.context, identifier, signing_share)
69
97
  end
70
98
 
71
99
  # Compute Group public key.
72
- # @param [FROST::Polynomial] polynomial Own polynomial contains own secret.
100
+ # @param [FROST::DKG::SecretPackage] secret_package Own secret received_package.
73
101
  # @param [Array] received_packages Array of FROST::DKG::Package received by other participants.
102
+ # @param [Boolean] apply_even In taproot context, whether tweak to public key or not.
74
103
  # @return [ECDSA::Point] Group public key.
75
- def compute_group_pubkey(polynomial, received_packages)
76
- raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial)
77
- received_packages.inject(polynomial.verification_point) {|sum, package| sum + package.commitments.first }
104
+ def compute_group_pubkey(secret_package, received_packages, apply_even: true)
105
+ raise ArgumentError, "polynomial must be FROST::DKG::SecretPackage." unless secret_package.is_a?(FROST::DKG::SecretPackage)
106
+ raise FROST::Error, "Invalid number of received_packages." unless secret_package.max_signers - 1 == received_packages.length
107
+
108
+ verification_key = received_packages.inject(secret_package.verification_point) {|sum, package| sum + package.commitments.first }
109
+ if secret_package.polynomial.context.taproot? && apply_even
110
+ verification_key = verification_key.negate unless verification_key.y.even?
111
+ verification_key + (verification_key.group.generator * tap_tweak(verification_key))
112
+ else
113
+ verification_key
114
+ end
115
+ end
116
+
117
+ def tap_tweak(point)
118
+ x_only = ECDSA::Format::IntegerOctetString.encode(point.x, 32)
119
+ FROST::Hash.tagged_hash('TapTweak', x_only)
78
120
  end
121
+
122
+ private_class_method :tap_tweak
79
123
  end
80
124
  end
data/lib/frost/hash.rb CHANGED
@@ -5,80 +5,77 @@ module FROST
5
5
 
6
6
  module_function
7
7
 
8
- CTX_STRING_SECP256K1 = "FROST-secp256k1-SHA256-v1"
9
- CTX_STRING_P256 = "FROST-P256-SHA256-v1"
10
-
11
8
  # H1 hash function.
12
9
  # @param [String] msg The message to be hashed.
13
- # param [ECDSA::Group] group The elliptic curve group.
10
+ # @param [FROST::Context] context FROST context.
14
11
  # @return [Integer]
15
- def h1(msg, group)
16
- hash_to_field(msg, group, "rho")
12
+ def h1(msg, context)
13
+ hash_to_field(msg, context, "rho")
17
14
  end
18
15
 
19
- # H3 hash function.
16
+ # H2 hash function.
20
17
  # @param [String] msg The message to be hashed.
21
- # @param [ECDSA::Group] group The elliptic curve group.
18
+ # @param [FROST::Context] context FROST context.
22
19
  # @return [Integer]
23
- def h2(msg, group)
24
- hash_to_field(msg, group, "chal")
20
+ def h2(msg, context)
21
+ if context.taproot?
22
+ tagged_hash('BIP0340/challenge', msg)
23
+ else
24
+ hash_to_field(msg, context, "chal")
25
+ end
25
26
  end
26
27
 
27
28
  # H3 hash function.
28
29
  # @param [String] msg The message to be hashed.
29
- # @param [ECDSA::Group] group The elliptic curve group.
30
+ # @param [FROST::Context] context FROST context.
30
31
  # @return [Integer]
31
- def h3(msg, group)
32
- hash_to_field(msg, group, "nonce")
32
+ def h3(msg, context)
33
+ hash_to_field(msg, context, "nonce")
33
34
  end
34
35
 
35
36
  # H4 hash function.
36
37
  # @param [String] msg The message to be hashed.
37
- # @param [ECDSA::Group] group The elliptic curve group.
38
+ # @param [FROST::Context] context FROST context.
38
39
  # @return [String] The hash value.
39
- def h4(msg, group)
40
- hash(msg, group, "msg")
40
+ def h4(msg, context)
41
+ hash(msg, context, "msg")
41
42
  end
42
43
 
43
44
  # H5 hash function.
44
45
  # @param [String] msg The message to be hashed.
45
- # @param [ECDSA::Group] group The elliptic curve group.
46
+ # @param [FROST::Context] context FROST context.
46
47
  # @return [String] The hash value.
47
- def h5(msg, group)
48
- hash(msg, group, "com")
48
+ def h5(msg, context)
49
+ hash(msg, context, "com")
49
50
  end
50
51
 
51
52
  # Hash function for a FROST ciphersuite, used for the DKG.
52
53
  # @param [String] msg The message to be hashed.
53
- # @param [ECDSA::Group] group The elliptic curve group.
54
+ # @param [FROST::Context] context FROST context.
54
55
  # @return [Integer] The hash value.
55
- def hdkg(msg, group)
56
- hash_to_field(msg, group, "dkg")
56
+ def hdkg(msg, context)
57
+ hash_to_field(msg, context, "dkg")
57
58
  end
58
59
 
59
- def hash_to_field(msg, group, context)
60
- h2c = case group
60
+ def hash_to_field(msg, context, tag)
61
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
62
+ h2c = case context.group
61
63
  when ECDSA::Group::Secp256k1
62
- H2C.get(H2C::Suite::SECP256K1_XMDSHA256_SSWU_NU_, CTX_STRING_SECP256K1 + context)
64
+ H2C.get(H2C::Suite::SECP256K1_XMDSHA256_SSWU_NU_, context.ctx_string + tag)
63
65
  when ECDSA::Group::Secp256r1
64
- H2C.get(H2C::Suite::P256_XMDSHA256_SSWU_NU_, CTX_STRING_P256 + context)
65
- else
66
- # TODO support other suite.
67
- raise RuntimeError, "group #{group} dose not supported."
66
+ H2C.get(H2C::Suite::P256_XMDSHA256_SSWU_NU_, context.ctx_string + tag)
68
67
  end
69
- h2c.hash_to_field(msg, 1, group.order).first
68
+ h2c.hash_to_field(msg, 1, context.group.order).first
70
69
  end
71
70
 
72
- def hash(msg, group, context)
73
- case group
74
- when ECDSA::Group::Secp256k1
75
- Digest::SHA256.digest(CTX_STRING_SECP256K1 + context + msg)
76
- when ECDSA::Group::Secp256r1
77
- Digest::SHA256.digest(CTX_STRING_P256 + context + msg)
78
- else
79
- # TODO support other suite.
80
- raise RuntimeError, "group #{group} dose not supported."
81
- end
71
+ def hash(msg, context, tag)
72
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
73
+ Digest::SHA256.digest(context.ctx_string + tag + msg)
74
+ end
75
+
76
+ def tagged_hash(tag, msg)
77
+ tag_hash = Digest::SHA256.digest(tag)
78
+ Digest::SHA256.hexdigest(tag_hash + tag_hash + msg).to_i(16)
82
79
  end
83
80
  end
84
81
  end
data/lib/frost/nonce.rb CHANGED
@@ -2,15 +2,23 @@ module FROST
2
2
  class Nonce
3
3
 
4
4
  attr_reader :value # nonce value
5
- attr_reader :group # Group of elliptic curve
5
+ attr_reader :context # Group of elliptic curve
6
6
 
7
7
  # Generate nonce.
8
+ # @param [FROST::Context] context
9
+ # @param [Integer] nonce
8
10
  # @return [FROST::Nonce]
9
- def initialize(nonce, group)
10
- raise ArgumentError, "group must by ECDSA::Group." unless group.is_a?(ECDSA::Group)
11
- raise ArgumentError, "nonce must by Integer." unless nonce.is_a?(Integer)
11
+ def initialize(context, nonce)
12
+ raise ArgumentError, "context must be FROST::Context." unless context.is_a?(FROST::Context)
13
+ raise ArgumentError, "nonce must be Integer." unless nonce.is_a?(Integer)
12
14
  @value = nonce
13
- @group = group
15
+ @context = context
16
+ end
17
+
18
+ # Get group
19
+ # @return [ECDSA::Group]
20
+ def group
21
+ context.group
14
22
  end
15
23
 
16
24
  # Generate nonce from secret share.
@@ -32,8 +40,8 @@ module FROST
32
40
 
33
41
  secret_bytes = ECDSA::Format::IntegerOctetString.encode(secret.scalar, 32)
34
42
  msg = random_bytes + secret_bytes
35
- nonce = FROST::Hash.h3(msg, secret.group)
36
- Nonce.new(nonce, secret.group)
43
+ k = FROST::Hash.h3(msg, secret.context)
44
+ Nonce.new(secret.context, k)
37
45
  end
38
46
 
39
47
  private_class_method :gen_from_random_bytes
@@ -50,5 +58,11 @@ module FROST
50
58
  group.generator * value
51
59
  end
52
60
 
61
+ # Generate negated nonce.
62
+ # @return [FROST::Nonce] Negated nonce.
63
+ def to_negate
64
+ Nonce.new(context, group.order - value)
65
+ end
66
+
53
67
  end
54
68
  end
@@ -3,32 +3,44 @@ module FROST
3
3
  # Polynomial class.
4
4
  class Polynomial
5
5
  attr_reader :coefficients
6
- attr_reader :group
6
+ attr_reader :context
7
7
 
8
8
  # Generate polynomial.
9
+ # @param [FROST::Context] context
9
10
  # @param [Array] coefficients Coefficients of polynomial.
10
11
  # The first is the constant term, followed by the coefficients in descending order of order.
11
- # @param [ECDSA::Group] group
12
- def initialize(coefficients, group)
12
+ # @raise [ArgumentError]
13
+ #
14
+ def initialize(context, coefficients)
15
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
13
16
  raise ArgumentError, "coefficients must be an Array." unless coefficients.is_a?(Array)
14
- raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
15
17
  raise ArgumentError, "Two or more coefficients are required." if coefficients.length < 2
18
+ coefficients.each do |coefficient|
19
+ raise ArgumentError "coefficient must be Integer." unless coefficient.is_a?(Integer)
20
+ end
16
21
 
17
22
  @coefficients = coefficients
18
- @group = group
23
+ @context = context
24
+ end
25
+
26
+ # Get group
27
+ # @return [ECDSA::Group]
28
+ def group
29
+ context.group
19
30
  end
20
31
 
21
32
  # Generate random polynomial using secret as constant term.
33
+ # @param [FROST::Context] context
22
34
  # @param [Integer|FROST::SigningKey] secret Secret value as constant term.
23
35
  # @param [Integer] degree Degree of polynomial.
24
36
  # @return [FROST::Polynomial] Polynomial
25
- def self.from_secret(secret, degree, group)
37
+ def self.from_secret(context, secret, degree)
26
38
  secret = secret.scalar if secret.is_a?(FROST::SigningKey)
27
39
  raise ArgumentError, "secret must be Integer." unless secret.is_a?(Integer)
28
40
  raise ArgumentError, "degree must be Integer." unless degree.is_a?(Integer)
29
41
  raise ArgumentError, "degree must be greater than or equal to 1." if degree < 1
30
- coeffs = degree.times.map {SecureRandom.random_number(group.order - 1)}
31
- Polynomial.new(coeffs.prepend(secret), group)
42
+ coeffs = degree.times.map {SecureRandom.random_number(context.group.order - 1)}
43
+ Polynomial.new(context, coeffs.prepend(secret))
32
44
  end
33
45
 
34
46
  # Generate secret share.
@@ -37,8 +49,8 @@ module FROST
37
49
  def gen_share(identifier)
38
50
  raise ArgumentError, "identifiers must be Integer." unless identifier.is_a?(Integer)
39
51
 
40
- return SecretShare.new(identifier, 0, group) if coefficients.empty?
41
- return SecretShare.new(identifier, coefficients.last, group) if identifier == 0
52
+ return SecretShare.new(context, identifier, 0) if coefficients.empty?
53
+ return SecretShare.new(context, identifier, coefficients.last) if identifier == 0
42
54
 
43
55
  # Calculate using Horner's method.
44
56
  last = coefficients.last
@@ -46,7 +58,7 @@ module FROST
46
58
  tmp = last * identifier
47
59
  last = (tmp + coefficients[i]) % group.order
48
60
  end
49
- SecretShare.new(identifier, last, group)
61
+ SecretShare.new(context, identifier, last)
50
62
  end
51
63
 
52
64
  # Generate coefficient commitments
@@ -31,26 +31,26 @@ module FROST
31
31
 
32
32
  # Step 2 for RTS.
33
33
  # Each helper sum received delta values from other helpers.
34
+ # @param [FROST::Context] context
34
35
  # @param [Array] step1_values Array of delta values.
35
- # @param [ECDSA::Group] group
36
36
  # @return [Integer] Sum of delta values.
37
- def step2(step1_values, group)
38
- raise ArgumentError, "group must be ECDSA::Group" unless group.is_a?(ECDSA::Group)
37
+ def step2(context, step1_values)
38
+ raise ArgumentError, "context must be FROST::Context." unless context.is_a?(FROST::Context)
39
39
 
40
- field = ECDSA::PrimeField.new(group.order)
40
+ field = ECDSA::PrimeField.new(context.group.order)
41
41
  field.mod(step1_values.sum)
42
42
  end
43
43
 
44
44
  # Participant compute own share with received sum of delta value.
45
+ # @param [FROST::Context] context
45
46
  # @param [Integer] identifier Identifier of the participant whose shares you want to restore.
46
47
  # @param [Array] step2_results Array of Step 2 results received from other helpers.
47
- # @param [ECDSA::Group] group
48
48
  # @return
49
- def step3(identifier, step2_results, group)
50
- raise ArgumentError, "group must be ECDSA::Group" unless group.is_a?(ECDSA::Group)
49
+ def step3(context, identifier, step2_results)
50
+ raise ArgumentError, "context must be FROST::Context." unless context.is_a?(FROST::Context)
51
51
 
52
- field = ECDSA::PrimeField.new(group.order)
53
- FROST::SecretShare.new(identifier, field.mod(step2_results.sum), group)
52
+ field = ECDSA::PrimeField.new(context.group.order)
53
+ FROST::SecretShare.new(context, identifier, field.mod(step2_results.sum))
54
54
  end
55
55
  end
56
56
  end
@@ -3,19 +3,26 @@ module FROST
3
3
  class SecretShare
4
4
  attr_reader :identifier
5
5
  attr_reader :share
6
- attr_reader :group
6
+ attr_reader :context
7
7
 
8
8
  # Generate secret share.
9
+ # @param [FROST::Context] context
9
10
  # @param [Integer] identifier Identifier of this share.
10
11
  # @param [Integer] share A share.
11
- def initialize(identifier, share, group)
12
+ def initialize(context, identifier, share)
12
13
  raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
13
14
  raise ArgumentError, "share must be Integer." unless share.is_a?(Integer)
14
- raise ArgumentError, "group must be ECDSA::Group" unless group.is_a?(ECDSA::Group)
15
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
15
16
 
16
17
  @identifier = identifier
17
18
  @share = share
18
- @group = group
19
+ @context = context
20
+ end
21
+
22
+ # Get group
23
+ # @return [ECDSA::Group]
24
+ def group
25
+ context.group
19
26
  end
20
27
 
21
28
  # Compute public key.
@@ -27,7 +34,7 @@ module FROST
27
34
  # Generate signing share key.
28
35
  # @return [FROST::SigningKey]
29
36
  def to_key
30
- FROST::SigningKey.new(share, group)
37
+ FROST::SigningKey.new(context, share)
31
38
  end
32
39
  end
33
40
  end
@@ -3,16 +3,20 @@ module FROST
3
3
  class Signature
4
4
  attr_reader :r
5
5
  attr_reader :s
6
+ attr_reader :context
6
7
 
7
8
  # Constructor
9
+ # @param [FROST::Context] context
8
10
  # @param [ECDSA::Point] r Public nonce of signature.
9
11
  # @param [Integer] s Scalar value of signature.
10
- def initialize(r, s)
12
+ def initialize(context, r, s)
11
13
  raise ArgumentError, "r must be ECDSA::Point" unless r.is_a?(ECDSA::Point)
12
14
  raise ArgumentError, "s must be Integer" unless s.is_a?(Integer)
15
+ raise ArgumentError, "context must be FROST::Context" unless context.is_a?(FROST::Context)
13
16
 
14
17
  @r = r
15
18
  @s = s
19
+ @context = context
16
20
  end
17
21
 
18
22
  # Encode signature to hex string.
@@ -24,19 +28,31 @@ module FROST
24
28
  # Encode signature to byte string.
25
29
  # @return [String]
26
30
  def encode
27
- ECDSA::Format::PointOctetString.encode(r, compression: true) +
28
- ECDSA::Format::IntegerOctetString.encode(s, 32)
31
+ if context.taproot?
32
+ ECDSA::Format::IntegerOctetString.encode(r.x, context.group.byte_length) +
33
+ ECDSA::Format::IntegerOctetString.encode(s, context.group.byte_length)
34
+ else
35
+ ECDSA::Format::PointOctetString.encode(r, compression: true) +
36
+ ECDSA::Format::IntegerOctetString.encode(s, context.group.byte_length)
37
+ end
29
38
  end
30
39
 
31
40
  # Decode hex value to FROST::Signature.
41
+ # @param [FROST::Context] context
32
42
  # @param [String] hex_value Hex value of signature.
33
- # @param [ECDSA::Group] group Group of elliptic curve.
34
43
  # @return [FROST::Signature]
35
- def self.decode(hex_value, group)
44
+ # @raise [ArgumentError]
45
+ def self.decode(context, hex_value)
46
+ raise ArgumentError, "context must be FROST::Context" unless context.is_a?(FROST::Context)
47
+ raise ArgumentError, "hex value must be String" unless hex_value.is_a?(String)
48
+
36
49
  data = [hex_value].pack("H*")
37
- r = ECDSA::Format::PointOctetString.decode(data[0...33], group)
38
- s = ECDSA::Format::IntegerOctetString.decode(data[33..-1])
39
- Signature.new(r,s )
50
+ data = [0x02].pack('C') + data if context.taproot?
51
+ len = context.group.byte_length + 1
52
+
53
+ r = ECDSA::Format::PointOctetString.decode(data[0...len], context.group)
54
+ s = ECDSA::Format::IntegerOctetString.decode(data[len..-1])
55
+ Signature.new(context, r, s)
40
56
  end
41
57
  end
42
58
  end
@@ -2,32 +2,44 @@ module FROST
2
2
  # A signing key for a Schnorr signature on a FROST.
3
3
  class SigningKey
4
4
  attr_reader :scalar
5
- attr_reader :group
5
+ attr_reader :context
6
6
 
7
7
  # Constructor
8
+ # @param [FROST::Context] context Frost context.
8
9
  # @param [Integer] scalar secret key value.
9
- # @param [ECDSA::Group] group Group of elliptic curve.
10
- def initialize(scalar, group = ECDSA::Group::Secp256k1)
10
+ def initialize(context, scalar)
11
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
11
12
  raise ArgumentError, "scalar must be integer." unless scalar.is_a?(Integer)
12
- raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
13
- raise ArgumentError, "Invalid scalar range." if scalar < 1 || group.order - 1 < scalar
13
+ raise ArgumentError, "Invalid scalar range." if scalar < 1 || context.group.order - 1 < scalar
14
14
 
15
15
  @scalar = scalar
16
- @group = group
16
+ @context = context
17
+ end
18
+
19
+ # Get group
20
+ # @return [ECDSA::Group]
21
+ def group
22
+ context.group
17
23
  end
18
24
 
19
25
  # Generate signing key.
20
- # @param [ECDSA::Group] group Group of elliptic curve.
21
- def self.generate(group)
22
- scalar = 1 + SecureRandom.random_number(group.order - 1)
23
- SigningKey.new(scalar, group)
26
+ # @param [FROST::Context] context
27
+ def self.generate(context)
28
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
29
+ scalar = 1 + SecureRandom.random_number(context.group.order - 1)
30
+ key = SigningKey.new(context, scalar)
31
+ if context.taproot? && !key.to_point.y.even?
32
+ self.generate(context)
33
+ else
34
+ key
35
+ end
24
36
  end
25
37
 
26
38
  # Generate random polynomial using this secret.
27
39
  # @param [Integer] degree Degree of polynomial.
28
40
  # @return [FROST::Polynomial] A polynomial
29
41
  def gen_poly(degree)
30
- Polynomial.from_secret(scalar, degree, group)
42
+ Polynomial.from_secret(context, scalar, degree)
31
43
  end
32
44
 
33
45
  # Compute public key.
data/lib/frost/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FROST
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/frost.rb CHANGED
@@ -9,6 +9,7 @@ require 'h2c'
9
9
  module FROST
10
10
  class Error < StandardError; end
11
11
 
12
+ autoload :Context, 'frost/context'
12
13
  autoload :Signature, "frost/signature"
13
14
  autoload :Commitments, "frost/commitments"
14
15
  autoload :Hash, "frost/hash"
@@ -19,6 +20,27 @@ module FROST
19
20
  autoload :DKG, "frost/dkg"
20
21
  autoload :Repairable, "frost/repairable"
21
22
 
23
+ module Type
24
+ RFC9591 = :rfc9591
25
+ TAPROOT = :taproot
26
+
27
+ module_function
28
+
29
+ # Check whether valid type or not.
30
+ # @param [Symbol] type
31
+ # @return [Boolean]
32
+ def supported?(type)
33
+ [RFC9591, TAPROOT].include?(type)
34
+ end
35
+
36
+ # Validate type
37
+ # @param [Symbol] type
38
+ # @raise [ArgumentError]
39
+ def validate!(type)
40
+ raise ArgumentError, "Unsupported type: #{type}." unless supported?(type)
41
+ end
42
+ end
43
+
22
44
  module_function
23
45
 
24
46
  # Encode identifier
@@ -36,23 +58,26 @@ module FROST
36
58
 
37
59
  # Compute binding factors.
38
60
  # https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-15.html#name-binding-factors-computation
61
+ # @param [FROST::Context] context
39
62
  # @param [ECDSA::Point] group_pubkey
40
63
  # @param [Array] commitment_list The list of commitments issued by each participants.
41
64
  # This list must be sorted in ascending order by identifier.
42
65
  # @param [String] msg The message to be signed.
43
66
  # @return [Hash] The hash of binding factor.
44
- def compute_binding_factors(group_pubkey, commitment_list, msg)
67
+ def compute_binding_factors(context, group_pubkey, commitment_list, msg)
68
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
45
69
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
46
70
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
71
+ raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
47
72
 
48
- msg_hash = Hash.h4(msg, group_pubkey.group)
73
+ msg_hash = Hash.h4(msg, context)
49
74
  encoded_commitment = Commitments.encode_group_commitment(commitment_list)
50
- encoded_commitment_hash = Hash.h5(encoded_commitment, group_pubkey.group)
75
+ encoded_commitment_hash = Hash.h5(encoded_commitment, context)
51
76
  rho_input_prefix = [group_pubkey.to_hex].pack("H*") + msg_hash + encoded_commitment_hash
52
77
  binding_factors = {}
53
78
  commitment_list.each do |commitments|
54
- preimage = rho_input_prefix + encode_identifier(commitments.identifier, group_pubkey.group)
55
- binding_factors[commitments.identifier] = Hash.h1(preimage, group_pubkey.group)
79
+ preimage = rho_input_prefix + encode_identifier(commitments.identifier, context.group)
80
+ binding_factors[commitments.identifier] = Hash.h1(preimage, context)
56
81
  end
57
82
  binding_factors
58
83
  end
@@ -70,29 +95,39 @@ module FROST
70
95
  end
71
96
 
72
97
  # Create the per-message challenge.
98
+ # If context type is BIP-340(taproot), only the X coordinate of R and group_pubkey are hashed, unlike vanilla FROST.
73
99
  # @param [ECDSA::Point] group_commitment The group commitment.
74
100
  # @param [ECDSA::Point] group_pubkey The public key corresponding to the group signing key.
75
101
  # @param [String] msg The message to be signed.
76
- def compute_challenge(group_commitment, group_pubkey, msg)
102
+ # @return [Integer] challenge
103
+ def compute_challenge(context, group_commitment, group_pubkey, msg)
104
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
77
105
  raise ArgumentError, "group_commitment must be ECDSA::Point." unless group_commitment.is_a?(ECDSA::Point)
78
106
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
79
107
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
108
+ raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
80
109
 
81
- input = [group_commitment.to_hex + group_pubkey.to_hex].pack("H*") + msg
82
- Hash.h2(input, group_commitment.group)
110
+ preimage = context.normalize(group_commitment) + context.normalize(group_pubkey) + msg
111
+ Hash.h2(preimage, context)
83
112
  end
84
113
 
85
114
  # Generate signature share.
115
+ # @param [FROST::Context] context FROST context.
86
116
  # @param [FROST::SecretShare] secret_share Signer secret key share.
87
117
  # @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
88
118
  # @param [Array] nonces Pair of nonce values (hiding_nonce, binding_nonce) for signer_i.
89
119
  # @param [String] msg The message to be signed
90
120
  # @param [Array] commitment_list A list of commitments issued by each participant.
91
121
  # @return [Integer] A signature share.
92
- def sign(secret_share, group_pubkey, nonces, msg, commitment_list)
122
+ def sign(context, secret_share, group_pubkey, nonces, msg, commitment_list)
123
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
124
+ raise ArgumentError, "msg must be String." unless msg.is_a?(String)
125
+ raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
126
+ raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
127
+
93
128
  identifier = secret_share.identifier
94
129
  # Compute binding factors
95
- binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
130
+ binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
96
131
  binding_factor = binding_factors[identifier]
97
132
 
98
133
  # Compute group commitment
@@ -100,42 +135,46 @@ module FROST
100
135
 
101
136
  # Compute Lagrange coefficient
102
137
  identifiers = commitment_list.map(&:identifier)
103
- lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, group_pubkey.group)
138
+ lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, context.group)
104
139
 
105
140
  # Compute the per-message challenge
106
- challenge = compute_challenge(group_commitment, group_pubkey, msg)
141
+ challenge = compute_challenge(context, group_commitment, group_pubkey, msg)
107
142
 
108
143
  # Compute the signature share
109
- hiding_nonce, binding_nonce = nonces
144
+ hiding_nonce, binding_nonce = context.convert_signer_nocnes(group_commitment, nonces)
110
145
  field = ECDSA::PrimeField.new(group_pubkey.group.order)
111
146
  field.mod(hiding_nonce.value +
112
147
  field.mod(binding_nonce.value * binding_factor) + field.mod(lambda_i * secret_share.share * challenge))
113
148
  end
114
149
 
115
150
  # Aggregates the signature shares to produce a final signature that can be verified with the group public key.
151
+ # @param [FROST::Context] context FROST context.
116
152
  # @param [Array] commitment_list A list of commitments issued by each participant.
117
153
  # @param [String] msg The message to be signed.
118
154
  # @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
119
155
  # @param [Array] sig_shares A set of signature shares z_i, integer values.
120
156
  # @return [FROST::Signature] Schnorr signature.
121
- def aggregate(commitment_list, msg, group_pubkey, sig_shares)
157
+ def aggregate(context, commitment_list, msg, group_pubkey, sig_shares)
158
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
122
159
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
123
160
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
161
+ raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
124
162
  raise ArgumentError, "The numbers of commitment_list and sig_shares do not match." unless commitment_list.length == sig_shares.length
125
163
 
126
- binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
164
+ binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
127
165
  group_commitment = compute_group_commitment(commitment_list, binding_factors)
128
166
 
129
- field = ECDSA::PrimeField.new(group_pubkey.group.order)
167
+ field = ECDSA::PrimeField.new(context.group.order)
130
168
  s = sig_shares.inject(0) do |sum, z_i|
131
169
  raise ArgumentError, "sig_shares must be array of integer" unless z_i.is_a?(Integer)
132
170
  field.mod(sum + z_i)
133
171
  end
134
172
 
135
- Signature.new(group_commitment, field.mod(s))
173
+ Signature.new(context, group_commitment, field.mod(s))
136
174
  end
137
175
 
138
176
  # Verify signature share.
177
+ # @param [FROST::Context] context FROST context.
139
178
  # @param [Integer] identifier Identifier i of the participant.
140
179
  # @param [ECDSA::Point] pubkey_i The public key for the i-th participant
141
180
  # @param [Integer] sig_share_i Integer value indicating the signature share as produced
@@ -144,13 +183,15 @@ module FROST
144
183
  # @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
145
184
  # @param [String] msg The message to be signed.
146
185
  # @return [Boolean] Verification result.
147
- def verify_share(identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg)
186
+ def verify_share(context, identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg)
187
+ raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
148
188
  raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
149
189
  raise ArgumentError, "sig_share_i must be Integer." unless sig_share_i.is_a?(Integer)
150
190
  raise ArgumentError, "pubkey_i must be ECDSA::Point." unless pubkey_i.is_a?(ECDSA::Point)
151
191
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
192
+ raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
152
193
 
153
- binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
194
+ binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
154
195
  binding_factor = binding_factors[identifier]
155
196
  group_commitment = compute_group_commitment(commitment_list, binding_factors)
156
197
  comm_i = commitment_list.find{|c| c.identifier == identifier}
@@ -159,11 +200,11 @@ module FROST
159
200
  raise ArgumentError, "hiding_commitment must be ECDSA::Point." unless hiding_commitment.is_a?(ECDSA::Point)
160
201
  raise ArgumentError, "binding_commitment must be ECDSA::Point." unless binding_commitment.is_a?(ECDSA::Point)
161
202
 
162
- comm_share = hiding_commitment + binding_commitment * binding_factor
163
- challenge = compute_challenge(group_commitment, group_pubkey, msg)
203
+ comm_share = context.convert_commitment_share(group_commitment, hiding_commitment + binding_commitment * binding_factor)
204
+ challenge = compute_challenge(context, group_commitment, group_pubkey, msg)
164
205
  identifiers = commitment_list.map(&:identifier)
165
- lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, group_pubkey.group)
166
- l = group_pubkey.group.generator * sig_share_i
206
+ lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, context.group)
207
+ l = context.group.generator * sig_share_i
167
208
  r = comm_share + pubkey_i * (challenge * lambda_i)
168
209
  l == r
169
210
  end
@@ -176,10 +217,14 @@ module FROST
176
217
  def verify(signature, public_key, msg)
177
218
  raise ArgumentError, "signature must be FROST::Signature" unless signature.is_a?(FROST::Signature)
178
219
  raise ArgumentError, "public_key must be ECDSA::Point" unless public_key.is_a?(ECDSA::Point)
220
+ raise ArgumentError, "public_key and context groups are different." unless signature.context.group == public_key.group
179
221
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
180
222
 
223
+ context = signature.context
224
+ public_key, signature = context.pre_verify(public_key, signature)
225
+
181
226
  # Compute challenge
182
- challenge = compute_challenge(signature.r, public_key, msg)
227
+ challenge = compute_challenge(context, signature.r, public_key, msg)
183
228
 
184
229
  s_g = public_key.group.generator * signature.s
185
230
  c_p = public_key * challenge
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frostrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-02-19 00:00:00.000000000 Z
10
+ date: 2025-05-15 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ecdsa_ext
@@ -59,8 +58,10 @@ files:
59
58
  - frost.gemspec
60
59
  - lib/frost.rb
61
60
  - lib/frost/commitments.rb
61
+ - lib/frost/context.rb
62
62
  - lib/frost/dkg.rb
63
- - lib/frost/dkg/package.rb
63
+ - lib/frost/dkg/public_package.rb
64
+ - lib/frost/dkg/secret_package.rb
64
65
  - lib/frost/hash.rb
65
66
  - lib/frost/nonce.rb
66
67
  - lib/frost/polynomial.rb
@@ -77,7 +78,6 @@ metadata:
77
78
  homepage_uri: https://github.com/azuchi/frostrb
78
79
  source_code_uri: https://github.com/azuchi/frostrb
79
80
  changelog_uri: https://github.com/azuchi/frostrb
80
- post_install_message:
81
81
  rdoc_options: []
82
82
  require_paths:
83
83
  - lib
@@ -92,8 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
94
  requirements: []
95
- rubygems_version: 3.5.3
96
- signing_key:
95
+ rubygems_version: 3.6.2
97
96
  specification_version: 4
98
97
  summary: Ruby implementations of Two-Round Threshold Schnorr Signatures with FROST.
99
98
  test_files: []