schnorr_sig 0.1.0.1 → 0.2.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 151da6ed14b8d23233e92c09c4354d00454d9c10a4abbbb6f8429f17bba10cfb
4
- data.tar.gz: 9391ae74a60a5275bf2704b8fef73b6d69591313c924f3bb9cddb24ee08e7372
3
+ metadata.gz: 22902f36da6e027cac43fc471364294806a80fdc6d88e3743566d06ae632b3a1
4
+ data.tar.gz: a3759fb782d4aec2fd78b02113d6717bbf9892f6fb3b82be866b08e07ba38d5a
5
5
  SHA512:
6
- metadata.gz: dc77694c5a19118374eab690d7fe6a2bf04e24a009e7c62108397a4dcb4fdf4e002d5e980018a9d83875c6fff93d4c132a9b6b8f1ffd1394304d584447706ab1
7
- data.tar.gz: 76aceaed2e98afacc253c20549d3b16a48f3d7159aed457f51d2a7f37ea4a9b7a3bfee239b92b04b9b40f8de9f855e5330aef96ec446e01ddec260505d6f0254
6
+ metadata.gz: 5afe8575ec3355d303720539e2e5019639214374f26cefa9c89f52dabd625dc79e12f055b4f5c775f40cf334a7ac11deaa095cc6295e6cd6e5ed3b3f27439dc2
7
+ data.tar.gz: 5950afef772f28370c7f1ca12bda717f560f8cf2f76d1c255fede1a8ce1677dea8dbd17ba4e4c55af7ac475058892c0da17e13235d6574b3155b7da6912d9f8c
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0.1
1
+ 0.2.1.1
@@ -1,5 +1,5 @@
1
- require 'schnorr_sig/util'
2
- require 'rbsecp256k1' # gem, C extension
1
+ require 'schnorr_sig/util' # project
2
+ require 'rbsecp256k1' # gem, C extension
3
3
 
4
4
  # re-open SchnorrSig to add more functions, errors, and constants
5
5
  module SchnorrSig
@@ -0,0 +1,221 @@
1
+ require 'schnorr_sig/util' # project
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
+ class InfinityPoint < Error; end
13
+
14
+ GROUP = ECDSA::Group::Secp256k1
15
+ P = GROUP.field.prime # smaller than 256**32
16
+ N = GROUP.order # smaller than P
17
+ B = GROUP.byte_length # 32
18
+
19
+ # val (dot) G, returns ECDSA::Point
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
24
+
25
+ # returns even_val or N - even_val
26
+ def self.select_even_y(point, even_val)
27
+ point.y.even? ? even_val : N - even_val
28
+ end
29
+
30
+ # int(x) function signature matches BIP340, returns a bignum (presumably)
31
+ class << self
32
+ alias_method :int, :bin2big
33
+ end
34
+
35
+ # bytes(val) function signature matches BIP340, returns a binary string
36
+ def self.bytes(val)
37
+ case val
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)
47
+ end
48
+ end
49
+
50
+ # Input
51
+ # The secret key, sk: 32 bytes binary
52
+ # The message, m: binary / UTF-8 / agnostic
53
+ # Auxiliary random data, a: 32 bytes binary
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
100
+
101
+ # see https://bips.xyz/340#design (Tagged hashes)
102
+ # Input
103
+ # A tag: UTF-8 > binary > agnostic
104
+ # The payload, msg: UTF-8 / binary / agnostic
105
+ # Output
106
+ # 32 bytes binary
107
+ def self.tagged_hash(tag, msg)
108
+ string!(tag) and string!(msg)
109
+ warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8
110
+
111
+ # BIP340: The function hash[name](x) where x is a byte array
112
+ # returns the 32-byte hash
113
+ # SHA256(SHA256(tag) || SHA256(tag) || x)
114
+ # where tag is the UTF-8 encoding of name.
115
+ tag_hash = Digest::SHA256.digest(tag)
116
+ Digest::SHA256.digest(tag_hash + tag_hash + msg)
117
+ end
118
+
119
+ # Input
120
+ # The public key, pk: 32 bytes binary
121
+ # The message, m: UTF-8 / binary / agnostic
122
+ # A signature, sig: 64 bytes binary
123
+ # Output
124
+ # Boolean
125
+ def self.verify?(pk, m, sig)
126
+ bytestring!(pk, B) and string!(m) and bytestring!(sig, B * 2)
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
152
+
153
+ # BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,
154
+ # returns the point P for which x(P) = x and has_even_y(P),
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)
162
+
163
+ # BIP340: Fail if x >= p
164
+ raise(BoundsError, "x") if x >= P or x <= 0
165
+
166
+ # BIP340: Let c = x^3 + 7 mod p
167
+ c = (x.pow(3, P) + 7) % P
168
+
169
+ # BIP340: Let y = c ^ ((p + 1) / 4) mod p
170
+ y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow
171
+
172
+ # BIP340: Fail if c != y^2 mod p
173
+ raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)
174
+
175
+ # BIP340: Return the unique point P such that:
176
+ # x(P) = x and y(P) = y if y mod 2 = 0
177
+ # y(P) = p - y otherwise
178
+ GROUP.new_point [x, y.even? ? y : P - y]
179
+ end
180
+
181
+ # Input
182
+ # The secret key, sk: 32 bytes binary
183
+ # Output
184
+ # 32 bytes binary (represents P.x for point P on the curve)
185
+ def self.pubkey(sk)
186
+ bytestring!(sk, B)
187
+
188
+ # BIP340: Let d' = int(sk)
189
+ # BIP340: Fail if d' = 0 or d' >= n
190
+ # BIP340: Return bytes(d' . G)
191
+ d0 = int(sk)
192
+ raise(BoundsError, "d0") if !d0.positive? or d0 >= N
193
+ bytes(dot_group(d0))
194
+ end
195
+
196
+ # generate a new keypair based on random data
197
+ def self.keypair
198
+ sk = Random.bytes(B)
199
+ [sk, pubkey(sk)]
200
+ end
201
+
202
+ # as above, but using SecureRandom
203
+ def self.secure_keypair
204
+ sk = SecureRandom.bytes(B)
205
+ [sk, pubkey(sk)]
206
+ end
207
+ end
208
+
209
+ if __FILE__ == $0
210
+ msg = 'hello world'
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}"
221
+ end
data/lib/schnorr_sig.rb CHANGED
@@ -1,221 +1,10 @@
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
- class InfinityPoint < Error; end
13
-
14
- GROUP = ECDSA::Group::Secp256k1
15
- P = GROUP.field.prime # smaller than 256**32
16
- N = GROUP.order # smaller than P
17
- B = GROUP.byte_length # 32
18
-
19
- # val (dot) G, returns ECDSA::Point
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
24
-
25
- # returns even_val or N - even_val
26
- def self.select_even_y(point, even_val)
27
- point.y.even? ? even_val : N - even_val
28
- end
29
-
30
- # int(x) function signature matches BIP340, returns a bignum (presumably)
31
- class << self
32
- alias_method :int, :bin2big
33
- end
34
-
35
- # bytes(val) function signature matches BIP340, returns a binary string
36
- def self.bytes(val)
37
- case val
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)
47
- end
48
- end
49
-
50
- # Input
51
- # The secret key, sk: 32 bytes binary
52
- # The message, m: binary / UTF-8 / agnostic
53
- # Auxiliary random data, a: 32 bytes binary
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
100
-
101
- # see https://bips.xyz/340#design (Tagged hashes)
102
- # Input
103
- # A tag: UTF-8 > binary > agnostic
104
- # The payload, msg: UTF-8 / binary / agnostic
105
- # Output
106
- # 32 bytes binary
107
- def self.tagged_hash(tag, msg)
108
- string!(tag) and string!(msg)
109
- warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8
110
-
111
- # BIP340: The function hash[name](x) where x is a byte array
112
- # returns the 32-byte hash
113
- # SHA256(SHA256(tag) || SHA256(tag) || x)
114
- # where tag is the UTF-8 encoding of name.
115
- tag_hash = Digest::SHA256.digest(tag)
116
- Digest::SHA256.digest(tag_hash + tag_hash + msg)
117
- end
118
-
119
- # Input
120
- # The public key, pk: 32 bytes binary
121
- # The message, m: UTF-8 / binary / agnostic
122
- # A signature, sig: 64 bytes binary
123
- # Output
124
- # Boolean
125
- def self.verify?(pk, m, sig)
126
- bytestring!(pk, B) and string!(m) and bytestring!(sig, B * 2)
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
152
-
153
- # BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,
154
- # returns the point P for which x(P) = x and has_even_y(P),
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)
162
-
163
- # BIP340: Fail if x >= p
164
- raise(BoundsError, "x") if x >= P or x <= 0
165
-
166
- # BIP340: Let c = x^3 + 7 mod p
167
- c = (x.pow(3, P) + 7) % P
168
-
169
- # BIP340: Let y = c ^ ((p + 1) / 4) mod p
170
- y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow
171
-
172
- # BIP340: Fail if c != y^2 mod p
173
- raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)
174
-
175
- # BIP340: Return the unique point P such that:
176
- # x(P) = x and y(P) = y if y mod 2 = 0
177
- # y(P) = p - y otherwise
178
- GROUP.new_point [x, y.even? ? y : P - y]
179
- end
180
-
181
- # Input
182
- # The secret key, sk: 32 bytes binary
183
- # Output
184
- # 32 bytes binary (represents P.x for point P on the curve)
185
- def self.pubkey(sk)
186
- bytestring!(sk, B)
187
-
188
- # BIP340: Let d' = int(sk)
189
- # BIP340: Fail if d' = 0 or d' >= n
190
- # BIP340: Return bytes(d' . G)
191
- d0 = int(sk)
192
- raise(BoundsError, "d0") if !d0.positive? or d0 >= N
193
- bytes(dot_group(d0))
194
- end
195
-
196
- # generate a new keypair based on random data
197
- def self.keypair
198
- sk = Random.bytes(B)
199
- [sk, pubkey(sk)]
200
- end
201
-
202
- # as above, but using SecureRandom
203
- def self.secure_keypair
204
- sk = SecureRandom.bytes(B)
205
- [sk, pubkey(sk)]
206
- end
207
- end
208
-
209
- if __FILE__ == $0
210
- msg = 'hello world'
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}"
1
+ if ENV['SCHNORR_SIG']&.downcase == 'fast'
2
+ begin
3
+ require 'schnorr_sig/fast'
4
+ rescue LoadError
5
+ warn "LoadError: schnorr_sig/fast cannot be loaded"
6
+ require 'schnorr_sig/pure'
7
+ end
8
+ else
9
+ require 'schnorr_sig/pure'
221
10
  end
data/test/vectors.rb CHANGED
@@ -1,5 +1,4 @@
1
- require ENV['SCHNORR_SIG']&.downcase == 'fast' ?
2
- 'schnorr_sig/fast' : 'schnorr_sig'
1
+ require 'schnorr_sig'
3
2
  require 'csv'
4
3
 
5
4
  path = File.join(__dir__, 'vectors.csv')
@@ -1,5 +1,4 @@
1
- require ENV['SCHNORR_SIG']&.downcase == 'fast' ?
2
- 'schnorr_sig/fast' : 'schnorr_sig'
1
+ require 'schnorr_sig'
3
2
  require 'csv'
4
3
 
5
4
  path = File.join(__dir__, 'vectors.csv')
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.1.0.1
4
+ version: 0.2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rick Hull
@@ -35,6 +35,7 @@ files:
35
35
  - VERSION
36
36
  - lib/schnorr_sig.rb
37
37
  - lib/schnorr_sig/fast.rb
38
+ - lib/schnorr_sig/pure.rb
38
39
  - lib/schnorr_sig/util.rb
39
40
  - schnorr_sig.gemspec
40
41
  - test/vectors.rb