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 +7 -0
- data/README.md +288 -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: 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
|
+

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