schnorr_sig 0.0.0.4

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