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 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.