schnorr_sig 0.2.0.1 → 1.0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +89 -5
- data/Rakefile +13 -1
- data/VERSION +1 -1
- data/lib/schnorr_sig/fast.rb +110 -72
- data/lib/schnorr_sig/pure.rb +198 -189
- data/lib/schnorr_sig/utils.rb +34 -0
- data/lib/schnorr_sig.rb +13 -2
- data/schnorr_sig.gemspec +1 -2
- data/test/fast.rb +76 -0
- data/test/pure.rb +111 -0
- data/test/utils.rb +49 -0
- data/test/vectors.rb +16 -7
- data/test/vectors_extra.rb +12 -7
- metadata +6 -3
- data/lib/schnorr_sig/util.rb +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2814a65e511ccfe596ddd02783ab04e697e1b8817331e1558928cc7fb1bcf502
|
4
|
+
data.tar.gz: f7305611c29e474f29dc97b813c520b46e3e583d443c920cf20941d0690ba190
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1a31085a79d32387d426e7e8600b3752feeec45127e1233bee14db7b931e66ff5de40ca1f520088a70f979cd14efced6f24d5d1d23505028c3dc4c9aa019602
|
7
|
+
data.tar.gz: 76486cad9ad52e2c9eb40c8c7e04a809f52708381368b4b9f2fa20bd1bb5815ba74c6a30d25918b6edc046feba6d3b9d3605f6afa2b06c708dfbb2aba4eeb828
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[![Tests Status](https://github.com/rickhull/schnorr_sig/actions/workflows/tests.yaml/badge.svg)](https://github.com/rickhull/schnorr_sig/actions/workflows/tests.yaml)
|
2
|
+
|
1
3
|
# Schnorr Signatures
|
2
4
|
|
3
5
|
This is a simple, minimal library written in Ruby for the purpose of
|
@@ -77,19 +79,101 @@ msg = 'hello world'
|
|
77
79
|
# generate secret key and public key
|
78
80
|
sk, pk = SchnorrSig.keypair
|
79
81
|
|
80
|
-
# sign
|
82
|
+
# we can sign the message itself
|
81
83
|
sig = SchnorrSig.sign(sk, msg)
|
82
84
|
|
83
85
|
# the signature has already been verified, but let's check
|
84
86
|
SchnorrSig.verify?(pk, msg, sig) # => true
|
87
|
+
|
88
|
+
# more commonly, you can sign a SHA256 hash of the message
|
89
|
+
h = Digest::SHA256.digest(msg)
|
90
|
+
sig = SchnorrSig.sign(sk, h)
|
91
|
+
SchnorrSig.verify?(pk, h, sig) # => true
|
92
|
+
|
93
|
+
# you can even use SchnorrSig's concept of a tagged hash
|
94
|
+
h = SchnorrSig.tagged_hash('signing', msg)
|
95
|
+
sig = SchnorrSig.sign(sk, h)
|
96
|
+
SchnorrSig.verify?(pk, h, sig) # => true
|
97
|
+
|
98
|
+
# validate that the hash corresponds to the message
|
99
|
+
# re-hash the message with the same tag
|
100
|
+
SchnorrSig.tagged_hash('signing', msg) == h # => true
|
85
101
|
```
|
86
102
|
|
87
|
-
|
103
|
+
## Fundamentals
|
104
|
+
|
105
|
+
Here are the fundamental functions common to both implementations:
|
106
|
+
|
107
|
+
* `sign(32B sk, str msg)` *returns* `64B sig`
|
108
|
+
* `verify?(32B pk, str msg, 64B sig)` *returns* `bool`
|
109
|
+
* `pubkey(32B sk)` *returns* `32B pk`
|
110
|
+
* `tagged_hash(str tag, str msg)` *returns* `32B hash`
|
111
|
+
* `keypair()` *returns* `[32B sk, 32B pk]`
|
112
|
+
|
113
|
+
Use `soft_verify?(pk, msg, sig)` to yield `false` if errors are raised.
|
114
|
+
|
115
|
+
### Differences
|
116
|
+
|
117
|
+
* Fast: `sign(32B sk, 32B msg)`
|
118
|
+
|
119
|
+
The fast implementation only signs 32 byte payloads. It expects to sign
|
120
|
+
a hash of the message and not the message itself. The pure implementation
|
121
|
+
is happy to sign any payload.
|
122
|
+
|
123
|
+
* Pure: `sign(32B sk, str msg, auxrand: 32B)` *(auxrand is optional)*
|
124
|
+
|
125
|
+
The fast implementation always generates `auxrand` at signing time via
|
126
|
+
`SecureRandom`. The pure implementation allows `auxrand` to be passed in,
|
127
|
+
but when omitted it will be generated by default by `SecureRandom`,
|
128
|
+
though `Random` may also be used via `NO_SECURERANDOM` environment variable.
|
129
|
+
|
130
|
+
## Enable Fast Implementation
|
131
|
+
|
132
|
+
*The `rbsecp256k1` gem must be installed,
|
133
|
+
otherwise there will be a `LoadError`.*
|
134
|
+
|
135
|
+
Ensure `ENV['SCHNORR_SIG']&.downcase == 'fast'`, and then
|
136
|
+
`require 'schnorr_sig'` will try the fast implementation first, before
|
137
|
+
falling back to the pure implementation.
|
138
|
+
|
139
|
+
After `require 'schnorr_sig'`, you can check which implementation is loaded
|
140
|
+
by the presence of `SchnorrSig::Pure` or `SchnorrSig::Fast`.
|
141
|
+
|
142
|
+
### Load Directly
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
require 'schnorr_sig/fast'
|
146
|
+
|
147
|
+
include SchnorrSig
|
148
|
+
|
149
|
+
sk, pk = Fast.keypair
|
150
|
+
msg = 'hello world'
|
151
|
+
hsh = Fast.tagged_hash('message', msg)
|
152
|
+
|
153
|
+
sig = Fast.sign(sk, hsh)
|
154
|
+
Fast.verify?(pk, hsh, sig) # => true
|
155
|
+
```
|
156
|
+
|
157
|
+
### Side by Side
|
158
|
+
|
159
|
+
You can run each implementation side by side as follows:
|
88
160
|
|
89
161
|
```ruby
|
90
|
-
require 'schnorr_sig/
|
162
|
+
require 'schnorr_sig/pure'
|
163
|
+
require 'schnorr_sig/fast'
|
164
|
+
|
165
|
+
include SchnorrSig
|
166
|
+
|
167
|
+
sk, pk = Pure.keypair # or Fast.keypair
|
168
|
+
|
169
|
+
msg = 'hello world'
|
170
|
+
hsh = Fast.tagged_hash('message', msg)
|
171
|
+
|
172
|
+
sig1 = Pure.sign(sk, hsh)
|
173
|
+
sig2 = Fast.sign(sk, hsh)
|
91
174
|
|
92
|
-
|
175
|
+
Fast.verify?(pk, hsh, sig1) # => true
|
176
|
+
Pure.verify?(pk, hsh, sig2) # => true
|
93
177
|
```
|
94
178
|
|
95
179
|
# Elliptic Curves
|
@@ -186,7 +270,7 @@ pk = big2bin(point.x) # public key: point.x as a binary string
|
|
186
270
|
```
|
187
271
|
|
188
272
|
The implementation of
|
189
|
-
[big2bin](https://github.com/rickhull/schnorr_sig/blob/master/lib/schnorr_sig/
|
273
|
+
[big2bin](https://github.com/rickhull/schnorr_sig/blob/master/lib/schnorr_sig/utils.rb#L26)
|
190
274
|
is left as an exercise for the reader.
|
191
275
|
|
192
276
|
## Formatting
|
data/Rakefile
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
require 'rake/testtask'
|
2
2
|
|
3
3
|
Rake::TestTask.new :test do |t|
|
4
|
-
t.
|
4
|
+
t.test_files = [
|
5
|
+
'test/utils.rb',
|
6
|
+
'test/pure.rb',
|
7
|
+
'test/vectors.rb',
|
8
|
+
]
|
9
|
+
t.warning = true
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::TestTask.new :vectors do |t|
|
13
|
+
t.test_files = [
|
14
|
+
'test/vectors.rb',
|
15
|
+
'test/vectors_extra.rb',
|
16
|
+
]
|
5
17
|
t.warning = true
|
6
18
|
end
|
7
19
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0.1
|
data/lib/schnorr_sig/fast.rb
CHANGED
@@ -1,85 +1,123 @@
|
|
1
|
-
require 'schnorr_sig/
|
2
|
-
require 'rbsecp256k1'
|
1
|
+
require 'schnorr_sig/utils'
|
2
|
+
require 'rbsecp256k1' # gem, C extension
|
3
3
|
|
4
|
-
# re-open SchnorrSig to add more functions, errors, and constants
|
5
4
|
module SchnorrSig
|
6
5
|
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
6
|
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
7
|
+
# KeyPair
|
8
|
+
# - Create / Split
|
9
|
+
# * Context.create.generate_keypair => KeyPair
|
10
|
+
# * KeyPair#xonly_public_key => XOnlyPublicKey
|
11
|
+
# * KeyPair#private_key => PrivateKey
|
12
|
+
# - String Conversion
|
13
|
+
# * Context.create.keypair_from_private_key(sk) => KeyPair
|
14
|
+
# * XOnlyPublicKey.from_data(pk) => XOnlyPublicKey
|
15
|
+
# * XOnlyPublicKey#serialized => pk
|
16
|
+
# * PrivateKey#data => sk
|
17
|
+
|
18
|
+
# Signature
|
19
|
+
# - Sign / Verify
|
20
|
+
# * Context.create.sign_schnorr(KeyPair, m) => Signature
|
21
|
+
# * Signature#verify(m, XOnlyPublicKey) => bool
|
22
|
+
# - String Conversion
|
23
|
+
# * Signature#serialized => sig (64B String)
|
24
|
+
# * Signature#from_data(sig) => Signature
|
31
25
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
26
|
+
module Fast
|
27
|
+
|
28
|
+
#
|
29
|
+
# Keys
|
30
|
+
#
|
31
|
+
|
32
|
+
# Input
|
33
|
+
# (The secret key, sk: 32 bytes binary)
|
34
|
+
# Output
|
35
|
+
# Secp256k1::KeyPair
|
36
|
+
def keypair_obj(sk = nil)
|
37
|
+
if sk
|
38
|
+
binary!(sk, KEY)
|
39
|
+
CONTEXT.key_pair_from_private_key(sk)
|
40
|
+
else
|
41
|
+
CONTEXT.generate_key_pair
|
42
|
+
end
|
42
43
|
end
|
43
|
-
end
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
45
|
+
# Input
|
46
|
+
# Secp256k1::KeyPair
|
47
|
+
# Output
|
48
|
+
# [sk, pk] (32 bytes binary)
|
49
|
+
def extract_keys(keypair_obj)
|
50
|
+
[keypair_obj.private_key.data, keypair_obj.xonly_public_key.serialized]
|
51
|
+
end
|
53
52
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
keypair(sk)[1]
|
60
|
-
end
|
53
|
+
# Input
|
54
|
+
# The secret key, sk: 32 bytes binary
|
55
|
+
# Output
|
56
|
+
# The public key: 32 bytes binary
|
57
|
+
def pubkey(sk) = keypair_obj(sk).xonly_public_key.serialized
|
61
58
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# Secp256k1::SchnorrSignature
|
66
|
-
def self.signature(str)
|
67
|
-
bytestring!(str, 64)
|
68
|
-
Secp256k1::SchnorrSignature.from_data(str)
|
69
|
-
end
|
70
|
-
end
|
59
|
+
# Output
|
60
|
+
# [sk, pk] (32 bytes binary)
|
61
|
+
def keypair = extract_keys(keypair_obj())
|
71
62
|
|
72
|
-
|
73
|
-
|
63
|
+
#
|
64
|
+
# Signatures
|
65
|
+
#
|
74
66
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
67
|
+
# Input
|
68
|
+
# The signature, str: 64 bytes binary
|
69
|
+
# Output
|
70
|
+
# Secp256k1::SchnorrSignature
|
71
|
+
def signature(str)
|
72
|
+
binary!(str, SIG)
|
73
|
+
Secp256k1::SchnorrSignature.from_data(str)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Input
|
77
|
+
# The secret key, sk: 32 bytes binary
|
78
|
+
# The message, m: 32 byte hash value
|
79
|
+
# Output
|
80
|
+
# 64 bytes binary
|
81
|
+
def sign(sk, m)
|
82
|
+
binary!(sk, KEY) and binary!(m, 32)
|
83
|
+
CONTEXT.sign_schnorr(keypair_obj(sk), m).serialized
|
84
|
+
end
|
85
|
+
|
86
|
+
# Input
|
87
|
+
# The public key, pk: 32 bytes binary
|
88
|
+
# The message, m: 32 byte hash value
|
89
|
+
# A signature, sig: 64 bytes binary
|
90
|
+
# Output
|
91
|
+
# Boolean, may raise SchnorrSig::Error, Secp256k1::Error
|
92
|
+
def verify?(pk, m, sig)
|
93
|
+
binary!(pk, KEY) and binary!(m, 32) and binary!(sig, SIG)
|
94
|
+
signature(sig).verify(m, Secp256k1::XOnlyPublicKey.from_data(pk))
|
95
|
+
end
|
96
|
+
|
97
|
+
# as above but swallow internal errors and return false
|
98
|
+
def soft_verify?(pk, m, sig)
|
99
|
+
begin
|
100
|
+
verify?(pk, m, sig)
|
101
|
+
rescue Secp256k1::Error
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Utility
|
108
|
+
#
|
109
|
+
|
110
|
+
# Input
|
111
|
+
# tag: UTF-8 > binary > agnostic
|
112
|
+
# msg: UTF-8 / binary / agnostic
|
113
|
+
# Output
|
114
|
+
# 32 bytes binary
|
115
|
+
def tagged_hash(tag, msg)
|
116
|
+
check!(tag, String) and check!(msg, String)
|
117
|
+
CONTEXT.tagged_sha256(tag, msg)
|
118
|
+
end
|
119
|
+
end
|
79
120
|
|
80
|
-
|
81
|
-
|
82
|
-
puts "Verified signature: #{SchnorrSig.bin2hex(sig)}"
|
83
|
-
puts "Encoding: #{sig.encoding}"
|
84
|
-
puts "Length: #{sig.length}"
|
121
|
+
Fast.include Utils
|
122
|
+
Fast.extend Fast
|
85
123
|
end
|
data/lib/schnorr_sig/pure.rb
CHANGED
@@ -1,12 +1,9 @@
|
|
1
|
-
require 'schnorr_sig/
|
2
|
-
require 'ecdsa_ext' # gem
|
1
|
+
require 'schnorr_sig/utils'
|
2
|
+
require 'ecdsa_ext' # gem, depends on ecdsa gem
|
3
3
|
autoload :SecureRandom, 'securerandom' # stdlib
|
4
4
|
|
5
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
6
|
module SchnorrSig
|
8
|
-
class Error < RuntimeError; end
|
9
|
-
class BoundsError < Error; end
|
10
7
|
class SanityCheck < Error; end
|
11
8
|
class VerifyFail < Error; end
|
12
9
|
class InfinityPoint < Error; end
|
@@ -16,206 +13,218 @@ module SchnorrSig
|
|
16
13
|
N = GROUP.order # smaller than P
|
17
14
|
B = GROUP.byte_length # 32
|
18
15
|
|
19
|
-
|
20
|
-
def self.dot_group(val)
|
21
|
-
# ecdsa_ext uses jacobian projection: 10x faster than GROUP.generator * val
|
22
|
-
(GROUP.generator.to_jacobian * val).to_affine
|
23
|
-
end
|
16
|
+
module Pure
|
24
17
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
18
|
+
#
|
19
|
+
# Utils
|
20
|
+
#
|
29
21
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
22
|
+
# use SecureRandom unless ENV['NO_SECURERANDOM'] is nonempty
|
23
|
+
def random_bytes(count)
|
24
|
+
nsr = ENV['NO_SECURERANDOM']
|
25
|
+
(nsr and !nsr.empty?) ? Random.bytes(count) : SecureRandom.bytes(count)
|
26
|
+
end
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
when Integer
|
39
|
-
# BIP340: The function bytes(x), where x is an integer,
|
40
|
-
# returns the 32-byte encoding of x, most significant byte first.
|
41
|
-
big2bin(val)
|
42
|
-
when ECDSA::Point
|
43
|
-
# BIP340: The function bytes(P), where P is a point, returns bytes(x(P)).
|
44
|
-
val.infinity? ? raise(InfinityPoint, va.inspect) : big2bin(val.x)
|
45
|
-
else
|
46
|
-
raise(SanityCheck, val.inspect)
|
28
|
+
# int (dot) G, returns ECDSA::Point
|
29
|
+
def point(int)
|
30
|
+
(GROUP.generator.to_jacobian * int).to_affine # 10x faster via ecdsa_ext
|
47
31
|
end
|
48
|
-
end
|
49
32
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
# Output
|
55
|
-
# The signature, sig: 64 bytes binary
|
56
|
-
def self.sign(sk, m, a = Random.bytes(B))
|
57
|
-
bytestring!(sk, B) and string!(m) and bytestring!(a, B)
|
58
|
-
|
59
|
-
# BIP340: Let d' = int(sk)
|
60
|
-
# BIP340: Fail if d' = 0 or d' >= n
|
61
|
-
d0 = int(sk)
|
62
|
-
raise(BoundsError, "d0") if !d0.positive? or d0 >= N
|
63
|
-
|
64
|
-
# BIP340: Let P = d' . G
|
65
|
-
p = dot_group(d0) # this is a point on the elliptic curve
|
66
|
-
bytes_p = bytes(p)
|
67
|
-
|
68
|
-
# BIP340: Let d = d' if has_even_y(P), otherwise let d = n - d'
|
69
|
-
d = select_even_y(p, d0)
|
70
|
-
|
71
|
-
# BIP340: Let t be the bytewise xor of bytes(d) and hash[BIP0340/aux](a)
|
72
|
-
t = d ^ int(tagged_hash('BIP0340/aux', a))
|
73
|
-
|
74
|
-
# BIP340: Let rand = hash[BIP0340/nonce](t || bytes(P) || m)
|
75
|
-
nonce = tagged_hash('BIP0340/nonce', bytes(t) + bytes_p + m)
|
76
|
-
|
77
|
-
# BIP340: Let k' = int(rand) mod n
|
78
|
-
# BIP340: Fail if k' = 0
|
79
|
-
k0 = int(nonce) % N
|
80
|
-
raise(BoundsError, "k0") if !k0.positive?
|
81
|
-
|
82
|
-
# BIP340: Let R = k' . G
|
83
|
-
r = dot_group(k0) # this is a point on the elliptic curve
|
84
|
-
bytes_r = bytes(r)
|
85
|
-
|
86
|
-
# BIP340: Let k = k' if has_even_y(R), otherwise let k = n - k'
|
87
|
-
k = select_even_y(r, k0)
|
88
|
-
|
89
|
-
# BIP340:
|
90
|
-
# Let e = int(hash[BIP0340/challenge](bytes(R) || bytes(P) || m)) mod n
|
91
|
-
e = int(tagged_hash('BIP0340/challenge', bytes_r + bytes_p + m)) % N
|
92
|
-
|
93
|
-
# BIP340: Let sig = bytes(R) || bytes((k + ed) mod n)
|
94
|
-
# BIP340: Fail unless Verify(bytes(P), m, sig)
|
95
|
-
# BIP340: Return the signature sig
|
96
|
-
sig = bytes_r + bytes((k + e * d) % N)
|
97
|
-
raise(VerifyFail) unless verify?(bytes_p, m, sig)
|
98
|
-
sig
|
99
|
-
end
|
33
|
+
# returns even_val or N - even_val
|
34
|
+
def select_even_y(point, even_val)
|
35
|
+
point.y.even? ? even_val : N - even_val
|
36
|
+
end
|
100
37
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
38
|
+
# int(x) function signature matches BIP340, returns a bignum (presumably)
|
39
|
+
def int(x) = bin2big(x)
|
40
|
+
|
41
|
+
# bytes(val) function signature matches BIP340, returns a binary string
|
42
|
+
def bytes(val)
|
43
|
+
case val
|
44
|
+
when Integer
|
45
|
+
# BIP340: The function bytes(x), where x is an integer,
|
46
|
+
# returns the 32-byte encoding of x, most significant byte first.
|
47
|
+
big2bin(val)
|
48
|
+
when ECDSA::Point
|
49
|
+
# BIP340: The function bytes(P), where P is a point,
|
50
|
+
# returns bytes(x(P)).
|
51
|
+
val.infinity? ? raise(InfinityPoint, val.inspect) : big2bin(val.x)
|
52
|
+
else
|
53
|
+
raise(SanityCheck, val.inspect)
|
54
|
+
end
|
55
|
+
end
|
118
56
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# BIP340: Let P = lift_x(int(pk))
|
129
|
-
p = lift_x(int(pk))
|
130
|
-
|
131
|
-
# BIP340: Let r = int(sig[0:32]) fail if r >= p
|
132
|
-
r = int(sig[0..B-1])
|
133
|
-
raise(BoundsError, "r >= p") if r >= P
|
134
|
-
|
135
|
-
# BIP340: Let s = int(sig[32:64]); fail if s >= n
|
136
|
-
s = int(sig[B..-1])
|
137
|
-
raise(BoundsError, "s >= n") if s >= N
|
138
|
-
|
139
|
-
# BIP340:
|
140
|
-
# Let e = int(hash[BIP0340/challenge](bytes(r) || bytes(P) || m)) mod n
|
141
|
-
e = bytes(r) + bytes(p) + m
|
142
|
-
e = int(tagged_hash('BIP0340/challenge', e)) % N
|
143
|
-
|
144
|
-
# BIP340: Let R = s . G - e . P
|
145
|
-
# BIP340: Fail if is_infinite(R)
|
146
|
-
# BIP340: Fail if not has_even_y(R)
|
147
|
-
# BIP340: Fail if x(R) != r
|
148
|
-
# BIP340: Return success iff no failure occurred before reaching this point
|
149
|
-
big_r = dot_group(s) + p.multiply_by_scalar(e).negate
|
150
|
-
!big_r.infinity? and big_r.y.even? and big_r.x == r
|
151
|
-
end
|
57
|
+
# BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,
|
58
|
+
# returns the point P for which x(P) = x and has_even_y(P),
|
59
|
+
# or fails if x is greater than p-1 or no such point exists.
|
60
|
+
# Input
|
61
|
+
# A large integer, x
|
62
|
+
# Output
|
63
|
+
# ECDSA::Point
|
64
|
+
def lift_x(x)
|
65
|
+
check!(x, Integer)
|
152
66
|
|
153
|
-
|
154
|
-
|
155
|
-
# or fails if x is greater than p-1 or no such point exists.
|
156
|
-
# Input
|
157
|
-
# A large integer, x
|
158
|
-
# Output
|
159
|
-
# ECDSA::Point
|
160
|
-
def self.lift_x(x)
|
161
|
-
integer!(x)
|
67
|
+
# BIP340: Fail if x >= p
|
68
|
+
raise(SanityCheck, "x") if x >= P or x <= 0
|
162
69
|
|
163
|
-
|
164
|
-
|
70
|
+
# BIP340: Let c = x^3 + 7 mod p
|
71
|
+
c = (x.pow(3, P) + 7) % P
|
165
72
|
|
166
|
-
|
167
|
-
|
73
|
+
# BIP340: Let y = c ^ ((p + 1) / 4) mod p
|
74
|
+
y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow
|
168
75
|
|
169
|
-
|
170
|
-
|
76
|
+
# BIP340: Fail if c != y^2 mod p
|
77
|
+
raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)
|
171
78
|
|
172
|
-
|
173
|
-
|
79
|
+
# BIP340: Return the unique point P such that:
|
80
|
+
# x(P) = x and y(P) = y if y mod 2 = 0
|
81
|
+
# y(P) = p - y otherwise
|
82
|
+
GROUP.new_point [x, y.even? ? y : P - y]
|
83
|
+
end
|
174
84
|
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
|
179
|
-
|
85
|
+
# see https://bips.xyz/340#design (Tagged hashes)
|
86
|
+
# Input
|
87
|
+
# A tag: UTF-8 > binary > agnostic
|
88
|
+
# The payload, msg: UTF-8 / binary / agnostic
|
89
|
+
# Output
|
90
|
+
# 32 bytes binary
|
91
|
+
def tagged_hash(tag, msg)
|
92
|
+
check!(tag, String) and check!(msg, String)
|
93
|
+
warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8
|
94
|
+
|
95
|
+
# BIP340: The function hash[name](x) where x is a byte array
|
96
|
+
# returns the 32-byte hash
|
97
|
+
# SHA256(SHA256(tag) || SHA256(tag) || x)
|
98
|
+
# where tag is the UTF-8 encoding of name.
|
99
|
+
tag_hash = Digest::SHA256.digest(tag)
|
100
|
+
Digest::SHA256.digest(tag_hash + tag_hash + msg)
|
101
|
+
end
|
180
102
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
#
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
103
|
+
#
|
104
|
+
# Keys
|
105
|
+
#
|
106
|
+
|
107
|
+
# Input
|
108
|
+
# The secret key, sk: 32 bytes binary
|
109
|
+
# Output
|
110
|
+
# 32 bytes binary (represents P.x for point P on the curve)
|
111
|
+
def pubkey(sk)
|
112
|
+
binary!(sk, KEY)
|
113
|
+
|
114
|
+
# BIP340: Let d' = int(sk)
|
115
|
+
# BIP340: Fail if d' = 0 or d' >= n
|
116
|
+
# BIP340: Return bytes(d' . G)
|
117
|
+
d0 = int(sk)
|
118
|
+
raise(SanityCheck, "d0") if !d0.positive? or d0 >= N
|
119
|
+
bytes(point(d0))
|
120
|
+
end
|
195
121
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
122
|
+
# generate a new keypair based on random data
|
123
|
+
def keypair
|
124
|
+
sk = random_bytes(KEY)
|
125
|
+
[sk, pubkey(sk)]
|
126
|
+
end
|
201
127
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
128
|
+
#
|
129
|
+
# Signatures
|
130
|
+
#
|
131
|
+
|
132
|
+
# Input
|
133
|
+
# The secret key, sk: 32 bytes binary
|
134
|
+
# The message, m: binary / UTF-8 / agnostic
|
135
|
+
# Auxiliary random data, a: 32 bytes binary
|
136
|
+
# Output
|
137
|
+
# The signature, sig: 64 bytes binary
|
138
|
+
def sign(sk, m, auxrand: nil)
|
139
|
+
a = auxrand.nil? ? random_bytes(B) : auxrand
|
140
|
+
binary!(sk, KEY) and check!(m, String) and binary!(a, B)
|
141
|
+
|
142
|
+
# BIP340: Let d' = int(sk)
|
143
|
+
# BIP340: Fail if d' = 0 or d' >= n
|
144
|
+
d0 = int(sk)
|
145
|
+
raise(SanityCheck, "d0") if !d0.positive? or d0 >= N
|
146
|
+
|
147
|
+
# BIP340: Let P = d' . G
|
148
|
+
p = point(d0) # this is a point on the elliptic curve
|
149
|
+
bytes_p = bytes(p)
|
150
|
+
|
151
|
+
# BIP340: Let d = d' if has_even_y(P), otherwise let d = n - d'
|
152
|
+
d = select_even_y(p, d0)
|
153
|
+
|
154
|
+
# BIP340: Let t be the bytewise xor of bytes(d) and hash[BIP0340/aux](a)
|
155
|
+
t = d ^ int(tagged_hash('BIP0340/aux', a))
|
156
|
+
|
157
|
+
# BIP340: Let rand = hash[BIP0340/nonce](t || bytes(P) || m)
|
158
|
+
nonce = tagged_hash('BIP0340/nonce', bytes(t) + bytes_p + m)
|
159
|
+
|
160
|
+
# BIP340: Let k' = int(rand) mod n
|
161
|
+
# BIP340: Fail if k' = 0
|
162
|
+
k0 = int(nonce) % N
|
163
|
+
raise(SanityCheck, "k0") if !k0.positive?
|
164
|
+
|
165
|
+
# BIP340: Let R = k' . G
|
166
|
+
r = point(k0) # this is a point on the elliptic curve
|
167
|
+
bytes_r = bytes(r)
|
168
|
+
|
169
|
+
# BIP340: Let k = k' if has_even_y(R), otherwise let k = n - k'
|
170
|
+
k = select_even_y(r, k0)
|
171
|
+
|
172
|
+
# BIP340:
|
173
|
+
# Let e = int(hash[BIP0340/challenge](bytes(R) || bytes(P) || m)) mod n
|
174
|
+
e = int(tagged_hash('BIP0340/challenge', bytes_r + bytes_p + m)) % N
|
175
|
+
|
176
|
+
# BIP340: Let sig = bytes(R) || bytes((k + ed) mod n)
|
177
|
+
# BIP340: Fail unless Verify(bytes(P), m, sig)
|
178
|
+
# BIP340: Return the signature sig
|
179
|
+
sig = bytes_r + bytes((k + e * d) % N)
|
180
|
+
raise(VerifyFail) unless verify?(bytes_p, m, sig)
|
181
|
+
sig
|
182
|
+
end
|
183
|
+
|
184
|
+
# Input
|
185
|
+
# The public key, pk: 32 bytes binary
|
186
|
+
# The message, m: UTF-8 / binary / agnostic
|
187
|
+
# A signature, sig: 64 bytes binary
|
188
|
+
# Output
|
189
|
+
# Boolean
|
190
|
+
def verify?(pk, m, sig)
|
191
|
+
binary!(pk, KEY) and check!(m, String) and binary!(sig, SIG)
|
192
|
+
|
193
|
+
# BIP340: Let P = lift_x(int(pk))
|
194
|
+
p = lift_x(int(pk))
|
195
|
+
|
196
|
+
# BIP340: Let r = int(sig[0:32]) fail if r >= p
|
197
|
+
r = int(sig[0..KEY-1])
|
198
|
+
raise(SanityCheck, "r >= p") if r >= P
|
199
|
+
|
200
|
+
# BIP340: Let s = int(sig[32:64]); fail if s >= n
|
201
|
+
s = int(sig[KEY..-1])
|
202
|
+
raise(SanityCheck, "s >= n") if s >= N
|
203
|
+
|
204
|
+
# BIP340:
|
205
|
+
# Let e = int(hash[BIP0340/challenge](bytes(r) || bytes(P) || m)) mod n
|
206
|
+
e = bytes(r) + bytes(p) + m
|
207
|
+
e = int(tagged_hash('BIP0340/challenge', e)) % N
|
208
|
+
|
209
|
+
# BIP340: Let R = s . G - e . P
|
210
|
+
# BIP340: Fail if is_infinite(R)
|
211
|
+
# BIP340: Fail if not has_even_y(R)
|
212
|
+
# BIP340: Fail if x(R) != r
|
213
|
+
# BIP340: Return success iff no prior failure
|
214
|
+
big_r = point(s) + p.multiply_by_scalar(e).negate
|
215
|
+
!big_r.infinity? and big_r.y.even? and big_r.x == r
|
216
|
+
end
|
217
|
+
|
218
|
+
# as above but swallow internal errors and return false
|
219
|
+
def soft_verify?(pk, m, sig)
|
220
|
+
begin
|
221
|
+
verify?(pk, m, sig)
|
222
|
+
rescue SanityCheck, InfinityPoint
|
223
|
+
false
|
224
|
+
end
|
225
|
+
end
|
206
226
|
end
|
207
|
-
end
|
208
227
|
|
209
|
-
|
210
|
-
|
211
|
-
sk, pk = SchnorrSig.keypair
|
212
|
-
puts "Message: #{msg}"
|
213
|
-
puts "Secret key: #{SchnorrSig.bin2hex(sk)}"
|
214
|
-
puts "Public key: #{SchnorrSig.bin2hex(pk)}"
|
215
|
-
|
216
|
-
sig = SchnorrSig.sign(sk, msg)
|
217
|
-
puts
|
218
|
-
puts "Verified signature: #{SchnorrSig.bin2hex(sig)}"
|
219
|
-
puts "Encoding: #{sig.encoding}"
|
220
|
-
puts "Length: #{sig.length}"
|
228
|
+
Pure.include Utils
|
229
|
+
Pure.extend Pure
|
221
230
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SchnorrSig
|
2
|
+
class Error < RuntimeError; end
|
3
|
+
class SizeError < Error; end
|
4
|
+
|
5
|
+
KEY = 32 # bytes
|
6
|
+
SIG = 64 # bytes
|
7
|
+
|
8
|
+
module Utils
|
9
|
+
# raise TypeError or return val
|
10
|
+
def check!(val, cls)
|
11
|
+
val.is_a?(cls) ? val : raise(TypeError, "#{cls}: #{val.inspect}")
|
12
|
+
end
|
13
|
+
|
14
|
+
# raise TypeError, EncodingError, or SizeError, or return str
|
15
|
+
def binary!(str, length)
|
16
|
+
check!(str, String)
|
17
|
+
raise(EncodingError, str.encoding) if str.encoding != Encoding::BINARY
|
18
|
+
raise(SizeError, str.length) if str.length != length
|
19
|
+
str
|
20
|
+
end
|
21
|
+
|
22
|
+
# likely returns a Bignum, larger than a 64-bit hardware integer
|
23
|
+
def bin2big(str) = bin2hex(str).to_i(16)
|
24
|
+
|
25
|
+
# convert a giant integer to a binary string
|
26
|
+
def big2bin(bignum) = hex2bin(bignum.to_s(16).rjust(64, '0'))
|
27
|
+
|
28
|
+
# convert a binary string to a lowercase hex string
|
29
|
+
def bin2hex(str) = str.unpack1('H*')
|
30
|
+
|
31
|
+
# convert a hex string to a binary string
|
32
|
+
def hex2bin(hex) = [hex].pack('H*')
|
33
|
+
end
|
34
|
+
end
|
data/lib/schnorr_sig.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
|
+
loaded = false
|
2
|
+
|
1
3
|
if ENV['SCHNORR_SIG']&.downcase == 'fast'
|
2
|
-
|
3
|
-
|
4
|
+
begin
|
5
|
+
require 'schnorr_sig/fast'
|
6
|
+
SchnorrSig.extend SchnorrSig::Fast
|
7
|
+
loaded = true
|
8
|
+
rescue LoadError => e
|
9
|
+
warn [e.class, e.message].join(': ')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
unless loaded
|
4
14
|
require 'schnorr_sig/pure'
|
15
|
+
SchnorrSig.extend SchnorrSig::Pure
|
5
16
|
end
|
data/schnorr_sig.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'schnorr_sig'
|
3
|
-
s.summary = "Schnorr signatures in Ruby
|
3
|
+
s.summary = "Schnorr signatures in Ruby; multiple implementations"
|
4
4
|
s.description = "Pure ruby based on ECDSA gem; separate libsecp256k1 impl"
|
5
5
|
s.authors = ["Rick Hull"]
|
6
6
|
s.homepage = "https://github.com/rickhull/schnorr_sig"
|
@@ -13,7 +13,6 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.files = %w[schnorr_sig.gemspec VERSION README.md Rakefile]
|
14
14
|
s.files += Dir['lib/**/*.rb']
|
15
15
|
s.files += Dir['test/**/*.rb']
|
16
|
-
# s.files += Dir['examples/**/*.rb']
|
17
16
|
|
18
17
|
s.add_dependency "ecdsa_ext", "~> 0"
|
19
18
|
end
|
data/test/fast.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'schnorr_sig/fast'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
include SchnorrSig
|
5
|
+
|
6
|
+
describe Fast do
|
7
|
+
describe "keys" do
|
8
|
+
it "generates a Secp256k1::KeyPair" do
|
9
|
+
kp = Fast.keypair_obj
|
10
|
+
expect(kp).must_be_kind_of Secp256k1::KeyPair
|
11
|
+
|
12
|
+
kp = Fast.keypair_obj(Random.bytes(32))
|
13
|
+
expect(kp).must_be_kind_of Secp256k1::KeyPair
|
14
|
+
end
|
15
|
+
|
16
|
+
it "extracts 32 byte binary strings from KeyPair" do
|
17
|
+
keys = Fast.extract_keys(Fast.keypair_obj)
|
18
|
+
keys.each { |key|
|
19
|
+
expect(key).must_be_kind_of String
|
20
|
+
expect(key.length).must_equal 32
|
21
|
+
expect(key.encoding).must_equal Encoding::BINARY
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates a pubkey for any secret key" do
|
26
|
+
sk = Random.bytes(32)
|
27
|
+
pk = Fast.pubkey(sk)
|
28
|
+
expect(pk).must_be_kind_of String
|
29
|
+
expect(pk.length).must_equal 32
|
30
|
+
expect(pk.encoding).must_equal Encoding::BINARY
|
31
|
+
end
|
32
|
+
|
33
|
+
it "generates a keypair of 32 byte binary strings" do
|
34
|
+
keys = Fast.keypair
|
35
|
+
keys.each { |key|
|
36
|
+
expect(key).must_be_kind_of String
|
37
|
+
expect(key.length).must_equal 32
|
38
|
+
expect(key.encoding).must_equal Encoding::BINARY
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "signatures" do
|
44
|
+
it "generates a Secp256k1::SchnorrSignature" do
|
45
|
+
sk = Random.bytes(32)
|
46
|
+
m = Fast.tagged_hash('test', 'hello world')
|
47
|
+
sig = Fast.sign(sk, m)
|
48
|
+
obj = Fast.signature(sig)
|
49
|
+
expect(obj).must_be_kind_of Secp256k1::SchnorrSignature
|
50
|
+
end
|
51
|
+
|
52
|
+
it "signs a message with a 64 byte binary signature" do
|
53
|
+
sk = Random.bytes(32)
|
54
|
+
m = Fast.tagged_hash('test', 'hello world')
|
55
|
+
sig = Fast.sign(sk, m)
|
56
|
+
expect(sig).must_be_kind_of String
|
57
|
+
expect(sig.length).must_equal 64
|
58
|
+
expect(sig.encoding).must_equal Encoding::BINARY
|
59
|
+
end
|
60
|
+
|
61
|
+
it "verifies signatures" do
|
62
|
+
sk, pk = Fast.keypair
|
63
|
+
m = Fast.tagged_hash('test', 'hello world')
|
64
|
+
sig = Fast.sign(sk, m)
|
65
|
+
expect(Fast.verify?(pk, m, sig)).must_equal true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it "implements tagged hashes" do
|
70
|
+
# SHA256.digest
|
71
|
+
h = Fast.tagged_hash('BIP0340/challenge', 'hello world')
|
72
|
+
expect(h).must_be_kind_of String
|
73
|
+
expect(h.length).must_equal 32
|
74
|
+
expect(h.encoding).must_equal Encoding::BINARY
|
75
|
+
end
|
76
|
+
end
|
data/test/pure.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'schnorr_sig/pure'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
include SchnorrSig
|
5
|
+
|
6
|
+
ENV['NO_SECURERANDOM'] = '1'
|
7
|
+
|
8
|
+
describe Pure do
|
9
|
+
describe "Utils" do
|
10
|
+
it "converts any integer to a point on the curve" do
|
11
|
+
expect(Pure.point(99)).must_be_kind_of ECDSA::Point
|
12
|
+
expect(Pure.point(0).infinity?).must_equal true
|
13
|
+
p1 = Pure.point(1)
|
14
|
+
expect(p1.x).must_be :>, 999_999
|
15
|
+
expect(p1.y).must_be :>, 999_999
|
16
|
+
end
|
17
|
+
|
18
|
+
it "selects (x) or (N-x), if point.y is even or odd" do
|
19
|
+
even_y = Pure.point(99)
|
20
|
+
expect(even_y.y.even?).must_equal true
|
21
|
+
|
22
|
+
expect(Pure.select_even_y(even_y, 0)).must_equal 0
|
23
|
+
expect(Pure.select_even_y(even_y, 1)).must_equal 1
|
24
|
+
|
25
|
+
odd_y = Pure.point(10)
|
26
|
+
expect(odd_y.y.even?).must_equal false
|
27
|
+
|
28
|
+
expect(Pure.select_even_y(odd_y, 0)).wont_equal 0
|
29
|
+
expect(Pure.select_even_y(odd_y, 1)).wont_equal 1
|
30
|
+
end
|
31
|
+
|
32
|
+
it "converts up to 64 byte binary values to large integers" do
|
33
|
+
b32 = Random.bytes(32)
|
34
|
+
expect(b32).must_be_kind_of String
|
35
|
+
expect(b32.length).must_equal 32
|
36
|
+
b64 = Random.bytes(64)
|
37
|
+
|
38
|
+
i32 = Pure.int(b32) # Pure.int() is an alias to bin2big()
|
39
|
+
i64 = Pure.bin2big(b64) # this comes from schnorr_sig/utils.rb
|
40
|
+
|
41
|
+
expect(i32).must_be_kind_of Integer
|
42
|
+
expect(i32.positive?).must_equal true
|
43
|
+
|
44
|
+
expect(i64).must_be :>, i32
|
45
|
+
|
46
|
+
expect(Pure.int("\x00")).must_equal 0
|
47
|
+
expect(Pure.int("\x00\xFF")).must_equal 255
|
48
|
+
end
|
49
|
+
|
50
|
+
it "converts an integer or point to a binary string" do
|
51
|
+
str = Pure.bytes(0)
|
52
|
+
expect(str).must_be_kind_of String
|
53
|
+
expect(str.length).must_equal 32
|
54
|
+
expect(str).must_equal ("\x00" * 32).b
|
55
|
+
|
56
|
+
p = Pure.point(1234)
|
57
|
+
expect(Pure.bytes(p)).must_equal Pure.big2bin(p.x)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "implements lift_x()" do
|
61
|
+
expect(Pure.lift_x(1)).must_be_kind_of ECDSA::Point
|
62
|
+
end
|
63
|
+
|
64
|
+
it "implements tagged hashes" do
|
65
|
+
h = Pure.tagged_hash('BIP0340/challenge', 'hello world') # SHA256
|
66
|
+
expect(h).must_be_kind_of String
|
67
|
+
expect(h.length).must_equal 32
|
68
|
+
expect(h.encoding).must_equal Encoding::BINARY
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "Keys" do
|
73
|
+
it "generates a pubkey for any secret key value" do
|
74
|
+
sk = Random.bytes(32)
|
75
|
+
pk = Pure.pubkey(sk)
|
76
|
+
expect(pk).must_be_kind_of String
|
77
|
+
expect(pk.length).must_equal 32
|
78
|
+
expect(pk.encoding).must_equal Encoding::BINARY
|
79
|
+
end
|
80
|
+
|
81
|
+
it "generates a keypair of 32 byte binary values" do
|
82
|
+
keys = Pure.keypair
|
83
|
+
keys.each { |key|
|
84
|
+
expect(key).must_be_kind_of String
|
85
|
+
expect(key.length).must_equal 32
|
86
|
+
expect(key.encoding).must_equal Encoding::BINARY
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "Signatures" do
|
92
|
+
it "signs a message" do
|
93
|
+
sk = Random.bytes(32)
|
94
|
+
m = 'hello world'
|
95
|
+
|
96
|
+
# typically you want to just sign the hash of the message (SHA256)
|
97
|
+
# but sure, you can sign the message itself
|
98
|
+
sig = Pure.sign(sk, m)
|
99
|
+
expect(sig).must_be_kind_of String
|
100
|
+
expect(sig.length).must_equal 64
|
101
|
+
expect(sig.encoding).must_equal Encoding::BINARY
|
102
|
+
end
|
103
|
+
|
104
|
+
it "verifies signatures" do
|
105
|
+
sk, pk = Pure.keypair
|
106
|
+
m = 'hello world'
|
107
|
+
sig = Pure.sign(sk, m)
|
108
|
+
expect(Pure.verify?(pk, m, sig)).must_equal true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/test/utils.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'schnorr_sig/utils'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
include SchnorrSig
|
5
|
+
|
6
|
+
Utils.extend(Utils)
|
7
|
+
|
8
|
+
describe Utils do
|
9
|
+
describe "type enforcement" do
|
10
|
+
it "enforces the class of any object" do
|
11
|
+
expect(Utils.check!('123', String)).must_equal '123'
|
12
|
+
expect(Utils.check!(123, Integer)).must_equal 123
|
13
|
+
expect { Utils.check!([], String) }.must_raise TypeError
|
14
|
+
end
|
15
|
+
|
16
|
+
it "enforces binary strings: type, encoding, length" do
|
17
|
+
expect(Utils.binary!("\x00\x01".b, 2)).must_equal "\x00\x01".b
|
18
|
+
expect {
|
19
|
+
Utils.binary!("\x00\x01".b, 3)
|
20
|
+
}.must_raise SchnorrSig::SizeError
|
21
|
+
expect {
|
22
|
+
Utils.binary!("\x00\x01", 2)
|
23
|
+
}.must_raise EncodingError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "conversion functions" do
|
28
|
+
it "converts binary strings (network order, big endian) to integers" do
|
29
|
+
expect(Utils.bin2big("\00")).must_equal 0
|
30
|
+
expect(Utils.bin2big("\xFF\xFF")).must_equal 65535
|
31
|
+
end
|
32
|
+
|
33
|
+
it "converts large integers to binary strings, null padded to 32 bytes" do
|
34
|
+
expect(Utils.big2bin(0)).must_equal ("\x00" * 32).b
|
35
|
+
expect(Utils.big2bin(1)).must_equal ("\x00" * 31 + "\x01").b
|
36
|
+
end
|
37
|
+
|
38
|
+
it "converts binary strings to lowercase hex strings" do
|
39
|
+
expect(Utils.bin2hex("\xDE\xAD\xBE\xEF")).must_equal "deadbeef"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "converts hex strings to binary strings" do
|
43
|
+
expect(Utils.hex2bin("deadbeef")).must_equal "\xDE\xAD\xBE\xEF".b
|
44
|
+
expect(Utils.hex2bin("deadbeef")).must_equal "\xde\xad\xbe\xef".b
|
45
|
+
expect(Utils.hex2bin("DEADBEEF")).must_equal "\xDE\xAD\xBE\xEF".b
|
46
|
+
expect(Utils.hex2bin("DEADBEEF")).must_equal "\xde\xad\xbe\xef".b
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/test/vectors.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
require 'schnorr_sig'
|
2
2
|
require 'csv'
|
3
3
|
|
4
|
+
ENV['NO_SECURERANDOM'] = '1'
|
5
|
+
|
4
6
|
path = File.join(__dir__, 'vectors.csv')
|
5
7
|
table = CSV.read(path, headers: true)
|
6
8
|
|
7
9
|
success = []
|
8
10
|
failure = []
|
11
|
+
skip = []
|
9
12
|
|
10
13
|
table.each { |row|
|
11
14
|
pk = SchnorrSig.hex2bin row.fetch('public key')
|
@@ -14,18 +17,24 @@ table.each { |row|
|
|
14
17
|
expected = row.fetch('verification result') == 'TRUE'
|
15
18
|
|
16
19
|
result = begin
|
17
|
-
SchnorrSig.
|
18
|
-
rescue SchnorrSig::
|
19
|
-
|
20
|
+
SchnorrSig.soft_verify?(pk, m, sig)
|
21
|
+
rescue SchnorrSig::SizeError
|
22
|
+
skip << row
|
23
|
+
next
|
20
24
|
end
|
21
|
-
|
25
|
+
|
26
|
+
if result == expected
|
27
|
+
success << row
|
28
|
+
else
|
29
|
+
failure << row
|
30
|
+
end
|
22
31
|
print '.'
|
23
32
|
}
|
24
33
|
puts
|
25
34
|
|
26
35
|
puts "Success: #{success.count}"
|
27
36
|
puts "Failure: #{failure.count}"
|
37
|
+
puts "Skipped: #{skip.count}"
|
28
38
|
|
29
|
-
|
30
|
-
|
31
|
-
# exit failure.count
|
39
|
+
failure.each { |row| p row }
|
40
|
+
exit failure.count
|
data/test/vectors_extra.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'schnorr_sig'
|
2
2
|
require 'csv'
|
3
3
|
|
4
|
+
ENV['NO_SECURERANDOM'] = '1'
|
5
|
+
|
4
6
|
path = File.join(__dir__, 'vectors.csv')
|
5
7
|
table = CSV.read(path, headers: true)
|
6
8
|
|
7
9
|
table.each { |row|
|
8
10
|
sk = SchnorrSig.hex2bin row.fetch('secret key')
|
9
11
|
pk = SchnorrSig.hex2bin row.fetch('public key')
|
10
|
-
#aux_rand = SchnorrSig.hex2bin row.fetch('aux_rand')
|
11
12
|
m = SchnorrSig.hex2bin row.fetch('message')
|
12
13
|
sig = SchnorrSig.hex2bin row.fetch('signature')
|
13
14
|
|
@@ -28,15 +29,19 @@ table.each { |row|
|
|
28
29
|
pk_msg = (pubkey == pk) ? "pk match" : "pk mismatch"
|
29
30
|
|
30
31
|
# calculate a signature
|
31
|
-
|
32
|
+
begin
|
33
|
+
calc_sig = SchnorrSig.sign(sk, m)
|
34
|
+
rescue SchnorrSig::Error
|
35
|
+
calc_sig = "sig error"
|
36
|
+
end
|
32
37
|
sig_msg = (calc_sig == sig) ? "sig match" : "sig mismatch"
|
33
38
|
end
|
34
39
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
begin
|
41
|
+
result = SchnorrSig.soft_verify?(pk, m, sig)
|
42
|
+
rescue SchnorrSig::SizeError
|
43
|
+
next
|
44
|
+
end
|
40
45
|
verify_msg = (result == expected) ? "verify match" : "verify mismatch"
|
41
46
|
puts [index, pk_msg, sig_msg, verify_msg, comment].join("\t")
|
42
47
|
}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: schnorr_sig
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rick Hull
|
@@ -36,8 +36,11 @@ files:
|
|
36
36
|
- lib/schnorr_sig.rb
|
37
37
|
- lib/schnorr_sig/fast.rb
|
38
38
|
- lib/schnorr_sig/pure.rb
|
39
|
-
- lib/schnorr_sig/
|
39
|
+
- lib/schnorr_sig/utils.rb
|
40
40
|
- schnorr_sig.gemspec
|
41
|
+
- test/fast.rb
|
42
|
+
- test/pure.rb
|
43
|
+
- test/utils.rb
|
41
44
|
- test/vectors.rb
|
42
45
|
- test/vectors_extra.rb
|
43
46
|
homepage: https://github.com/rickhull/schnorr_sig
|
@@ -62,5 +65,5 @@ requirements: []
|
|
62
65
|
rubygems_version: 3.5.9
|
63
66
|
signing_key:
|
64
67
|
specification_version: 4
|
65
|
-
summary: Schnorr signatures in Ruby
|
68
|
+
summary: Schnorr signatures in Ruby; multiple implementations
|
66
69
|
test_files: []
|
data/lib/schnorr_sig/util.rb
DELETED
@@ -1,44 +0,0 @@
|
|
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
|