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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 726ee90533a30264534d3ba2fff1e967334c56e870a25e778d86688001e6a5e2
4
- data.tar.gz: 50b8e0df0e3c5bacb276b5dc77b088e2b59dddf6485c8f62ba95f68f4a4a4e3d
3
+ metadata.gz: 69bdb0abd7ac947cf22620a9255558c156f33dd9176a4624303aa331e5c47604
4
+ data.tar.gz: 251e9488c10c854c94ce4335382616c3efbb4b976797c5cecf6bb80925bdfa1a
5
5
  SHA512:
6
- metadata.gz: 0ac0a2ec193d10e41cba96f1a5071998abf6602a0cf05fc44dc8bdd82853fb392f554857d6632cb1d3c655d7d0155c22efaea20ec0b0d089ee690de4a7443af0
7
- data.tar.gz: 8b9023e307606166981b1ac136fb6e84382a0de003175fc67bc6e07624ae997c0554674ad96f5f56782539427d2d1cf441b993b78b9a62f0812881538604dd5a
6
+ metadata.gz: 11b9401858d45aecac6f26564c31cbb3829233ce9ada0dee75d7456b95d2ec7299522e47691636da7cc54e110b936343b8862afef2184a610ba51f5095df8b4d
7
+ data.tar.gz: c823d9c89c26494389f53e3502b4a450d6a1dc0a5ad6a4ca5cb39bfdf154e97c893f16b9cbb6ef84bcc3f05ecb87f4edb960b15e8ab69ddaee562aca55aeb263
@@ -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-version: ['2.6', '2.7', '3.0']
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
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
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-version }}
33
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
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.0.0
1
+ ruby-3.2.0
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # bip-schnorrrb [![Build Status](https://travis-ci.org/chaintope/bip-schnorrrb.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)
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
- * `#has_even_y?` check the y-coordinate of this point is an even.
78
- * `#encode(only_x = false)` encode this point into a binary string.
79
- * `ECDSA::Format::PointOctetString#decode` supports decoding only from x coordinate.
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.
@@ -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 "ecdsa", "~> 1.2.0"
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"
@@ -14,7 +14,11 @@ module ECDSA
14
14
  if only_x
15
15
  ECDSA::Format::FieldElementOctetString.encode(x, group.field)
16
16
  else
17
- ECDSA::Format::PointOctetString.encode(self, {compression: true})
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, 1
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
@@ -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].unpack1('H*').to_i(16)
26
- s = string[32..-1].unpack1('H*').to_i(16)
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Schnorr
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/schnorr.rb CHANGED
@@ -1,36 +1,43 @@
1
- require 'ecdsa'
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 (String) A message to be signed with binary format.
13
- # @param private_key (String) The private key with binary format.
14
- # @param aux_rand (String) The auxiliary random data with binary format.
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 (Schnorr::Signature)
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.unpack1('H*').to_i(16)
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
- raise 'aux_rand must be 32 bytes.' if !aux_rand.nil? && aux_rand.bytesize != 32
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.new_point(d0)
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).unpack1('H*').to_i(16)
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.new_point(k0)
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 (String) A message to be signed with binary format.
45
- # @param public_key (String) The public key with binary format.
46
- # @param signature (String) The signature with binary format.
47
- # @return (Boolean) whether signature is valid.
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 (String) A message to be signed with binary format.
56
- # @param public_key (String) The public key with binary format.
57
- # @param signature (String) The signature with binary format.
58
- # @return (Boolean)
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 (Integer) x a x coordinate for R.
85
- # @param (ECDSA::Point) p a public key.
86
- # @return (Integer) digest e.
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 (String) tag tag value.
94
- # @param (String) msg the message to be hashed.
95
- # @return (String) the hash value with binary format.
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.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: 2021-06-29 00:00:00.000000000 Z
11
+ date: 2023-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ecdsa
14
+ name: ecdsa_ext
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.0
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: 1.2.0
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.2.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.