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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +5 -3
- data/bip-schnorrrb.gemspec +2 -1
- data/lib/schnorr/ec_point_ext.rb +1 -0
- data/lib/schnorr/musig2/context/session.rb +24 -19
- data/lib/schnorr/musig2.rb +23 -13
- data/lib/schnorr/signature.rb +2 -2
- data/lib/schnorr/util.rb +2 -2
- data/lib/schnorr/version.rb +1 -1
- data/lib/schnorr.rb +15 -39
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ee1e80d49aa7b7ef61e0d2b03894a6f5bc81f6cef32cb08cad62e88c2c42d91
|
|
4
|
+
data.tar.gz: 99f5898f8c5726dd88138c03febce6787a0bb8eff18f97b180a5af82ae52aef4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 [](https://
|
|
1
|
+
# bip-schnorrrb [](https://github.com/chaintope/bip-schnorrrb/actions/workflows/ruby.yml) [](https://badge.fury.io/rb/bip-schnorr) [](LICENSE)
|
|
2
2
|
|
|
3
3
|
This is a Ruby implementation of the Schnorr signature scheme over the elliptic curve.
|
|
4
4
|
This implementation relies on the [ecdsa gem](https://github.com/DavidEGrayson/ruby_ecdsa) for operate elliptic curves.
|
|
@@ -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 =
|
|
112
|
-
pub_nonce2, sig2 =
|
|
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
|
data/bip-schnorrrb.gemspec
CHANGED
|
@@ -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.
|
|
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"
|
data/lib/schnorr/ec_point_ext.rb
CHANGED
|
@@ -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
|
|
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
|
-
@
|
|
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
|
|
30
|
-
r2 = string2point(@agg_nonce
|
|
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, '
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
86
|
-
r2 = string2point(pub_nonce
|
|
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 =>
|
|
95
|
-
raise ArgumentError,
|
|
99
|
+
rescue ECDSA::Format::DecodeError => err
|
|
100
|
+
raise ArgumentError, err.message
|
|
96
101
|
end
|
|
97
102
|
end
|
|
98
103
|
|
data/lib/schnorr/musig2.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
121
|
+
pub_nonces.each do |nonce|
|
|
118
122
|
nonce = hex2bin(nonce)
|
|
119
|
-
raise ArgumentError,
|
|
123
|
+
raise ArgumentError, 'Public nonce must be 66 bytes.' unless nonce.bytesize == 66
|
|
120
124
|
begin
|
|
121
|
-
p = string2point(nonce
|
|
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
|
-
|
|
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) +
|
data/lib/schnorr/signature.rb
CHANGED
|
@@ -22,8 +22,8 @@ module Schnorr
|
|
|
22
22
|
# @return (Signature) signature instance.
|
|
23
23
|
def self.decode(string)
|
|
24
24
|
raise InvalidSignatureError, 'Invalid schnorr signature length.' unless string.bytesize == 64
|
|
25
|
-
r = string
|
|
26
|
-
s = string
|
|
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
|
|
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.
|
data/lib/schnorr/version.rb
CHANGED
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
|
|
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
|
-
|
|
28
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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: '
|
|
108
|
+
version: '3.2'
|
|
108
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
110
|
requirements:
|
|
110
111
|
- - ">="
|