frostrb 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +137 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/frost.gemspec +38 -0
- data/lib/frost/commitments.rb +36 -0
- data/lib/frost/dkg/package.rb +42 -0
- data/lib/frost/dkg.rb +80 -0
- data/lib/frost/hash.rb +84 -0
- data/lib/frost/nonce.rb +54 -0
- data/lib/frost/polynomial.rb +100 -0
- data/lib/frost/secret_share.rb +33 -0
- data/lib/frost/signature.rb +42 -0
- data/lib/frost/signing_key.rb +39 -0
- data/lib/frost/version.rb +5 -0
- data/lib/frost.rb +188 -0
- data/sig/frost.rbs +4 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '09af4e893de9e1260635431b1feef84a9eb6273534f0f9e7e2e4bcde82d8788e'
|
4
|
+
data.tar.gz: 66f39e7bff887c34c65844ac42b94d8e77cdd7fb86caedb575d8ae0a7c0b12a1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba8c33e24ace6174176ec30bb5afaf0222e6c5046df34960ca28c8772286c236611bdb119c3184448fc308f491d1408e57a7fa1c715e3a7e0036e557a6788c46
|
7
|
+
data.tar.gz: b738d2fe05fa151032e9ff877758f580d6d96153ce179b384a95d6e353ce3e5003638622afbdfd2a4c219825d45f77e78f3e5238484b4cb34de5f8e8de8b768a
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
frostrb
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-3.3.0
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at azuchi@chaintope.com. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 azuchi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# FROST for Ruby [![Build Status](https://github.com/azuchi/frostrb/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/azuchi/frostrb/actions/workflows/main.yml)
|
2
|
+
|
3
|
+
This library is ruby implementations of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/).
|
4
|
+
|
5
|
+
Note: This library has not been security audited and tested widely, so should not be used in production.
|
6
|
+
|
7
|
+
The cipher suites currently supported by this library are:
|
8
|
+
|
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)
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'frostrb', require: 'frost'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle install
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install frostrb
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require 'frost'
|
32
|
+
|
33
|
+
group = ECDSA::Group::Secp256k1
|
34
|
+
|
35
|
+
# Dealer generate secret.
|
36
|
+
secret = FROST::SigningKey.generate(group)
|
37
|
+
group_pubkey = secret.to_point
|
38
|
+
|
39
|
+
# Generate polynomial(f(x) = ax + b)
|
40
|
+
polynomial = secret.gen_poly(1)
|
41
|
+
|
42
|
+
# Calculate secret shares.
|
43
|
+
share1 = polynomial.gen_share(1)
|
44
|
+
share2 = polynomial.gen_share(2)
|
45
|
+
share3 = polynomial.gen_share(3)
|
46
|
+
|
47
|
+
# Round 1: Generate nonce and commitment
|
48
|
+
## each party generate hiding and binding nonce.
|
49
|
+
hiding_nonce1 = FROST::Nonce.gen_from_secret(share1)
|
50
|
+
binding_nonce1 = FROST::Nonce.gen_from_secret(share1)
|
51
|
+
hiding_nonce3 = FROST::Nonce.gen_from_secret(share3)
|
52
|
+
binding_nonce3 = FROST::Nonce.gen_from_secret(share3)
|
53
|
+
|
54
|
+
comm1 = FROST::Commitments.new(1, hiding_nonce1.to_point, binding_nonce1.to_point)
|
55
|
+
comm3 = FROST::Commitments.new(3, hiding_nonce3.to_point, binding_nonce3.to_point)
|
56
|
+
commitment_list = [comm1, comm3]
|
57
|
+
|
58
|
+
msg = ["74657374"].pack("H*")
|
59
|
+
|
60
|
+
# 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)
|
63
|
+
|
64
|
+
# verify signature share
|
65
|
+
FROST.verify_share(1, share1.to_point, sig_share1, commitment_list, group_pubkey, msg)
|
66
|
+
FROST.verify_share(3, share3.to_point, sig_share3, commitment_list, group_pubkey, msg)
|
67
|
+
|
68
|
+
# Aggregation
|
69
|
+
sig = FROST.aggregate(commitment_list, msg, group_pubkey, [sig_share1, sig_share3])
|
70
|
+
|
71
|
+
# verify final signature
|
72
|
+
FROST.verify(sig, group_pubkey, msg)
|
73
|
+
```
|
74
|
+
|
75
|
+
### Using DKG
|
76
|
+
|
77
|
+
DKG can be run as below.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
max_signer = 5
|
81
|
+
min_signer = 3
|
82
|
+
|
83
|
+
secrets = {}
|
84
|
+
round1_outputs = {}
|
85
|
+
# Round 1:
|
86
|
+
# For each participant, perform the first part of the DKG protocol.
|
87
|
+
1.upto(max_signer) do |i|
|
88
|
+
polynomial, package = FROST::DKG.part1(i, min_signer, max_signer, group)
|
89
|
+
secrets[i] = polynomial
|
90
|
+
round1_outputs[i] = package
|
91
|
+
end
|
92
|
+
|
93
|
+
# Each participant sends their commitments and proof to other participants.
|
94
|
+
received_package = {}
|
95
|
+
1.upto(max_signer) do |i|
|
96
|
+
received_package[i] = round1_outputs.select {|k, _| k != i}.values
|
97
|
+
end
|
98
|
+
|
99
|
+
# Each participant verify knowledge of proof in received package.
|
100
|
+
received_package.each do |id, packages|
|
101
|
+
packages.each do |package|
|
102
|
+
expect(FROST::DKG.verify_proof_of_knowledge(package)).to be true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Round 2:
|
107
|
+
# Each participant generate share for other participants and send it.
|
108
|
+
received_shares = {}
|
109
|
+
1.upto(max_signer) do |i|
|
110
|
+
polynomial = secrets[i] # own secret
|
111
|
+
1.upto(max_signer) do |o|
|
112
|
+
next if i == o
|
113
|
+
received_shares[o] ||= []
|
114
|
+
received_shares[o] << [i, polynomial.gen_share(o)]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Each participant verify received shares.
|
119
|
+
1.upto(max_signer) do |i|
|
120
|
+
received_shares[i].each do |send_by, share|
|
121
|
+
target_package = received_package[i].find{ |package| package.identifier == send_by }
|
122
|
+
expect(target_package.verify_share(share)).to be true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Each participant compute signing share.
|
127
|
+
signing_shares = {}
|
128
|
+
1.upto(max_signer) do |i|
|
129
|
+
shares = received_shares[i].map{|_, share| share}
|
130
|
+
signing_shares[i] = FROST::DKG.compute_signing_share(secrets[i], shares)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Participant 1 compute group public key.
|
134
|
+
group_pubkey = FROST::DKG.compute_group_pubkey(secrets[1], received_package[1])
|
135
|
+
|
136
|
+
# The subsequent signing phase is the same as above with signing_shares as the secret.
|
137
|
+
```
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "frost"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/frost.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/frost/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "frostrb"
|
7
|
+
spec.version = FROST::VERSION
|
8
|
+
spec.authors = ["azuchi"]
|
9
|
+
spec.email = ["azuchi@chaintope.com"]
|
10
|
+
|
11
|
+
spec.summary = "Ruby implementations of Two-Round Threshold Schnorr Signatures with FROST."
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = "https://github.com/azuchi/frostrb"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
19
|
+
spec.metadata["changelog_uri"] = spec.homepage
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
# Uncomment to register a new dependency of your gem
|
33
|
+
spec.add_dependency "ecdsa_ext", "~> 0.5.1"
|
34
|
+
spec.add_dependency "h2c", "~> 0.2.1"
|
35
|
+
|
36
|
+
# For more information and examples about making a new gem, check out our
|
37
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module FROST
|
2
|
+
# Participant commitment
|
3
|
+
class Commitments
|
4
|
+
|
5
|
+
attr_reader :identifier
|
6
|
+
attr_reader :hiding
|
7
|
+
attr_reader :binding
|
8
|
+
|
9
|
+
# Constructor
|
10
|
+
# @param [Integer] identifier Identifier of participant.
|
11
|
+
# @param [ECDSA::Point] hiding Commitment point.
|
12
|
+
# @param [ECDSA::Point] binding Commitment point.
|
13
|
+
def initialize(identifier, hiding, binding)
|
14
|
+
raise ArgumentError, "id must be Integer." unless identifier.is_a?(Integer)
|
15
|
+
raise ArgumentError, "id must be greater than 0." if identifier < 1
|
16
|
+
raise ArgumentError, "hiding must be ECDSA::Point." unless hiding.is_a?(ECDSA::Point)
|
17
|
+
raise ArgumentError, "binding must be ECDSA::Point." unless binding.is_a?(ECDSA::Point)
|
18
|
+
|
19
|
+
@identifier = identifier
|
20
|
+
@hiding = hiding
|
21
|
+
@binding = binding
|
22
|
+
end
|
23
|
+
|
24
|
+
def encode
|
25
|
+
id = FROST.encode_identifier(identifier, hiding.group)
|
26
|
+
id + [hiding.to_hex + binding.to_hex].pack("H*")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Encodes a list of participant commitments into a byte string
|
30
|
+
# @param [Array] commitment_list The list of FROST::Commitments
|
31
|
+
# @return [String] The encoded byte string.
|
32
|
+
def self.encode_group_commitment(commitment_list)
|
33
|
+
commitment_list.map(&:encode).join
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module FROST
|
2
|
+
module DKG
|
3
|
+
class Package
|
4
|
+
attr_reader :identifier
|
5
|
+
attr_reader :commitments
|
6
|
+
attr_reader :proof
|
7
|
+
|
8
|
+
# Constructor
|
9
|
+
# @param [Integer] identifier
|
10
|
+
# @param [Array] commitments The list of commitment.
|
11
|
+
# @param [FROST::Signature] proof
|
12
|
+
def initialize(identifier, commitments, proof)
|
13
|
+
raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
|
14
|
+
raise ArgumentError, "identifier must be greater than 0." if identifier < 1
|
15
|
+
raise ArgumentError, "proof must be FROST::Signature." unless proof.is_a?(FROST::Signature)
|
16
|
+
|
17
|
+
@identifier = identifier
|
18
|
+
@commitments = commitments
|
19
|
+
@proof = proof
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get verification key for this proof.
|
23
|
+
# @return [ECDSA::Point]
|
24
|
+
def verification_key
|
25
|
+
commitments.first
|
26
|
+
end
|
27
|
+
|
28
|
+
# Verify share.
|
29
|
+
# @param [FROST::SecretShare] share
|
30
|
+
# @return [Boolean]
|
31
|
+
def verify_share(share)
|
32
|
+
x = share.identifier
|
33
|
+
result = commitments[1..-1].inject(commitments.first) do |sum, com|
|
34
|
+
tmp = com * x
|
35
|
+
x *= x
|
36
|
+
sum + tmp
|
37
|
+
end
|
38
|
+
result == share.to_point
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/frost/dkg.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module FROST
|
2
|
+
# Distributed Key Generation feature.
|
3
|
+
module DKG
|
4
|
+
|
5
|
+
autoload :Package, "frost/dkg/package"
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Performs the first part of the DKG.
|
10
|
+
# Participant generate key and commitments, proof of knowledge for secret.
|
11
|
+
# @param [Integer] identifier
|
12
|
+
# @param [ECDSA::Group] group Group of elliptic curve.
|
13
|
+
# @return [Array] The triple of polynomial and public package(FROST::DKG::Package)
|
14
|
+
def part1(identifier, min_signers, max_signers, group)
|
15
|
+
raise ArgumentError, "identifier must be Integer" unless identifier.is_a?(Integer)
|
16
|
+
raise ArgumentError, "identifier must be greater than 0." if identifier < 1
|
17
|
+
raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
|
18
|
+
raise ArgumentError, "max_signers must be greater than or equal to min_signers." if max_signers < min_signers
|
19
|
+
|
20
|
+
secret = FROST::SigningKey.generate(group)
|
21
|
+
# Every participant P_i samples t random values (a_{i0}, ..., a_{i(t−1)}) ← Z_q
|
22
|
+
polynomial = secret.gen_poly(min_signers - 1)
|
23
|
+
[polynomial, Package.new(identifier, polynomial.gen_commitments, polynomial.gen_proof_of_knowledge(identifier))]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generate proof of knowledge for secret.
|
27
|
+
# @param [Integer] identifier Identifier of the owner of polynomial.
|
28
|
+
# @param [FROST::Polynomial] polynomial Polynomial containing secret.
|
29
|
+
# @return [FROST::Signature]
|
30
|
+
def gen_proof_of_knowledge(identifier, polynomial)
|
31
|
+
raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
|
32
|
+
raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial)
|
33
|
+
|
34
|
+
k = SecureRandom.random_number(polynomial.group.order - 1)
|
35
|
+
r = polynomial.group.generator * k
|
36
|
+
a0 = polynomial.coefficients.first
|
37
|
+
a0_g = polynomial.group.generator * a0
|
38
|
+
msg = FROST.encode_identifier(identifier, polynomial.group) + [a0_g.to_hex + r.to_hex].pack("H*")
|
39
|
+
challenge = Hash.hdkg(msg, polynomial.group)
|
40
|
+
field = ECDSA::PrimeField.new(polynomial.group.order)
|
41
|
+
s = field.mod(k + a0 * challenge)
|
42
|
+
FROST::Signature.new(r, s)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Verify proof of knowledge for received commitment.
|
46
|
+
# @param [FROST::DKG::Package] package Received package.
|
47
|
+
# @return [Boolean]
|
48
|
+
def verify_proof_of_knowledge(package)
|
49
|
+
raise ArgumentError, "package must be FROST::DKG::Package." unless package.is_a?(FROST::DKG::Package)
|
50
|
+
|
51
|
+
verification_key = package.verification_key
|
52
|
+
msg = FROST.encode_identifier(package.identifier, verification_key.group) +
|
53
|
+
[verification_key.to_hex + package.proof.r.to_hex].pack("H*")
|
54
|
+
challenge = Hash.hdkg(msg, verification_key.group)
|
55
|
+
package.proof.r == verification_key.group.generator * package.proof.s + (verification_key * challenge).negate
|
56
|
+
end
|
57
|
+
|
58
|
+
# Compute signing share using received shares from other participants
|
59
|
+
# @param [FROST::Polynomial] polynomial Own polynomial contains own secret.
|
60
|
+
# @param [Array] received_shares Array of FROST::SecretShare received by other participants.
|
61
|
+
# @return [FROST::SecretShare] Signing share.
|
62
|
+
def compute_signing_share(polynomial, received_shares)
|
63
|
+
raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial)
|
64
|
+
identifier = received_shares.first.identifier
|
65
|
+
s_id = received_shares.sum {|share| share.share}
|
66
|
+
field = ECDSA::PrimeField.new(polynomial.group.order)
|
67
|
+
FROST::SecretShare.new(
|
68
|
+
identifier, field.mod(s_id + polynomial.gen_share(identifier).share), polynomial.group)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Compute Group public key.
|
72
|
+
# @param [FROST::Polynomial] polynomial Own polynomial contains own secret.
|
73
|
+
# @param [Array] received_packages Array of FROST::DKG::Package received by other participants.
|
74
|
+
# @return [ECDSA::Point] Group public key.
|
75
|
+
def compute_group_pubkey(polynomial, received_packages)
|
76
|
+
raise ArgumentError, "polynomial must be FROST::Polynomial." unless polynomial.is_a?(FROST::Polynomial)
|
77
|
+
received_packages.inject(polynomial.verification_point) {|sum, package| sum + package.commitments.first }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/frost/hash.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module FROST
|
2
|
+
# Cryptographic hash function using FROST.
|
3
|
+
# https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-15.html#name-cryptographic-hash-function
|
4
|
+
module Hash
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
CTX_STRING_SECP256K1 = "FROST-secp256k1-SHA256-v1"
|
9
|
+
CTX_STRING_P256 = "FROST-P256-SHA256-v1"
|
10
|
+
|
11
|
+
# H1 hash function.
|
12
|
+
# @param [String] msg The message to be hashed.
|
13
|
+
# param [ECDSA::Group] group The elliptic curve group.
|
14
|
+
# @return [Integer]
|
15
|
+
def h1(msg, group)
|
16
|
+
hash_to_field(msg, group, "rho")
|
17
|
+
end
|
18
|
+
|
19
|
+
# H3 hash function.
|
20
|
+
# @param [String] msg The message to be hashed.
|
21
|
+
# @param [ECDSA::Group] group The elliptic curve group.
|
22
|
+
# @return [Integer]
|
23
|
+
def h2(msg, group)
|
24
|
+
hash_to_field(msg, group, "chal")
|
25
|
+
end
|
26
|
+
|
27
|
+
# H3 hash function.
|
28
|
+
# @param [String] msg The message to be hashed.
|
29
|
+
# @param [ECDSA::Group] group The elliptic curve group.
|
30
|
+
# @return [Integer]
|
31
|
+
def h3(msg, group)
|
32
|
+
hash_to_field(msg, group, "nonce")
|
33
|
+
end
|
34
|
+
|
35
|
+
# H4 hash function.
|
36
|
+
# @param [String] msg The message to be hashed.
|
37
|
+
# @param [ECDSA::Group] group The elliptic curve group.
|
38
|
+
# @return [String] The hash value.
|
39
|
+
def h4(msg, group)
|
40
|
+
hash(msg, group, "msg")
|
41
|
+
end
|
42
|
+
|
43
|
+
# H5 hash function.
|
44
|
+
# @param [String] msg The message to be hashed.
|
45
|
+
# @param [ECDSA::Group] group The elliptic curve group.
|
46
|
+
# @return [String] The hash value.
|
47
|
+
def h5(msg, group)
|
48
|
+
hash(msg, group, "com")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Hash function for a FROST ciphersuite, used for the DKG.
|
52
|
+
# @param [String] msg The message to be hashed.
|
53
|
+
# @param [ECDSA::Group] group The elliptic curve group.
|
54
|
+
# @return [Integer] The hash value.
|
55
|
+
def hdkg(msg, group)
|
56
|
+
hash_to_field(msg, group, "dkg")
|
57
|
+
end
|
58
|
+
|
59
|
+
def hash_to_field(msg, group, context)
|
60
|
+
h2c = case group
|
61
|
+
when ECDSA::Group::Secp256k1
|
62
|
+
H2C.get(H2C::Suite::SECP256K1_XMDSHA256_SSWU_NU_, CTX_STRING_SECP256K1 + context)
|
63
|
+
when ECDSA::Group::Secp256r1
|
64
|
+
H2C.get(H2C::Suite::P256_XMDSHA256_SSWU_NU_, CTX_STRING_P256 + context)
|
65
|
+
else
|
66
|
+
# TODO support other suite.
|
67
|
+
raise RuntimeError, "group #{group} dose not supported."
|
68
|
+
end
|
69
|
+
h2c.hash_to_field(msg, 1, group.order).first
|
70
|
+
end
|
71
|
+
|
72
|
+
def hash(msg, group, context)
|
73
|
+
case group
|
74
|
+
when ECDSA::Group::Secp256k1
|
75
|
+
Digest::SHA256.digest(CTX_STRING_SECP256K1 + context + msg)
|
76
|
+
when ECDSA::Group::Secp256r1
|
77
|
+
Digest::SHA256.digest(CTX_STRING_P256 + context + msg)
|
78
|
+
else
|
79
|
+
# TODO support other suite.
|
80
|
+
raise RuntimeError, "group #{group} dose not supported."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/frost/nonce.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module FROST
|
2
|
+
class Nonce
|
3
|
+
|
4
|
+
attr_reader :value # nonce value
|
5
|
+
attr_reader :group # Group of elliptic curve
|
6
|
+
|
7
|
+
# Generate nonce.
|
8
|
+
# @return [FROST::Nonce]
|
9
|
+
def initialize(nonce, group)
|
10
|
+
raise ArgumentError, "group must by ECDSA::Group." unless group.is_a?(ECDSA::Group)
|
11
|
+
raise ArgumentError, "nonce must by Integer." unless nonce.is_a?(Integer)
|
12
|
+
@value = nonce
|
13
|
+
@group = group
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generate nonce from secret share.
|
17
|
+
# @param [FROST::SigningKey] secret
|
18
|
+
def self.gen_from_secret(secret)
|
19
|
+
gen_from_random_bytes(secret)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Generates a nonce from the given random bytes.
|
23
|
+
# This method allows only testing.
|
24
|
+
# https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-15.html#section-4.1
|
25
|
+
# @param [FROST::SigningKey] secret
|
26
|
+
# @param [String] random_bytes Random bytes.
|
27
|
+
# @return [FROST::Nonce]
|
28
|
+
def self.gen_from_random_bytes(secret, random_bytes = SecureRandom.bytes(32))
|
29
|
+
secret = secret.to_key if secret.is_a?(SecretShare)
|
30
|
+
raise ArgumentError, "secret must be FROST::SigningKey" unless secret.is_a?(FROST::SigningKey)
|
31
|
+
raise ArgumentError, "random_bytes must be 32 bytes." unless random_bytes.bytesize == 32
|
32
|
+
|
33
|
+
secret_bytes = ECDSA::Format::IntegerOctetString.encode(secret.scalar, 32)
|
34
|
+
msg = random_bytes + secret_bytes
|
35
|
+
nonce = FROST::Hash.h3(msg, secret.group)
|
36
|
+
Nonce.new(nonce, secret.group)
|
37
|
+
end
|
38
|
+
|
39
|
+
private_class_method :gen_from_random_bytes
|
40
|
+
|
41
|
+
# Convert nonce as hex string.
|
42
|
+
# @return [String]
|
43
|
+
def to_hex
|
44
|
+
ECDSA::Format::IntegerOctetString.encode(value, 32).unpack1('H*')
|
45
|
+
end
|
46
|
+
|
47
|
+
# Compute public key.
|
48
|
+
# @return [ECDSA::Point]
|
49
|
+
def to_point
|
50
|
+
group.generator * value
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module FROST
|
2
|
+
|
3
|
+
# Polynomial class.
|
4
|
+
class Polynomial
|
5
|
+
attr_reader :coefficients
|
6
|
+
attr_reader :group
|
7
|
+
|
8
|
+
# Generate polynomial.
|
9
|
+
# @param [Array] coefficients Coefficients of polynomial.
|
10
|
+
# The first is the constant term, followed by the coefficients in descending order of order.
|
11
|
+
# @param [ECDSA::Group] group
|
12
|
+
def initialize(coefficients, group)
|
13
|
+
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
|
+
raise ArgumentError, "Two or more coefficients are required." if coefficients.length < 2
|
16
|
+
|
17
|
+
@coefficients = coefficients
|
18
|
+
@group = group
|
19
|
+
end
|
20
|
+
|
21
|
+
# Generate random polynomial using secret as constant term.
|
22
|
+
# @param [Integer|FROST::SigningKey] secret Secret value as constant term.
|
23
|
+
# @param [Integer] degree Degree of polynomial.
|
24
|
+
# @return [FROST::Polynomial] Polynomial
|
25
|
+
def self.from_secret(secret, degree, group)
|
26
|
+
secret = secret.scalar if secret.is_a?(FROST::SigningKey)
|
27
|
+
raise ArgumentError, "secret must be Integer." unless secret.is_a?(Integer)
|
28
|
+
raise ArgumentError, "degree must be Integer." unless degree.is_a?(Integer)
|
29
|
+
raise ArgumentError, "degree must be greater than or equal to 1." if degree < 1
|
30
|
+
coeffs = degree.times.map {SecureRandom.random_number(group.order - 1)}
|
31
|
+
Polynomial.new(coeffs.prepend(secret), group)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generate secret share.
|
35
|
+
# @param [Integer] identifier Identifier for evaluating polynomials.
|
36
|
+
# @return [FROST::SecretShare] Generate share.
|
37
|
+
def gen_share(identifier)
|
38
|
+
raise ArgumentError, "identifiers must be Integer." unless identifier.is_a?(Integer)
|
39
|
+
|
40
|
+
return SecretShare.new(identifier, 0, group) if coefficients.empty?
|
41
|
+
return SecretShare.new(identifier, coefficients.last, group) if identifier == 0
|
42
|
+
|
43
|
+
# Calculate using Horner's method.
|
44
|
+
last = coefficients.last
|
45
|
+
(coefficients.length - 2).step(0, -1) do |i|
|
46
|
+
tmp = last * identifier
|
47
|
+
last = (tmp + coefficients[i]) % group.order
|
48
|
+
end
|
49
|
+
SecretShare.new(identifier, last, group)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Generate coefficient commitments
|
53
|
+
# @return [Array] A list of coefficient commitment (ECDSA::Point).
|
54
|
+
def gen_commitments
|
55
|
+
coefficients.map{|c| group.generator * c }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generate proof of knowledge for secret.
|
59
|
+
# @param [Integer] identifier Identifier of the owner of this polynomial.
|
60
|
+
# @return [FROST::Signature]
|
61
|
+
def gen_proof_of_knowledge(identifier)
|
62
|
+
FROST::DKG.gen_proof_of_knowledge(identifier, self)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get secret value in this polynomial.
|
66
|
+
# @return [Integer] secret
|
67
|
+
def secret
|
68
|
+
coefficients.first
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get point to correspond to secret in this polynomial.
|
72
|
+
# @return [ECDSA::Point] secret point
|
73
|
+
def verification_point
|
74
|
+
group.generator * secret
|
75
|
+
end
|
76
|
+
|
77
|
+
# Generates the lagrange coefficient for the i'th participant.
|
78
|
+
# @param [Array] x_coordinates The list of x-coordinates.
|
79
|
+
# @param [Integer] xi an x-coordinate contained in x_coordinates.
|
80
|
+
# @param [ECDSA::Group] group Elliptic curve group.
|
81
|
+
# @return [Integer] The lagrange coefficient.
|
82
|
+
def self.derive_interpolating_value(x_coordinates, xi, group)
|
83
|
+
raise ArgumentError, "xi is not included in x_coordinates." unless x_coordinates.include?(xi)
|
84
|
+
raise ArgumentError, "Duplicate values in x_coordinates." if (x_coordinates.length - x_coordinates.uniq.length) > 0
|
85
|
+
raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
|
86
|
+
|
87
|
+
field = ECDSA::PrimeField.new(group.order)
|
88
|
+
numerator = 1
|
89
|
+
denominator = 1
|
90
|
+
x_coordinates.each do |xj|
|
91
|
+
next if xi == xj
|
92
|
+
numerator *= xj
|
93
|
+
denominator *= (xj - xi)
|
94
|
+
end
|
95
|
+
|
96
|
+
field.mod(numerator * field.inverse(denominator))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module FROST
|
2
|
+
# A secret share generated by performing a (t-out-of-n) secret sharing scheme.
|
3
|
+
class SecretShare
|
4
|
+
attr_reader :identifier
|
5
|
+
attr_reader :share
|
6
|
+
attr_reader :group
|
7
|
+
|
8
|
+
# Generate secret share.
|
9
|
+
# @param [Integer] identifier Identifier of this share.
|
10
|
+
# @param [Integer] share A share.
|
11
|
+
def initialize(identifier, share, group)
|
12
|
+
raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
|
13
|
+
raise ArgumentError, "share must be Integer." unless share.is_a?(Integer)
|
14
|
+
raise ArgumentError, "group must be ECDSA::Group" unless group.is_a?(ECDSA::Group)
|
15
|
+
|
16
|
+
@identifier = identifier
|
17
|
+
@share = share
|
18
|
+
@group = group
|
19
|
+
end
|
20
|
+
|
21
|
+
# Compute public key.
|
22
|
+
# @return [ECDSA::Point]
|
23
|
+
def to_point
|
24
|
+
group.generator * share
|
25
|
+
end
|
26
|
+
|
27
|
+
# Generate signing share key.
|
28
|
+
# @return [FROST::SigningKey]
|
29
|
+
def to_key
|
30
|
+
FROST::SigningKey.new(share, group)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module FROST
|
2
|
+
# A Schnorr signature over some prime order group (or subgroup).
|
3
|
+
class Signature
|
4
|
+
attr_reader :r
|
5
|
+
attr_reader :s
|
6
|
+
|
7
|
+
# Constructor
|
8
|
+
# @param [ECDSA::Point] r Public nonce of signature.
|
9
|
+
# @param [Integer] s Scalar value of signature.
|
10
|
+
def initialize(r, s)
|
11
|
+
raise ArgumentError, "r must be ECDSA::Point" unless r.is_a?(ECDSA::Point)
|
12
|
+
raise ArgumentError, "s must be Integer" unless s.is_a?(Integer)
|
13
|
+
|
14
|
+
@r = r
|
15
|
+
@s = s
|
16
|
+
end
|
17
|
+
|
18
|
+
# Encode signature to hex string.
|
19
|
+
# @return [String]
|
20
|
+
def to_hex
|
21
|
+
encode.unpack1("H*")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Encode signature to byte string.
|
25
|
+
# @return [String]
|
26
|
+
def encode
|
27
|
+
ECDSA::Format::PointOctetString.encode(r, compression: true) +
|
28
|
+
ECDSA::Format::IntegerOctetString.encode(s, 32)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Decode hex value to FROST::Signature.
|
32
|
+
# @param [String] hex_value Hex value of signature.
|
33
|
+
# @param [ECDSA::Group] group Group of elliptic curve.
|
34
|
+
# @return [FROST::Signature]
|
35
|
+
def self.decode(hex_value, group)
|
36
|
+
data = [hex_value].pack("H*")
|
37
|
+
r = ECDSA::Format::PointOctetString.decode(data[0...33], group)
|
38
|
+
s = ECDSA::Format::IntegerOctetString.decode(data[33..-1])
|
39
|
+
Signature.new(r,s )
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module FROST
|
2
|
+
# A signing key for a Schnorr signature on a FROST.
|
3
|
+
class SigningKey
|
4
|
+
attr_reader :scalar
|
5
|
+
attr_reader :group
|
6
|
+
|
7
|
+
# Constructor
|
8
|
+
# @param [Integer] scalar secret key value.
|
9
|
+
# @param [ECDSA::Group] group Group of elliptic curve.
|
10
|
+
def initialize(scalar, group = ECDSA::Group::Secp256k1)
|
11
|
+
raise ArgumentError, "scalar must be integer." unless scalar.is_a?(Integer)
|
12
|
+
raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group)
|
13
|
+
raise ArgumentError, "Invalid scalar range." if scalar < 1 || group.order - 1 < scalar
|
14
|
+
|
15
|
+
@scalar = scalar
|
16
|
+
@group = group
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generate signing key.
|
20
|
+
# @param [ECDSA::Group] group Group of elliptic curve.
|
21
|
+
def self.generate(group)
|
22
|
+
scalar = 1 + SecureRandom.random_number(group.order - 1)
|
23
|
+
SigningKey.new(scalar, group)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generate random polynomial using this secret.
|
27
|
+
# @param [Integer] degree Degree of polynomial.
|
28
|
+
# @return [FROST::Polynomial] A polynomial
|
29
|
+
def gen_poly(degree)
|
30
|
+
Polynomial.from_secret(scalar, degree, group)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Compute public key.
|
34
|
+
# @return [ECDSA::Point]
|
35
|
+
def to_point
|
36
|
+
group.generator * scalar
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/frost.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "frost/version"
|
4
|
+
require 'ecdsa_ext'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'digest'
|
7
|
+
require 'h2c'
|
8
|
+
|
9
|
+
module FROST
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
autoload :Signature, "frost/signature"
|
13
|
+
autoload :Commitments, "frost/commitments"
|
14
|
+
autoload :Hash, "frost/hash"
|
15
|
+
autoload :Nonce, "frost/nonce"
|
16
|
+
autoload :SecretShare, "frost/secret_share"
|
17
|
+
autoload :Polynomial, "frost/polynomial"
|
18
|
+
autoload :SigningKey, "frost/signing_key"
|
19
|
+
autoload :DKG, "frost/dkg"
|
20
|
+
|
21
|
+
module_function
|
22
|
+
|
23
|
+
# Encode identifier
|
24
|
+
# @param [Integer] identifier
|
25
|
+
# @param [ECDSA::Group] group
|
26
|
+
# @return [String] The encoded identifier
|
27
|
+
def encode_identifier(identifier, group)
|
28
|
+
case group
|
29
|
+
when ECDSA::Group::Secp256k1, ECDSA::Group::Secp256r1
|
30
|
+
ECDSA::Format::IntegerOctetString.encode(identifier, 32)
|
31
|
+
else
|
32
|
+
raise RuntimeError, "group #{group} dose not supported."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Compute binding factors.
|
37
|
+
# https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-15.html#name-binding-factors-computation
|
38
|
+
# @param [ECDSA::Point] group_pubkey
|
39
|
+
# @param [Array] commitment_list The list of commitments issued by each participants.
|
40
|
+
# This list must be sorted in ascending order by identifier.
|
41
|
+
# @param [String] msg The message to be signed.
|
42
|
+
# @return [Hash] The hash of binding factor.
|
43
|
+
def compute_binding_factors(group_pubkey, commitment_list, msg)
|
44
|
+
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
45
|
+
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
46
|
+
|
47
|
+
msg_hash = Hash.h4(msg, group_pubkey.group)
|
48
|
+
encoded_commitment = Commitments.encode_group_commitment(commitment_list)
|
49
|
+
encoded_commitment_hash = Hash.h5(encoded_commitment, group_pubkey.group)
|
50
|
+
rho_input_prefix = [group_pubkey.to_hex].pack("H*") + msg_hash + encoded_commitment_hash
|
51
|
+
binding_factors = {}
|
52
|
+
commitment_list.each do |commitments|
|
53
|
+
preimage = rho_input_prefix + encode_identifier(commitments.identifier, group_pubkey.group)
|
54
|
+
binding_factors[commitments.identifier] = Hash.h1(preimage, group_pubkey.group)
|
55
|
+
end
|
56
|
+
binding_factors
|
57
|
+
end
|
58
|
+
|
59
|
+
# Compute the group commitment
|
60
|
+
# @param [Array] commitment_list The list of commitments.
|
61
|
+
# @param [Hash] binding_factors The map of binding factors.
|
62
|
+
# @return [ECDSA::Point]
|
63
|
+
def compute_group_commitment(commitment_list, binding_factors)
|
64
|
+
commitment_list.inject(commitment_list.first.hiding.group.infinity) do |sum, commitments|
|
65
|
+
binding_factor = binding_factors[commitments.identifier]
|
66
|
+
binding_nonce = commitments.binding * binding_factor
|
67
|
+
binding_nonce + commitments.hiding + sum
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Create the per-message challenge.
|
72
|
+
# @param [ECDSA::Point] group_commitment The group commitment.
|
73
|
+
# @param [ECDSA::Point] group_pubkey The public key corresponding to the group signing key.
|
74
|
+
# @param [String] msg The message to be signed.
|
75
|
+
def compute_challenge(group_commitment, group_pubkey, msg)
|
76
|
+
raise ArgumentError, "group_commitment must be ECDSA::Point." unless group_commitment.is_a?(ECDSA::Point)
|
77
|
+
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
78
|
+
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
79
|
+
|
80
|
+
input = [group_commitment.to_hex + group_pubkey.to_hex].pack("H*") + msg
|
81
|
+
Hash.h2(input, group_commitment.group)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Generate signature share.
|
85
|
+
# @param [FROST::SecretShare] secret_share Signer secret key share.
|
86
|
+
# @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
|
87
|
+
# @param [Array] nonces Pair of nonce values (hiding_nonce, binding_nonce) for signer_i.
|
88
|
+
# @param [String] msg The message to be signed
|
89
|
+
# @param [Array] commitment_list A list of commitments issued by each participant.
|
90
|
+
# @return [Integer] A signature share.
|
91
|
+
def sign(secret_share, group_pubkey, nonces, msg, commitment_list)
|
92
|
+
identifier = secret_share.identifier
|
93
|
+
# Compute binding factors
|
94
|
+
binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
|
95
|
+
binding_factor = binding_factors[identifier]
|
96
|
+
|
97
|
+
# Compute group commitment
|
98
|
+
group_commitment = compute_group_commitment(commitment_list, binding_factors)
|
99
|
+
|
100
|
+
# Compute Lagrange coefficient
|
101
|
+
identifiers = commitment_list.map(&:identifier)
|
102
|
+
lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, group_pubkey.group)
|
103
|
+
|
104
|
+
# Compute the per-message challenge
|
105
|
+
challenge = compute_challenge(group_commitment, group_pubkey, msg)
|
106
|
+
|
107
|
+
# Compute the signature share
|
108
|
+
hiding_nonce, binding_nonce = nonces
|
109
|
+
field = ECDSA::PrimeField.new(group_pubkey.group.order)
|
110
|
+
field.mod(hiding_nonce.value +
|
111
|
+
field.mod(binding_nonce.value * binding_factor) + field.mod(lambda_i * secret_share.share * challenge))
|
112
|
+
end
|
113
|
+
|
114
|
+
# Aggregates the signature shares to produce a final signature that can be verified with the group public key.
|
115
|
+
# @param [Array] commitment_list A list of commitments issued by each participant.
|
116
|
+
# @param [String] msg The message to be signed.
|
117
|
+
# @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
|
118
|
+
# @param [Array] sig_shares A set of signature shares z_i, integer values.
|
119
|
+
# @return [FROST::Signature] Schnorr signature.
|
120
|
+
def aggregate(commitment_list, msg, group_pubkey, sig_shares)
|
121
|
+
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
122
|
+
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
123
|
+
raise ArgumentError, "The numbers of commitment_list and sig_shares do not match." unless commitment_list.length == sig_shares.length
|
124
|
+
|
125
|
+
binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
|
126
|
+
group_commitment = compute_group_commitment(commitment_list, binding_factors)
|
127
|
+
|
128
|
+
field = ECDSA::PrimeField.new(group_pubkey.group.order)
|
129
|
+
s = sig_shares.inject(0) do |sum, z_i|
|
130
|
+
raise ArgumentError, "sig_shares must be array of integer" unless z_i.is_a?(Integer)
|
131
|
+
field.mod(sum + z_i)
|
132
|
+
end
|
133
|
+
|
134
|
+
Signature.new(group_commitment, field.mod(s))
|
135
|
+
end
|
136
|
+
|
137
|
+
# Verify signature share.
|
138
|
+
# @param [Integer] identifier Identifier i of the participant.
|
139
|
+
# @param [ECDSA::Point] pubkey_i The public key for the i-th participant
|
140
|
+
# @param [Integer] sig_share_i Integer value indicating the signature share as produced
|
141
|
+
# in round two from the i-th participant.
|
142
|
+
# @param [Array] commitment_list A list of commitments issued by each participant.
|
143
|
+
# @param [ECDSA::Point] group_pubkey Public key corresponding to the group signing key.
|
144
|
+
# @param [String] msg The message to be signed.
|
145
|
+
# @return [Boolean] Verification result.
|
146
|
+
def verify_share(identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg)
|
147
|
+
raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
|
148
|
+
raise ArgumentError, "sig_share_i must be Integer." unless sig_share_i.is_a?(Integer)
|
149
|
+
raise ArgumentError, "pubkey_i must be ECDSA::Point." unless pubkey_i.is_a?(ECDSA::Point)
|
150
|
+
raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
|
151
|
+
|
152
|
+
binding_factors = compute_binding_factors(group_pubkey, commitment_list, msg)
|
153
|
+
binding_factor = binding_factors[identifier]
|
154
|
+
group_commitment = compute_group_commitment(commitment_list, binding_factors)
|
155
|
+
comm_i = commitment_list.find{|c| c.identifier == identifier}
|
156
|
+
hiding_commitment = comm_i.hiding
|
157
|
+
binding_commitment = comm_i.binding
|
158
|
+
raise ArgumentError, "hiding_commitment must be ECDSA::Point." unless hiding_commitment.is_a?(ECDSA::Point)
|
159
|
+
raise ArgumentError, "binding_commitment must be ECDSA::Point." unless binding_commitment.is_a?(ECDSA::Point)
|
160
|
+
|
161
|
+
comm_share = hiding_commitment + binding_commitment * binding_factor
|
162
|
+
challenge = compute_challenge(group_commitment, group_pubkey, msg)
|
163
|
+
identifiers = commitment_list.map(&:identifier)
|
164
|
+
lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, group_pubkey.group)
|
165
|
+
l = group_pubkey.group.generator * sig_share_i
|
166
|
+
r = comm_share + pubkey_i * (challenge * lambda_i)
|
167
|
+
l == r
|
168
|
+
end
|
169
|
+
|
170
|
+
# Verify signature.
|
171
|
+
# @param [FROST::Signature] signature
|
172
|
+
# @param [ECDSA::Point] public_key
|
173
|
+
# @param [String] msg
|
174
|
+
# @return [Boolean] Verification result.
|
175
|
+
def verify(signature, public_key, msg)
|
176
|
+
raise ArgumentError, "signature must be FROST::Signature" unless signature.is_a?(FROST::Signature)
|
177
|
+
raise ArgumentError, "public_key must be ECDSA::Point" unless public_key.is_a?(ECDSA::Point)
|
178
|
+
raise ArgumentError, "msg must be String." unless msg.is_a?(String)
|
179
|
+
|
180
|
+
# Compute challenge
|
181
|
+
challenge = compute_challenge(signature.r, public_key, msg)
|
182
|
+
|
183
|
+
s_g = public_key.group.generator * signature.s
|
184
|
+
c_p = public_key * challenge
|
185
|
+
result = (s_g + signature.r.negate + c_p.negate) * public_key.group.cofactor
|
186
|
+
result.infinity?
|
187
|
+
end
|
188
|
+
end
|
data/sig/frost.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: frostrb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- azuchi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-02-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ecdsa_ext
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.5.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: h2c
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.2.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.2.1
|
41
|
+
description: Ruby implementations of Two-Round Threshold Schnorr Signatures with FROST.
|
42
|
+
email:
|
43
|
+
- azuchi@chaintope.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rspec"
|
49
|
+
- ".ruby-gemset"
|
50
|
+
- ".ruby-version"
|
51
|
+
- CHANGELOG.md
|
52
|
+
- CODE_OF_CONDUCT.md
|
53
|
+
- Gemfile
|
54
|
+
- LICENSE.txt
|
55
|
+
- README.md
|
56
|
+
- Rakefile
|
57
|
+
- bin/console
|
58
|
+
- bin/setup
|
59
|
+
- frost.gemspec
|
60
|
+
- lib/frost.rb
|
61
|
+
- lib/frost/commitments.rb
|
62
|
+
- lib/frost/dkg.rb
|
63
|
+
- lib/frost/dkg/package.rb
|
64
|
+
- lib/frost/hash.rb
|
65
|
+
- lib/frost/nonce.rb
|
66
|
+
- lib/frost/polynomial.rb
|
67
|
+
- lib/frost/secret_share.rb
|
68
|
+
- lib/frost/signature.rb
|
69
|
+
- lib/frost/signing_key.rb
|
70
|
+
- lib/frost/version.rb
|
71
|
+
- sig/frost.rbs
|
72
|
+
homepage: https://github.com/azuchi/frostrb
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata:
|
76
|
+
homepage_uri: https://github.com/azuchi/frostrb
|
77
|
+
source_code_uri: https://github.com/azuchi/frostrb
|
78
|
+
changelog_uri: https://github.com/azuchi/frostrb
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 3.0.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.5.3
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Ruby implementations of Two-Round Threshold Schnorr Signatures with FROST.
|
98
|
+
test_files: []
|