bip-schnorr 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +9 -9
- data/.ruby-version +1 -1
- data/README.md +75 -4
- data/bip-schnorrrb.gemspec +1 -1
- data/lib/schnorr/ec_point_ext.rb +7 -2
- data/lib/schnorr/musig2/context/key_agg.rb +46 -0
- data/lib/schnorr/musig2/context/session.rb +124 -0
- data/lib/schnorr/musig2.rb +207 -0
- data/lib/schnorr/signature.rb +8 -2
- data/lib/schnorr/util.rb +28 -0
- data/lib/schnorr/version.rb +1 -1
- data/lib/schnorr.rb +45 -26
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69bdb0abd7ac947cf22620a9255558c156f33dd9176a4624303aa331e5c47604
|
4
|
+
data.tar.gz: 251e9488c10c854c94ce4335382616c3efbb4b976797c5cecf6bb80925bdfa1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11b9401858d45aecac6f26564c31cbb3829233ce9ada0dee75d7456b95d2ec7299522e47691636da7cc54e110b936343b8862afef2184a610ba51f5095df8b4d
|
7
|
+
data.tar.gz: c823d9c89c26494389f53e3502b4a450d6a1dc0a5ad6a4ca5cb39bfdf154e97c893f16b9cbb6ef84bcc3f05ecb87f4edb960b15e8ab69ddaee562aca55aeb263
|
data/.github/workflows/ruby.yml
CHANGED
@@ -15,21 +15,21 @@ on:
|
|
15
15
|
|
16
16
|
jobs:
|
17
17
|
test:
|
18
|
-
|
19
18
|
runs-on: ubuntu-latest
|
19
|
+
name: Ruby ${{ matrix.ruby }}
|
20
20
|
strategy:
|
21
21
|
matrix:
|
22
|
-
ruby
|
23
|
-
|
22
|
+
ruby:
|
23
|
+
- '2.7.7'
|
24
|
+
- '3.0.5'
|
25
|
+
- '3.1.3'
|
26
|
+
- '3.2.1'
|
24
27
|
steps:
|
25
28
|
- uses: actions/checkout@v2
|
26
29
|
- name: Set up Ruby
|
27
|
-
|
28
|
-
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
29
|
-
# uses: ruby/setup-ruby@v1
|
30
|
-
uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
30
|
+
uses: ruby/setup-ruby@v1
|
31
31
|
with:
|
32
|
-
ruby-version: ${{ matrix.ruby
|
33
|
-
bundler-cache: true
|
32
|
+
ruby-version: ${{ matrix.ruby }}
|
33
|
+
bundler-cache: true
|
34
34
|
- name: Run tests
|
35
35
|
run: bundle exec rake spec
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-3.
|
1
|
+
ruby-3.2.0
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# bip-schnorrrb [![Build Status](https://
|
1
|
+
# bip-schnorrrb [![Build Status](https://github.com/chaintope/bip-schnorrrb/actions/workflows/ruby.yml/badge.svg?branch=master)](https://travis-ci.org/chaintope/bip-schnorrrb) [![Gem Version](https://badge.fury.io/rb/bip-schnorr.svg)](https://badge.fury.io/rb/bip-schnorr) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
|
2
2
|
|
3
3
|
This is a Ruby implementation of the Schnorr signature scheme over the elliptic curve.
|
4
4
|
This implementation relies on the [ecdsa gem](https://github.com/DavidEGrayson/ruby_ecdsa) for operate elliptic curves.
|
@@ -69,11 +69,82 @@ result = Schnorr.valid_sig?(message, public_key, signature)
|
|
69
69
|
sig = Schnorr::Signature.decode(signature)
|
70
70
|
```
|
71
71
|
|
72
|
+
### MuSig2*
|
73
|
+
|
74
|
+
This library support MuSig2* as defined [BIP-327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki).
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require 'schnorr'
|
78
|
+
|
79
|
+
sk1 = 1 + SecureRandom.random_number(Schnorr::GROUP.order - 1)
|
80
|
+
pk1 = (Schnorr::GROUP.generator.to_jacobian * sk1).to_affine.encode
|
81
|
+
|
82
|
+
sk2 = 1 + SecureRandom.random_number(Schnorr::GROUP.order - 1)
|
83
|
+
pk2 = (Schnorr::GROUP.generator.to_jacobian * sk2).to_affine.encode
|
84
|
+
|
85
|
+
pubkeys = [pk1, pk2]
|
86
|
+
|
87
|
+
# Key aggregation.
|
88
|
+
agg_ctx = Schnorr::MuSig2.aggregate(pubkeys)
|
89
|
+
# if you have tweak value.
|
90
|
+
agg_ctx = Schnorr::MuSig2.aggregate_with_tweaks(pubkeys, tweaks, modes)
|
91
|
+
|
92
|
+
## Aggregated pubkey is
|
93
|
+
### Return point:
|
94
|
+
agg_ctx.q
|
95
|
+
### Return x-only pubkey string
|
96
|
+
agg_ctx.x_only_pubkey
|
97
|
+
|
98
|
+
msg = SecureRandom.bytes(32)
|
99
|
+
|
100
|
+
# Generate secret nonce and public nonce.
|
101
|
+
sec_nonce1, pub_nonce1 = Schnorr::MuSig2.gen_nonce(
|
102
|
+
pk: pk1,
|
103
|
+
sk: sk1, # optional
|
104
|
+
agg_pubkey: agg_ctx.x_only_pubkey, # optional
|
105
|
+
msg: msg, # optional
|
106
|
+
extra_in: SecureRandom.bytes(4), # optional
|
107
|
+
rand: SecureRandom.bytes(32) # optional
|
108
|
+
)
|
109
|
+
|
110
|
+
## for stateless signer.
|
111
|
+
agg_other_nonce = described_class.aggregate_nonce([pub_nonce1])
|
112
|
+
pub_nonce2, sig2 = described_class.deterministic_sign(
|
113
|
+
sk2, agg_other_nonce, pubkeys, msg,
|
114
|
+
tweaks: tweaks, # optional
|
115
|
+
modes: modes, # optional
|
116
|
+
rand: SecureRandom.bytes(32) # optional
|
117
|
+
)
|
118
|
+
|
119
|
+
# Nonce aggregation
|
120
|
+
agg_nonce = Schnorr::MuSig2.aggregate_nonce([pub_nonce1, pub_nonce2])
|
121
|
+
|
122
|
+
# Generate partial signature.
|
123
|
+
session_ctx = Schnorr::MuSig2::SessionContext.new(
|
124
|
+
agg_nonce, pubkeys, msg,
|
125
|
+
tweaks, # optional
|
126
|
+
modes # optional
|
127
|
+
)
|
128
|
+
sig1 = session_ctx.sign(sec_nonce1, sk1)
|
129
|
+
|
130
|
+
# Verify partial signature.
|
131
|
+
signer_index = 0
|
132
|
+
session_ctx.valid_partial_sig?(sig1, pub_nonce1, signer_index)
|
133
|
+
|
134
|
+
# Signature aggregation.
|
135
|
+
sig = session_ctx.aggregate_partial_sigs([sig1, sig2])
|
136
|
+
|
137
|
+
# Verify signature.
|
138
|
+
Schnorr.valid_sig?(msg, agg_ctx.x_only_pubkey, sig.encode)
|
139
|
+
```
|
140
|
+
|
72
141
|
## Note
|
73
142
|
|
74
143
|
This library changes the following functions of `ecdsa` gem in `lib/schnorr/ec_point_ext.rb`.
|
75
144
|
|
76
145
|
* `ECDSA::Point` class has following two instance methods.
|
77
|
-
|
78
|
-
|
79
|
-
* `ECDSA::Format::PointOctetString#decode
|
146
|
+
* `#has_even_y?` check the y-coordinate of this point is an even.
|
147
|
+
* `#encode(only_x = false)` encode this point into a binary string.
|
148
|
+
* `ECDSA::Format::PointOctetString#decode`:
|
149
|
+
* supports decoding only from x coordinate.
|
150
|
+
* decode 33 bytes of zeros as infinity points.
|
data/bip-schnorrrb.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.bindir = "exe"
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
|
-
spec.add_runtime_dependency "
|
23
|
+
spec.add_runtime_dependency "ecdsa_ext", "~> 0.5.0"
|
24
24
|
|
25
25
|
spec.add_development_dependency "bundler"
|
26
26
|
spec.add_development_dependency "rake", ">= 12.3.3"
|
data/lib/schnorr/ec_point_ext.rb
CHANGED
@@ -14,7 +14,11 @@ module ECDSA
|
|
14
14
|
if only_x
|
15
15
|
ECDSA::Format::FieldElementOctetString.encode(x, group.field)
|
16
16
|
else
|
17
|
-
|
17
|
+
if infinity?
|
18
|
+
"\x00" * 33
|
19
|
+
else
|
20
|
+
ECDSA::Format::PointOctetString.encode(self, {compression: true})
|
21
|
+
end
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
@@ -34,7 +38,8 @@ module ECDSA
|
|
34
38
|
else
|
35
39
|
case string[0].ord
|
36
40
|
when 0
|
37
|
-
check_length string,
|
41
|
+
check_length string, 33
|
42
|
+
raise DecodeError, 'Unrecognized infinity point.' unless ['00' * 33].pack('H*') == string
|
38
43
|
return group.infinity
|
39
44
|
when 2
|
40
45
|
decode_compressed string, group, 0
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Schnorr
|
2
|
+
module MuSig2
|
3
|
+
class KeyAggContext
|
4
|
+
include Schnorr::Util
|
5
|
+
|
6
|
+
attr_reader :q, :gacc, :tacc
|
7
|
+
|
8
|
+
# @param [ECDSA::Point] q Aggregated point.
|
9
|
+
# @param [Integer] gacc
|
10
|
+
# @param [Integer] tacc
|
11
|
+
def initialize(q, gacc, tacc)
|
12
|
+
raise ArgumentError, 'The gacc must be Integer.' unless gacc.is_a?(Integer)
|
13
|
+
raise ArgumentError, 'The tacc must be Integer.' unless tacc.is_a?(Integer)
|
14
|
+
raise ArgumentError, 'The q must be ECDSA::Point.' unless q.is_a?(ECDSA::Point)
|
15
|
+
@q = q
|
16
|
+
@gacc = gacc
|
17
|
+
@tacc = tacc
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get x-only public key.
|
21
|
+
# @return [String] x-only public key(hex format).
|
22
|
+
def x_only_pubkey
|
23
|
+
q.encode(true).unpack1('H*')
|
24
|
+
end
|
25
|
+
|
26
|
+
# Tweaking the aggregate public key
|
27
|
+
# @param [String] tweak 32 bytes tweak value.
|
28
|
+
# @param [Boolean] is_xonly_t Tweak mode.
|
29
|
+
# @return [Schnorr::MuSig2::KeyAggContext] Tweaked context.
|
30
|
+
def apply_tweak(tweak, is_xonly_t)
|
31
|
+
tweak = hex2bin(tweak)
|
32
|
+
raise ArgumentError, 'The tweak must be a 32-bytes.' unless tweak.bytesize == 32
|
33
|
+
|
34
|
+
g = is_xonly_t && !q.has_even_y? ? q.group.order - 1 : 1
|
35
|
+
t = tweak.bti
|
36
|
+
|
37
|
+
raise ArgumentError, 'The tweak must be less than curve order.' if t >= q.group.order
|
38
|
+
new_q = (q.to_jacobian * g + q.group.generator.to_jacobian * t).to_affine
|
39
|
+
raise ArgumentError, 'The result of tweaking cannot be infinity.' if new_q.infinity?
|
40
|
+
new_gacc = (g * gacc) % q.group.order
|
41
|
+
new_tacc = (t + g * tacc) % q.group.order
|
42
|
+
KeyAggContext.new(new_q, new_gacc, new_tacc)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Schnorr
|
2
|
+
module MuSig2
|
3
|
+
class SessionContext
|
4
|
+
include Schnorr::Util
|
5
|
+
attr_reader :agg_nonce, :pubkeys, :tweaks, :modes, :msg, :r, :agg_ctx, :b, :used_secnonces
|
6
|
+
|
7
|
+
# @param [String] agg_nonce
|
8
|
+
# @param [Array(String)] pubkeys An array of public keys.
|
9
|
+
# @param [String] msg A message to be signed.
|
10
|
+
# @param [Array(String)] tweaks An array of tweaks(32 bytes).
|
11
|
+
# @param [Array(Boolean)] modes An array of tweak mode(Boolean).
|
12
|
+
def initialize(agg_nonce, pubkeys, msg, tweaks = [], modes = [])
|
13
|
+
@used_secnonces = []
|
14
|
+
@agg_nonce = hex2bin(agg_nonce)
|
15
|
+
@pubkeys = pubkeys.map do |pubkey|
|
16
|
+
pubkey = hex2bin(pubkey)
|
17
|
+
raise ArgumentError, 'pubkey must be 33 bytes' unless pubkey.bytesize == 33
|
18
|
+
pubkey
|
19
|
+
end
|
20
|
+
@msg = hex2bin(msg)
|
21
|
+
@tweaks = tweaks
|
22
|
+
@modes = modes
|
23
|
+
@modes.each do |mode|
|
24
|
+
raise ArgumentError, 'mode must be Boolean.' unless [TrueClass, FalseClass].include?(mode.class)
|
25
|
+
end
|
26
|
+
@agg_ctx = MuSig2.aggregate_with_tweaks(@pubkeys, @tweaks, @modes)
|
27
|
+
@b = Schnorr.tagged_hash('MuSig/noncecoef', @agg_nonce + agg_ctx.q.encode(true) + @msg).bti
|
28
|
+
begin
|
29
|
+
r1 = string2point(@agg_nonce[0...33]).to_jacobian
|
30
|
+
r2 = string2point(@agg_nonce[33...66]).to_jacobian
|
31
|
+
rescue ECDSA::Format::DecodeError
|
32
|
+
raise ArgumentError, 'Invalid agg_nonce'
|
33
|
+
end
|
34
|
+
r = (r1 + r2 * @b).to_affine
|
35
|
+
@r = r.infinity? ? GROUP.generator : r
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get message digest.
|
39
|
+
# @return [Integer]
|
40
|
+
def e
|
41
|
+
Schnorr.tagged_hash('BIP0340/challenge', r.encode(true) + agg_ctx.q.encode(true) + msg).bti
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create partial signature.
|
45
|
+
# @param [String] nonce The secret nonce.
|
46
|
+
# @param [String] sk The secret key.
|
47
|
+
# @return [String] Partial signature with hex format.
|
48
|
+
def sign(nonce, sk)
|
49
|
+
nonce = hex2bin(nonce)
|
50
|
+
raise ArgumentError, 'Same nonce already used.' if used_secnonces.include?(nonce)
|
51
|
+
sk = hex2bin(sk)
|
52
|
+
k1 = nonce[0...32].bti
|
53
|
+
k2 = nonce[32...64].bti
|
54
|
+
raise ArgumentError, 'first nonce value is out of range.' if k1 <= 0 || GROUP.order <= k1
|
55
|
+
raise ArgumentError, 'second nonce value is out of range.' if k2 <= 0 || GROUP.order <= k2
|
56
|
+
k1 = r.has_even_y? ? k1 : GROUP.order - k1
|
57
|
+
k2 = r.has_even_y? ? k2 : GROUP.order - k2
|
58
|
+
d = sk.bti
|
59
|
+
raise ArgumentError, 'secret key value is out of range.' if d <= 0 || GROUP.order <= d
|
60
|
+
p = (GROUP.generator.to_jacobian * d).to_affine.encode
|
61
|
+
raise ArgumentError, 'Public key does not match nonce_gen argument' unless p == nonce[64...97]
|
62
|
+
a = key_agg_coeff(pubkeys, p)
|
63
|
+
g = agg_ctx.q.has_even_y? ? 1 : GROUP.order - 1
|
64
|
+
d = (g * agg_ctx.gacc * d) % GROUP.order
|
65
|
+
s = (k1 + b * k2 + e * a * d) % GROUP.order
|
66
|
+
r1 = (GROUP.generator.to_jacobian * k1).to_affine
|
67
|
+
r2 = (GROUP.generator.to_jacobian * k2).to_affine
|
68
|
+
raise ArgumentError, 'R1 can not be infinity.' if r1.infinity?
|
69
|
+
raise ArgumentError, 'R2 can not be infinity.' if r2.infinity?
|
70
|
+
used_secnonces << nonce
|
71
|
+
ECDSA::Format::IntegerOctetString.encode(s, GROUP.byte_length).unpack1('H*')
|
72
|
+
end
|
73
|
+
|
74
|
+
# Verify partial signature.
|
75
|
+
# @param [String] partial_sig The partial signature.
|
76
|
+
# @param [String] pub_nonce A public nonce.
|
77
|
+
# @param [Integer] signer_index The index of signer.
|
78
|
+
# @return [Boolean]
|
79
|
+
def valid_partial_sig?(partial_sig, pub_nonce, signer_index)
|
80
|
+
begin
|
81
|
+
partial_sig = hex2bin(partial_sig)
|
82
|
+
pub_nonce = hex2bin(pub_nonce)
|
83
|
+
s = partial_sig.bti
|
84
|
+
return false if s >= GROUP.order
|
85
|
+
r1 = string2point(pub_nonce[0...33]).to_jacobian
|
86
|
+
r2 = string2point(pub_nonce[33...66]).to_jacobian
|
87
|
+
r_s = (r1 + r2 * b).to_affine
|
88
|
+
r_s = r.has_even_y? ? r_s : r_s.negate
|
89
|
+
pk = string2point(pubkeys[signer_index])
|
90
|
+
a = key_agg_coeff(pubkeys, pubkeys[signer_index])
|
91
|
+
g = agg_ctx.q.has_even_y? ? 1 : GROUP.order - 1
|
92
|
+
g = (g * agg_ctx.gacc) % GROUP.order
|
93
|
+
GROUP.generator.to_jacobian * s == r_s.to_jacobian + pk.to_jacobian * (e * a * g % GROUP.order)
|
94
|
+
rescue ECDSA::Format::DecodeError => e
|
95
|
+
raise ArgumentError, e
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Aggregate partial signatures.
|
100
|
+
# @param [Array] partial_sigs An array of partial signature.
|
101
|
+
# @return [Schnorr::Signature] An aggregated signature.
|
102
|
+
def aggregate_partial_sigs(partial_sigs)
|
103
|
+
s = 0
|
104
|
+
partial_sigs.each do |partial_sig|
|
105
|
+
s_i = hex2bin(partial_sig).bti
|
106
|
+
raise ArgumentError, 'Invalid partial sig.' if s_i >= GROUP.order
|
107
|
+
s = (s + s_i) % GROUP.order
|
108
|
+
end
|
109
|
+
g = agg_ctx.q.has_even_y? ? 1 : GROUP.order - 1
|
110
|
+
s = (s + e * g * agg_ctx.tacc) % GROUP.order
|
111
|
+
Schnorr::Signature.new(r.x, s)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def key_agg_coeff(pubkeys, public_key)
|
117
|
+
raise ArgumentError, 'The signer\'s pubkey must be included in the list of pubkeys.' unless pubkeys.include?(public_key)
|
118
|
+
l = MuSig2.hash_keys(pubkeys)
|
119
|
+
pk2 = MuSig2.second_key(pubkeys)
|
120
|
+
public_key == pk2 ? 1 : Schnorr.tagged_hash('KeyAgg coefficient', l + public_key).bti
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require_relative 'musig2/context/key_agg'
|
2
|
+
require_relative 'musig2/context/session'
|
3
|
+
|
4
|
+
module Schnorr
|
5
|
+
module MuSig2
|
6
|
+
extend Util
|
7
|
+
|
8
|
+
class Error < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# Sort the list of public keys in lexicographical order.
|
14
|
+
# @param [Array(String)] pubkeys An array of public keys.
|
15
|
+
# @return [Array(String)] Sorted public keys with hex format.
|
16
|
+
def sort_pubkeys(pubkeys)
|
17
|
+
pubkeys.map{|p| hex_string?(p) ? p : p.unpack1('H*')}.sort
|
18
|
+
end
|
19
|
+
|
20
|
+
# Compute aggregate public key.
|
21
|
+
# @param [Array[String]] pubkeys An array of public keys.
|
22
|
+
# @return [Schnorr::MuSig2::KeyAggContext]
|
23
|
+
def aggregate(pubkeys)
|
24
|
+
pubkeys = pubkeys.map do |p|
|
25
|
+
pubkey = hex2bin(p)
|
26
|
+
raise ArgumentError, "Public key must be 33 bytes." unless pubkey.bytesize == 33
|
27
|
+
pubkey
|
28
|
+
end
|
29
|
+
pk2 = second_key(pubkeys)
|
30
|
+
q = ECDSA::Ext::JacobianPoint.infinity_point(GROUP)
|
31
|
+
l = hash_keys(pubkeys)
|
32
|
+
pubkeys.each do |p|
|
33
|
+
begin
|
34
|
+
point = string2point(p).to_jacobian
|
35
|
+
rescue ECDSA::Format::DecodeError
|
36
|
+
raise ArgumentError, 'Invalid public key.'
|
37
|
+
end
|
38
|
+
coeff = p == pk2 ? 1 : Schnorr.tagged_hash('KeyAgg coefficient', l + p).bti
|
39
|
+
q += point * coeff
|
40
|
+
end
|
41
|
+
KeyAggContext.new(q.to_affine, 1, 0)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Compute aggregate public key with tweaks.
|
45
|
+
# @param [Array[String]] pubkeys An array of public keys.
|
46
|
+
# @param [Array] tweaks An array of tweak.
|
47
|
+
# @param [Array] modes An array of x_only mode.
|
48
|
+
# @return [Schnorr::MuSig2::KeyAggContext]
|
49
|
+
def aggregate_with_tweaks(pubkeys, tweaks, modes)
|
50
|
+
raise ArgumentError, 'tweaks and modes must be same length' unless tweaks.length == modes.length
|
51
|
+
agg_ctx = aggregate(pubkeys)
|
52
|
+
tweaks.each.with_index do |tweak, i|
|
53
|
+
tweak = hex2bin(tweak)
|
54
|
+
raise ArgumentError, 'tweak value must be 32 bytes' unless tweak.bytesize == 32
|
55
|
+
agg_ctx = agg_ctx.apply_tweak(tweak, modes[i])
|
56
|
+
end
|
57
|
+
agg_ctx
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate nonce.
|
61
|
+
# @param [String] pk The public key (33 bytes).
|
62
|
+
# @param [String] sk (Optional) The secret key string (32 bytes).
|
63
|
+
# @param [String] agg_pubkey (Optional) The aggregated public key (32 bytes).
|
64
|
+
# @param [String] msg (Optional) The message to be signed.
|
65
|
+
# @param [String] extra_in (Optional) The auxiliary input.
|
66
|
+
# @param [String] rand (Optional) A 32-byte array freshly drawn uniformly at random.
|
67
|
+
# @return [Array(String)] The array of sec nonce and pub nonce with hex format.
|
68
|
+
def gen_nonce(pk: , sk: nil, agg_pubkey: nil, msg: nil, extra_in: nil, rand: SecureRandom.bytes(32))
|
69
|
+
rand = hex2bin(rand)
|
70
|
+
raise ArgumentError, 'The rand must be 32 bytes.' unless rand.bytesize == 32
|
71
|
+
|
72
|
+
pk = hex2bin(pk)
|
73
|
+
raise ArgumentError, 'The pk must be 33 bytes.' unless pk.bytesize == 33
|
74
|
+
|
75
|
+
rand = if sk.nil?
|
76
|
+
rand
|
77
|
+
else
|
78
|
+
sk = hex2bin(sk)
|
79
|
+
raise ArgumentError, "The sk must be 32 bytes." unless sk.bytesize == 32
|
80
|
+
gen_aux(sk, rand)
|
81
|
+
end
|
82
|
+
agg_pubkey = if agg_pubkey
|
83
|
+
agg_pubkey = hex2bin(agg_pubkey)
|
84
|
+
raise ArgumentError, 'The agg_pubkey must be 33 bytes.' unless agg_pubkey.bytesize == 32
|
85
|
+
agg_pubkey
|
86
|
+
else
|
87
|
+
''
|
88
|
+
end
|
89
|
+
msg_prefixed = if msg.nil?
|
90
|
+
[0].pack('C')
|
91
|
+
else
|
92
|
+
msg = hex2bin(msg)
|
93
|
+
[1, msg.bytesize].pack('CQ>') + msg
|
94
|
+
end
|
95
|
+
extra_in = extra_in ? hex2bin(extra_in) : ''
|
96
|
+
|
97
|
+
k1 = nonce_hash(rand, pk, agg_pubkey, 0, msg_prefixed, extra_in)
|
98
|
+
k1_i = k1.bti % GROUP.order
|
99
|
+
k2 = nonce_hash(rand, pk, agg_pubkey, 1, msg_prefixed, extra_in)
|
100
|
+
k2_i = k2.bti % GROUP.order
|
101
|
+
raise ArgumentError, 'k1 must not be zero.' if k1_i.zero?
|
102
|
+
raise ArgumentError, 'k2 must not be zero.' if k2_i.zero?
|
103
|
+
|
104
|
+
r1 = (GROUP.generator.to_jacobian * k1_i).to_affine
|
105
|
+
r2 = (GROUP.generator.to_jacobian * k2_i).to_affine
|
106
|
+
pub_nonce = r1.encode + r2.encode
|
107
|
+
sec_nonce = k1 + k2 + pk
|
108
|
+
[sec_nonce.unpack1('H*'), pub_nonce.unpack1('H*')]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Aggregate public nonces.
|
112
|
+
# @param [Array] pub_nonces Array of public nonce. Each public nonce consists 66 bytes.
|
113
|
+
# @return [String] An aggregated public nonce(R1 || R2) with hex format.
|
114
|
+
def aggregate_nonce(pub_nonces)
|
115
|
+
2.times.map do |i|
|
116
|
+
r = GROUP.generator.to_jacobian.infinity_point
|
117
|
+
pub_nonces = pub_nonces.each do |nonce|
|
118
|
+
nonce = hex2bin(nonce)
|
119
|
+
raise ArgumentError, "" unless nonce.bytesize == 66
|
120
|
+
begin
|
121
|
+
p = string2point(nonce[(i * 33)...(i + 1)*33]).to_jacobian
|
122
|
+
raise ArgumentError, 'Public nonce is infinity' if p.infinity?
|
123
|
+
rescue ECDSA::Format::DecodeError
|
124
|
+
raise ArgumentError, "Invalid public nonce."
|
125
|
+
end
|
126
|
+
r += p
|
127
|
+
end
|
128
|
+
r.to_affine.encode.unpack1('H*')
|
129
|
+
end.join
|
130
|
+
end
|
131
|
+
|
132
|
+
# Generate deterministic signature.
|
133
|
+
# @param [String] sk The secret key string (32 bytes).
|
134
|
+
# @param [String] agg_other_nonce Other aggregated nonce.
|
135
|
+
# @param [Array] pubkeys An array of public keys.
|
136
|
+
# @param [String] msg The message to be signed.
|
137
|
+
# @param [Array(String)] tweaks (Optional) An array of tweak value.
|
138
|
+
# @param [Array(Boolean)] modes (Optional) An array of tweak mode.
|
139
|
+
# @param [String] rand (Optional) A 32-byte array freshly drawn uniformly at random.
|
140
|
+
# @return [Array] [public nonce, partial signature]
|
141
|
+
def deterministic_sign(sk, agg_other_nonce, pubkeys, msg, tweaks: [], modes: [], rand: nil)
|
142
|
+
raise ArgumentError, 'The tweaks and modes arrays must have the same length.' unless tweaks.length == modes.length
|
143
|
+
sk = hex2bin(sk)
|
144
|
+
msg = hex2bin(msg)
|
145
|
+
agg_other_nonce = hex2bin(agg_other_nonce)
|
146
|
+
sk_ = rand ? gen_aux(sk, hex2bin(rand)) : sk
|
147
|
+
agg_ctx = aggregate_with_tweaks(pubkeys, tweaks, modes)
|
148
|
+
agg_pk = [agg_ctx.x_only_pubkey].pack("H*")
|
149
|
+
k1 = deterministic_nonce_hash(sk_, agg_other_nonce, agg_pk, msg, 0).bti
|
150
|
+
k2 = deterministic_nonce_hash(sk_, agg_other_nonce, agg_pk, msg, 1).bti
|
151
|
+
r1 = (GROUP.generator.to_jacobian * k1).to_affine
|
152
|
+
r2 = (GROUP.generator.to_jacobian * k2).to_affine
|
153
|
+
raise ArgumentError, 'R1 must not be infinity.' if r1.infinity?
|
154
|
+
raise ArgumentError, 'R2 must not be infinity.' if r2.infinity?
|
155
|
+
pub_nonce = r1.encode + r2.encode
|
156
|
+
pk = (GROUP.generator.to_jacobian * sk.bti).to_affine
|
157
|
+
sec_nonce = ECDSA::Format::IntegerOctetString.encode(k1, GROUP.byte_length) +
|
158
|
+
ECDSA::Format::IntegerOctetString.encode(k2, GROUP.byte_length) + pk.encode
|
159
|
+
agg_nonce = aggregate_nonce([pub_nonce, agg_other_nonce])
|
160
|
+
ctx = SessionContext.new(agg_nonce, pubkeys, msg, tweaks, modes)
|
161
|
+
sig = ctx.sign(sec_nonce, sk)
|
162
|
+
[pub_nonce.unpack1('H*'), sig]
|
163
|
+
end
|
164
|
+
|
165
|
+
def second_key(pubkeys)
|
166
|
+
pubkeys[1..].each do |p|
|
167
|
+
return p unless p == pubkeys[0]
|
168
|
+
end
|
169
|
+
['00'].pack("H*") * 33
|
170
|
+
end
|
171
|
+
|
172
|
+
# Compute
|
173
|
+
def hash_keys(pubkeys)
|
174
|
+
Schnorr.tagged_hash('KeyAgg list', pubkeys.join)
|
175
|
+
end
|
176
|
+
|
177
|
+
def nonce_hash(rand, pk, agg_pubkey, i, prefixed_msg, extra_in)
|
178
|
+
buf = ''
|
179
|
+
buf << rand
|
180
|
+
buf << [pk.bytesize].pack('C') + pk
|
181
|
+
buf << [agg_pubkey.bytesize].pack('C') + agg_pubkey
|
182
|
+
buf << prefixed_msg
|
183
|
+
buf << [extra_in.bytesize].pack('N') + extra_in
|
184
|
+
buf << [i].pack('C')
|
185
|
+
Schnorr.tagged_hash('MuSig/nonce', buf)
|
186
|
+
end
|
187
|
+
private_class_method :nonce_hash
|
188
|
+
|
189
|
+
def gen_aux(sk, rand)
|
190
|
+
sk.unpack('C*').zip(Schnorr.tagged_hash('MuSig/aux', rand).
|
191
|
+
unpack('C*')).map{|a, b| a ^ b}.pack('C*')
|
192
|
+
end
|
193
|
+
private_class_method :gen_aux
|
194
|
+
|
195
|
+
def deterministic_nonce_hash(sk_, agg_other_nonce, agg_pk, msg, i)
|
196
|
+
buf = ''
|
197
|
+
buf << sk_
|
198
|
+
buf << agg_other_nonce
|
199
|
+
buf << agg_pk
|
200
|
+
buf << [msg.bytesize].pack('Q>')
|
201
|
+
buf << msg
|
202
|
+
buf << [i].pack('C')
|
203
|
+
Schnorr.tagged_hash('MuSig/deterministic/nonce', buf)
|
204
|
+
end
|
205
|
+
private_class_method :deterministic_nonce_hash
|
206
|
+
end
|
207
|
+
end
|
data/lib/schnorr/signature.rb
CHANGED
@@ -22,8 +22,8 @@ module Schnorr
|
|
22
22
|
# @return (Signature) signature instance.
|
23
23
|
def self.decode(string)
|
24
24
|
raise InvalidSignatureError, 'Invalid schnorr signature length.' unless string.bytesize == 64
|
25
|
-
r = string[0...32].
|
26
|
-
s = string[32..-1].
|
25
|
+
r = string[0...32].bti
|
26
|
+
s = string[32..-1].bti
|
27
27
|
new(r, s)
|
28
28
|
end
|
29
29
|
|
@@ -33,6 +33,12 @@ module Schnorr
|
|
33
33
|
ECDSA::Format::IntegerOctetString.encode(r, 32) + ECDSA::Format::IntegerOctetString.encode(s, 32)
|
34
34
|
end
|
35
35
|
|
36
|
+
# Check whether same signature or not.
|
37
|
+
# @return [Boolean]
|
38
|
+
def ==(other)
|
39
|
+
return false unless other.is_a?(Signature)
|
40
|
+
r == other.r && s == other.s
|
41
|
+
end
|
36
42
|
end
|
37
43
|
|
38
44
|
end
|
data/lib/schnorr/util.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Schnorr
|
2
|
+
module Util
|
3
|
+
|
4
|
+
# Check whether +str+ is hex string or not.
|
5
|
+
# @param [String] str string.
|
6
|
+
# @return [Boolean]
|
7
|
+
def hex_string?(str)
|
8
|
+
return false if str.bytes.any? { |b| b > 127 }
|
9
|
+
return false if str.length % 2 != 0
|
10
|
+
hex_chars = str.chars.to_a
|
11
|
+
hex_chars.all? { |c| c =~ /[0-9a-fA-F]/ }
|
12
|
+
end
|
13
|
+
|
14
|
+
# If +str+ is a hex value, it is converted to binary. Otherwise, it is returned as is.
|
15
|
+
# @param [String] str
|
16
|
+
# @return [String]
|
17
|
+
def hex2bin(str)
|
18
|
+
hex_string?(str) ? [str].pack('H*') : str
|
19
|
+
end
|
20
|
+
|
21
|
+
# Convert +str+ to the point of elliptic curve.
|
22
|
+
# @param [String] str A byte string for point.
|
23
|
+
# @return [ECDSA::Point]
|
24
|
+
def string2point(str)
|
25
|
+
ECDSA::Format::PointOctetString.decode(str, GROUP)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/schnorr/version.rb
CHANGED
data/lib/schnorr.rb
CHANGED
@@ -1,36 +1,43 @@
|
|
1
|
-
require '
|
1
|
+
require 'ecdsa_ext'
|
2
2
|
require 'securerandom'
|
3
|
+
require_relative 'schnorr/util'
|
3
4
|
require_relative 'schnorr/ec_point_ext'
|
4
5
|
require_relative 'schnorr/signature'
|
6
|
+
require_relative 'schnorr/musig2'
|
5
7
|
|
6
8
|
module Schnorr
|
9
|
+
extend Util
|
7
10
|
module_function
|
8
11
|
|
9
12
|
GROUP = ECDSA::Group::Secp256k1
|
10
13
|
|
11
14
|
# Generate schnorr signature.
|
12
|
-
# @param message
|
13
|
-
# @param private_key
|
14
|
-
# @param aux_rand
|
15
|
+
# @param [String] message A message to be signed with binary format.
|
16
|
+
# @param [String] private_key The private key(binary format or hex format).
|
17
|
+
# @param [String] aux_rand The auxiliary random data(binary format or hex format).
|
15
18
|
# If not specified, random data is not used and the private key is used to calculate the nonce.
|
16
|
-
# @return
|
19
|
+
# @return [Schnorr::Signature]
|
17
20
|
def sign(message, private_key, aux_rand = nil)
|
18
21
|
raise 'The message must be a 32-byte array.' unless message.bytesize == 32
|
22
|
+
private_key = private_key.unpack1('H*') unless hex_string?(private_key)
|
19
23
|
|
20
|
-
d0 = private_key.
|
24
|
+
d0 = private_key.to_i(16)
|
21
25
|
raise 'private_key must be an integer in the range 1..n-1.' unless 0 < d0 && d0 <= (GROUP.order - 1)
|
22
|
-
|
26
|
+
if aux_rand
|
27
|
+
aux_rand = [aux_rand].pack("H*") if hex_string?(aux_rand)
|
28
|
+
raise 'aux_rand must be 32 bytes.' unless aux_rand.bytesize == 32
|
29
|
+
end
|
23
30
|
|
24
|
-
p = GROUP.
|
31
|
+
p = (GROUP.generator.to_jacobian * d0).to_affine
|
25
32
|
d = p.has_even_y? ? d0 : GROUP.order - d0
|
26
33
|
|
27
|
-
t = aux_rand.nil? ? d : d ^ tagged_hash('BIP0340/aux', aux_rand).
|
34
|
+
t = aux_rand.nil? ? d : d ^ tagged_hash('BIP0340/aux', aux_rand).bti
|
28
35
|
t = ECDSA::Format::IntegerOctetString.encode(t, GROUP.byte_length)
|
29
36
|
|
30
37
|
k0 = ECDSA::Format::IntegerOctetString.decode(tagged_hash('BIP0340/nonce', t + p.encode(true) + message)) % GROUP.order
|
31
38
|
raise 'Creation of signature failed. k is zero' if k0.zero?
|
32
39
|
|
33
|
-
r = GROUP.
|
40
|
+
r = (GROUP.generator.to_jacobian * k0).to_affine
|
34
41
|
k = r.has_even_y? ? k0 : GROUP.order - k0
|
35
42
|
e = create_challenge(r.x, p, message)
|
36
43
|
|
@@ -41,10 +48,10 @@ module Schnorr
|
|
41
48
|
end
|
42
49
|
|
43
50
|
# Verifies the given {Signature} and returns true if it is valid.
|
44
|
-
# @param message
|
45
|
-
# @param public_key
|
46
|
-
# @param signature
|
47
|
-
# @return
|
51
|
+
# @param [String] message A message to be signed with binary format.
|
52
|
+
# @param [String] public_key The public key with binary format.
|
53
|
+
# @param [String] signature The signature with binary format.
|
54
|
+
# @return [Boolean] whether signature is valid.
|
48
55
|
def valid_sig?(message, public_key, signature)
|
49
56
|
check_sig!(message, public_key, signature)
|
50
57
|
rescue InvalidSignatureError, ECDSA::Format::DecodeError
|
@@ -52,12 +59,15 @@ module Schnorr
|
|
52
59
|
end
|
53
60
|
|
54
61
|
# Verifies the given {Signature} and raises an {InvalidSignatureError} if it is invalid.
|
55
|
-
# @param message
|
56
|
-
# @param public_key
|
57
|
-
# @param signature
|
58
|
-
# @return
|
62
|
+
# @param [String] message A message to be signed with binary format.
|
63
|
+
# @param [String] public_key The public key with binary format.
|
64
|
+
# @param [String] signature The signature with binary format.
|
65
|
+
# @return [Boolean]
|
59
66
|
def check_sig!(message, public_key, signature)
|
67
|
+
message = hex2bin(message)
|
68
|
+
public_key = hex2bin(public_key)
|
60
69
|
raise InvalidSignatureError, 'The message must be a 32-byte array.' unless message.bytesize == 32
|
70
|
+
public_key = [public_key].pack('H*') if hex_string?(public_key)
|
61
71
|
raise InvalidSignatureError, 'The public key must be a 32-byte array.' unless public_key.bytesize == 32
|
62
72
|
|
63
73
|
sig = Schnorr::Signature.decode(signature)
|
@@ -70,8 +80,7 @@ module Schnorr
|
|
70
80
|
raise Schnorr::InvalidSignatureError, 'Invalid signature: s is larger than group order.' if sig.s >= GROUP.order
|
71
81
|
|
72
82
|
e = create_challenge(sig.r, pubkey, message)
|
73
|
-
|
74
|
-
r = GROUP.new_point(sig.s) + pubkey.multiply_by_scalar(GROUP.order - e)
|
83
|
+
r = (GROUP.generator.to_jacobian * sig.s + pubkey.to_jacobian * (GROUP.order - e)).to_affine
|
75
84
|
|
76
85
|
if r.infinity? || !r.has_even_y? || r.x != sig.r
|
77
86
|
raise Schnorr::InvalidSignatureError, 'signature verification failed.'
|
@@ -81,23 +90,33 @@ module Schnorr
|
|
81
90
|
end
|
82
91
|
|
83
92
|
# create signature digest.
|
84
|
-
# @param
|
85
|
-
# @param
|
86
|
-
# @return
|
93
|
+
# @param [Integer] x A x coordinate for R.
|
94
|
+
# @param [ECDSA::Point] p A public key.
|
95
|
+
# @return [Integer] digest e.
|
87
96
|
def create_challenge(x, p, message)
|
88
97
|
r_x = ECDSA::Format::IntegerOctetString.encode(x, GROUP.byte_length)
|
89
98
|
(ECDSA.normalize_digest(tagged_hash('BIP0340/challenge', r_x + p.encode(true) + message), GROUP.bit_length)) % GROUP.order
|
90
99
|
end
|
91
100
|
|
92
101
|
# Generate tagged hash value.
|
93
|
-
# @param
|
94
|
-
# @param
|
95
|
-
# @return
|
102
|
+
# @param [String] tag tag value.
|
103
|
+
# @param [String] msg the message to be hashed.
|
104
|
+
# @return [String] the hash value with binary format.
|
96
105
|
def tagged_hash(tag, msg)
|
97
106
|
tag_hash = Digest::SHA256.digest(tag)
|
98
107
|
Digest::SHA256.digest(tag_hash + tag_hash + msg)
|
99
108
|
end
|
100
109
|
|
110
|
+
class ::String
|
111
|
+
|
112
|
+
# Convert binary to integer.
|
113
|
+
# @return [Integer]
|
114
|
+
def bti
|
115
|
+
self.unpack1('H*').to_i(16)
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
101
120
|
class ::Integer
|
102
121
|
def to_hex
|
103
122
|
hex = to_s(16)
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bip-schnorr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- azuchi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: ecdsa_ext
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.5.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 0.5.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,7 +88,11 @@ files:
|
|
88
88
|
- bip-schnorrrb.gemspec
|
89
89
|
- lib/schnorr.rb
|
90
90
|
- lib/schnorr/ec_point_ext.rb
|
91
|
+
- lib/schnorr/musig2.rb
|
92
|
+
- lib/schnorr/musig2/context/key_agg.rb
|
93
|
+
- lib/schnorr/musig2/context/session.rb
|
91
94
|
- lib/schnorr/signature.rb
|
95
|
+
- lib/schnorr/util.rb
|
92
96
|
- lib/schnorr/version.rb
|
93
97
|
homepage: https://github.com/chaintope/bip-schnorrrb
|
94
98
|
licenses:
|
@@ -109,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
113
|
- !ruby/object:Gem::Version
|
110
114
|
version: '0'
|
111
115
|
requirements: []
|
112
|
-
rubygems_version: 3.
|
116
|
+
rubygems_version: 3.4.1
|
113
117
|
signing_key:
|
114
118
|
specification_version: 4
|
115
119
|
summary: The ruby implementation of bip-schnorr.
|