frostrb 0.3.0 → 0.5.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 +49 -30
- data/lib/frost/context.rb +87 -0
- data/lib/frost/dealer.rb +50 -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 +70 -24
- metadata +6 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 971ebd8ce7d6cf7faae3a152a2ac1ab89af43ba8317276b1a9595610c543b5b5
|
4
|
+
data.tar.gz: 03b261ac549b8745c35ef35b61a9b5c63534d4043e2b287a58d680c79e510318
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96d05d23ca3dec9b7d187a4a6ffdda6c78e193d7bf9f56699e0abbb85926d3763a61ceb7b05952fcd16ad748f15a7ad1712c03719ba0ac75ae11613d80cda895
|
7
|
+
data.tar.gz: 8f7a5926c9c755704f8cc13260956c08d4a2ba1f46ed8e20e8ec8786cfc7fe70fad27250d03ce83e64168b99a100f4eaec42c96025e02d1ed1d665be88d998d2
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-3.
|
1
|
+
ruby-3.4.1
|
data/README.md
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
# FROST for Ruby [](https://github.com/azuchi/frostrb/actions/workflows/main.yml)
|
2
2
|
|
3
|
-
This library is ruby
|
3
|
+
This library is a ruby implementation of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/).
|
4
4
|
|
5
5
|
Note: This library has not been security audited and tested widely, so should not be used in production.
|
6
6
|
|
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,22 +34,21 @@ 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
|
-
|
36
|
-
|
37
|
-
group_pubkey = secret.to_point
|
40
|
+
max_signers = 3
|
41
|
+
min_signers = 2
|
38
42
|
|
39
|
-
#
|
40
|
-
|
43
|
+
# Setup dealer.
|
44
|
+
dealer = FROST::Dealer.new(ctx, max_signers, min_signers)
|
45
|
+
group_pubkey = dealer.group_public_key
|
41
46
|
|
42
47
|
# Calculate secret shares.
|
43
|
-
share1 =
|
44
|
-
share2 = polynomial.gen_share(2)
|
45
|
-
share3 = polynomial.gen_share(3)
|
48
|
+
share1, _, share3 = dealer.gen_shares
|
46
49
|
|
47
50
|
# Round 1: Generate nonce and commitment
|
48
|
-
## each party
|
51
|
+
## each party generates hiding and binding nonce.
|
49
52
|
hiding_nonce1 = FROST::Nonce.gen_from_secret(share1)
|
50
53
|
binding_nonce1 = FROST::Nonce.gen_from_secret(share1)
|
51
54
|
hiding_nonce3 = FROST::Nonce.gen_from_secret(share3)
|
@@ -58,25 +61,36 @@ commitment_list = [comm1, comm3]
|
|
58
61
|
msg = ["74657374"].pack("H*")
|
59
62
|
|
60
63
|
# 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)
|
64
|
+
sig_share1 = FROST.sign(ctx, share1, group_pubkey, [hiding_nonce1, binding_nonce1], msg, commitment_list)
|
65
|
+
sig_share3 = FROST.sign(ctx, share3, group_pubkey, [hiding_nonce3, binding_nonce3], msg, commitment_list)
|
63
66
|
|
64
67
|
# verify signature share
|
65
68
|
FROST.verify_share(1, share1.to_point, sig_share1, commitment_list, group_pubkey, msg)
|
66
69
|
FROST.verify_share(3, share3.to_point, sig_share3, commitment_list, group_pubkey, msg)
|
67
70
|
|
68
71
|
# Aggregation
|
69
|
-
sig = FROST.aggregate(commitment_list, msg, group_pubkey, [sig_share1, sig_share3])
|
72
|
+
sig = FROST.aggregate(ctx, commitment_list, msg, group_pubkey, [sig_share1, sig_share3])
|
70
73
|
|
71
74
|
# verify final signature
|
72
75
|
FROST.verify(sig, group_pubkey, msg)
|
73
76
|
```
|
74
77
|
|
78
|
+
### Bitcoin support
|
79
|
+
|
80
|
+
When using Bitcoin(taproot), the context type must be `FROST::Type::TAPROOT` instead of `FROST::Type::RFC9591`.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::TAPROOT)
|
84
|
+
```
|
85
|
+
|
75
86
|
### Using DKG
|
76
87
|
|
77
88
|
DKG can be run as below.
|
78
89
|
|
79
90
|
```ruby
|
91
|
+
# Setup context.
|
92
|
+
ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::RFC9591)
|
93
|
+
|
80
94
|
max_signer = 5
|
81
95
|
min_signer = 3
|
82
96
|
|
@@ -85,7 +99,7 @@ round1_outputs = {}
|
|
85
99
|
# Round 1:
|
86
100
|
# For each participant, perform the first part of the DKG protocol.
|
87
101
|
1.upto(max_signer) do |i|
|
88
|
-
secret_package = FROST::DKG.generate_secret(i, min_signer, max_signer
|
102
|
+
secret_package = FROST::DKG.generate_secret(ctx, i, min_signer, max_signer)
|
89
103
|
secret_packages[i] = secret_package
|
90
104
|
round1_outputs[i] = secret_package.public_package
|
91
105
|
end
|
@@ -93,10 +107,10 @@ end
|
|
93
107
|
# Each participant sends their commitments and proof to other participants.
|
94
108
|
received_package = {}
|
95
109
|
1.upto(max_signer) do |i|
|
96
|
-
received_package[i] = round1_outputs.select {
|
110
|
+
received_package[i] = round1_outputs.select {|k, _| k != i}.values
|
97
111
|
end
|
98
112
|
|
99
|
-
# Each participant
|
113
|
+
# Each participant verifies knowledge of proof in a received package.
|
100
114
|
received_package.each do |id, packages|
|
101
115
|
secret_package = secret_packages[id]
|
102
116
|
packages.each do |package|
|
@@ -105,7 +119,7 @@ received_package.each do |id, packages|
|
|
105
119
|
end
|
106
120
|
|
107
121
|
# Round 2:
|
108
|
-
# Each participant
|
122
|
+
# Each participant generates a share for other participants and send it.
|
109
123
|
received_shares = {}
|
110
124
|
1.upto(max_signer) do |i|
|
111
125
|
secret_package = secret_packages[i] # own secret
|
@@ -119,16 +133,16 @@ end
|
|
119
133
|
# Each participant verify received shares.
|
120
134
|
1.upto(max_signer) do |i|
|
121
135
|
received_shares[i].each do |send_by, share|
|
122
|
-
target_package = received_package[i].find
|
136
|
+
target_package = received_package[i].find{ |package| package.identifier == send_by }
|
123
137
|
expect(target_package.verify_share(share)).to be true
|
124
138
|
end
|
125
139
|
end
|
126
140
|
|
127
|
-
# Each participant
|
141
|
+
# Each participant computes a signing share.
|
128
142
|
signing_shares = {}
|
129
143
|
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)
|
144
|
+
shares = received_shares[i].map{|_, share| share}
|
145
|
+
signing_shares[i] = FROST::DKG.compute_signing_share(secret_packages[i], received_package[i], shares)
|
132
146
|
end
|
133
147
|
|
134
148
|
# Participant 1 compute group public key.
|
@@ -142,10 +156,15 @@ group_pubkey = FROST::DKG.compute_group_pubkey(secret_packages[1], received_pack
|
|
142
156
|
Using `FROST::Repairable` module, you can repair existing (or new) participant's share with the cooperation of T participants.
|
143
157
|
|
144
158
|
```ruby
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
159
|
+
ctx = FROST::Context.new(ECDSA::Group::Secp256k1, FROST::Type::RFC9591)
|
160
|
+
max_signers = 5
|
161
|
+
min_signers = 3
|
162
|
+
|
163
|
+
# Setup daler
|
164
|
+
dealer = FROST::SigningKey.generate(ctx, max_signers, min_signers)
|
165
|
+
|
166
|
+
# Dealer generates shares.
|
167
|
+
shares = dealer.gen_shares
|
149
168
|
|
150
169
|
# Signer 2 will lose their share
|
151
170
|
# Signers (helpers) 1, 4 and 5 will help signer 2 (participant) to recover their share
|
@@ -169,9 +188,9 @@ end
|
|
169
188
|
# Each helper send sum value to participant.
|
170
189
|
participant_received_values = []
|
171
190
|
received_values.each do |_, values|
|
172
|
-
participant_received_values << FROST::Repairable.step2(
|
191
|
+
participant_received_values << FROST::Repairable.step2(ctx, values)
|
173
192
|
end
|
174
193
|
|
175
|
-
# Participant can
|
176
|
-
repair_share = FROST::Repairable.step3(2, participant_received_values
|
194
|
+
# Participant can get his share.
|
195
|
+
repair_share = FROST::Repairable.step3(ctx, 2, participant_received_values)
|
177
196
|
```
|
@@ -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
|
data/lib/frost/dealer.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module FROST
|
2
|
+
# Dealer
|
3
|
+
class Dealer
|
4
|
+
attr_reader :ctx
|
5
|
+
attr_reader :max_signers
|
6
|
+
attr_reader :min_signers
|
7
|
+
attr_reader :polynomial
|
8
|
+
|
9
|
+
# Create a new dealer.
|
10
|
+
# @param [FROST::Context] ctx FROST context.
|
11
|
+
# @param [Integer] max_signers Maximum number of signers.
|
12
|
+
# @return [FROST::Dealer]
|
13
|
+
# @raise [ArgumentError]
|
14
|
+
def initialize(ctx, max_signers, min_signers)
|
15
|
+
raise ArgumentError, "context must be FROST::Context." unless ctx.is_a?(FROST::Context)
|
16
|
+
raise ArgumentError, "min_signers must be Integer." unless min_signers.is_a?(Integer)
|
17
|
+
raise ArgumentError, "min_signers must be greater than 1." if min_signers < 2
|
18
|
+
raise ArgumentError, "max_signers must be Integer." unless max_signers.is_a?(Integer)
|
19
|
+
raise ArgumentError, "max_signers must be greater than or equal to min_signers." if max_signers < min_signers
|
20
|
+
@ctx = ctx
|
21
|
+
@min_signers = min_signers
|
22
|
+
@max_signers = max_signers
|
23
|
+
key = SigningKey.generate(ctx)
|
24
|
+
@polynomial = key.gen_poly(min_signers - 1)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Generate shares.
|
28
|
+
# @return [Array] Array of shares(FROST::SecretShare).
|
29
|
+
# @raise [ArgumentError]
|
30
|
+
def gen_shares(identifiers = nil)
|
31
|
+
raise ArgumentError, "identifiers must be Array." if identifiers && !identifiers.is_a?(Array)
|
32
|
+
identifiers = if identifiers
|
33
|
+
identifiers.each do |id|
|
34
|
+
raise ArgumentError, "identifier must be Integer." unless id.is_a?(Integer)
|
35
|
+
raise ArgumentError, "identifier must be greater than 0." if id < 1
|
36
|
+
end
|
37
|
+
identifiers
|
38
|
+
else
|
39
|
+
(1..max_signers).to_a
|
40
|
+
end
|
41
|
+
identifiers.map{ |i| polynomial.gen_share(i) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get a group public key.
|
45
|
+
# @return [ECDSA::Point]
|
46
|
+
def group_public_key
|
47
|
+
polynomial.verification_point
|
48
|
+
end
|
49
|
+
end
|
50
|
+
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"
|
@@ -16,9 +17,31 @@ module FROST
|
|
16
17
|
autoload :SecretShare, "frost/secret_share"
|
17
18
|
autoload :Polynomial, "frost/polynomial"
|
18
19
|
autoload :SigningKey, "frost/signing_key"
|
20
|
+
autoload :Dealer, "frost/dealer"
|
19
21
|
autoload :DKG, "frost/dkg"
|
20
22
|
autoload :Repairable, "frost/repairable"
|
21
23
|
|
24
|
+
module Type
|
25
|
+
RFC9591 = :rfc9591
|
26
|
+
TAPROOT = :taproot
|
27
|
+
|
28
|
+
module_function
|
29
|
+
|
30
|
+
# Check whether valid type or not.
|
31
|
+
# @param [Symbol] type
|
32
|
+
# @return [Boolean]
|
33
|
+
def supported?(type)
|
34
|
+
[RFC9591, TAPROOT].include?(type)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Validate type
|
38
|
+
# @param [Symbol] type
|
39
|
+
# @raise [ArgumentError]
|
40
|
+
def validate!(type)
|
41
|
+
raise ArgumentError, "Unsupported type: #{type}." unless supported?(type)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
22
45
|
module_function
|
23
46
|
|
24
47
|
# Encode identifier
|
@@ -36,23 +59,26 @@ module FROST
|
|
36
59
|
|
37
60
|
# Compute binding factors.
|
38
61
|
# https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-15.html#name-binding-factors-computation
|
62
|
+
# @param [FROST::Context] context
|
39
63
|
# @param [ECDSA::Point] group_pubkey
|
40
64
|
# @param [Array] commitment_list The list of commitments issued by each participants.
|
41
65
|
# This list must be sorted in ascending order by identifier.
|
42
66
|
# @param [String] msg The message to be signed.
|
43
67
|
# @return [Hash] The hash of binding factor.
|
44
|
-
def compute_binding_factors(group_pubkey, commitment_list, msg)
|
68
|
+
def compute_binding_factors(context, group_pubkey, commitment_list, msg)
|
69
|
+
raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
|
45
70
|
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
46
71
|
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
72
|
+
raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
|
47
73
|
|
48
|
-
msg_hash = Hash.h4(msg,
|
74
|
+
msg_hash = Hash.h4(msg, context)
|
49
75
|
encoded_commitment = Commitments.encode_group_commitment(commitment_list)
|
50
|
-
encoded_commitment_hash = Hash.h5(encoded_commitment,
|
76
|
+
encoded_commitment_hash = Hash.h5(encoded_commitment, context)
|
51
77
|
rho_input_prefix = [group_pubkey.to_hex].pack("H*") + msg_hash + encoded_commitment_hash
|
52
78
|
binding_factors = {}
|
53
79
|
commitment_list.each do |commitments|
|
54
|
-
preimage = rho_input_prefix + encode_identifier(commitments.identifier,
|
55
|
-
binding_factors[commitments.identifier] = Hash.h1(preimage,
|
80
|
+
preimage = rho_input_prefix + encode_identifier(commitments.identifier, context.group)
|
81
|
+
binding_factors[commitments.identifier] = Hash.h1(preimage, context)
|
56
82
|
end
|
57
83
|
binding_factors
|
58
84
|
end
|
@@ -70,29 +96,39 @@ module FROST
|
|
70
96
|
end
|
71
97
|
|
72
98
|
# Create the per-message challenge.
|
99
|
+
# If context type is BIP-340(taproot), only the X coordinate of R and group_pubkey are hashed, unlike vanilla FROST.
|
73
100
|
# @param [ECDSA::Point] group_commitment The group commitment.
|
74
101
|
# @param [ECDSA::Point] group_pubkey The public key corresponding to the group signing key.
|
75
102
|
# @param [String] msg The message to be signed.
|
76
|
-
|
103
|
+
# @return [Integer] challenge
|
104
|
+
def compute_challenge(context, group_commitment, group_pubkey, msg)
|
105
|
+
raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
|
77
106
|
raise ArgumentError, "group_commitment must be ECDSA::Point." unless group_commitment.is_a?(ECDSA::Point)
|
78
107
|
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
79
108
|
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
109
|
+
raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
|
80
110
|
|
81
|
-
|
82
|
-
Hash.h2(
|
111
|
+
preimage = context.normalize(group_commitment) + context.normalize(group_pubkey) + msg
|
112
|
+
Hash.h2(preimage, context)
|
83
113
|
end
|
84
114
|
|
85
115
|
# Generate signature share.
|
116
|
+
# @param [FROST::Context] context FROST context.
|
86
117
|
# @param [FROST::SecretShare] secret_share Signer secret key share.
|
87
118
|
# @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
|
88
119
|
# @param [Array] nonces Pair of nonce values (hiding_nonce, binding_nonce) for signer_i.
|
89
120
|
# @param [String] msg The message to be signed
|
90
121
|
# @param [Array] commitment_list A list of commitments issued by each participant.
|
91
122
|
# @return [Integer] A signature share.
|
92
|
-
def sign(secret_share, group_pubkey, nonces, msg, commitment_list)
|
123
|
+
def sign(context, secret_share, group_pubkey, nonces, msg, commitment_list)
|
124
|
+
raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
|
125
|
+
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
126
|
+
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
127
|
+
raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
|
128
|
+
|
93
129
|
identifier = secret_share.identifier
|
94
130
|
# Compute binding factors
|
95
|
-
binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
|
131
|
+
binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
|
96
132
|
binding_factor = binding_factors[identifier]
|
97
133
|
|
98
134
|
# Compute group commitment
|
@@ -100,42 +136,46 @@ module FROST
|
|
100
136
|
|
101
137
|
# Compute Lagrange coefficient
|
102
138
|
identifiers = commitment_list.map(&:identifier)
|
103
|
-
lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier,
|
139
|
+
lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, context.group)
|
104
140
|
|
105
141
|
# Compute the per-message challenge
|
106
|
-
challenge = compute_challenge(group_commitment, group_pubkey, msg)
|
142
|
+
challenge = compute_challenge(context, group_commitment, group_pubkey, msg)
|
107
143
|
|
108
144
|
# Compute the signature share
|
109
|
-
hiding_nonce, binding_nonce = nonces
|
145
|
+
hiding_nonce, binding_nonce = context.convert_signer_nocnes(group_commitment, nonces)
|
110
146
|
field = ECDSA::PrimeField.new(group_pubkey.group.order)
|
111
147
|
field.mod(hiding_nonce.value +
|
112
148
|
field.mod(binding_nonce.value * binding_factor) + field.mod(lambda_i * secret_share.share * challenge))
|
113
149
|
end
|
114
150
|
|
115
151
|
# Aggregates the signature shares to produce a final signature that can be verified with the group public key.
|
152
|
+
# @param [FROST::Context] context FROST context.
|
116
153
|
# @param [Array] commitment_list A list of commitments issued by each participant.
|
117
154
|
# @param [String] msg The message to be signed.
|
118
155
|
# @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
|
119
156
|
# @param [Array] sig_shares A set of signature shares z_i, integer values.
|
120
157
|
# @return [FROST::Signature] Schnorr signature.
|
121
|
-
def aggregate(commitment_list, msg, group_pubkey, sig_shares)
|
158
|
+
def aggregate(context, commitment_list, msg, group_pubkey, sig_shares)
|
159
|
+
raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
|
122
160
|
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
123
161
|
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
162
|
+
raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
|
124
163
|
raise ArgumentError, "The numbers of commitment_list and sig_shares do not match." unless commitment_list.length == sig_shares.length
|
125
164
|
|
126
|
-
binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
|
165
|
+
binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
|
127
166
|
group_commitment = compute_group_commitment(commitment_list, binding_factors)
|
128
167
|
|
129
|
-
field = ECDSA::PrimeField.new(
|
168
|
+
field = ECDSA::PrimeField.new(context.group.order)
|
130
169
|
s = sig_shares.inject(0) do |sum, z_i|
|
131
170
|
raise ArgumentError, "sig_shares must be array of integer" unless z_i.is_a?(Integer)
|
132
171
|
field.mod(sum + z_i)
|
133
172
|
end
|
134
173
|
|
135
|
-
Signature.new(group_commitment, field.mod(s))
|
174
|
+
Signature.new(context, group_commitment, field.mod(s))
|
136
175
|
end
|
137
176
|
|
138
177
|
# Verify signature share.
|
178
|
+
# @param [FROST::Context] context FROST context.
|
139
179
|
# @param [Integer] identifier Identifier i of the participant.
|
140
180
|
# @param [ECDSA::Point] pubkey_i The public key for the i-th participant
|
141
181
|
# @param [Integer] sig_share_i Integer value indicating the signature share as produced
|
@@ -144,13 +184,15 @@ module FROST
|
|
144
184
|
# @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
|
145
185
|
# @param [String] msg The message to be signed.
|
146
186
|
# @return [Boolean] Verification result.
|
147
|
-
def verify_share(identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg)
|
187
|
+
def verify_share(context, identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg)
|
188
|
+
raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
|
148
189
|
raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
|
149
190
|
raise ArgumentError, "sig_share_i must be Integer." unless sig_share_i.is_a?(Integer)
|
150
191
|
raise ArgumentError, "pubkey_i must be ECDSA::Point." unless pubkey_i.is_a?(ECDSA::Point)
|
151
192
|
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
193
|
+
raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
|
152
194
|
|
153
|
-
binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
|
195
|
+
binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
|
154
196
|
binding_factor = binding_factors[identifier]
|
155
197
|
group_commitment = compute_group_commitment(commitment_list, binding_factors)
|
156
198
|
comm_i = commitment_list.find{|c| c.identifier == identifier}
|
@@ -159,11 +201,11 @@ module FROST
|
|
159
201
|
raise ArgumentError, "hiding_commitment must be ECDSA::Point." unless hiding_commitment.is_a?(ECDSA::Point)
|
160
202
|
raise ArgumentError, "binding_commitment must be ECDSA::Point." unless binding_commitment.is_a?(ECDSA::Point)
|
161
203
|
|
162
|
-
comm_share = hiding_commitment + binding_commitment * binding_factor
|
163
|
-
challenge = compute_challenge(group_commitment, group_pubkey, msg)
|
204
|
+
comm_share = context.convert_commitment_share(group_commitment, hiding_commitment + binding_commitment * binding_factor)
|
205
|
+
challenge = compute_challenge(context, group_commitment, group_pubkey, msg)
|
164
206
|
identifiers = commitment_list.map(&:identifier)
|
165
|
-
lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier,
|
166
|
-
l =
|
207
|
+
lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, context.group)
|
208
|
+
l = context.group.generator * sig_share_i
|
167
209
|
r = comm_share + pubkey_i * (challenge * lambda_i)
|
168
210
|
l == r
|
169
211
|
end
|
@@ -176,10 +218,14 @@ module FROST
|
|
176
218
|
def verify(signature, public_key, msg)
|
177
219
|
raise ArgumentError, "signature must be FROST::Signature" unless signature.is_a?(FROST::Signature)
|
178
220
|
raise ArgumentError, "public_key must be ECDSA::Point" unless public_key.is_a?(ECDSA::Point)
|
221
|
+
raise ArgumentError, "public_key and context groups are different." unless signature.context.group == public_key.group
|
179
222
|
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
180
223
|
|
224
|
+
context = signature.context
|
225
|
+
public_key, signature = context.pre_verify(public_key, signature)
|
226
|
+
|
181
227
|
# Compute challenge
|
182
|
-
challenge = compute_challenge(signature.r, public_key, msg)
|
228
|
+
challenge = compute_challenge(context, signature.r, public_key, msg)
|
183
229
|
|
184
230
|
s_g = public_key.group.generator * signature.s
|
185
231
|
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.5.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-17 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
|
+
- lib/frost/dealer.rb
|
62
63
|
- lib/frost/dkg.rb
|
63
|
-
- lib/frost/dkg/
|
64
|
+
- lib/frost/dkg/public_package.rb
|
64
65
|
- lib/frost/dkg/secret_package.rb
|
65
66
|
- lib/frost/hash.rb
|
66
67
|
- lib/frost/nonce.rb
|
@@ -78,7 +79,6 @@ metadata:
|
|
78
79
|
homepage_uri: https://github.com/azuchi/frostrb
|
79
80
|
source_code_uri: https://github.com/azuchi/frostrb
|
80
81
|
changelog_uri: https://github.com/azuchi/frostrb
|
81
|
-
post_install_message:
|
82
82
|
rdoc_options: []
|
83
83
|
require_paths:
|
84
84
|
- lib
|
@@ -93,8 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
93
|
- !ruby/object:Gem::Version
|
94
94
|
version: '0'
|
95
95
|
requirements: []
|
96
|
-
rubygems_version: 3.
|
97
|
-
signing_key:
|
96
|
+
rubygems_version: 3.6.2
|
98
97
|
specification_version: 4
|
99
98
|
summary: Ruby implementations of Two-Round Threshold Schnorr Signatures with FROST.
|
100
99
|
test_files: []
|