schnorr_sig 0.1.0.1 → 0.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 151da6ed14b8d23233e92c09c4354d00454d9c10a4abbbb6f8429f17bba10cfb
4
- data.tar.gz: 9391ae74a60a5275bf2704b8fef73b6d69591313c924f3bb9cddb24ee08e7372
3
+ metadata.gz: 37ba87126b971e1e74099bbed572cb7e088902db0cba9fdf38ef5de96ab75f69
4
+ data.tar.gz: 9001aae50944fcf3daf558db01a44c6b5f4ee7e74f729292026c83fe38c1367d
5
5
  SHA512:
6
- metadata.gz: dc77694c5a19118374eab690d7fe6a2bf04e24a009e7c62108397a4dcb4fdf4e002d5e980018a9d83875c6fff93d4c132a9b6b8f1ffd1394304d584447706ab1
7
- data.tar.gz: 76aceaed2e98afacc253c20549d3b16a48f3d7159aed457f51d2a7f37ea4a9b7a3bfee239b92b04b9b40f8de9f855e5330aef96ec446e01ddec260505d6f0254
6
+ metadata.gz: a85306c995daa8ce6b30b303e530892a4014f367854dccda5a3f119dc2170087e04fd069d6a8516f30bfe5d1b2e5e351ae495f331b65d1ab920ec767d60f154b
7
+ data.tar.gz: e7abe94145d870f072924e555e7510e886159aef5119375892ac10243abd08ccf72ff078f545b68b3502a9d09f644f38d5512220796e71b138b24f642813bc78
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0.1
1
+ 0.2.0.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,5 @@
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
+ require 'schnorr_sig/fast'
3
+ else
4
+ require 'schnorr_sig/pure'
221
5
  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.0.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