bip-schnorr 0.7.0 → 0.8.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: 62086f5dc865721a602e4ac25f1c58920eba47d0e7bb5ab59d389af297095750
4
- data.tar.gz: 3e6ddfbc75956f04e84f01412b034974cb592ef1adea72775acdc949946d2ebf
3
+ metadata.gz: 1ee1e80d49aa7b7ef61e0d2b03894a6f5bc81f6cef32cb08cad62e88c2c42d91
4
+ data.tar.gz: 99f5898f8c5726dd88138c03febce6787a0bb8eff18f97b180a5af82ae52aef4
5
5
  SHA512:
6
- metadata.gz: 4f599b83ba35a76df8425eb7eb436740f690ec011dcfc6eac65af1591bb748d1421dd71d2cf15c480e44302bd821ffda6f385799549a37f2a8159a592c510482
7
- data.tar.gz: f7cb59f75a709bb05509697a55bd3864944a82d4a9768bc34db84da0b6a215b9060fa875722ebb597731f3df6a4155e738b419c413b5ef06278378f2b3055627
6
+ metadata.gz: 800862f45dbc98f9b82da03777b9373d5b910f3b8bf503741935851294e536fcccd3738c349e848424775e19d9b7661dc1b31b73c8394769ed76daace0e1b280
7
+ data.tar.gz: 54c26354859fe49d2d56e91c3895b11077aee81c97d47ce17aef0a24a6bd97ecf6f6771a22c04fcf738a59c28e37aaf8e194b9af8223116d39ab76932587f829
@@ -20,11 +20,12 @@ jobs:
20
20
  strategy:
21
21
  matrix:
22
22
  ruby:
23
- - '3.0.6'
24
- - '3.1.4'
25
- - '3.2.2'
23
+ - '3.2'
24
+ - '3.3'
25
+ - '3.4'
26
+ - '4.0'
26
27
  steps:
27
- - uses: actions/checkout@v2
28
+ - uses: actions/checkout@v4
28
29
  - name: Set up Ruby
29
30
  uses: ruby/setup-ruby@v1
30
31
  with:
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.2.0
1
+ ruby-4.0.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.8.0] - 2026-11-01
10
+
11
+ ### Fixed
12
+
13
+ - Signatures, secret nonces, public nonces and aggregated nonces are now sliced by bytes (`byteslice`).
14
+ Previously a binary string tagged as UTF-8 (e.g. read without binmode) could be split by characters and decoded incorrectly.
15
+ - Non-hex inputs are normalized to ASCII-8BIT, so signing a UTF-8 message containing non-ASCII characters no longer raises `Encoding::CompatibilityError`.
16
+ - `Schnorr.sign` now applies the same hex detection to the message as verification and MuSig2 do, so a hex-encoded message can be signed and verified consistently.
17
+ - `Schnorr.check_sig!` raises `Schnorr::InvalidSignatureError` instead of `ECDSA::Format::DecodeError` when the public key is not on the curve.
18
+ - `Schnorr::MuSig2.sort_pubkeys` sorts by raw bytes, so the order no longer depends on the letter case of hex input.
19
+ - `Schnorr::MuSig2.deterministic_sign` reduces the nonces modulo the group order and rejects zero, and `gen_nonce` stores the reduced nonces in the secret nonce, as specified in BIP-327.
20
+ - `Schnorr::MuSig2.aggregate` fails when the aggregate public key is the point at infinity, as specified in BIP-327.
21
+ - Fixed wrong or empty error messages in `gen_nonce` and `aggregate_nonce`.
22
+
23
+ ### Added
24
+
25
+ - Length validation for `agg_nonce` (66 bytes), secret nonces (97 bytes), public nonces (66 bytes), secret keys (32 bytes) and `rand` (32 bytes) in the MuSig2 API, and range validation of `signer_index` in `SessionContext#valid_partial_sig?`. Invalid inputs raise `ArgumentError` instead of obscure errors.
26
+ - `gemspec` declares `required_ruby_version >= 3.2`.
27
+ - This CHANGELOG.
28
+
29
+ ### Changed
30
+
31
+ - `Schnorr.sign` raises `ArgumentError` (instead of `RuntimeError`) for an invalid `private_key` or `aux_rand`.
32
+ - `SessionContext` keeps only SHA-256 digests of used secret nonces to detect reuse and no longer exposes `used_secnonces`.
33
+ - `SessionContext#sign` no longer performs two unnecessary scalar multiplications per partial signature.
34
+ - The challenge hash in `SessionContext` is computed once per session.
35
+ - Bumped `ecdsa_ext` to 0.6.0.
36
+
37
+ ### Removed
38
+
39
+ - The dead `Integer#method_missing` / `Integer#mod_pow` shim for Ruby 2.4 and the unused `Integer#to_hex`.
40
+
41
+ ### Notes
42
+
43
+ - Omitting `aux_rand` in `Schnorr.sign` still yields a deterministic signature (all-zero auxiliary data) for backward compatibility. Passing fresh randomness is recommended, as described in BIP-340.
data/Gemfile CHANGED
@@ -4,3 +4,5 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in bip-schnorrrb.gemspec
6
6
  gemspec
7
+
8
+ gem 'csv'
data/README.md CHANGED
@@ -1,4 +1,4 @@
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)
1
+ # bip-schnorrrb [![Build Status](https://github.com/chaintope/bip-schnorrrb/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/chaintope/bip-schnorrrb/actions/workflows/ruby.yml) [![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.
@@ -33,6 +33,8 @@ private_key = ['B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF
33
33
  message = ['5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C'].pack('H*')
34
34
 
35
35
  # create signature
36
+ # NOTE: if aux_rand is omitted, it is treated as 32 zero bytes and the signature is deterministic.
37
+ # It is recommended to pass fresh randomness as the 3rd argument (see BIP-340 "Default Signing").
36
38
  signature = Schnorr.sign(message, private_key)
37
39
  # if use auxiliary random data, specify it to the 3rd arguments.
38
40
  aux_rand = SecureRandom.bytes(32) # aux_rand must be a 32-byte binary.
@@ -108,8 +110,8 @@ sec_nonce1, pub_nonce1 = Schnorr::MuSig2.gen_nonce(
108
110
  )
109
111
 
110
112
  ## for stateless signer.
111
- agg_other_nonce = described_class.aggregate_nonce([pub_nonce1])
112
- pub_nonce2, sig2 = described_class.deterministic_sign(
113
+ agg_other_nonce = Schnorr::MuSig2.aggregate_nonce([pub_nonce1])
114
+ pub_nonce2, sig2 = Schnorr::MuSig2.deterministic_sign(
113
115
  sk2, agg_other_nonce, pubkeys, msg,
114
116
  tweaks: tweaks, # optional
115
117
  modes: modes, # optional
@@ -20,7 +20,8 @@ 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_ext", "~> 0.5.0"
23
+ spec.required_ruby_version = ">= 3.2"
24
+ spec.add_runtime_dependency "ecdsa_ext", "~> 0.6.0"
24
25
 
25
26
  spec.add_development_dependency "bundler"
26
27
  spec.add_development_dependency "rake", ">= 12.3.3"
@@ -54,6 +54,7 @@ module ECDSA
54
54
  end
55
55
 
56
56
  # decode from x coordinate.
57
+ # NOTE: This assumes secp256k1 (y^2 = x^3 + 7 and p = 3 mod 4), as used by BIP-340.
57
58
  # @param (String) x_string X-coordinate binary string
58
59
  # @param (ECDSA::Group) group A group of elliptic curves to use.
59
60
  # @return (ECDSA::Point) decoded point.
@@ -2,16 +2,17 @@ module Schnorr
2
2
  module MuSig2
3
3
  class SessionContext
4
4
  include Schnorr::Util
5
- attr_reader :agg_nonce, :pubkeys, :tweaks, :modes, :msg, :r, :agg_ctx, :b, :used_secnonces
5
+ attr_reader :agg_nonce, :pubkeys, :tweaks, :modes, :msg, :r, :agg_ctx, :b
6
6
 
7
7
  # @param [String] agg_nonce
8
8
  # @param [Array(String)] pubkeys An array of public keys.
9
- # @param [String] msg A message to be signed.
9
+ # @param [String] msg A message to be signed(binary format or hex format).
10
10
  # @param [Array(String)] tweaks An array of tweaks(32 bytes).
11
11
  # @param [Array(Boolean)] modes An array of tweak mode(Boolean).
12
12
  def initialize(agg_nonce, pubkeys, msg, tweaks = [], modes = [])
13
- @used_secnonces = []
13
+ @used_secnonce_digests = []
14
14
  @agg_nonce = hex2bin(agg_nonce)
15
+ raise ArgumentError, 'agg_nonce must be 66 bytes' unless @agg_nonce.bytesize == 66
15
16
  @pubkeys = pubkeys.map do |pubkey|
16
17
  pubkey = hex2bin(pubkey)
17
18
  raise ArgumentError, 'pubkey must be 33 bytes' unless pubkey.bytesize == 33
@@ -26,8 +27,8 @@ module Schnorr
26
27
  @agg_ctx = MuSig2.aggregate_with_tweaks(@pubkeys, @tweaks, @modes)
27
28
  @b = Schnorr.tagged_hash('MuSig/noncecoef', @agg_nonce + agg_ctx.q.encode(true) + @msg).bti
28
29
  begin
29
- r1 = string2point(@agg_nonce[0...33]).to_jacobian
30
- r2 = string2point(@agg_nonce[33...66]).to_jacobian
30
+ r1 = string2point(@agg_nonce.byteslice(0, 33)).to_jacobian
31
+ r2 = string2point(@agg_nonce.byteslice(33, 33)).to_jacobian
31
32
  rescue ECDSA::Format::DecodeError
32
33
  raise ArgumentError, 'Invalid agg_nonce'
33
34
  end
@@ -38,7 +39,7 @@ module Schnorr
38
39
  # Get message digest.
39
40
  # @return [Integer]
40
41
  def e
41
- Schnorr.tagged_hash('BIP0340/challenge', r.encode(true) + agg_ctx.q.encode(true) + msg).bti
42
+ @e ||= Schnorr.tagged_hash('BIP0340/challenge', r.encode(true) + agg_ctx.q.encode(true) + msg).bti
42
43
  end
43
44
 
44
45
  # Create partial signature.
@@ -47,27 +48,27 @@ module Schnorr
47
48
  # @return [String] Partial signature with hex format.
48
49
  def sign(nonce, sk)
49
50
  nonce = hex2bin(nonce)
50
- raise ArgumentError, 'Same nonce already used.' if used_secnonces.include?(nonce)
51
+ raise ArgumentError, 'nonce must be 97 bytes.' unless nonce.bytesize == 97
52
+ nonce_digest = Digest::SHA256.digest(nonce)
53
+ raise ArgumentError, 'Same nonce already used.' if @used_secnonce_digests.include?(nonce_digest)
51
54
  sk = hex2bin(sk)
52
- k1 = nonce[0...32].bti
53
- k2 = nonce[32...64].bti
55
+ raise ArgumentError, 'sk must be 32 bytes.' unless sk.bytesize == 32
56
+ k1 = nonce.byteslice(0, 32).bti
57
+ k2 = nonce.byteslice(32, 32).bti
54
58
  raise ArgumentError, 'first nonce value is out of range.' if k1 <= 0 || GROUP.order <= k1
55
59
  raise ArgumentError, 'second nonce value is out of range.' if k2 <= 0 || GROUP.order <= k2
56
60
  k1 = r.has_even_y? ? k1 : GROUP.order - k1
57
61
  k2 = r.has_even_y? ? k2 : GROUP.order - k2
58
62
  d = sk.bti
59
63
  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]
64
+ p = (GROUP.generator.to_jacobian * pad_scalar(d)).to_affine.encode
65
+ raise ArgumentError, 'Public key does not match nonce_gen argument' unless p == nonce.byteslice(64, 33)
62
66
  a = key_agg_coeff(pubkeys, p)
63
67
  g = agg_ctx.q.has_even_y? ? 1 : GROUP.order - 1
64
68
  d = (g * agg_ctx.gacc * d) % GROUP.order
65
69
  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
70
+ # Only a digest is kept so that the secret nonce itself is not retained in memory.
71
+ @used_secnonce_digests << nonce_digest
71
72
  ECDSA::Format::IntegerOctetString.encode(s, GROUP.byte_length).unpack1('H*')
72
73
  end
73
74
 
@@ -77,13 +78,17 @@ module Schnorr
77
78
  # @param [Integer] signer_index The index of signer.
78
79
  # @return [Boolean]
79
80
  def valid_partial_sig?(partial_sig, pub_nonce, signer_index)
81
+ unless signer_index.is_a?(Integer) && signer_index >= 0 && signer_index < pubkeys.length
82
+ raise ArgumentError, 'signer_index is out of range.'
83
+ end
80
84
  begin
81
85
  partial_sig = hex2bin(partial_sig)
82
86
  pub_nonce = hex2bin(pub_nonce)
87
+ raise ArgumentError, 'pub_nonce must be 66 bytes.' unless pub_nonce.bytesize == 66
83
88
  s = partial_sig.bti
84
89
  return false if s >= GROUP.order
85
- r1 = string2point(pub_nonce[0...33]).to_jacobian
86
- r2 = string2point(pub_nonce[33...66]).to_jacobian
90
+ r1 = string2point(pub_nonce.byteslice(0, 33)).to_jacobian
91
+ r2 = string2point(pub_nonce.byteslice(33, 33)).to_jacobian
87
92
  r_s = (r1 + r2 * b).to_affine
88
93
  r_s = r.has_even_y? ? r_s : r_s.negate
89
94
  pk = string2point(pubkeys[signer_index])
@@ -91,8 +96,8 @@ module Schnorr
91
96
  g = agg_ctx.q.has_even_y? ? 1 : GROUP.order - 1
92
97
  g = (g * agg_ctx.gacc) % GROUP.order
93
98
  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
99
+ rescue ECDSA::Format::DecodeError => err
100
+ raise ArgumentError, err.message
96
101
  end
97
102
  end
98
103
 
@@ -14,13 +14,15 @@ module Schnorr
14
14
  # @param [Array(String)] pubkeys An array of public keys.
15
15
  # @return [Array(String)] Sorted public keys with hex format.
16
16
  def sort_pubkeys(pubkeys)
17
- pubkeys.map{|p| hex_string?(p) ? p : p.unpack1('H*')}.sort
17
+ # Sort by the raw bytes so that the result does not depend on the letter case of hex input.
18
+ pubkeys.map{|p| hex_string?(p) ? p : p.unpack1('H*')}.sort_by{|p| [p].pack('H*')}
18
19
  end
19
20
 
20
21
  # Compute aggregate public key.
21
22
  # @param [Array[String]] pubkeys An array of public keys.
22
23
  # @return [Schnorr::MuSig2::KeyAggContext]
23
24
  def aggregate(pubkeys)
25
+ raise ArgumentError, 'pubkeys must not be empty.' if pubkeys.empty?
24
26
  pubkeys = pubkeys.map do |p|
25
27
  pubkey = hex2bin(p)
26
28
  raise ArgumentError, "Public key must be 33 bytes." unless pubkey.bytesize == 33
@@ -38,6 +40,7 @@ module Schnorr
38
40
  coeff = p == pk2 ? 1 : Schnorr.tagged_hash('KeyAgg coefficient', l + p).bti
39
41
  q += point * coeff
40
42
  end
43
+ raise ArgumentError, 'The aggregate public key must not be infinity.' if q.infinity?
41
44
  KeyAggContext.new(q.to_affine, 1, 0)
42
45
  end
43
46
 
@@ -61,7 +64,7 @@ module Schnorr
61
64
  # @param [String] pk The public key (33 bytes).
62
65
  # @param [String] sk (Optional) The secret key string (32 bytes).
63
66
  # @param [String] agg_pubkey (Optional) The aggregated public key (32 bytes).
64
- # @param [String] msg (Optional) The message to be signed.
67
+ # @param [String] msg (Optional) The message to be signed(binary format or hex format).
65
68
  # @param [String] extra_in (Optional) The auxiliary input.
66
69
  # @param [String] rand (Optional) A 32-byte array freshly drawn uniformly at random.
67
70
  # @return [Array(String)] The array of sec nonce and pub nonce with hex format.
@@ -81,7 +84,7 @@ module Schnorr
81
84
  end
82
85
  agg_pubkey = if agg_pubkey
83
86
  agg_pubkey = hex2bin(agg_pubkey)
84
- raise ArgumentError, 'The agg_pubkey must be 33 bytes.' unless agg_pubkey.bytesize == 32
87
+ raise ArgumentError, 'The agg_pubkey must be 32 bytes.' unless agg_pubkey.bytesize == 32
85
88
  agg_pubkey
86
89
  else
87
90
  ''
@@ -101,10 +104,11 @@ module Schnorr
101
104
  raise ArgumentError, 'k1 must not be zero.' if k1_i.zero?
102
105
  raise ArgumentError, 'k2 must not be zero.' if k2_i.zero?
103
106
 
104
- r1 = (GROUP.generator.to_jacobian * k1_i).to_affine
105
- r2 = (GROUP.generator.to_jacobian * k2_i).to_affine
107
+ r1 = (GROUP.generator.to_jacobian * pad_scalar(k1_i)).to_affine
108
+ r2 = (GROUP.generator.to_jacobian * pad_scalar(k2_i)).to_affine
106
109
  pub_nonce = r1.encode + r2.encode
107
- sec_nonce = k1 + k2 + pk
110
+ sec_nonce = ECDSA::Format::IntegerOctetString.encode(k1_i, GROUP.byte_length) +
111
+ ECDSA::Format::IntegerOctetString.encode(k2_i, GROUP.byte_length) + pk
108
112
  [sec_nonce.unpack1('H*'), pub_nonce.unpack1('H*')]
109
113
  end
110
114
 
@@ -114,11 +118,11 @@ module Schnorr
114
118
  def aggregate_nonce(pub_nonces)
115
119
  2.times.map do |i|
116
120
  r = GROUP.generator.to_jacobian.infinity_point
117
- pub_nonces = pub_nonces.each do |nonce|
121
+ pub_nonces.each do |nonce|
118
122
  nonce = hex2bin(nonce)
119
- raise ArgumentError, "" unless nonce.bytesize == 66
123
+ raise ArgumentError, 'Public nonce must be 66 bytes.' unless nonce.bytesize == 66
120
124
  begin
121
- p = string2point(nonce[(i * 33)...(i + 1)*33]).to_jacobian
125
+ p = string2point(nonce.byteslice(i * 33, 33)).to_jacobian
122
126
  raise ArgumentError, 'Public nonce is infinity' if p.infinity?
123
127
  rescue ECDSA::Format::DecodeError
124
128
  raise ArgumentError, "Invalid public nonce."
@@ -133,7 +137,7 @@ module Schnorr
133
137
  # @param [String] sk The secret key string (32 bytes).
134
138
  # @param [String] agg_other_nonce Other aggregated nonce.
135
139
  # @param [Array] pubkeys An array of public keys.
136
- # @param [String] msg The message to be signed.
140
+ # @param [String] msg The message to be signed(binary format or hex format).
137
141
  # @param [Array(String)] tweaks (Optional) An array of tweak value.
138
142
  # @param [Array(Boolean)] modes (Optional) An array of tweak mode.
139
143
  # @param [String] rand (Optional) A 32-byte array freshly drawn uniformly at random.
@@ -141,19 +145,25 @@ module Schnorr
141
145
  def deterministic_sign(sk, agg_other_nonce, pubkeys, msg, tweaks: [], modes: [], rand: nil)
142
146
  raise ArgumentError, 'The tweaks and modes arrays must have the same length.' unless tweaks.length == modes.length
143
147
  sk = hex2bin(sk)
148
+ raise ArgumentError, 'The sk must be 32 bytes.' unless sk.bytesize == 32
144
149
  msg = hex2bin(msg)
145
150
  agg_other_nonce = hex2bin(agg_other_nonce)
146
- sk_ = rand ? gen_aux(sk, hex2bin(rand)) : sk
151
+ raise ArgumentError, 'The agg_other_nonce must be 66 bytes.' unless agg_other_nonce.bytesize == 66
152
+ if rand
153
+ rand = hex2bin(rand)
154
+ raise ArgumentError, 'The rand must be 32 bytes.' unless rand.bytesize == 32
155
+ end
156
+ sk_ = rand ? gen_aux(sk, rand) : sk
147
157
  agg_ctx = aggregate_with_tweaks(pubkeys, tweaks, modes)
148
158
  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?
159
+ k1 = deterministic_nonce_hash(sk_, agg_other_nonce, agg_pk, msg, 0).bti % GROUP.order
160
+ k2 = deterministic_nonce_hash(sk_, agg_other_nonce, agg_pk, msg, 1).bti % GROUP.order
161
+ raise ArgumentError, 'k1 must not be zero.' if k1.zero?
162
+ raise ArgumentError, 'k2 must not be zero.' if k2.zero?
163
+ r1 = (GROUP.generator.to_jacobian * pad_scalar(k1)).to_affine
164
+ r2 = (GROUP.generator.to_jacobian * pad_scalar(k2)).to_affine
155
165
  pub_nonce = r1.encode + r2.encode
156
- pk = (GROUP.generator.to_jacobian * sk.bti).to_affine
166
+ pk = (GROUP.generator.to_jacobian * pad_scalar(sk.bti)).to_affine
157
167
  sec_nonce = ECDSA::Format::IntegerOctetString.encode(k1, GROUP.byte_length) +
158
168
  ECDSA::Format::IntegerOctetString.encode(k2, GROUP.byte_length) + pk.encode
159
169
  agg_nonce = aggregate_nonce([pub_nonce, agg_other_nonce])
@@ -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].bti
26
- s = string[32..-1].bti
25
+ r = string.byteslice(0, 32).bti
26
+ s = string.byteslice(32, 32).bti
27
27
  new(r, s)
28
28
  end
29
29
 
data/lib/schnorr/util.rb CHANGED
@@ -11,11 +11,11 @@ module Schnorr
11
11
  hex_chars.all? { |c| c =~ /[0-9a-fA-F]/ }
12
12
  end
13
13
 
14
- # If +str+ is a hex value, it is converted to binary. Otherwise, it is returned as is.
14
+ # If +str+ is a hex value, it is converted to binary. Otherwise, it is returned as a binary (ASCII-8BIT) string.
15
15
  # @param [String] str
16
16
  # @return [String]
17
17
  def hex2bin(str)
18
- hex_string?(str) ? [str].pack('H*') : str
18
+ hex_string?(str) ? [str].pack('H*') : str.b
19
19
  end
20
20
 
21
21
  # Convert +str+ to the point of elliptic curve.
@@ -24,5 +24,17 @@ module Schnorr
24
24
  def string2point(str)
25
25
  ECDSA::Format::PointOctetString.decode(str, GROUP)
26
26
  end
27
+
28
+ # Pad +scalar+ with the group order so that its bit length is fixed,
29
+ # mitigating a timing leak of the bit length of a secret scalar.
30
+ # Since (k + i * n)G = kG, this does not change the resulting point.
31
+ # Only use this for secret scalars; padding a public scalar just costs an extra doubling.
32
+ # @param [Integer] scalar A secret scalar.
33
+ # @return [Integer] The padded scalar.
34
+ def pad_scalar(scalar)
35
+ k = scalar + GROUP.order
36
+ k += GROUP.order if k.bit_length == GROUP.order.bit_length
37
+ k
38
+ end
27
39
  end
28
40
  end
@@ -1,3 +1,3 @@
1
1
  module Schnorr
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
data/lib/schnorr.rb CHANGED
@@ -13,34 +13,32 @@ module Schnorr
13
13
  DEFAULT_AUX = ([0x00] * 32).pack('C*')
14
14
 
15
15
  # Generate schnorr signature.
16
- # @param [String] message A message to be signed with binary format.
16
+ # @param [String] message A message to be signed(binary format or hex format).
17
17
  # @param [String] private_key The private key(binary format or hex format).
18
18
  # @param [String] aux_rand The auxiliary random data(binary format or hex format).
19
- # If aux_rand is nil, it is treated the same as an all-zero one.
19
+ # If aux_rand is nil, it is treated the same as an all-zero one, which makes the signature deterministic.
20
+ # It is recommended to pass 32 bytes of fresh randomness (e.g. SecureRandom.bytes(32)).
20
21
  # See BIP-340 "Default Signing" for a full explanation of this argument and for guidance if randomness is expensive.
21
22
  # @return [Schnorr::Signature]
22
23
  def sign(message, private_key, aux_rand = nil)
24
+ message = hex2bin(message)
23
25
  private_key = private_key.unpack1('H*') unless hex_string?(private_key)
24
26
 
25
27
  d0 = private_key.to_i(16)
26
- raise 'private_key must be an integer in the range 1..n-1.' unless 0 < d0 && d0 <= (GROUP.order - 1)
27
- if aux_rand
28
- aux_rand = [aux_rand].pack("H*") if hex_string?(aux_rand)
29
- raise 'aux_rand must be 32 bytes.' unless aux_rand.bytesize == 32
30
- else
31
- aux_rand = DEFAULT_AUX
32
- end
28
+ raise ArgumentError, 'private_key must be an integer in the range 1..n-1.' unless 0 < d0 && d0 <= (GROUP.order - 1)
29
+ aux_rand = aux_rand ? hex2bin(aux_rand) : DEFAULT_AUX
30
+ raise ArgumentError, 'aux_rand must be 32 bytes.' unless aux_rand.bytesize == 32
33
31
 
34
- p = (GROUP.generator.to_jacobian * d0).to_affine
32
+ p = (GROUP.generator.to_jacobian * pad_scalar(d0)).to_affine
35
33
  d = p.has_even_y? ? d0 : GROUP.order - d0
36
34
 
37
- t = aux_rand.nil? ? d : d ^ tagged_hash('BIP0340/aux', aux_rand).bti
35
+ t = d ^ tagged_hash('BIP0340/aux', aux_rand).bti
38
36
  t = ECDSA::Format::IntegerOctetString.encode(t, GROUP.byte_length)
39
37
 
40
38
  k0 = ECDSA::Format::IntegerOctetString.decode(tagged_hash('BIP0340/nonce', t + p.encode(true) + message)) % GROUP.order
41
39
  raise 'Creation of signature failed. k is zero' if k0.zero?
42
40
 
43
- r = (GROUP.generator.to_jacobian * k0).to_affine
41
+ r = (GROUP.generator.to_jacobian * pad_scalar(k0)).to_affine
44
42
  k = r.has_even_y? ? k0 : GROUP.order - k0
45
43
  e = create_challenge(r.x, p, message)
46
44
 
@@ -51,7 +49,7 @@ module Schnorr
51
49
  end
52
50
 
53
51
  # Verifies the given {Signature} and returns true if it is valid.
54
- # @param [String] message A message to be signed with binary format.
52
+ # @param [String] message A message to be signed(binary format or hex format).
55
53
  # @param [String] public_key The public key with binary format.
56
54
  # @param [String] signature The signature with binary format.
57
55
  # @return [Boolean] whether signature is valid.
@@ -62,18 +60,21 @@ module Schnorr
62
60
  end
63
61
 
64
62
  # Verifies the given {Signature} and raises an {InvalidSignatureError} if it is invalid.
65
- # @param [String] message A message to be signed with binary format.
63
+ # @param [String] message A message to be signed(binary format or hex format).
66
64
  # @param [String] public_key The public key with binary format.
67
65
  # @param [String] signature The signature with binary format.
68
66
  # @return [Boolean]
69
67
  def check_sig!(message, public_key, signature)
70
68
  message = hex2bin(message)
71
69
  public_key = hex2bin(public_key)
72
- public_key = [public_key].pack('H*') if hex_string?(public_key)
73
70
  raise InvalidSignatureError, 'The public key must be a 32-byte array.' unless public_key.bytesize == 32
74
71
 
75
72
  sig = Schnorr::Signature.decode(signature)
76
- pubkey = ECDSA::Format::PointOctetString.decode_from_x(public_key, GROUP)
73
+ pubkey = begin
74
+ ECDSA::Format::PointOctetString.decode_from_x(public_key, GROUP)
75
+ rescue ECDSA::Format::DecodeError => e
76
+ raise InvalidSignatureError, "Invalid public key: #{e.message}"
77
+ end
77
78
  field = GROUP.field
78
79
 
79
80
  raise Schnorr::InvalidSignatureError, 'Invalid signature: r is zero.' if sig.r.zero?
@@ -118,30 +119,4 @@ module Schnorr
118
119
  end
119
120
 
120
121
  end
121
-
122
- class ::Integer
123
- def to_hex
124
- hex = to_s(16)
125
- hex.rjust((hex.length / 2.0).ceil * 2, '0')
126
- end
127
-
128
- def method_missing(method, *args)
129
- return mod_pow(args[0], args[1]) if method == :pow && args.length < 3
130
- super
131
- end
132
-
133
- # alternative implementation of Integer#pow for ruby 2.4 and earlier.
134
- def mod_pow(x, y)
135
- return self**x unless y
136
-
137
- b = self
138
- result = 1
139
- while x > 0
140
- result = (result * b) % y if (x & 1) == 1
141
- x >>= 1
142
- b = (b * b) % y
143
- end
144
- result
145
- end
146
- end
147
122
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bip-schnorr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-12-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ecdsa_ext
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 0.5.0
18
+ version: 0.6.0
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 0.5.0
25
+ version: 0.6.0
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: bundler
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -78,6 +77,7 @@ files:
78
77
  - ".rspec"
79
78
  - ".ruby-gemset"
80
79
  - ".ruby-version"
80
+ - CHANGELOG.md
81
81
  - CODE_OF_CONDUCT.md
82
82
  - Gemfile
83
83
  - LICENSE.txt
@@ -98,7 +98,6 @@ homepage: https://github.com/chaintope/bip-schnorrrb
98
98
  licenses:
99
99
  - MIT
100
100
  metadata: {}
101
- post_install_message:
102
101
  rdoc_options: []
103
102
  require_paths:
104
103
  - lib
@@ -106,15 +105,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
106
105
  requirements:
107
106
  - - ">="
108
107
  - !ruby/object:Gem::Version
109
- version: '0'
108
+ version: '3.2'
110
109
  required_rubygems_version: !ruby/object:Gem::Requirement
111
110
  requirements:
112
111
  - - ">="
113
112
  - !ruby/object:Gem::Version
114
113
  version: '0'
115
114
  requirements: []
116
- rubygems_version: 3.4.1
117
- signing_key:
115
+ rubygems_version: 3.6.9
118
116
  specification_version: 4
119
117
  summary: The ruby implementation of bip-schnorr.
120
118
  test_files: []