schnorr_sig 0.0.0.4

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: 9cd55fdf9335f6ea817837d76d3ce130f9752ab7cbf582c822e061dc68574583
4
+ data.tar.gz: dbc20c45aac2f0d9443d278a93ff6e7ae88cea2fc26b68eaf5226c6dd47979bc
5
+ SHA512:
6
+ metadata.gz: e180ad80feddacf2ee05dd4a66d8518a53233ac56ba393917bac45e19e5cae25530e87aacdcd330523ce6751e2fb754efc5359571a564db21197d7063bb3e737
7
+ data.tar.gz: fc67524a97a42b517efff14b7c4bcc21732d8f6e83a353d51220f3e7627aab646edce9e25fd9676f6f7e13bc62eceb3d52062bdbc234a0d640dfe3eba7f78086
data/README.md ADDED
@@ -0,0 +1,288 @@
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
+ # Usage
18
+
19
+ This library is provided as a RubyGem. It has a single dependency on
20
+ [ecdsa_ext](https://github.com/azuchi/ruby_ecdsa_ext), with a
21
+ corresponding transitive dependency on
22
+ [ecdsa](https://github.com/DavidEGrayson/ruby_ecdsa/).
23
+
24
+ ## Install
25
+
26
+ Install locally:
27
+
28
+ ```
29
+ $ gem install schnorr_sig
30
+ ```
31
+
32
+ Or add to your project `Gemfile`:
33
+
34
+ ```
35
+ gem 'schnorr_sig'
36
+ ```
37
+
38
+ By default, only the dependencies for the Ruby implementation will be
39
+ installed: **ecdsa_ext** gem and its dependencies.
40
+
41
+ ### Fast Implementation
42
+
43
+ After installing the **schnorr_sig** gem, then install
44
+ [rbsecp256k1](https://github.com/etscrivner/rbsecp256k1).
45
+ Here's how I did it on NixOS:
46
+
47
+ ```
48
+ nix-shell -p secp256k1 autoconf automake libtool
49
+ gem install rbsecp256k1 -- --with-system-libraries
50
+ ```
51
+
52
+ ## Example
53
+
54
+ ```ruby
55
+ require 'schnorr_sig'
56
+
57
+ msg = 'hello world'
58
+
59
+ # generate secret key and public key
60
+ sk, pk = SchnorrSig.keypair
61
+
62
+ # sign a message; exception raised on failure
63
+ sig = SchnorrSig.sign(sk, msg)
64
+
65
+ # the signature has already been verified, but let's check
66
+ SchnorrSig.verify?(pk, msg, sig) # => true
67
+ ```
68
+
69
+ ### Fast Implementation
70
+
71
+ ```ruby
72
+ require 'schnorr_sig/fast' # not 'schnorr_sig'
73
+
74
+ # everything else as above ...
75
+ ```
76
+
77
+ # Elliptic Curves
78
+
79
+ Note that [elliptic curves](https://en.wikipedia.org/wiki/Elliptic_curve)
80
+ are not ellipses, but can instead be described by cubic equations of
81
+ the form: `y^2 = x^3 + ax + b` where `a` and `b` are the parameters of the
82
+ resulting curve. All points `(x, y)` which satisfy a given parameterized
83
+ equation provide the exact definition of an elliptic curve.
84
+
85
+ ## Curve `secp256k1`
86
+
87
+ `secp256k1` uses `a = 0` and `b = 7`, so `y^2 = x^3 + 7`
88
+
89
+ ![secp256k1: y^2 = x^3 + 7](assets/secp256k1.png)
90
+
91
+ Here is one
92
+ [minimal definition of `secp256k1`](https://github.com/DavidEGrayson/ruby_ecdsa/blob/master/lib/ecdsa/group/secp256k1.rb):
93
+
94
+ ```
95
+ {
96
+ name: 'secp256k1',
97
+ p: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F,
98
+ a: 0,
99
+ b: 7,
100
+ g: [0x79BE667E_F9DCBBAC_55A06295_CE870B07_029BFCDB_2DCE28D9_59F2815B_16F81798,
101
+ 0x483ADA77_26A3C465_5DA4FBFC_0E1108A8_FD17B448_A6855419_9C47D08F_FB10D4B8],
102
+ n: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141,
103
+ h: 1,
104
+ }
105
+ ```
106
+
107
+ * `p` is the prime for the Field, below INTMAX(32) (256^32)
108
+ * `a` is zero, as above
109
+ * `b` is seven, as above
110
+ * `g` is the generator point: [x, y]
111
+ * `n` is the Group order, significantly below INTMAX(32)
112
+
113
+ Elliptic curves have algebraic structures called
114
+ [Groups](https://en.wikipedia.org/wiki/Group_\(mathematics\)) and
115
+ [Fields](https://en.wikipedia.org/wiki/Field_\(mathematics\)),
116
+ and prime numbers are useful. I won't elaborate further here, as I am
117
+ still learning in this area and reluctant to publish misunderstandings.
118
+
119
+ ## Generator Point
120
+
121
+ Every elliptic curve has an *infinity point*, and one step away from the
122
+ infinity point is a so-called *generator point*, `G`, a pair of large
123
+ integers, `(x, y)`. Add `G` to the infinity point; the result is `G`.
124
+ Add `G` again, and the result is `2G`. Where `N` is the *order* of the
125
+ curve, `NG` returns to the infinity point.
126
+
127
+ You can multiply `G` by any integer < `N` to get a corresponding point on
128
+ the curve, `(x, y)`. `G` can be compressed to just the x-value, as the
129
+ y-value can be derived from the x-value with a little bit of algebra:
130
+ `y = sign(x) * sqrt(x^3 + ax + b)`.
131
+
132
+ ## Bignums
133
+
134
+ We can conjure into existence a gigantic *32-byte* integer. Until recently,
135
+ most consumer CPUs could only handle *32-bit* integers. A 32-byte integer
136
+ is 8x larger than common hardware integers, so math on large integers must
137
+ be done in software.
138
+
139
+ In Ruby, you can get a 32-byte value with: `Random.bytes(32)`, which will
140
+ return a 32-byte binary string. There are several ways to convert this to
141
+ an integer value, which in Ruby is called a **Bignum** when it exceeds
142
+ the highest value for a **Fixnum**, which corresponds to a hardware integer.
143
+
144
+ *Fixnums are fast; Bignums are slow*
145
+
146
+ ## Keypairs
147
+
148
+ Let's conjure into existence a gigantic 32-byte integer, `sk` (secret key):
149
+
150
+ ```
151
+ sk = Random.bytes(32) # a binary string, length 32
152
+ hex = [sk].pack('H*') # convert to a hex string like: "199ace9bc1 ..."
153
+ bignum = hex.to_i(16) # convert hex to integer, possibly a bignum
154
+ ```
155
+
156
+ `sk` is now our 32-byte **secret key**, and `bignum` is the integer value
157
+ of `sk`. We can multiply `bignum` by `G` to get a corresponding point on
158
+ the elliptic curve, `P`.
159
+
160
+ `P.x` is now our **public key**, the x-value of a point on
161
+ the curve. Technically, we would want to convert the large integer `P.x`
162
+ to a binary string in order to make it a peer with `sk`.
163
+
164
+ ```
165
+ group = ECDSA::Group::Secp256k1 # get a handle for secp256k1 curve
166
+ point = group.generator * bignum # generate a point corresponding to sk
167
+ pk = big2bin(point.x) # public key: point.x as a binary string
168
+ ```
169
+
170
+ The implementation of
171
+ [big2bin](https://github.com/rickhull/schnorr_sig/blob/master/lib/schnorr_sig/util.rb#L30)
172
+ is left as an exercise for the reader.
173
+
174
+ ## Formatting
175
+
176
+ * Binary String
177
+ * Integer
178
+ * Hexadecimal String
179
+
180
+ Our baseline format is the binary string:
181
+
182
+ ```
183
+ 'asdf'.encoding # => #<Encoding:UTF-8>
184
+ 'asdf'.b # => "asdf"
185
+ 'asdf'.b.encoding # => #<Encoding:ASCII-8BIT>
186
+ Encoding::BINARY # => #<Encoding:ASCII-8BIT>
187
+ "\x00".encoding # => #<Encoding:UTF-8>
188
+ "\xFF".encoding # => #<Encoding:UTF-8>
189
+ ```
190
+
191
+ Default encoding for Ruby's `String` is `UTF-8`. This encoding can be used
192
+ for messages and tags in BIP340. Call `String#b` to return
193
+ a new string with the same value, but with `BINARY` encoding. Note that
194
+ Ruby still calls `BINARY` encoding `ASCII-8BIT`, but this may change, and
195
+ `BINARY` is preferred. Anywhere you might say `ASCII-8BIT` you can say
196
+ `BINARY` instead.
197
+
198
+ Any `UTF-8` strings will never be converted to integers. `UTF-8` strings tend
199
+ to be unrestricted or undeclared in size. So let's turn to the `BINARY`
200
+ strings.
201
+
202
+ `BINARY` strings will tend to have a known, fixed size (almost certainly 32),
203
+ and they can be expected to be converted to integers, likely Bignums.
204
+
205
+ Hexadecimal strings (aka "hex") are like `"deadbeef0123456789abcdef00ff00ff"`.
206
+ These are never used internally, but they are typically used at the user
207
+ interface layer, as binary strings are not handled well by most user
208
+ interfaces.
209
+
210
+ Any Schnorr Signature implementation must be able to efficiently convert:
211
+
212
+ * binary to bignum
213
+ * bignum to binary
214
+ * binary to hex
215
+ * hex to binary
216
+
217
+ Note that "bignum to hex" can be handled transitively and is typically not
218
+ required.
219
+
220
+ ## Takeaways
221
+
222
+ * For any given secret key (32 byte value), a public key is easily generated
223
+ * A public key is an x-value on the curve
224
+ * For any given x-value on the curve, the y-value is easily generated
225
+ * For most curves, there are two different y-values for an x-value
226
+ * We are always dealing with 32-byte integers: **Bignums**
227
+ * Converting between integer format and 32-byte strings can be expensive
228
+ * The Schnorr algorithm requires lots of `string <--> integer` conversion
229
+ * Hex strings are never used internally
230
+ * Provide efficient, obvious routines for the fundamental conversions
231
+
232
+ # Implementation
233
+
234
+ There are two independent implementations, the primary aiming for as
235
+ pure Ruby as is feasible while matching the BIP340 pseudocode,
236
+ with the secondary aiming for speed and correctness, relying on the
237
+ battle-tested [sep256k1 library](https://github.com/bitcoin-core/secp256k1)
238
+ provided by the Bitcoin project.
239
+
240
+ ## Ruby Implementation
241
+
242
+ This is the default implementation and the only implementation for which
243
+ this gem specifies its dependencies:
244
+ the [ecdsa_ext](https://github.com/azuchi/ruby_ecdsa_ext) gem, which depends
245
+ on the [ecdsa](https://github.com/DavidEGrayson/ruby_ecdsa/) gem,
246
+ which implements the Elliptic Curve Digital Signature Algorithm (ECDSA)
247
+ almost entirely in pure Ruby.
248
+
249
+ **ecdsa_ext** provides computational efficiency for points on elliptic
250
+ curves by using projective (Jacobian) rather than affine coordinates.
251
+ Very little of the code in this library relies on these gems -- mainly
252
+ for elliptical curve computations and the `secp256k1` curve definition.
253
+
254
+ Most of the code in this implementaion is based directly on the pseudocode
255
+ from [BIP340](https://bips.xyz/340). i.e. A top-to-bottom implementation
256
+ of most of the spec. Enough to generate keypairs, signatures, and perform
257
+ signature verification. Extra care was taken to make the Ruby code match
258
+ the pseudocode as close as feasible. The pseudocode is commented
259
+ [inline](lib/schnorr_sig.rb#L58).
260
+
261
+ A lot of care was taken to keep conversions and checks to a minimum. The
262
+ functions are very strict about what they accept and attempt to be as fast
263
+ as possible, while remaining expressive. This implementation should
264
+ outperform [bip-schnorrb](https://github.com/chaintope/bip-schnorrrb)
265
+ in speed, simplicity, and expressiveness.
266
+
267
+ ## Fast Implementation
268
+
269
+ This implementation depends on the
270
+ [rbsecp256k1](https://github.com/etscrivner/rbsecp256k1) gem, which is a
271
+ C extension that wraps the
272
+ [secp256k1](https://github.com/bitcoin-core/secp256k1) library, also known
273
+ as **libsecp256k1**. There is much less code here, but the `SchnorrSig`
274
+ module functions perform some input checking and match the function
275
+ signatures from the Ruby implementation. There are many advantages to
276
+ using this implementation over the Ruby implementation, aside from
277
+ efficiency, mostly having to with resistance to timing and side-channel
278
+ attacks.
279
+
280
+ The downside of using this implementation is a more difficult and involved
281
+ install process, along with a certain level of inscrutability.
282
+
283
+ ### Temporary Restriction
284
+
285
+ Currently, **rbsecp256k1** restricts messages to exactly 32 bytes, which
286
+ was part of the BIPS340 spec until April 2023, when the restriction was lifted.
287
+
288
+ 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.4
@@ -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.size == size or raise(SizeError, str.size)
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.4
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: []