bip-schnorr 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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 [](https://travis-ci.org/chaintope/bip-schnorrrb) [](https://badge.fury.io/rb/bip-schnorr) [](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.
|