frostrb 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-01-29
4
+
5
+ - Initial release
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in frost.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FROST
4
+ VERSION = "0.1.1"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module FROST
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []