bip-schnorr 0.7.1 → 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: d448f075885c68f9c74b1627d328180ee271392781bd1b8f9850ec6fd9e2ee53
4
- data.tar.gz: 633ddb345ba97a474205bf7d42953cffc4d2fae42f23aae2454cfdde57ba2c55
3
+ metadata.gz: 1ee1e80d49aa7b7ef61e0d2b03894a6f5bc81f6cef32cb08cad62e88c2c42d91
4
+ data.tar.gz: 99f5898f8c5726dd88138c03febce6787a0bb8eff18f97b180a5af82ae52aef4
5
5
  SHA512:
6
- metadata.gz: 33b5ae79ea9aa07a76e532e7095d5f82b6dae8002610bdf19da74410f46240a2c06c2615cacf33a720f9b2ac5ec68fbf0837ed33e3f5e729fe0d474a83b82946
7
- data.tar.gz: da1643275dbaf585d58add1265614be32496d7e4edeb70a7513be2290d30834f47f5f97f1044fa747b8f72ac35603595022c02914b82fd01fea265c669e5e98e
6
+ metadata.gz: 800862f45dbc98f9b82da03777b9373d5b910f3b8bf503741935851294e536fcccd3738c349e848424775e19d9b7661dc1b31b73c8394769ed76daace0e1b280
7
+ data.tar.gz: 54c26354859fe49d2d56e91c3895b11077aee81c97d47ce17aef0a24a6bd97ecf6f6771a22c04fcf738a59c28e37aaf8e194b9af8223116d39ab76932587f829
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/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.2"
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,10 +48,13 @@ 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
@@ -58,16 +62,13 @@ module Schnorr
58
62
  d = sk.bti
59
63
  raise ArgumentError, 'secret key value is out of range.' if d <= 0 || GROUP.order <= d
60
64
  p = (GROUP.generator.to_jacobian * pad_scalar(d)).to_affine.encode
61
- raise ArgumentError, 'Public key does not match nonce_gen argument' unless p == nonce[64...97]
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 * pad_scalar(k1)).to_affine
67
- r2 = (GROUP.generator.to_jacobian * pad_scalar(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
  ''
@@ -104,7 +107,8 @@ module Schnorr
104
107
  r1 = (GROUP.generator.to_jacobian * pad_scalar(k1_i)).to_affine
105
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,17 +145,23 @@ 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
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?
151
163
  r1 = (GROUP.generator.to_jacobian * pad_scalar(k1)).to_affine
152
164
  r2 = (GROUP.generator.to_jacobian * pad_scalar(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
165
  pub_nonce = r1.encode + r2.encode
156
166
  pk = (GROUP.generator.to_jacobian * pad_scalar(sk.bti)).to_affine
157
167
  sec_nonce = ECDSA::Format::IntegerOctetString.encode(k1, GROUP.byte_length) +
@@ -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.
@@ -1,3 +1,3 @@
1
1
  module Schnorr
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
data/lib/schnorr.rb CHANGED
@@ -13,28 +13,26 @@ 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
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
@@ -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,7 +60,7 @@ 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]
@@ -72,7 +70,11 @@ module Schnorr
72
70
  raise InvalidSignatureError, 'The public key must be a 32-byte array.' unless public_key.bytesize == 32
73
71
 
74
72
  sig = Schnorr::Signature.decode(signature)
75
- 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
76
78
  field = GROUP.field
77
79
 
78
80
  raise Schnorr::InvalidSignatureError, 'Invalid signature: r is zero.' if sig.r.zero?
@@ -117,30 +119,4 @@ module Schnorr
117
119
  end
118
120
 
119
121
  end
120
-
121
- class ::Integer
122
- def to_hex
123
- hex = to_s(16)
124
- hex.rjust((hex.length / 2.0).ceil * 2, '0')
125
- end
126
-
127
- def method_missing(method, *args)
128
- return mod_pow(args[0], args[1]) if method == :pow && args.length < 3
129
- super
130
- end
131
-
132
- # alternative implementation of Integer#pow for ruby 2.4 and earlier.
133
- def mod_pow(x, y)
134
- return self**x unless y
135
-
136
- b = self
137
- result = 1
138
- while x > 0
139
- result = (result * b) % y if (x & 1) == 1
140
- x >>= 1
141
- b = (b * b) % y
142
- end
143
- result
144
- end
145
- end
146
122
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bip-schnorr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.5.2
18
+ version: 0.6.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.5.2
25
+ version: 0.6.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -77,6 +77,7 @@ files:
77
77
  - ".rspec"
78
78
  - ".ruby-gemset"
79
79
  - ".ruby-version"
80
+ - CHANGELOG.md
80
81
  - CODE_OF_CONDUCT.md
81
82
  - Gemfile
82
83
  - LICENSE.txt
@@ -104,7 +105,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
105
  requirements:
105
106
  - - ">="
106
107
  - !ruby/object:Gem::Version
107
- version: '0'
108
+ version: '3.2'
108
109
  required_rubygems_version: !ruby/object:Gem::Requirement
109
110
  requirements:
110
111
  - - ">="