schnorr_sig 0.2.1.1 → 1.0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22902f36da6e027cac43fc471364294806a80fdc6d88e3743566d06ae632b3a1
4
- data.tar.gz: a3759fb782d4aec2fd78b02113d6717bbf9892f6fb3b82be866b08e07ba38d5a
3
+ metadata.gz: d07355d7c8e4e0ce43bd586ba7cea831b72d2fd40b6c2efa1d49c46c536ab3ab
4
+ data.tar.gz: 346f2e23f326259d0e86f0837377e03cef56518b31828b22d3f7fa7ddca8b76b
5
5
  SHA512:
6
- metadata.gz: 5afe8575ec3355d303720539e2e5019639214374f26cefa9c89f52dabd625dc79e12f055b4f5c775f40cf334a7ac11deaa095cc6295e6cd6e5ed3b3f27439dc2
7
- data.tar.gz: 5950afef772f28370c7f1ca12bda717f560f8cf2f76d1c255fede1a8ce1677dea8dbd17ba4e4c55af7ac475058892c0da17e13235d6574b3155b7da6912d9f8c
6
+ metadata.gz: f91e124755a889779f7b5bda0991f9cd082078c855a5f8bcf2fe30886b779d5033d712801d776558bc454a5e5ba1a219fbd05f66b77dfd53098551dc7d6462e1
7
+ data.tar.gz: 7709c7e780d437dce791ac7f959350b71622c05c9838cdd6f5173fef3f232ace1a2e4b258ba56f697c4025266fab6e7b8ab0e0f6a77358d07880412963ae717f
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Tests Status](https://github.com/rickhull/schnorr_sig/actions/workflows/tests.yaml/badge.svg)](https://github.com/rickhull/schnorr_sig/actions/workflows/tests.yaml)
2
+
1
3
  # Schnorr Signatures
2
4
 
3
5
  This is a simple, minimal library written in Ruby for the purpose of
@@ -77,19 +79,101 @@ msg = 'hello world'
77
79
  # generate secret key and public key
78
80
  sk, pk = SchnorrSig.keypair
79
81
 
80
- # sign a message; exception raised on failure
82
+ # we can sign the message itself
81
83
  sig = SchnorrSig.sign(sk, msg)
82
84
 
83
85
  # the signature has already been verified, but let's check
84
86
  SchnorrSig.verify?(pk, msg, sig) # => true
87
+
88
+ # more commonly, you can sign a SHA256 hash of the message
89
+ h = Digest::SHA256.digest(msg)
90
+ sig = SchnorrSig.sign(sk, h)
91
+ SchnorrSig.verify?(pk, h, sig) # => true
92
+
93
+ # you can even use SchnorrSig's concept of a tagged hash
94
+ h = SchnorrSig.tagged_hash('signing', msg)
95
+ sig = SchnorrSig.sign(sk, h)
96
+ SchnorrSig.verify?(pk, h, sig) # => true
97
+
98
+ # validate that the hash corresponds to the message
99
+ # re-hash the message with the same tag
100
+ SchnorrSig.tagged_hash('signing', msg) == h # => true
85
101
  ```
86
102
 
87
- ### Fast Implementation
103
+ ## Fundamentals
104
+
105
+ Here are the fundamental functions common to both implementations:
106
+
107
+ * `sign(32B sk, str msg)` *returns* `64B sig`
108
+ * `verify?(32B pk, str msg, 64B sig)` *returns* `bool`
109
+ * `pubkey(32B sk)` *returns* `32B pk`
110
+ * `tagged_hash(str tag, str msg)` *returns* `32B hash`
111
+ * `keypair()` *returns* `[32B sk, 32B pk]`
112
+
113
+ Use `soft_verify?(pk, msg, sig)` to yield `false` if errors are raised.
114
+
115
+ ### Differences
116
+
117
+ * Fast: `sign(32B sk, 32B msg)`
118
+
119
+ The fast implementation only signs 32 byte payloads. It expects to sign
120
+ a hash of the message and not the message itself. The pure implementation
121
+ is happy to sign any payload.
122
+
123
+ * Pure: `sign(32B sk, str msg, auxrand: 32B)` *(auxrand is optional)*
124
+
125
+ The fast implementation always generates `auxrand` at signing time via
126
+ `SecureRandom`. The pure implementation allows `auxrand` to be passed in,
127
+ but when omitted it will be generated by default by `SecureRandom`,
128
+ though `Random` may also be used via `NO_SECURERANDOM` environment variable.
129
+
130
+ ## Enable Fast Implementation
131
+
132
+ *The `rbsecp256k1` gem must be installed,
133
+ otherwise there will be a `LoadError`.*
134
+
135
+ Ensure `ENV['SCHNORR_SIG']&.downcase == 'fast'`, and then
136
+ `require 'schnorr_sig'` will try the fast implementation first, before
137
+ falling back to the pure implementation.
138
+
139
+ After `require 'schnorr_sig'`, you can check which implementation is loaded
140
+ by the presence of `SchnorrSig::Pure` or `SchnorrSig::Fast`.
141
+
142
+ ### Load Directly
143
+
144
+ ```ruby
145
+ require 'schnorr_sig/fast'
146
+
147
+ include SchnorrSig
148
+
149
+ sk, pk = Fast.keypair
150
+ msg = 'hello world'
151
+ hsh = Fast.tagged_hash('message', msg)
152
+
153
+ sig = Fast.sign(sk, hsh)
154
+ Fast.verify?(pk, hsh, sig) # => true
155
+ ```
156
+
157
+ ### Side by Side
158
+
159
+ You can run each implementation side by side as follows:
88
160
 
89
161
  ```ruby
90
- require 'schnorr_sig/fast' # not 'schnorr_sig'
162
+ require 'schnorr_sig/pure'
163
+ require 'schnorr_sig/fast'
164
+
165
+ include SchnorrSig
166
+
167
+ sk, pk = Pure.keypair # or Fast.keypair
168
+
169
+ msg = 'hello world'
170
+ hsh = Fast.tagged_hash('message', msg)
171
+
172
+ sig1 = Pure.sign(sk, hsh)
173
+ sig2 = Fast.sign(sk, hsh)
91
174
 
92
- # everything else as above ...
175
+ Fast.verify?(pk, hsh, sig1) # => true
176
+ Pure.verify?(pk, hsh, sig2) # => true
93
177
  ```
94
178
 
95
179
  # Elliptic Curves
@@ -186,7 +270,7 @@ pk = big2bin(point.x) # public key: point.x as a binary string
186
270
  ```
187
271
 
188
272
  The implementation of
189
- [big2bin](https://github.com/rickhull/schnorr_sig/blob/master/lib/schnorr_sig/util.rb#L30)
273
+ [big2bin](https://github.com/rickhull/schnorr_sig/blob/master/lib/schnorr_sig/utils.rb#L26)
190
274
  is left as an exercise for the reader.
191
275
 
192
276
  ## Formatting
data/Rakefile CHANGED
@@ -1,7 +1,26 @@
1
1
  require 'rake/testtask'
2
2
 
3
3
  Rake::TestTask.new :test do |t|
4
- t.pattern = "test/*.rb"
4
+ t.test_files = [
5
+ 'test/utils.rb',
6
+ 'test/pure.rb',
7
+ ]
8
+ t.warning = true
9
+ end
10
+
11
+ Rake::TestTask.new :vectors do |t|
12
+ t.test_files = [
13
+ 'test/vectors.rb',
14
+ 'test/vectors_extra.rb',
15
+ ]
16
+ t.warning = true
17
+ end
18
+
19
+ Rake::TestTask.new :fast do |t|
20
+ t.test_files = [
21
+ 'test/utils.rb',
22
+ 'test/fast.rb',
23
+ ]
5
24
  t.warning = true
6
25
  end
7
26
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1.1
1
+ 1.0.0.2
@@ -1,85 +1,123 @@
1
- require 'schnorr_sig/util' # project
2
- require 'rbsecp256k1' # gem, C extension
1
+ require 'schnorr_sig/utils'
2
+ require 'rbsecp256k1' # gem, C extension
3
3
 
4
- # re-open SchnorrSig to add more functions, errors, and constants
5
4
  module SchnorrSig
6
5
  CONTEXT = Secp256k1::Context.create
7
- Error = Secp256k1::Error # enable: rescue SchnorrSig::Error
8
- FORCE_32_BYTE_MSG = true
9
-
10
- # Input
11
- # The secret key, sk: 32 bytes binary
12
- # The message, m: UTF-8 / binary / agnostic
13
- # Output
14
- # 64 bytes binary
15
- def self.sign(sk, m)
16
- bytestring!(sk, 32) and string!(m)
17
- m = m[0..31].ljust(32, ' ') if FORCE_32_BYTE_MSG
18
- CONTEXT.sign_schnorr(key_pair(sk), m).serialized
19
- end
20
6
 
21
- # Input
22
- # The public key, pk: 32 bytes binary
23
- # The message, m: UTF-8 / binary / agnostic
24
- # A signature, sig: 64 bytes binary
25
- # Output
26
- # Boolean, may raise SchnorrSig::Error
27
- def self.verify?(pk, m, sig)
28
- bytestring!(pk, 32) and string!(m) and bytestring!(sig, 64)
29
- signature(sig).verify(m, Secp256k1::XOnlyPublicKey.from_data(pk))
30
- end
7
+ # KeyPair
8
+ # - Create / Split
9
+ # * Context.create.generate_keypair => KeyPair
10
+ # * KeyPair#xonly_public_key => XOnlyPublicKey
11
+ # * KeyPair#private_key => PrivateKey
12
+ # - String Conversion
13
+ # * Context.create.keypair_from_private_key(sk) => KeyPair
14
+ # * XOnlyPublicKey.from_data(pk) => XOnlyPublicKey
15
+ # * XOnlyPublicKey#serialized => pk
16
+ # * PrivateKey#data => sk
17
+
18
+ # Signature
19
+ # - Sign / Verify
20
+ # * Context.create.sign_schnorr(KeyPair, m) => Signature
21
+ # * Signature#verify(m, XOnlyPublicKey) => bool
22
+ # - String Conversion
23
+ # * Signature#serialized => sig (64B String)
24
+ # * Signature#from_data(sig) => Signature
31
25
 
32
- # Input
33
- # (The secret key, sk: 32 bytes binary)
34
- # Output
35
- # Secp256k1::KeyPair
36
- def self.key_pair(sk = nil)
37
- if sk
38
- bytestring!(sk, 32)
39
- CONTEXT.key_pair_from_private_key(sk)
40
- else
41
- CONTEXT.generate_key_pair
26
+ module Fast
27
+
28
+ #
29
+ # Keys
30
+ #
31
+
32
+ # Input
33
+ # (The secret key, sk: 32 bytes binary)
34
+ # Output
35
+ # Secp256k1::KeyPair
36
+ def keypair_obj(sk = nil)
37
+ if sk
38
+ binary!(sk, KEY)
39
+ CONTEXT.key_pair_from_private_key(sk)
40
+ else
41
+ CONTEXT.generate_key_pair
42
+ end
42
43
  end
43
- end
44
44
 
45
- # Input
46
- # (The secret key, sk: 32 bytes binary)
47
- # Output
48
- # [sk, pk]
49
- def self.keypair(sk = nil)
50
- kp = self.key_pair(sk)
51
- [kp.private_key.data, kp.xonly_public_key.serialized]
52
- end
45
+ # Input
46
+ # Secp256k1::KeyPair
47
+ # Output
48
+ # [sk, pk] (32 bytes binary)
49
+ def extract_keys(keypair_obj)
50
+ [keypair_obj.private_key.data, keypair_obj.xonly_public_key.serialized]
51
+ end
53
52
 
54
- # Input
55
- # The secret key, sk: 32 bytes binary
56
- # Output
57
- # The public key: 32 bytes binary
58
- def self.pubkey(sk)
59
- keypair(sk)[1]
60
- end
53
+ # Input
54
+ # The secret key, sk: 32 bytes binary
55
+ # Output
56
+ # The public key: 32 bytes binary
57
+ def pubkey(sk) = keypair_obj(sk).xonly_public_key.serialized
61
58
 
62
- # Input
63
- # The signature, str: 64 bytes binary
64
- # Output
65
- # Secp256k1::SchnorrSignature
66
- def self.signature(str)
67
- bytestring!(str, 64)
68
- Secp256k1::SchnorrSignature.from_data(str)
69
- end
70
- end
59
+ # Output
60
+ # [sk, pk] (32 bytes binary)
61
+ def keypair = extract_keys(keypair_obj())
71
62
 
72
- if __FILE__ == $0
73
- msg = 'hello world'
63
+ #
64
+ # Signatures
65
+ #
74
66
 
75
- sk, pk = SchnorrSig.keypair
76
- puts "Message: #{msg}"
77
- puts "Secret key: #{SchnorrSig.bin2hex(sk)}"
78
- puts "Public key: #{SchnorrSig.bin2hex(pk)}"
67
+ # Input
68
+ # The signature, str: 64 bytes binary
69
+ # Output
70
+ # Secp256k1::SchnorrSignature
71
+ def signature(str)
72
+ binary!(str, SIG)
73
+ Secp256k1::SchnorrSignature.from_data(str)
74
+ end
75
+
76
+ # Input
77
+ # The secret key, sk: 32 bytes binary
78
+ # The message, m: 32 byte hash value
79
+ # Output
80
+ # 64 bytes binary
81
+ def sign(sk, m)
82
+ binary!(sk, KEY) and binary!(m, 32)
83
+ CONTEXT.sign_schnorr(keypair_obj(sk), m).serialized
84
+ end
85
+
86
+ # Input
87
+ # The public key, pk: 32 bytes binary
88
+ # The message, m: 32 byte hash value
89
+ # A signature, sig: 64 bytes binary
90
+ # Output
91
+ # Boolean, may raise SchnorrSig::Error, Secp256k1::Error
92
+ def verify?(pk, m, sig)
93
+ binary!(pk, KEY) and binary!(m, 32) and binary!(sig, SIG)
94
+ signature(sig).verify(m, Secp256k1::XOnlyPublicKey.from_data(pk))
95
+ end
96
+
97
+ # as above but swallow internal errors and return false
98
+ def soft_verify?(pk, m, sig)
99
+ begin
100
+ verify?(pk, m, sig)
101
+ rescue Secp256k1::Error
102
+ false
103
+ end
104
+ end
105
+
106
+ #
107
+ # Utility
108
+ #
109
+
110
+ # Input
111
+ # tag: UTF-8 > binary > agnostic
112
+ # msg: UTF-8 / binary / agnostic
113
+ # Output
114
+ # 32 bytes binary
115
+ def tagged_hash(tag, msg)
116
+ check!(tag, String) and check!(msg, String)
117
+ CONTEXT.tagged_sha256(tag, msg)
118
+ end
119
+ end
79
120
 
80
- sig = SchnorrSig.sign(sk, msg)
81
- puts
82
- puts "Verified signature: #{SchnorrSig.bin2hex(sig)}"
83
- puts "Encoding: #{sig.encoding}"
84
- puts "Length: #{sig.length}"
121
+ Fast.include Utils
122
+ Fast.extend Fast
85
123
  end
@@ -1,221 +1,228 @@
1
- require 'schnorr_sig/util' # project
2
- require 'ecdsa_ext' # gem
1
+ require 'schnorr_sig/utils'
2
+ require 'ecdsa_ext' # gem, depends on ecdsa gem
3
3
  autoload :SecureRandom, 'securerandom' # stdlib
4
4
 
5
5
  # This implementation is based on the BIP340 spec: https://bips.xyz/340
6
- # re-open SchnorrSig to add more functions, errors, and constants
7
6
  module SchnorrSig
8
- class Error < RuntimeError; end
9
- class BoundsError < Error; end
10
7
  class SanityCheck < Error; end
11
- class VerifyFail < Error; end
12
- class InfinityPoint < Error; end
13
8
 
14
9
  GROUP = ECDSA::Group::Secp256k1
15
10
  P = GROUP.field.prime # smaller than 256**32
16
11
  N = GROUP.order # smaller than P
17
12
  B = GROUP.byte_length # 32
18
13
 
19
- # val (dot) G, returns ECDSA::Point
20
- def self.dot_group(val)
21
- # ecdsa_ext uses jacobian projection: 10x faster than GROUP.generator * val
22
- (GROUP.generator.to_jacobian * val).to_affine
23
- end
14
+ module Pure
24
15
 
25
- # returns even_val or N - even_val
26
- def self.select_even_y(point, even_val)
27
- point.y.even? ? even_val : N - even_val
28
- end
16
+ #
17
+ # Utils
18
+ #
29
19
 
30
- # int(x) function signature matches BIP340, returns a bignum (presumably)
31
- class << self
32
- alias_method :int, :bin2big
33
- end
20
+ # use SecureRandom unless ENV['NO_SECURERANDOM'] is nonempty
21
+ def random_bytes(count)
22
+ nsr = ENV['NO_SECURERANDOM']
23
+ (nsr and !nsr.empty?) ? Random.bytes(count) : SecureRandom.bytes(count)
24
+ end
34
25
 
35
- # bytes(val) function signature matches BIP340, returns a binary string
36
- def self.bytes(val)
37
- case val
38
- when Integer
39
- # BIP340: The function bytes(x), where x is an integer,
40
- # returns the 32-byte encoding of x, most significant byte first.
41
- big2bin(val)
42
- when ECDSA::Point
43
- # BIP340: The function bytes(P), where P is a point, returns bytes(x(P)).
44
- val.infinity? ? raise(InfinityPoint, va.inspect) : big2bin(val.x)
45
- else
46
- raise(SanityCheck, val.inspect)
26
+ # int (dot) G, returns ECDSA::Point
27
+ def point(int)
28
+ (GROUP.generator.to_jacobian * int).to_affine # 10x faster via ecdsa_ext
47
29
  end
48
- end
49
30
 
50
- # Input
51
- # The secret key, sk: 32 bytes binary
52
- # The message, m: binary / UTF-8 / agnostic
53
- # Auxiliary random data, a: 32 bytes binary
54
- # Output
55
- # The signature, sig: 64 bytes binary
56
- def self.sign(sk, m, a = Random.bytes(B))
57
- bytestring!(sk, B) and string!(m) and bytestring!(a, B)
58
-
59
- # BIP340: Let d' = int(sk)
60
- # BIP340: Fail if d' = 0 or d' >= n
61
- d0 = int(sk)
62
- raise(BoundsError, "d0") if !d0.positive? or d0 >= N
63
-
64
- # BIP340: Let P = d' . G
65
- p = dot_group(d0) # this is a point on the elliptic curve
66
- bytes_p = bytes(p)
67
-
68
- # BIP340: Let d = d' if has_even_y(P), otherwise let d = n - d'
69
- d = select_even_y(p, d0)
70
-
71
- # BIP340: Let t be the bytewise xor of bytes(d) and hash[BIP0340/aux](a)
72
- t = d ^ int(tagged_hash('BIP0340/aux', a))
73
-
74
- # BIP340: Let rand = hash[BIP0340/nonce](t || bytes(P) || m)
75
- nonce = tagged_hash('BIP0340/nonce', bytes(t) + bytes_p + m)
76
-
77
- # BIP340: Let k' = int(rand) mod n
78
- # BIP340: Fail if k' = 0
79
- k0 = int(nonce) % N
80
- raise(BoundsError, "k0") if !k0.positive?
81
-
82
- # BIP340: Let R = k' . G
83
- r = dot_group(k0) # this is a point on the elliptic curve
84
- bytes_r = bytes(r)
85
-
86
- # BIP340: Let k = k' if has_even_y(R), otherwise let k = n - k'
87
- k = select_even_y(r, k0)
88
-
89
- # BIP340:
90
- # Let e = int(hash[BIP0340/challenge](bytes(R) || bytes(P) || m)) mod n
91
- e = int(tagged_hash('BIP0340/challenge', bytes_r + bytes_p + m)) % N
92
-
93
- # BIP340: Let sig = bytes(R) || bytes((k + ed) mod n)
94
- # BIP340: Fail unless Verify(bytes(P), m, sig)
95
- # BIP340: Return the signature sig
96
- sig = bytes_r + bytes((k + e * d) % N)
97
- raise(VerifyFail) unless verify?(bytes_p, m, sig)
98
- sig
99
- end
31
+ # returns even_val or N - even_val
32
+ def select_even_y(point, even_val)
33
+ point.y.even? ? even_val : N - even_val
34
+ end
100
35
 
101
- # see https://bips.xyz/340#design (Tagged hashes)
102
- # Input
103
- # A tag: UTF-8 > binary > agnostic
104
- # The payload, msg: UTF-8 / binary / agnostic
105
- # Output
106
- # 32 bytes binary
107
- def self.tagged_hash(tag, msg)
108
- string!(tag) and string!(msg)
109
- warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8
110
-
111
- # BIP340: The function hash[name](x) where x is a byte array
112
- # returns the 32-byte hash
113
- # SHA256(SHA256(tag) || SHA256(tag) || x)
114
- # where tag is the UTF-8 encoding of name.
115
- tag_hash = Digest::SHA256.digest(tag)
116
- Digest::SHA256.digest(tag_hash + tag_hash + msg)
117
- end
36
+ # int(x) function signature matches BIP340, returns a bignum (presumably)
37
+ def int(x) = bin2big(x)
38
+
39
+ # bytes(val) function signature matches BIP340, returns a binary string
40
+ def bytes(val)
41
+ case val
42
+ when Integer
43
+ # BIP340: The function bytes(x), where x is an integer,
44
+ # returns the 32-byte encoding of x, most significant byte first.
45
+ big2bin(val)
46
+ when ECDSA::Point
47
+ # BIP340: The function bytes(P), where P is a point,
48
+ # returns bytes(x(P)).
49
+ val.infinity? ? raise(SanityCheck, val.inspect) : big2bin(val.x)
50
+ else
51
+ raise(SanityCheck, val.inspect)
52
+ end
53
+ end
118
54
 
119
- # Input
120
- # The public key, pk: 32 bytes binary
121
- # The message, m: UTF-8 / binary / agnostic
122
- # A signature, sig: 64 bytes binary
123
- # Output
124
- # Boolean
125
- def self.verify?(pk, m, sig)
126
- bytestring!(pk, B) and string!(m) and bytestring!(sig, B * 2)
127
-
128
- # BIP340: Let P = lift_x(int(pk))
129
- p = lift_x(int(pk))
130
-
131
- # BIP340: Let r = int(sig[0:32]) fail if r >= p
132
- r = int(sig[0..B-1])
133
- raise(BoundsError, "r >= p") if r >= P
134
-
135
- # BIP340: Let s = int(sig[32:64]); fail if s >= n
136
- s = int(sig[B..-1])
137
- raise(BoundsError, "s >= n") if s >= N
138
-
139
- # BIP340:
140
- # Let e = int(hash[BIP0340/challenge](bytes(r) || bytes(P) || m)) mod n
141
- e = bytes(r) + bytes(p) + m
142
- e = int(tagged_hash('BIP0340/challenge', e)) % N
143
-
144
- # BIP340: Let R = s . G - e . P
145
- # BIP340: Fail if is_infinite(R)
146
- # BIP340: Fail if not has_even_y(R)
147
- # BIP340: Fail if x(R) != r
148
- # BIP340: Return success iff no failure occurred before reaching this point
149
- big_r = dot_group(s) + p.multiply_by_scalar(e).negate
150
- !big_r.infinity? and big_r.y.even? and big_r.x == r
151
- end
55
+ # BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,
56
+ # returns the point P for which x(P) = x and has_even_y(P),
57
+ # or fails if x is greater than p-1 or no such point exists.
58
+ # Input
59
+ # A large integer, x
60
+ # Output
61
+ # ECDSA::Point
62
+ def lift_x(x)
63
+ check!(x, Integer)
152
64
 
153
- # BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,
154
- # returns the point P for which x(P) = x and has_even_y(P),
155
- # or fails if x is greater than p-1 or no such point exists.
156
- # Input
157
- # A large integer, x
158
- # Output
159
- # ECDSA::Point
160
- def self.lift_x(x)
161
- integer!(x)
65
+ # BIP340: Fail if x >= p
66
+ raise(SanityCheck, "x") if x >= P or x <= 0
162
67
 
163
- # BIP340: Fail if x >= p
164
- raise(BoundsError, "x") if x >= P or x <= 0
68
+ # BIP340: Let c = x^3 + 7 mod p
69
+ c = (x.pow(3, P) + 7) % P
165
70
 
166
- # BIP340: Let c = x^3 + 7 mod p
167
- c = (x.pow(3, P) + 7) % P
71
+ # BIP340: Let y = c ^ ((p + 1) / 4) mod p
72
+ y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow
168
73
 
169
- # BIP340: Let y = c ^ ((p + 1) / 4) mod p
170
- y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow
74
+ # BIP340: Fail if c != y^2 mod p
75
+ raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)
171
76
 
172
- # BIP340: Fail if c != y^2 mod p
173
- raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)
77
+ # BIP340: Return the unique point P such that:
78
+ # x(P) = x and y(P) = y if y mod 2 = 0
79
+ # y(P) = p - y otherwise
80
+ GROUP.new_point [x, y.even? ? y : P - y]
81
+ end
174
82
 
175
- # BIP340: Return the unique point P such that:
176
- # x(P) = x and y(P) = y if y mod 2 = 0
177
- # y(P) = p - y otherwise
178
- GROUP.new_point [x, y.even? ? y : P - y]
179
- end
83
+ # see https://bips.xyz/340#design (Tagged hashes)
84
+ # Input
85
+ # A tag: UTF-8 > binary > agnostic
86
+ # The payload, msg: UTF-8 / binary / agnostic
87
+ # Output
88
+ # 32 bytes binary
89
+ def tagged_hash(tag, msg)
90
+ check!(tag, String) and check!(msg, String)
91
+ warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8
92
+
93
+ # BIP340: The function hash[name](x) where x is a byte array
94
+ # returns the 32-byte hash
95
+ # SHA256(SHA256(tag) || SHA256(tag) || x)
96
+ # where tag is the UTF-8 encoding of name.
97
+ tag_hash = Digest::SHA256.digest(tag)
98
+ Digest::SHA256.digest(tag_hash + tag_hash + msg)
99
+ end
180
100
 
181
- # Input
182
- # The secret key, sk: 32 bytes binary
183
- # Output
184
- # 32 bytes binary (represents P.x for point P on the curve)
185
- def self.pubkey(sk)
186
- bytestring!(sk, B)
187
-
188
- # BIP340: Let d' = int(sk)
189
- # BIP340: Fail if d' = 0 or d' >= n
190
- # BIP340: Return bytes(d' . G)
191
- d0 = int(sk)
192
- raise(BoundsError, "d0") if !d0.positive? or d0 >= N
193
- bytes(dot_group(d0))
194
- end
101
+ #
102
+ # Keys
103
+ #
104
+
105
+ # Input
106
+ # The secret key, sk: 32 bytes binary
107
+ # Output
108
+ # 32 bytes binary (represents P.x for point P on the curve)
109
+ def pubkey(sk)
110
+ binary!(sk, KEY)
111
+
112
+ # BIP340: Let d' = int(sk)
113
+ # BIP340: Fail if d' = 0 or d' >= n
114
+ # BIP340: Return bytes(d' . G)
115
+ d0 = int(sk)
116
+ raise(SanityCheck, "d0") if !d0.positive? or d0 >= N
117
+ bytes(point(d0))
118
+ end
195
119
 
196
- # generate a new keypair based on random data
197
- def self.keypair
198
- sk = Random.bytes(B)
199
- [sk, pubkey(sk)]
200
- end
120
+ # generate a new keypair based on random data
121
+ def keypair
122
+ sk = random_bytes(KEY)
123
+ [sk, pubkey(sk)]
124
+ end
201
125
 
202
- # as above, but using SecureRandom
203
- def self.secure_keypair
204
- sk = SecureRandom.bytes(B)
205
- [sk, pubkey(sk)]
126
+ #
127
+ # Signatures
128
+ #
129
+
130
+ # Input
131
+ # The secret key, sk: 32 bytes binary
132
+ # The message, m: binary / UTF-8 / agnostic
133
+ # Auxiliary random data, a: 32 bytes binary
134
+ # Output
135
+ # The signature, sig: 64 bytes binary
136
+ def sign(sk, m, auxrand: nil)
137
+ a = auxrand.nil? ? random_bytes(B) : auxrand
138
+ binary!(sk, KEY) and check!(m, String) and binary!(a, B)
139
+
140
+ # BIP340: Let d' = int(sk)
141
+ # BIP340: Fail if d' = 0 or d' >= n
142
+ d0 = int(sk)
143
+ raise(SanityCheck, "d0") if !d0.positive? or d0 >= N
144
+
145
+ # BIP340: Let P = d' . G
146
+ p = point(d0) # this is a point on the elliptic curve
147
+ bytes_p = bytes(p)
148
+
149
+ # BIP340: Let d = d' if has_even_y(P), otherwise let d = n - d'
150
+ d = select_even_y(p, d0)
151
+
152
+ # BIP340: Let t be the bytewise xor of bytes(d) and hash[BIP0340/aux](a)
153
+ t = d ^ int(tagged_hash('BIP0340/aux', a))
154
+
155
+ # BIP340: Let rand = hash[BIP0340/nonce](t || bytes(P) || m)
156
+ nonce = tagged_hash('BIP0340/nonce', bytes(t) + bytes_p + m)
157
+
158
+ # BIP340: Let k' = int(rand) mod n
159
+ # BIP340: Fail if k' = 0
160
+ k0 = int(nonce) % N
161
+ raise(SanityCheck, "k0") if !k0.positive?
162
+
163
+ # BIP340: Let R = k' . G
164
+ r = point(k0) # this is a point on the elliptic curve
165
+ bytes_r = bytes(r)
166
+
167
+ # BIP340: Let k = k' if has_even_y(R), otherwise let k = n - k'
168
+ k = select_even_y(r, k0)
169
+
170
+ # BIP340:
171
+ # Let e = int(hash[BIP0340/challenge](bytes(R) || bytes(P) || m)) mod n
172
+ e = int(tagged_hash('BIP0340/challenge', bytes_r + bytes_p + m)) % N
173
+
174
+ # BIP340: Let sig = bytes(R) || bytes((k + ed) mod n)
175
+ # BIP340: Fail unless Verify(bytes(P), m, sig)
176
+ # BIP340: Return the signature sig
177
+ sig = bytes_r + bytes((k + e * d) % N)
178
+ raise(SanityCheck, "sig did not verify") unless verify?(bytes_p, m, sig)
179
+ sig
180
+ end
181
+
182
+ # Input
183
+ # The public key, pk: 32 bytes binary
184
+ # The message, m: UTF-8 / binary / agnostic
185
+ # A signature, sig: 64 bytes binary
186
+ # Output
187
+ # Boolean
188
+ def verify?(pk, m, sig)
189
+ binary!(pk, KEY) and check!(m, String) and binary!(sig, SIG)
190
+
191
+ # BIP340: Let P = lift_x(int(pk))
192
+ p = lift_x(int(pk))
193
+
194
+ # BIP340: Let r = int(sig[0:32]) fail if r >= p
195
+ r = int(sig[0..KEY-1])
196
+ raise(SanityCheck, "r >= p") if r >= P
197
+
198
+ # BIP340: Let s = int(sig[32:64]); fail if s >= n
199
+ s = int(sig[KEY..-1])
200
+ raise(SanityCheck, "s >= n") if s >= N
201
+
202
+ # BIP340:
203
+ # Let e = int(hash[BIP0340/challenge](bytes(r) || bytes(P) || m)) mod n
204
+ e = bytes(r) + bytes(p) + m
205
+ e = int(tagged_hash('BIP0340/challenge', e)) % N
206
+
207
+ # BIP340: Let R = s . G - e . P
208
+ # BIP340: Fail if is_infinite(R)
209
+ # BIP340: Fail if not has_even_y(R)
210
+ # BIP340: Fail if x(R) != r
211
+ # BIP340: Return success iff no prior failure
212
+ big_r = point(s) + p.multiply_by_scalar(e).negate
213
+ !big_r.infinity? and big_r.y.even? and big_r.x == r
214
+ end
215
+
216
+ # as above but swallow internal errors and return false
217
+ def soft_verify?(pk, m, sig)
218
+ begin
219
+ verify?(pk, m, sig)
220
+ rescue SanityCheck
221
+ false
222
+ end
223
+ end
206
224
  end
207
- end
208
225
 
209
- if __FILE__ == $0
210
- msg = 'hello world'
211
- sk, pk = SchnorrSig.keypair
212
- puts "Message: #{msg}"
213
- puts "Secret key: #{SchnorrSig.bin2hex(sk)}"
214
- puts "Public key: #{SchnorrSig.bin2hex(pk)}"
215
-
216
- sig = SchnorrSig.sign(sk, msg)
217
- puts
218
- puts "Verified signature: #{SchnorrSig.bin2hex(sig)}"
219
- puts "Encoding: #{sig.encoding}"
220
- puts "Length: #{sig.length}"
226
+ Pure.include Utils
227
+ Pure.extend Pure
221
228
  end
@@ -0,0 +1,36 @@
1
+ module SchnorrSig
2
+ class Error < RuntimeError; end
3
+ class SpecError < Error; end
4
+
5
+ KEY = 32 # bytes
6
+ SIG = 64 # bytes
7
+
8
+ module Utils
9
+ # raise SpecError or return val
10
+ def check!(val, cls)
11
+ val.is_a?(cls) ? val : raise(SpecError, "#{cls}: #{val.inspect}")
12
+ end
13
+
14
+ # raise SpecError or return str
15
+ def binary!(str, length)
16
+ check!(str, String)
17
+ if str.encoding != Encoding::BINARY
18
+ raise(SpecError, "Encoding: #{str.encoding}")
19
+ end
20
+ raise(SpecError, "Length: #{str.length}") if str.length != length
21
+ str
22
+ end
23
+
24
+ # likely returns a Bignum, larger than a 64-bit hardware integer
25
+ def bin2big(str) = bin2hex(str).to_i(16)
26
+
27
+ # convert a giant integer to a binary string
28
+ def big2bin(bignum) = hex2bin(bignum.to_s(16).rjust(64, '0'))
29
+
30
+ # convert a binary string to a lowercase hex string
31
+ def bin2hex(str) = str.unpack1('H*')
32
+
33
+ # convert a hex string to a binary string
34
+ def hex2bin(hex) = [hex].pack('H*')
35
+ end
36
+ end
data/lib/schnorr_sig.rb CHANGED
@@ -1,10 +1,16 @@
1
+ loaded = false
2
+
1
3
  if ENV['SCHNORR_SIG']&.downcase == 'fast'
2
4
  begin
3
5
  require 'schnorr_sig/fast'
4
- rescue LoadError
5
- warn "LoadError: schnorr_sig/fast cannot be loaded"
6
- require 'schnorr_sig/pure'
6
+ SchnorrSig.extend SchnorrSig::Fast
7
+ loaded = true
8
+ rescue LoadError => e
9
+ warn [e.class, e.message].join(': ')
7
10
  end
8
- else
11
+ end
12
+
13
+ unless loaded
9
14
  require 'schnorr_sig/pure'
15
+ SchnorrSig.extend SchnorrSig::Pure
10
16
  end
data/schnorr_sig.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'schnorr_sig'
3
- s.summary = "Schnorr signatures in Ruby, multiple implementations"
3
+ s.summary = "Schnorr signatures in Ruby; multiple implementations"
4
4
  s.description = "Pure ruby based on ECDSA gem; separate libsecp256k1 impl"
5
5
  s.authors = ["Rick Hull"]
6
6
  s.homepage = "https://github.com/rickhull/schnorr_sig"
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.files = %w[schnorr_sig.gemspec VERSION README.md Rakefile]
14
14
  s.files += Dir['lib/**/*.rb']
15
15
  s.files += Dir['test/**/*.rb']
16
- # s.files += Dir['examples/**/*.rb']
16
+ s.files += Dir['sig/**/*.rbs']
17
17
 
18
18
  s.add_dependency "ecdsa_ext", "~> 0"
19
19
  end
data/sig/fast.rbs ADDED
@@ -0,0 +1,15 @@
1
+ module SchnorrSig
2
+ CONTEXT: Secp256k1::Context
3
+
4
+ module Fast
5
+ def keypair_obj: (?String sk) -> Secp256k1::KeyPair
6
+ def extract_keys: (Secp256k1::KeyPair keypair_obj) -> [String, String]
7
+ def pubkey: (String sk) -> String
8
+ def keypair: -> [String, String]
9
+ def signature: (String str) -> Secp256k1::SchnorrSignature
10
+ def sign: (String sk, String m) -> String
11
+ def verify?: (String pk, String m, String sig) -> bool
12
+ def soft_verify?: (String pk, String m, String sig) -> bool
13
+ def tagged_hash: (String tag, String msg) -> String
14
+ end
15
+ end
data/sig/pure.rbs ADDED
@@ -0,0 +1,24 @@
1
+ module SchnorrSig
2
+ class SanityCheck < Error
3
+ end
4
+
5
+ GROUP: ECDSA::Group::Secp256k1
6
+ P: Integer
7
+ N: Integer
8
+ B: Integer
9
+
10
+ module Pure
11
+ def random_bytes: (Integer count) -> String
12
+ def point: (Integer int) -> ECDSA::Point
13
+ def select_even_y: (ECDSA::Point point, Integer even_val) -> Integer
14
+ def int: (String x) -> Integer
15
+ def bytes: (Integer | ECDSA::Point val) -> String
16
+ def lift_x: (Integer x) -> ECDSA::Point
17
+ def tagged_hash: (String tag, String msg) -> String
18
+ def pubkey: (String sk) -> String
19
+ def keypair: -> [String, String]
20
+ def sign: (String sk, String m, ?auxrand: String?) -> String
21
+ def verify?: (String pk, String m, String sig) -> bool
22
+ def soft_verify?: (String pk, String m, String sig) -> bool
23
+ end
24
+ end
data/sig/utils.rbs ADDED
@@ -0,0 +1,18 @@
1
+ module SchnorrSig
2
+ class Error < RuntimeError
3
+ end
4
+ class SpecError < Error
5
+ end
6
+
7
+ KEY: 32
8
+ SIG: 64
9
+
10
+ module Utils
11
+ def check!: (untyped val, Class cls) -> untyped
12
+ def binary!: (String str, Integer length) -> String
13
+ def bin2big: (String str) -> Integer
14
+ def big2bin: (Integer bignum) -> String
15
+ def bin2hex: (String str) -> String
16
+ def hex2bin: (String hex) -> String
17
+ end
18
+ end
data/test/fast.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'schnorr_sig/fast'
2
+ require 'minitest/autorun'
3
+
4
+ include SchnorrSig
5
+
6
+ describe Fast do
7
+ describe "keys" do
8
+ it "generates a Secp256k1::KeyPair" do
9
+ kp = Fast.keypair_obj
10
+ expect(kp).must_be_kind_of Secp256k1::KeyPair
11
+
12
+ kp = Fast.keypair_obj(Random.bytes(32))
13
+ expect(kp).must_be_kind_of Secp256k1::KeyPair
14
+ end
15
+
16
+ it "extracts 32 byte binary strings from KeyPair" do
17
+ keys = Fast.extract_keys(Fast.keypair_obj)
18
+ keys.each { |key|
19
+ expect(key).must_be_kind_of String
20
+ expect(key.length).must_equal 32
21
+ expect(key.encoding).must_equal Encoding::BINARY
22
+ }
23
+ end
24
+
25
+ it "generates a pubkey for any secret key" do
26
+ sk = Random.bytes(32)
27
+ pk = Fast.pubkey(sk)
28
+ expect(pk).must_be_kind_of String
29
+ expect(pk.length).must_equal 32
30
+ expect(pk.encoding).must_equal Encoding::BINARY
31
+ end
32
+
33
+ it "generates a keypair of 32 byte binary strings" do
34
+ keys = Fast.keypair
35
+ keys.each { |key|
36
+ expect(key).must_be_kind_of String
37
+ expect(key.length).must_equal 32
38
+ expect(key.encoding).must_equal Encoding::BINARY
39
+ }
40
+ end
41
+ end
42
+
43
+ describe "signatures" do
44
+ it "generates a Secp256k1::SchnorrSignature" do
45
+ sk = Random.bytes(32)
46
+ m = Fast.tagged_hash('test', 'hello world')
47
+ sig = Fast.sign(sk, m)
48
+ obj = Fast.signature(sig)
49
+ expect(obj).must_be_kind_of Secp256k1::SchnorrSignature
50
+ end
51
+
52
+ it "signs a message with a 64 byte binary signature" do
53
+ sk = Random.bytes(32)
54
+ m = Fast.tagged_hash('test', 'hello world')
55
+ sig = Fast.sign(sk, m)
56
+ expect(sig).must_be_kind_of String
57
+ expect(sig.length).must_equal 64
58
+ expect(sig.encoding).must_equal Encoding::BINARY
59
+ end
60
+
61
+ it "verifies signatures" do
62
+ sk, pk = Fast.keypair
63
+ m = Fast.tagged_hash('test', 'hello world')
64
+ sig = Fast.sign(sk, m)
65
+ expect(Fast.verify?(pk, m, sig)).must_equal true
66
+ end
67
+ end
68
+
69
+ it "implements tagged hashes" do
70
+ # SHA256.digest
71
+ h = Fast.tagged_hash('BIP0340/challenge', 'hello world')
72
+ expect(h).must_be_kind_of String
73
+ expect(h.length).must_equal 32
74
+ expect(h.encoding).must_equal Encoding::BINARY
75
+ end
76
+ end
data/test/pure.rb ADDED
@@ -0,0 +1,111 @@
1
+ require 'schnorr_sig/pure'
2
+ require 'minitest/autorun'
3
+
4
+ include SchnorrSig
5
+
6
+ ENV['NO_SECURERANDOM'] = '1'
7
+
8
+ describe Pure do
9
+ describe "Utils" do
10
+ it "converts any integer to a point on the curve" do
11
+ expect(Pure.point(99)).must_be_kind_of ECDSA::Point
12
+ expect(Pure.point(0).infinity?).must_equal true
13
+ p1 = Pure.point(1)
14
+ expect(p1.x).must_be :>, 999_999
15
+ expect(p1.y).must_be :>, 999_999
16
+ end
17
+
18
+ it "selects (x) or (N-x), if point.y is even or odd" do
19
+ even_y = Pure.point(99)
20
+ expect(even_y.y.even?).must_equal true
21
+
22
+ expect(Pure.select_even_y(even_y, 0)).must_equal 0
23
+ expect(Pure.select_even_y(even_y, 1)).must_equal 1
24
+
25
+ odd_y = Pure.point(10)
26
+ expect(odd_y.y.even?).must_equal false
27
+
28
+ expect(Pure.select_even_y(odd_y, 0)).wont_equal 0
29
+ expect(Pure.select_even_y(odd_y, 1)).wont_equal 1
30
+ end
31
+
32
+ it "converts up to 64 byte binary values to large integers" do
33
+ b32 = Random.bytes(32)
34
+ expect(b32).must_be_kind_of String
35
+ expect(b32.length).must_equal 32
36
+ b64 = Random.bytes(64)
37
+
38
+ i32 = Pure.int(b32) # Pure.int() is an alias to bin2big()
39
+ i64 = Pure.bin2big(b64) # this comes from schnorr_sig/utils.rb
40
+
41
+ expect(i32).must_be_kind_of Integer
42
+ expect(i32.positive?).must_equal true
43
+
44
+ expect(i64).must_be :>, i32
45
+
46
+ expect(Pure.int("\x00")).must_equal 0
47
+ expect(Pure.int("\x00\xFF")).must_equal 255
48
+ end
49
+
50
+ it "converts an integer or point to a binary string" do
51
+ str = Pure.bytes(0)
52
+ expect(str).must_be_kind_of String
53
+ expect(str.length).must_equal 32
54
+ expect(str).must_equal ("\x00" * 32).b
55
+
56
+ p = Pure.point(1234)
57
+ expect(Pure.bytes(p)).must_equal Pure.big2bin(p.x)
58
+ end
59
+
60
+ it "implements lift_x()" do
61
+ expect(Pure.lift_x(1)).must_be_kind_of ECDSA::Point
62
+ end
63
+
64
+ it "implements tagged hashes" do
65
+ h = Pure.tagged_hash('BIP0340/challenge', 'hello world') # SHA256
66
+ expect(h).must_be_kind_of String
67
+ expect(h.length).must_equal 32
68
+ expect(h.encoding).must_equal Encoding::BINARY
69
+ end
70
+ end
71
+
72
+ describe "Keys" do
73
+ it "generates a pubkey for any secret key value" do
74
+ sk = Random.bytes(32)
75
+ pk = Pure.pubkey(sk)
76
+ expect(pk).must_be_kind_of String
77
+ expect(pk.length).must_equal 32
78
+ expect(pk.encoding).must_equal Encoding::BINARY
79
+ end
80
+
81
+ it "generates a keypair of 32 byte binary values" do
82
+ keys = Pure.keypair
83
+ keys.each { |key|
84
+ expect(key).must_be_kind_of String
85
+ expect(key.length).must_equal 32
86
+ expect(key.encoding).must_equal Encoding::BINARY
87
+ }
88
+ end
89
+ end
90
+
91
+ describe "Signatures" do
92
+ it "signs a message" do
93
+ sk = Random.bytes(32)
94
+ m = 'hello world'
95
+
96
+ # typically you want to just sign the hash of the message (SHA256)
97
+ # but sure, you can sign the message itself
98
+ sig = Pure.sign(sk, m)
99
+ expect(sig).must_be_kind_of String
100
+ expect(sig.length).must_equal 64
101
+ expect(sig.encoding).must_equal Encoding::BINARY
102
+ end
103
+
104
+ it "verifies signatures" do
105
+ sk, pk = Pure.keypair
106
+ m = 'hello world'
107
+ sig = Pure.sign(sk, m)
108
+ expect(Pure.verify?(pk, m, sig)).must_equal true
109
+ end
110
+ end
111
+ end
data/test/utils.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'schnorr_sig/utils'
2
+ require 'minitest/autorun'
3
+
4
+ include SchnorrSig
5
+
6
+ Utils.extend(Utils)
7
+
8
+ describe Utils do
9
+ describe "type enforcement" do
10
+ it "enforces the class of any object" do
11
+ expect(Utils.check!('123', String)).must_equal '123'
12
+ expect(Utils.check!(123, Integer)).must_equal 123
13
+ expect { Utils.check!([], String) }.must_raise SpecError
14
+ end
15
+
16
+ it "enforces binary strings: type, encoding, length" do
17
+ expect(Utils.binary!("\x00\x01".b, 2)).must_equal "\x00\x01".b
18
+ expect {
19
+ Utils.binary!("\x00\x01".b, 3)
20
+ }.must_raise SpecError
21
+ expect {
22
+ Utils.binary!("\x00\x01", 2)
23
+ }.must_raise SpecError
24
+ end
25
+ end
26
+
27
+ describe "conversion functions" do
28
+ it "converts binary strings (network order, big endian) to integers" do
29
+ expect(Utils.bin2big("\00")).must_equal 0
30
+ expect(Utils.bin2big("\xFF\xFF")).must_equal 65535
31
+ end
32
+
33
+ it "converts large integers to binary strings, null padded to 32 bytes" do
34
+ expect(Utils.big2bin(0)).must_equal ("\x00" * 32).b
35
+ expect(Utils.big2bin(1)).must_equal ("\x00" * 31 + "\x01").b
36
+ end
37
+
38
+ it "converts binary strings to lowercase hex strings" do
39
+ expect(Utils.bin2hex("\xDE\xAD\xBE\xEF")).must_equal "deadbeef"
40
+ end
41
+
42
+ it "converts hex strings to binary strings" do
43
+ expect(Utils.hex2bin("deadbeef")).must_equal "\xDE\xAD\xBE\xEF".b
44
+ expect(Utils.hex2bin("deadbeef")).must_equal "\xde\xad\xbe\xef".b
45
+ expect(Utils.hex2bin("DEADBEEF")).must_equal "\xDE\xAD\xBE\xEF".b
46
+ expect(Utils.hex2bin("DEADBEEF")).must_equal "\xde\xad\xbe\xef".b
47
+ end
48
+ end
49
+ end
data/test/vectors.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  require 'schnorr_sig'
2
2
  require 'csv'
3
3
 
4
+ ENV['NO_SECURERANDOM'] = '1'
5
+
4
6
  path = File.join(__dir__, 'vectors.csv')
5
7
  table = CSV.read(path, headers: true)
6
8
 
7
9
  success = []
8
10
  failure = []
11
+ skip = []
9
12
 
10
13
  table.each { |row|
11
14
  pk = SchnorrSig.hex2bin row.fetch('public key')
@@ -14,18 +17,23 @@ table.each { |row|
14
17
  expected = row.fetch('verification result') == 'TRUE'
15
18
 
16
19
  result = begin
17
- SchnorrSig.verify?(pk, m, sig)
18
- rescue SchnorrSig::Error
19
- false
20
+ SchnorrSig.soft_verify?(pk, m, sig)
21
+ rescue SchnorrSig::SpecError
22
+ skip << row
23
+ next
20
24
  end
21
- (result == expected ? success : failure) << row
25
+
26
+ if result == expected
27
+ success << row
28
+ else
29
+ failure << row
30
+ end
22
31
  print '.'
23
32
  }
24
33
  puts
25
34
 
26
35
  puts "Success: #{success.count}"
27
36
  puts "Failure: #{failure.count}"
37
+ puts "Skipped: #{skip.count}"
28
38
 
29
- puts failure unless failure.empty?
30
-
31
- # exit failure.count
39
+ failure.each { |row| p row }
@@ -1,13 +1,14 @@
1
1
  require 'schnorr_sig'
2
2
  require 'csv'
3
3
 
4
+ ENV['NO_SECURERANDOM'] = '1'
5
+
4
6
  path = File.join(__dir__, 'vectors.csv')
5
7
  table = CSV.read(path, headers: true)
6
8
 
7
9
  table.each { |row|
8
10
  sk = SchnorrSig.hex2bin row.fetch('secret key')
9
11
  pk = SchnorrSig.hex2bin row.fetch('public key')
10
- #aux_rand = SchnorrSig.hex2bin row.fetch('aux_rand')
11
12
  m = SchnorrSig.hex2bin row.fetch('message')
12
13
  sig = SchnorrSig.hex2bin row.fetch('signature')
13
14
 
@@ -28,16 +29,24 @@ table.each { |row|
28
29
  pk_msg = (pubkey == pk) ? "pk match" : "pk mismatch"
29
30
 
30
31
  # calculate a signature
31
- calc_sig = SchnorrSig.sign(sk, m)
32
- sig_msg = (calc_sig == sig) ? "sig match" : "sig mismatch"
32
+ begin
33
+ calc_sig = SchnorrSig.sign(sk, m)
34
+ sig_msg = (calc_sig == sig) ? "sig match" : "sig mismatch"
35
+ rescue SchnorrSig::SpecError
36
+ sig_msg = "sig error"
37
+ end
33
38
  end
34
39
 
35
- result = begin
36
- SchnorrSig.verify?(pk, m, sig)
37
- rescue SchnorrSig::Error
38
- false
39
- end
40
- verify_msg = (result == expected) ? "verify match" : "verify mismatch"
40
+ if sig_msg != "sig error"
41
+ begin
42
+ result = SchnorrSig.soft_verify?(pk, m, sig)
43
+ verify_msg = (result == expected) ? "verify match" : "verify mismatch"
44
+ rescue SchnorrSig::SpecError
45
+ verify_msg = "verify error"
46
+ end
47
+ else
48
+ verify_msg = "sig error"
49
+ end
41
50
  puts [index, pk_msg, sig_msg, verify_msg, comment].join("\t")
42
51
  }
43
52
  puts
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schnorr_sig
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1.1
4
+ version: 1.0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rick Hull
@@ -36,8 +36,14 @@ files:
36
36
  - lib/schnorr_sig.rb
37
37
  - lib/schnorr_sig/fast.rb
38
38
  - lib/schnorr_sig/pure.rb
39
- - lib/schnorr_sig/util.rb
39
+ - lib/schnorr_sig/utils.rb
40
40
  - schnorr_sig.gemspec
41
+ - sig/fast.rbs
42
+ - sig/pure.rbs
43
+ - sig/utils.rbs
44
+ - test/fast.rb
45
+ - test/pure.rb
46
+ - test/utils.rb
41
47
  - test/vectors.rb
42
48
  - test/vectors_extra.rb
43
49
  homepage: https://github.com/rickhull/schnorr_sig
@@ -62,5 +68,5 @@ requirements: []
62
68
  rubygems_version: 3.5.9
63
69
  signing_key:
64
70
  specification_version: 4
65
- summary: Schnorr signatures in Ruby, multiple implementations
71
+ summary: Schnorr signatures in Ruby; multiple implementations
66
72
  test_files: []
@@ -1,44 +0,0 @@
1
- module SchnorrSig
2
- class InputError < RuntimeError; end
3
- class SizeError < InputError; end
4
- class TypeError < InputError; end
5
- class EncodingError < InputError; end
6
-
7
- # true or raise
8
- def self.integer!(i)
9
- i.is_a?(Integer) or raise(TypeError, i.class)
10
- end
11
-
12
- # true or raise
13
- def self.string!(str)
14
- str.is_a?(String) or raise(TypeError, str.class)
15
- end
16
-
17
- # true or raise
18
- def self.bytestring!(str, size)
19
- string!(str)
20
- raise(EncodingError, str.encoding) unless str.encoding == Encoding::BINARY
21
- str.bytesize == size or raise(SizeError, str.bytesize)
22
- end
23
-
24
- # likely returns a Bignum, larger than a 64-bit hardware integer
25
- def self.bin2big(str)
26
- bin2hex(str).to_i(16)
27
- end
28
-
29
- # convert a giant integer to a binary string
30
- def self.big2bin(bignum)
31
- # much faster than ECDSA::Format -- thanks ParadoxV5
32
- hex2bin(bignum.to_s(16).rjust(B * 2, '0'))
33
- end
34
-
35
- # convert a binary string to a lowercase hex string
36
- def self.bin2hex(str)
37
- str.unpack1('H*')
38
- end
39
-
40
- # convert a hex string to a binary string
41
- def self.hex2bin(hex)
42
- [hex].pack('H*')
43
- end
44
- end