schnorr_sig 0.0.0.3

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 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: []