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 +7 -0
- data/README.md +307 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/lib/schnorr_sig/fast.rb +85 -0
- data/lib/schnorr_sig/util.rb +44 -0
- data/lib/schnorr_sig.rb +220 -0
- data/schnorr_sig.gemspec +19 -0
- data/test/vectors.rb +32 -0
- data/test/vectors_extra.rb +44 -0
- metadata +65 -0
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
|
data/lib/schnorr_sig.rb
ADDED
@@ -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
|
data/schnorr_sig.gemspec
ADDED
@@ -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: []
|