schnorr_sig 0.0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d56ed017447f4066587dcfcde13389459ed331a4f0cb6ac68183dcf874ee3fba
4
+ data.tar.gz: 464672a9491ab3daf95cf78153d6d78022e73a06e4fd520a6cad40bbe0200e80
5
+ SHA512:
6
+ metadata.gz: 3e07879e1ac5f4577cde9edac986b1fe14cdc149635a358dc214e39712101a95cac2920fbf31af09792abbbc34b029a4363ac6e5945403d7e1ed1f1ea5643451
7
+ data.tar.gz: 9cb5847aa6c04d377c261e672b155bb3a6e467c223bd4b6785fe6e494f1f144fa5e48a0719f6b3beb543bff883369ec111362d9f4eaa8a7a776e8b10f9cd20d0
data/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # Schnorr Signatures
2
+
3
+ This is a simple, minimal library written in Ruby for the purpose of
4
+ calculating and verifying so-called
5
+ [Schnorr signatures](https://en.wikipedia.org/wiki/Schnorr_signature),
6
+ based on elliptic curve cryptography. This cryptographic method was
7
+ [patented by Claus P. Schnorr](https://patents.google.com/patent/US4995082)
8
+ in 1989 (expired 2010), and by 2021 it was adopted and popularized
9
+ by the [Bitcoin](https://en.wikipedia.org/wiki/Bitcoin) project.
10
+
11
+ This work is based on [BIP340](https://bips.xyz/340), one of the many
12
+ [Bitcoin Improvement Proposals](https://bips.xyz/), which are open documents
13
+ and specifications similar to
14
+ [IETF RFCs](https://en.wikipedia.org/wiki/Request_for_Comments).
15
+ BIP340 specifies elliptic curve `secp256k1` for use with Schnorr signatures.
16
+
17
+ Two separate implementations are provided.
18
+
19
+ ## Ruby Implementation
20
+
21
+ This is the default implementation: entirely Ruby code within this library,
22
+ with mostly-Ruby dependencies:
23
+
24
+ * [ecdsa_ext](https://github.com/azuchi/ruby_ecdsa_ext)
25
+ - [ecdsa](https://github.com/DavidEGrayson/ruby_ecdsa/)
26
+
27
+ ## "Fast" Implementation
28
+
29
+ This is based on the [rbsecp256k1](https://github.com/etscrivner/rbsecp256k1)
30
+ gem, which is not installed by default. The gem wraps the
31
+ [secp256k1](https://github.com/bitcoin-core/secp256k1) library from the
32
+ Bitcoin project, which provides battle-tested performance, correctness, and
33
+ security guarantees.
34
+
35
+ # Usage
36
+
37
+ This library is provided as a RubyGem. It has a single dependency on
38
+ [ecdsa_ext](https://github.com/azuchi/ruby_ecdsa_ext), with a
39
+ corresponding transitive dependency on
40
+ [ecdsa](https://github.com/DavidEGrayson/ruby_ecdsa/).
41
+
42
+ ## Install
43
+
44
+ Install locally:
45
+
46
+ ```
47
+ $ gem install schnorr_sig
48
+ ```
49
+
50
+ Or add to your project `Gemfile`:
51
+
52
+ ```
53
+ gem 'schnorr_sig'
54
+ ```
55
+
56
+ By default, only the dependencies for the Ruby implementation will be
57
+ installed: **ecdsa_ext** gem and its dependencies.
58
+
59
+ ### Fast Implementation
60
+
61
+ After installing the **schnorr_sig** gem, then install
62
+ [rbsecp256k1](https://github.com/etscrivner/rbsecp256k1).
63
+ Here's how I did it on NixOS:
64
+
65
+ ```
66
+ nix-shell -p secp256k1 autoconf automake libtool
67
+ gem install rbsecp256k1 -- --with-system-libraries
68
+ ```
69
+
70
+ ## Example
71
+
72
+ ```ruby
73
+ require 'schnorr_sig'
74
+
75
+ msg = 'hello world'
76
+
77
+ # generate secret key and public key
78
+ sk, pk = SchnorrSig.keypair
79
+
80
+ # sign a message; exception raised on failure
81
+ sig = SchnorrSig.sign(sk, msg)
82
+
83
+ # the signature has already been verified, but let's check
84
+ SchnorrSig.verify?(pk, msg, sig) # => true
85
+ ```
86
+
87
+ ### Fast Implementation
88
+
89
+ ```ruby
90
+ require 'schnorr_sig/fast' # not 'schnorr_sig'
91
+
92
+ # everything else as above ...
93
+ ```
94
+
95
+ # Elliptic Curves
96
+
97
+ Note that [elliptic curves](https://en.wikipedia.org/wiki/Elliptic_curve)
98
+ are not ellipses, but are instead described by cubic equations of
99
+ the form: `y^2 = x^3 + ax + b` where `a` and `b` are the parameters of the
100
+ resulting equation. All points `(x, y)` which satisfy a given parameterized
101
+ equation provide the exact definition of an elliptic curve.
102
+
103
+ ## Curve `secp256k1`
104
+
105
+ `secp256k1` uses `a = 0` and `b = 7`, so `y^2 = x^3 + 7`
106
+
107
+ ![secp256k1: y^2 = x^3 + 7](assets/secp256k1.png)
108
+
109
+ Here is one
110
+ [minimal definition of `secp256k1`](https://github.com/DavidEGrayson/ruby_ecdsa/blob/master/lib/ecdsa/group/secp256k1.rb):
111
+
112
+ ```
113
+ {
114
+ name: 'secp256k1',
115
+ p: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F,
116
+ a: 0,
117
+ b: 7,
118
+ g: [0x79BE667E_F9DCBBAC_55A06295_CE870B07_029BFCDB_2DCE28D9_59F2815B_16F81798,
119
+ 0x483ADA77_26A3C465_5DA4FBFC_0E1108A8_FD17B448_A6855419_9C47D08F_FB10D4B8],
120
+ n: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141,
121
+ h: 1,
122
+ }
123
+ ```
124
+
125
+ * `p` is the prime for the Field, below `INTMAX(32)` (256^32)
126
+ * `a` is zero, as above
127
+ * `b` is seven, as above
128
+ * `g` is the generator point: `[x, y]`
129
+ * `n` is the Group order, significantly below `INTMAX(32)`
130
+
131
+ Elliptic curves have algebraic structures called
132
+ [Groups](https://en.wikipedia.org/wiki/Group_\(mathematics\)) and
133
+ [Fields](https://en.wikipedia.org/wiki/Field_\(mathematics\)),
134
+ and prime numbers are useful. I won't elaborate further here, as I am
135
+ still learning in this area and reluctant to publish misunderstandings.
136
+
137
+ ## Generator Point
138
+
139
+ Every elliptic curve has an *infinity point*, and one step away from the
140
+ infinity point is a so-called *generator point*, `G`, a pair of large
141
+ integers, `(x, y)`. Add `G` to the infinity point; the result is `G`.
142
+ Add `G` again, and the result is `2G`. Where `N` is the *order* of the
143
+ curve, `NG` returns to the infinity point.
144
+
145
+ You can multiply `G` by any integer < `N` to get a corresponding point on
146
+ the curve, `(x, y)`. `G` can be compressed to just the x-value, as the
147
+ y-value can be derived from the x-value with a little bit of algebra:
148
+ `y = sign(x) * sqrt(x^3 + ax + b)`.
149
+
150
+ ## Bignums
151
+
152
+ We can conjure into existence a gigantic *32-byte* integer. Until recently,
153
+ most consumer CPUs could only handle *32-bit* integers. A 32-byte integer
154
+ is 8x larger than common hardware integers, so math on large integers must
155
+ be done in software.
156
+
157
+ In Ruby, you can get a 32-byte value with: `Random.bytes(32)`, which will
158
+ return a 32-byte binary string. There are several ways to convert this to
159
+ an integer value, which in Ruby is called a **Bignum** when it exceeds
160
+ the highest value for a **Fixnum**, which corresponds to a hardware integer.
161
+
162
+ *Fixnums are fast; Bignums are slow*
163
+
164
+ ## Keypairs
165
+
166
+ Let's conjure into existence a gigantic 32-byte integer, `sk` (secret key):
167
+
168
+ ```
169
+ sk = Random.bytes(32) # a binary string, length 32
170
+ hex = [sk].pack('H*') # convert to a hex string like: "199ace9bc1 ..."
171
+ bignum = hex.to_i(16) # convert hex to integer, possibly a bignum
172
+ ```
173
+
174
+ `sk` is now our 32-byte **secret key**, and `bignum` is the integer value
175
+ of `sk`. We can multiply `bignum` by `G` to get a corresponding point on
176
+ the elliptic curve, `P`.
177
+
178
+ `P.x` is now our **public key**, the x-value of a point on
179
+ the curve. Technically, we would want to convert the large integer `P.x`
180
+ to a binary string in order to make it a peer with `sk`.
181
+
182
+ ```
183
+ group = ECDSA::Group::Secp256k1 # get a handle for secp256k1 curve
184
+ point = group.generator * bignum # generate a point corresponding to sk
185
+ pk = big2bin(point.x) # public key: point.x as a binary string
186
+ ```
187
+
188
+ The implementation of
189
+ [big2bin](https://github.com/rickhull/schnorr_sig/blob/master/lib/schnorr_sig/util.rb#L30)
190
+ is left as an exercise for the reader.
191
+
192
+ ## Formatting
193
+
194
+ * Binary String
195
+ * Integer
196
+ * Hexadecimal String
197
+
198
+ Our baseline format is the binary string:
199
+
200
+ ```
201
+ 'asdf'.encoding # => #<Encoding:UTF-8>
202
+ 'asdf'.b # => "asdf"
203
+ 'asdf'.b.encoding # => #<Encoding:ASCII-8BIT>
204
+ Encoding::BINARY # => #<Encoding:ASCII-8BIT>
205
+ "\x00".encoding # => #<Encoding:UTF-8>
206
+ "\xFF".encoding # => #<Encoding:UTF-8>
207
+ ```
208
+
209
+ Default encoding for Ruby's `String` is `UTF-8`. This encoding can be used
210
+ for messages and tags in BIP340. Call `String#b` to return
211
+ a new string with the same value, but with `BINARY` encoding. Note that
212
+ Ruby still calls `BINARY` encoding `ASCII-8BIT`, but this may change, and
213
+ `BINARY` is preferred. Anywhere you might say `ASCII-8BIT` you can say
214
+ `BINARY` instead.
215
+
216
+ Any `UTF-8` strings will never be converted to integers. `UTF-8` strings tend
217
+ to be unrestricted or undeclared in size. So let's turn to the `BINARY`
218
+ strings.
219
+
220
+ `BINARY` strings will tend to have a known, fixed size (almost certainly 32),
221
+ and they can be expected to be converted to integers, likely Bignums.
222
+
223
+ Hexadecimal strings (aka "hex") are like `"deadbeef0123456789abcdef00ff00ff"`.
224
+ These are never used internally, but they are typically used at the user
225
+ interface layer, as binary strings are not handled well by most user
226
+ interfaces.
227
+
228
+ Any Schnorr Signature implementation must be able to efficiently convert:
229
+
230
+ * binary to bignum
231
+ * bignum to binary
232
+ * binary to hex
233
+ * hex to binary
234
+
235
+ Note that "bignum to hex" can be handled transitively and is typically not
236
+ required.
237
+
238
+ ## Takeaways
239
+
240
+ * For any given secret key (32 byte value), a public key is easily generated
241
+ * A public key is an x-value on the curve
242
+ * For any given x-value on the curve, the y-value is easily generated
243
+ * For most curves, there are two different y-values for an x-value
244
+ * We are always dealing with 32-byte integers: **Bignums**
245
+ * Bignum math can be expensive
246
+ * Converting between integer format and 32-byte strings can be expensive
247
+ * The Schnorr algorithm requires lots of `string <--> integer` conversion
248
+ * Hex strings are never used internally
249
+ * Provide efficient, obvious routines for the fundamental conversions
250
+
251
+ # Implementation
252
+
253
+ There are two independent implementations, the primary aiming for as
254
+ pure Ruby as is feasible while matching the BIP340 pseudocode,
255
+ with the secondary aiming for speed and correctness, relying on the
256
+ battle-tested [sep256k1 library](https://github.com/bitcoin-core/secp256k1)
257
+ provided by the Bitcoin project.
258
+
259
+ ## Ruby Implementation
260
+
261
+ This is the default implementation and the only implementation for which
262
+ this gem specifies its dependencies:
263
+ the [ecdsa_ext](https://github.com/azuchi/ruby_ecdsa_ext) gem, which depends
264
+ on the [ecdsa](https://github.com/DavidEGrayson/ruby_ecdsa/) gem,
265
+ which implements the Elliptic Curve Digital Signature Algorithm (ECDSA)
266
+ almost entirely in pure Ruby.
267
+
268
+ **ecdsa_ext** provides computational efficiency for points on elliptic
269
+ curves by using projective (Jacobian) rather than affine coordinates.
270
+ Very little of the code in this library relies on these gems -- mainly
271
+ for elliptical curve computations and the `secp256k1` curve definition.
272
+
273
+ Most of the code in this implementaion is based directly on the pseudocode
274
+ from [BIP340](https://bips.xyz/340). i.e. A top-to-bottom implementation
275
+ of most of the spec. Enough to generate keypairs, signatures, and perform
276
+ signature verification. Extra care was taken to make the Ruby code match
277
+ the pseudocode as close as feasible. The pseudocode is commented
278
+ [inline](lib/schnorr_sig.rb#L58).
279
+
280
+ A lot of care was taken to keep conversions and checks to a minimum. The
281
+ functions are very strict about what they accept and attempt to be as fast
282
+ as possible, while remaining expressive. This implementation should
283
+ outperform [bip-schnorrb](https://github.com/chaintope/bip-schnorrrb)
284
+ in speed, simplicity, and expressiveness.
285
+
286
+ ## Fast Implementation
287
+
288
+ This implementation depends on the
289
+ [rbsecp256k1](https://github.com/etscrivner/rbsecp256k1) gem, which is a
290
+ C extension that wraps the
291
+ [secp256k1](https://github.com/bitcoin-core/secp256k1) library, also known
292
+ as **libsecp256k1**. There is much less code here, but the `SchnorrSig`
293
+ module functions perform some input checking and match the function
294
+ signatures from the Ruby implementation. There are many advantages to
295
+ using this implementation over the Ruby implementation, aside from
296
+ efficiency, mostly having to with resistance to timing and side-channel
297
+ attacks.
298
+
299
+ The downside of using this implementation is a more difficult and involved
300
+ install process, along with a certain level of inscrutability.
301
+
302
+ ### Temporary Restriction
303
+
304
+ Currently, **rbsecp256k1** restricts messages to exactly 32 bytes, which
305
+ was part of the BIPS340 spec until April 2023, when the restriction was lifted.
306
+
307
+ See https://github.com/etscrivner/rbsecp256k1/issues/80
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new :test do |t|
4
+ t.pattern = "test/*.rb"
5
+ t.warning = true
6
+ end
7
+
8
+ task default: :test
9
+
10
+ begin
11
+ require 'buildar'
12
+
13
+ Buildar.new do |b|
14
+ b.gemspec_file = 'schnorr_sig.gemspec'
15
+ b.version_file = 'VERSION'
16
+ b.use_git = true
17
+ end
18
+ rescue LoadError
19
+ warn "buildar tasks unavailable"
20
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0.3
@@ -0,0 +1,85 @@
1
+ require 'schnorr_sig/util'
2
+ require 'rbsecp256k1' # gem, C extension
3
+
4
+ # re-open SchnorrSig to add more functions, errors, and constants
5
+ module SchnorrSig
6
+ 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
+
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
31
+
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
42
+ end
43
+ end
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
53
+
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
61
+
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
71
+
72
+ if __FILE__ == $0
73
+ msg = 'hello world'
74
+
75
+ sk, pk = SchnorrSig.keypair
76
+ puts "Message: #{msg}"
77
+ puts "Secret key: #{SchnorrSig.bin2hex(sk)}"
78
+ puts "Public key: #{SchnorrSig.bin2hex(pk)}"
79
+
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}"
85
+ end
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,220 @@
1
+ require 'schnorr_sig/util'
2
+ require 'ecdsa_ext' # gem
3
+ autoload :SecureRandom, 'securerandom' # stdlib
4
+
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
+ module SchnorrSig
8
+ class Error < RuntimeError; end
9
+ class BoundsError < Error; end
10
+ class SanityCheck < Error; end
11
+ class VerifyFail < Error; end
12
+
13
+ GROUP = ECDSA::Group::Secp256k1
14
+ P = GROUP.field.prime # smaller than 256**32
15
+ N = GROUP.order # smaller than P
16
+ B = GROUP.byte_length # 32
17
+
18
+ # val (dot) G, returns ECDSA::Point
19
+ def self.dot_group(val)
20
+ # ecdsa_ext uses jacobian projection: 10x faster than GROUP.generator * val
21
+ (GROUP.generator.to_jacobian * val).to_affine
22
+ end
23
+
24
+ # returns even_val or N - even_val
25
+ def self.select_even_y(point, even_val)
26
+ point.y.even? ? even_val : N - even_val
27
+ end
28
+
29
+ # int(x) function signature matches BIP340, returns a bignum (presumably)
30
+ class << self
31
+ alias_method :int, :bin2big
32
+ end
33
+
34
+ # bytes(val) function signature matches BIP340, returns a binary string
35
+ def self.bytes(val)
36
+ case val
37
+ when Integer
38
+ # BIP340: The function bytes(x), where x is an integer,
39
+ # returns the 32-byte encoding of x, most significant byte first.
40
+ big2bin(val)
41
+ when ECDSA::Point
42
+ # BIP340: The function bytes(P), where P is a point, returns bytes(x(P)).
43
+ val.infinity? ? ("\x00" * B).b : big2bin(val.x)
44
+ else
45
+ raise(SanityCheck, val.inspect)
46
+ end
47
+ end
48
+
49
+ # Input
50
+ # The secret key, sk: 32 bytes binary
51
+ # The message, m: binary / UTF-8 / agnostic
52
+ # Auxiliary random data, a: 32 bytes binary
53
+ # Output
54
+ # The signature, sig: 64 bytes binary
55
+ def self.sign(sk, m, a = Random.bytes(B))
56
+ bytestring!(sk, B) and string!(m) and bytestring!(a, B)
57
+
58
+ # BIP340: Let d' = int(sk)
59
+ # BIP340: Fail if d' = 0 or d' >= n
60
+ d0 = int(sk)
61
+ raise(BoundsError, "d0") if !d0.positive? or d0 >= N
62
+
63
+ # BIP340: Let P = d' . G
64
+ p = dot_group(d0) # this is a point on the elliptic curve
65
+ bytes_p = bytes(p)
66
+
67
+ # BIP340: Let d = d' if has_even_y(P), otherwise let d = n - d'
68
+ d = select_even_y(p, d0)
69
+
70
+ # BIP340: Let t be the bytewise xor of bytes(d) and hash[BIP0340/aux](a)
71
+ t = d ^ int(tagged_hash('BIP0340/aux', a))
72
+
73
+ # BIP340: Let rand = hash[BIP0340/nonce](t || bytes(P) || m)
74
+ nonce = tagged_hash('BIP0340/nonce', bytes(t) + bytes_p + m)
75
+
76
+ # BIP340: Let k' = int(rand) mod n
77
+ # BIP340: Fail if k' = 0
78
+ k0 = int(nonce) % N
79
+ raise(BoundsError, "k0") if !k0.positive?
80
+
81
+ # BIP340: Let R = k' . G
82
+ r = dot_group(k0) # this is a point on the elliptic curve
83
+ bytes_r = bytes(r)
84
+
85
+ # BIP340: Let k = k' if has_even_y(R), otherwise let k = n - k'
86
+ k = select_even_y(r, k0)
87
+
88
+ # BIP340:
89
+ # Let e = int(hash[BIP0340/challenge](bytes(R) || bytes(P) || m)) mod n
90
+ e = int(tagged_hash('BIP0340/challenge', bytes_r + bytes_p + m)) % N
91
+
92
+ # BIP340: Let sig = bytes(R) || bytes((k + ed) mod n)
93
+ # BIP340: Fail unless Verify(bytes(P), m, sig)
94
+ # BIP340: Return the signature sig
95
+ sig = bytes_r + bytes((k + e * d) % N)
96
+ raise(VerifyFail) unless verify?(bytes_p, m, sig)
97
+ sig
98
+ end
99
+
100
+ # see https://bips.xyz/340#design (Tagged hashes)
101
+ # Input
102
+ # A tag: UTF-8 > binary > agnostic
103
+ # The payload, msg: UTF-8 / binary / agnostic
104
+ # Output
105
+ # 32 bytes binary
106
+ def self.tagged_hash(tag, msg)
107
+ string!(tag) and string!(msg)
108
+ warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8
109
+
110
+ # BIP340: The function hash[name](x) where x is a byte array
111
+ # returns the 32-byte hash
112
+ # SHA256(SHA256(tag) || SHA256(tag) || x)
113
+ # where tag is the UTF-8 encoding of name.
114
+ tag_hash = Digest::SHA256.digest(tag)
115
+ Digest::SHA256.digest(tag_hash + tag_hash + msg)
116
+ end
117
+
118
+ # Input
119
+ # The public key, pk: 32 bytes binary
120
+ # The message, m: UTF-8 / binary / agnostic
121
+ # A signature, sig: 64 bytes binary
122
+ # Output
123
+ # Boolean
124
+ def self.verify?(pk, m, sig)
125
+ bytestring!(pk, B) and string!(m) and bytestring!(sig, B * 2)
126
+
127
+ # BIP340: Let P = lift_x(int(pk))
128
+ p = lift_x(int(pk))
129
+
130
+ # BIP340: Let r = int(sig[0:32]) fail if r >= p
131
+ r = int(sig[0..B-1])
132
+ raise(BoundsError, "r >= p") if r >= P
133
+
134
+ # BIP340: Let s = int(sig[32:64]); fail if s >= n
135
+ s = int(sig[B..-1])
136
+ raise(BoundsError, "s >= n") if s >= N
137
+
138
+ # BIP340:
139
+ # Let e = int(hash[BIP0340/challenge](bytes(r) || bytes(P) || m)) mod n
140
+ e = bytes(r) + bytes(p) + m
141
+ e = int(tagged_hash('BIP0340/challenge', e)) % N
142
+
143
+ # BIP340: Let R = s . G - e . P
144
+ # BIP340: Fail if is_infinite(R)
145
+ # BIP340: Fail if not has_even_y(R)
146
+ # BIP340: Fail if x(R) != r
147
+ # BIP340: Return success iff no failure occurred before reaching this point
148
+ big_r = dot_group(s) + p.multiply_by_scalar(e).negate
149
+ !big_r.infinity? and big_r.y.even? and big_r.x == r
150
+ end
151
+
152
+ # BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,
153
+ # returns the point P for which x(P) = x[10] and has_even_y(P),
154
+ # or fails if x is greater than p-1 or no such point exists.
155
+ # Input
156
+ # A large integer, x
157
+ # Output
158
+ # ECDSA::Point
159
+ def self.lift_x(x)
160
+ integer!(x)
161
+
162
+ # BIP340: Fail if x >= p
163
+ raise(BoundsError, "x") if x >= P or x <= 0
164
+
165
+ # BIP340: Let c = x^3 + 7 mod p
166
+ c = (x.pow(3, P) + 7) % P
167
+
168
+ # BIP340: Let y = c ^ ((p + 1) / 4) mod p
169
+ y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow
170
+
171
+ # BIP340: Fail if c != y^2 mod p
172
+ raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)
173
+
174
+ # BIP340: Return the unique point P such that:
175
+ # x(P) = x and y(P) = y if y mod 2 = 0
176
+ # y(P) = p - y otherwise
177
+ GROUP.new_point [x, y.even? ? y : P - y]
178
+ end
179
+
180
+ # Input
181
+ # The secret key, sk: 32 bytes binary
182
+ # Output
183
+ # 32 bytes binary (represents P.x for point P on the curve)
184
+ def self.pubkey(sk)
185
+ bytestring!(sk, B)
186
+
187
+ # BIP340: Let d' = int(sk)
188
+ # BIP340: Fail if d' = 0 or d' >= n
189
+ # BIP340: Return bytes(d' . G)
190
+ d0 = int(sk)
191
+ raise(BoundsError, "d0") if !d0.positive? or d0 >= N
192
+ bytes(dot_group(d0))
193
+ end
194
+
195
+ # generate a new keypair based on random data
196
+ def self.keypair
197
+ sk = Random.bytes(B)
198
+ [sk, pubkey(sk)]
199
+ end
200
+
201
+ # as above, but using SecureRandom
202
+ def self.secure_keypair
203
+ sk = SecureRandom.bytes(B)
204
+ [sk, pubkey(sk)]
205
+ end
206
+ end
207
+
208
+ if __FILE__ == $0
209
+ msg = 'hello world'
210
+ sk, pk = SchnorrSig.keypair
211
+ puts "Message: #{msg}"
212
+ puts "Secret key: #{SchnorrSig.bin2hex(sk)}"
213
+ puts "Public key: #{SchnorrSig.bin2hex(pk)}"
214
+
215
+ sig = SchnorrSig.sign(sk, msg)
216
+ puts
217
+ puts "Verified signature: #{SchnorrSig.bin2hex(sig)}"
218
+ puts "Encoding: #{sig.encoding}"
219
+ puts "Length: #{sig.length}"
220
+ end
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'schnorr_sig'
3
+ s.summary = "Schnorr signatures in Ruby, multiple implementations"
4
+ s.description = "Pure ruby based on ECDSA gem; separate libsecp256k1 impl"
5
+ s.authors = ["Rick Hull"]
6
+ s.homepage = "https://github.com/rickhull/schnorr_sig"
7
+ s.license = "LGPL-2.1-only"
8
+
9
+ s.required_ruby_version = "~> 3.0"
10
+
11
+ s.version = File.read(File.join(__dir__, 'VERSION')).chomp
12
+
13
+ s.files = %w[schnorr_sig.gemspec VERSION README.md Rakefile]
14
+ s.files += Dir['lib/**/*.rb']
15
+ s.files += Dir['test/**/*.rb']
16
+ # s.files += Dir['examples/**/*.rb']
17
+
18
+ s.add_dependency "ecdsa_ext", "~> 0"
19
+ end
data/test/vectors.rb ADDED
@@ -0,0 +1,32 @@
1
+ require ENV['SCHNORR_SIG']&.downcase == 'fast' ?
2
+ 'schnorr_sig/fast' : 'schnorr_sig'
3
+ require 'csv'
4
+
5
+ path = File.join(__dir__, 'vectors.csv')
6
+ table = CSV.read(path, headers: true)
7
+
8
+ success = []
9
+ failure = []
10
+
11
+ table.each { |row|
12
+ pk = SchnorrSig.hex2bin row.fetch('public key')
13
+ m = SchnorrSig.hex2bin row.fetch('message')
14
+ sig = SchnorrSig.hex2bin row.fetch('signature')
15
+ expected = row.fetch('verification result') == 'TRUE'
16
+
17
+ result = begin
18
+ SchnorrSig.verify?(pk, m, sig)
19
+ rescue SchnorrSig::Error
20
+ false
21
+ end
22
+ (result == expected ? success : failure) << row
23
+ print '.'
24
+ }
25
+ puts
26
+
27
+ puts "Success: #{success.count}"
28
+ puts "Failure: #{failure.count}"
29
+
30
+ puts failure unless failure.empty?
31
+
32
+ # exit failure.count
@@ -0,0 +1,44 @@
1
+ require ENV['SCHNORR_SIG']&.downcase == 'fast' ?
2
+ 'schnorr_sig/fast' : 'schnorr_sig'
3
+ require 'csv'
4
+
5
+ path = File.join(__dir__, 'vectors.csv')
6
+ table = CSV.read(path, headers: true)
7
+
8
+ table.each { |row|
9
+ sk = SchnorrSig.hex2bin row.fetch('secret key')
10
+ pk = SchnorrSig.hex2bin row.fetch('public key')
11
+ #aux_rand = SchnorrSig.hex2bin row.fetch('aux_rand')
12
+ m = SchnorrSig.hex2bin row.fetch('message')
13
+ sig = SchnorrSig.hex2bin row.fetch('signature')
14
+
15
+ index = row.fetch('index')
16
+ comment = row.fetch('comment')
17
+ expected = row.fetch('verification result') == 'TRUE'
18
+
19
+ pk_msg = nil
20
+ sig_msg = nil
21
+ verify_msg = nil
22
+
23
+ if sk.empty?
24
+ pk_msg = "sk empty"
25
+ sig_msg = "sk empty"
26
+ else
27
+ # let's derive pk from sk
28
+ pubkey = SchnorrSig.pubkey(sk)
29
+ pk_msg = (pubkey == pk) ? "pk match" : "pk mismatch"
30
+
31
+ # calculate a signature
32
+ calc_sig = SchnorrSig.sign(sk, m)
33
+ sig_msg = (calc_sig == sig) ? "sig match" : "sig mismatch"
34
+ end
35
+
36
+ result = begin
37
+ SchnorrSig.verify?(pk, m, sig)
38
+ rescue SchnorrSig::Error
39
+ false
40
+ end
41
+ verify_msg = (result == expected) ? "verify match" : "verify mismatch"
42
+ puts [index, pk_msg, sig_msg, verify_msg, comment].join("\t")
43
+ }
44
+ puts
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schnorr_sig
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Rick Hull
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ecdsa_ext
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Pure ruby based on ECDSA gem; separate libsecp256k1 impl
28
+ email:
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - Rakefile
35
+ - VERSION
36
+ - lib/schnorr_sig.rb
37
+ - lib/schnorr_sig/fast.rb
38
+ - lib/schnorr_sig/util.rb
39
+ - schnorr_sig.gemspec
40
+ - test/vectors.rb
41
+ - test/vectors_extra.rb
42
+ homepage: https://github.com/rickhull/schnorr_sig
43
+ licenses:
44
+ - LGPL-2.1-only
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.5.9
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Schnorr signatures in Ruby, multiple implementations
65
+ test_files: []