frostrb 0.3.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 +4 -4
- data/.ruby-version +1 -1
- data/README.md +35 -17
- data/lib/frost/context.rb +87 -0
- data/lib/frost/dkg/{package.rb → public_package.rb} +1 -1
- data/lib/frost/dkg/secret_package.rb +7 -1
- data/lib/frost/dkg.rb +53 -18
- data/lib/frost/hash.rb +37 -40
- data/lib/frost/nonce.rb +21 -7
- data/lib/frost/polynomial.rb +23 -11
- data/lib/frost/repairable.rb +9 -9
- data/lib/frost/secret_share.rb +12 -5
- data/lib/frost/signature.rb +24 -8
- data/lib/frost/signing_key.rb +23 -11
- data/lib/frost/version.rb +1 -1
- data/lib/frost.rb +69 -24
- metadata +5 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ec10bec048f4bbf00419a36f27e901afb7ca0b12ae20140d827e1e2eaba107e
|
4
|
+
data.tar.gz: 13bce5e0d48b971619c698224e0051162b57b0e08dcc9dabad7eef8a770ecda5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21d11540bbb90c3f3e0e4e96b17204140b40ee52b783da59d12de0898e64d11ed7a4feefc937fd08dff87c001592c14eed39f4d06757211198581047045aa98f
|
7
|
+
data.tar.gz: ca60c7fd06432c8c8d6b72fd2a5fb77b42e6184f892af8496c1d540d4215c36ca074fef9c5b495ea8bbcd1e675649a166d41a07b8ab64257f520490ae4ffd869
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-3.
|
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
|
-
|
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(
|
41
|
+
secret = FROST::SigningKey.generate(ctx)
|
37
42
|
group_pubkey = secret.to_point
|
38
43
|
|
39
44
|
# Generate polynomial(f(x) = ax + b)
|
@@ -58,25 +63,36 @@ 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
|
|
@@ -85,15 +101,15 @@ 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
|
-
secret_package = FROST::DKG.generate_secret(i, min_signer, max_signer
|
104
|
+
secret_package = FROST::DKG.generate_secret(ctx, i, min_signer, max_signer)
|
89
105
|
secret_packages[i] = secret_package
|
90
106
|
round1_outputs[i] = secret_package.public_package
|
91
107
|
end
|
92
108
|
|
93
|
-
# Each participant
|
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 {
|
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.
|
@@ -119,16 +135,16 @@ end
|
|
119
135
|
# Each participant verify received shares.
|
120
136
|
1.upto(max_signer) do |i|
|
121
137
|
received_shares[i].each do |send_by, share|
|
122
|
-
target_package = received_package[i].find
|
138
|
+
target_package = received_package[i].find{ |package| package.identifier == send_by }
|
123
139
|
expect(target_package.verify_share(share)).to be true
|
124
140
|
end
|
125
141
|
end
|
126
142
|
|
127
|
-
# Each participant
|
143
|
+
# Each participant computes signing share.
|
128
144
|
signing_shares = {}
|
129
145
|
1.upto(max_signer) do |i|
|
130
|
-
shares = received_shares[i].map
|
131
|
-
signing_shares[i] = FROST::DKG.compute_signing_share(secret_packages[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)
|
132
148
|
end
|
133
149
|
|
134
150
|
# Participant 1 compute group public key.
|
@@ -142,8 +158,10 @@ group_pubkey = FROST::DKG.compute_group_pubkey(secret_packages[1], received_pack
|
|
142
158
|
Using `FROST::Repairable` module, you can repair existing (or new) participant's share with the cooperation of T participants.
|
143
159
|
|
144
160
|
```ruby
|
161
|
+
ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::RFC9591)
|
162
|
+
dealer = FROST::SigningKey.generate(ctx)
|
163
|
+
|
145
164
|
# Dealer generate shares.
|
146
|
-
FROST::SigningKey.generate(ECDSA::Group::Secp256k1)
|
147
165
|
polynomial = dealer.gen_poly(min_signers - 1)
|
148
166
|
shares = 1.upto(max_signers).map {|identifier| polynomial.gen_share(identifier) }
|
149
167
|
|
@@ -169,9 +187,9 @@ end
|
|
169
187
|
# Each helper send sum value to participant.
|
170
188
|
participant_received_values = []
|
171
189
|
received_values.each do |_, values|
|
172
|
-
participant_received_values << FROST::Repairable.step2(
|
190
|
+
participant_received_values << FROST::Repairable.step2(ctx, values)
|
173
191
|
end
|
174
192
|
|
175
|
-
# Participant can
|
176
|
-
repair_share = FROST::Repairable.step3(2, participant_received_values
|
193
|
+
# Participant can get his share.
|
194
|
+
repair_share = FROST::Repairable.step3(ctx, 2, participant_received_values)
|
177
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
|
@@ -27,7 +27,7 @@ module FROST
|
|
27
27
|
@min_signers = min_signers
|
28
28
|
@max_signers = max_signers
|
29
29
|
@polynomial = polynomial
|
30
|
-
@public_package =
|
30
|
+
@public_package = PublicPackage.new(identifier, polynomial.gen_commitments, polynomial.gen_proof_of_knowledge(identifier))
|
31
31
|
end
|
32
32
|
|
33
33
|
# Generate secret share for identifier.
|
@@ -43,6 +43,12 @@ module FROST
|
|
43
43
|
polynomial.group
|
44
44
|
end
|
45
45
|
|
46
|
+
# Get FROST context.
|
47
|
+
# @return [FROST::Context]
|
48
|
+
def context
|
49
|
+
polynomial.context
|
50
|
+
end
|
51
|
+
|
46
52
|
# Get verification point.
|
47
53
|
# @return [ECDSA::Point]
|
48
54
|
def verification_point
|
data/lib/frost/dkg.rb
CHANGED
@@ -3,24 +3,25 @@ module FROST
|
|
3
3
|
module DKG
|
4
4
|
|
5
5
|
autoload :SecretPackage, "frost/dkg/secret_package"
|
6
|
-
autoload :
|
6
|
+
autoload :PublicPackage, "frost/dkg/public_package"
|
7
7
|
|
8
8
|
module_function
|
9
9
|
|
10
10
|
# Performs the first part of the DKG.
|
11
11
|
# Participant generate key and commitments, proof of knowledge for secret.
|
12
|
-
# @param [
|
13
|
-
# @param [
|
12
|
+
# @param [FROST::Context] context
|
13
|
+
# @param [Integer] identifier Party's identifier.
|
14
|
+
# @param [Integer] min_signers The number of min signers.
|
14
15
|
# @return [FROST::DKG::SecretPackage] Secret received_package for owner.
|
15
|
-
def generate_secret(identifier, min_signers, max_signers
|
16
|
+
def generate_secret(context, identifier, min_signers, max_signers)
|
16
17
|
raise ArgumentError, "identifier must be Integer" unless identifier.is_a?(Integer)
|
17
18
|
raise ArgumentError, "identifier must be greater than 0." if identifier < 1
|
18
|
-
raise ArgumentError, "
|
19
|
+
raise ArgumentError, "context must be FROST::Context." unless context.is_a?(FROST::Context)
|
19
20
|
raise ArgumentError, "min_signers must be Integer." unless min_signers.is_a?(Integer)
|
20
21
|
raise ArgumentError, "max_singers must be Integer." unless max_signers.is_a?(Integer)
|
21
22
|
raise ArgumentError, "max_signers must be greater than or equal to min_signers." if max_signers < min_signers
|
22
23
|
|
23
|
-
secret = FROST::SigningKey.generate(
|
24
|
+
secret = FROST::SigningKey.generate(context)
|
24
25
|
polynomial = secret.gen_poly(min_signers - 1)
|
25
26
|
SecretPackage.new(identifier, min_signers, max_signers, polynomial)
|
26
27
|
end
|
@@ -38,52 +39,86 @@ module FROST
|
|
38
39
|
a0 = polynomial.coefficients.first
|
39
40
|
a0_g = polynomial.group.generator * a0
|
40
41
|
msg = FROST.encode_identifier(identifier, polynomial.group) + [a0_g.to_hex + r.to_hex].pack("H*")
|
41
|
-
challenge = Hash.hdkg(msg, polynomial.
|
42
|
+
challenge = Hash.hdkg(msg, polynomial.context)
|
42
43
|
field = ECDSA::PrimeField.new(polynomial.group.order)
|
43
44
|
s = field.mod(k + a0 * challenge)
|
44
|
-
FROST::Signature.new(r, s)
|
45
|
+
FROST::Signature.new(polynomial.context, r, s)
|
45
46
|
end
|
46
47
|
|
47
48
|
# Verify proof of knowledge for received commitment.
|
48
49
|
# @param [FROST::DKG::SecretPackage] secret_package Verifier's secret package.
|
49
|
-
# @param [FROST::DKG::
|
50
|
+
# @param [FROST::DKG::PublicPackage] received_package Received received_package.
|
50
51
|
# @return [Boolean]
|
51
52
|
def verify_proof_of_knowledge(secret_package, received_package)
|
52
53
|
raise ArgumentError, "secret_package must be FROST::DKG::SecretPackage." unless secret_package.is_a?(FROST::DKG::SecretPackage)
|
53
|
-
raise ArgumentError, "received_package must be FROST::DKG::Package." unless received_package.is_a?(FROST::DKG::
|
54
|
+
raise ArgumentError, "received_package must be FROST::DKG::Package." unless received_package.is_a?(FROST::DKG::PublicPackage)
|
54
55
|
raise FROST::Error, "Invalid number of commitments in package." unless secret_package.min_signers == received_package.commitments.length
|
55
56
|
|
56
57
|
verification_key = received_package.verification_key
|
57
58
|
msg = FROST.encode_identifier(received_package.identifier, verification_key.group) +
|
58
59
|
[verification_key.to_hex + received_package.proof.r.to_hex].pack("H*")
|
59
|
-
challenge = Hash.hdkg(msg,
|
60
|
+
challenge = Hash.hdkg(msg, secret_package.polynomial.context)
|
60
61
|
received_package.proof.r == verification_key.group.generator * received_package.proof.s + (verification_key * challenge).negate
|
61
62
|
end
|
62
63
|
|
63
|
-
# Compute signing share using received shares from other participants
|
64
|
+
# Compute signing share using received shares from other participants.
|
65
|
+
#
|
64
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.
|
65
68
|
# @param [Array] received_shares Array of FROST::SecretShare received by other participants.
|
66
69
|
# @return [FROST::SecretShare] Signing share.
|
67
|
-
|
70
|
+
# @raise [ArgumentError]
|
71
|
+
def compute_signing_share(secret_package, received_packages, received_shares)
|
68
72
|
raise ArgumentError, "polynomial must be FROST::DKG::SecretPackage." unless secret_package.is_a?(FROST::DKG::SecretPackage)
|
69
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
|
70
75
|
|
71
76
|
identifier = received_shares.first.identifier
|
72
|
-
|
77
|
+
signing_share = received_shares.sum {|share| share.share}
|
73
78
|
field = ECDSA::PrimeField.new(secret_package.group.order)
|
74
|
-
|
75
|
-
|
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)
|
76
97
|
end
|
77
98
|
|
78
99
|
# Compute Group public key.
|
79
100
|
# @param [FROST::DKG::SecretPackage] secret_package Own secret received_package.
|
80
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.
|
81
103
|
# @return [ECDSA::Point] Group public key.
|
82
|
-
def compute_group_pubkey(secret_package, received_packages)
|
104
|
+
def compute_group_pubkey(secret_package, received_packages, apply_even: true)
|
83
105
|
raise ArgumentError, "polynomial must be FROST::DKG::SecretPackage." unless secret_package.is_a?(FROST::DKG::SecretPackage)
|
84
106
|
raise FROST::Error, "Invalid number of received_packages." unless secret_package.max_signers - 1 == received_packages.length
|
85
107
|
|
86
|
-
received_packages.inject(secret_package.verification_point) {|sum, package| sum + package.commitments.first }
|
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)
|
87
120
|
end
|
121
|
+
|
122
|
+
private_class_method :tap_tweak
|
88
123
|
end
|
89
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 [
|
10
|
+
# @param [FROST::Context] context FROST context.
|
14
11
|
# @return [Integer]
|
15
|
-
def h1(msg,
|
16
|
-
hash_to_field(msg,
|
12
|
+
def h1(msg, context)
|
13
|
+
hash_to_field(msg, context, "rho")
|
17
14
|
end
|
18
15
|
|
19
|
-
#
|
16
|
+
# H2 hash function.
|
20
17
|
# @param [String] msg The message to be hashed.
|
21
|
-
# @param [
|
18
|
+
# @param [FROST::Context] context FROST context.
|
22
19
|
# @return [Integer]
|
23
|
-
def h2(msg,
|
24
|
-
|
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 [
|
30
|
+
# @param [FROST::Context] context FROST context.
|
30
31
|
# @return [Integer]
|
31
|
-
def h3(msg,
|
32
|
-
hash_to_field(msg,
|
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 [
|
38
|
+
# @param [FROST::Context] context FROST context.
|
38
39
|
# @return [String] The hash value.
|
39
|
-
def h4(msg,
|
40
|
-
hash(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 [
|
46
|
+
# @param [FROST::Context] context FROST context.
|
46
47
|
# @return [String] The hash value.
|
47
|
-
def h5(msg,
|
48
|
-
hash(msg,
|
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 [
|
54
|
+
# @param [FROST::Context] context FROST context.
|
54
55
|
# @return [Integer] The hash value.
|
55
|
-
def hdkg(msg,
|
56
|
-
hash_to_field(msg,
|
56
|
+
def hdkg(msg, context)
|
57
|
+
hash_to_field(msg, context, "dkg")
|
57
58
|
end
|
58
59
|
|
59
|
-
def hash_to_field(msg,
|
60
|
-
|
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_,
|
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_,
|
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,
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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 :
|
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(
|
10
|
-
raise ArgumentError, "
|
11
|
-
raise ArgumentError, "nonce must
|
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
|
-
@
|
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
|
-
|
36
|
-
Nonce.new(
|
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
|
data/lib/frost/polynomial.rb
CHANGED
@@ -3,32 +3,44 @@ module FROST
|
|
3
3
|
# Polynomial class.
|
4
4
|
class Polynomial
|
5
5
|
attr_reader :coefficients
|
6
|
-
attr_reader :
|
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
|
-
# @
|
12
|
-
|
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
|
-
@
|
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
|
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)
|
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
|
41
|
-
return SecretShare.new(identifier, coefficients.last
|
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
|
61
|
+
SecretShare.new(context, identifier, last)
|
50
62
|
end
|
51
63
|
|
52
64
|
# Generate coefficient commitments
|
data/lib/frost/repairable.rb
CHANGED
@@ -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(
|
38
|
-
raise ArgumentError, "
|
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
|
50
|
-
raise ArgumentError, "
|
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)
|
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
|
data/lib/frost/secret_share.rb
CHANGED
@@ -3,19 +3,26 @@ module FROST
|
|
3
3
|
class SecretShare
|
4
4
|
attr_reader :identifier
|
5
5
|
attr_reader :share
|
6
|
-
attr_reader :
|
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
|
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
|
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
|
-
@
|
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(
|
37
|
+
FROST::SigningKey.new(context, share)
|
31
38
|
end
|
32
39
|
end
|
33
40
|
end
|
data/lib/frost/signature.rb
CHANGED
@@ -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
|
-
|
28
|
-
ECDSA::Format::IntegerOctetString.encode(
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
data/lib/frost/signing_key.rb
CHANGED
@@ -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 :
|
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
|
-
|
10
|
-
|
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, "
|
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
|
-
@
|
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 [
|
21
|
-
def self.generate(
|
22
|
-
|
23
|
-
|
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
|
42
|
+
Polynomial.from_secret(context, scalar, degree)
|
31
43
|
end
|
32
44
|
|
33
45
|
# Compute public key.
|
data/lib/frost/version.rb
CHANGED
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,
|
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,
|
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,
|
55
|
-
binding_factors[commitments.identifier] = Hash.h1(preimage,
|
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
|
-
|
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
|
-
|
82
|
-
Hash.h2(
|
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,
|
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(
|
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,
|
166
|
-
l =
|
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.
|
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:
|
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,9 @@ 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/
|
63
|
+
- lib/frost/dkg/public_package.rb
|
64
64
|
- lib/frost/dkg/secret_package.rb
|
65
65
|
- lib/frost/hash.rb
|
66
66
|
- lib/frost/nonce.rb
|
@@ -78,7 +78,6 @@ metadata:
|
|
78
78
|
homepage_uri: https://github.com/azuchi/frostrb
|
79
79
|
source_code_uri: https://github.com/azuchi/frostrb
|
80
80
|
changelog_uri: https://github.com/azuchi/frostrb
|
81
|
-
post_install_message:
|
82
81
|
rdoc_options: []
|
83
82
|
require_paths:
|
84
83
|
- lib
|
@@ -93,8 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
92
|
- !ruby/object:Gem::Version
|
94
93
|
version: '0'
|
95
94
|
requirements: []
|
96
|
-
rubygems_version: 3.
|
97
|
-
signing_key:
|
95
|
+
rubygems_version: 3.6.2
|
98
96
|
specification_version: 4
|
99
97
|
summary: Ruby implementations of Two-Round Threshold Schnorr Signatures with FROST.
|
100
98
|
test_files: []
|