Module: SchnorrSig::Pure

Extended by:
Pure
Includes:
Utils
Included in:
SchnorrSig, Pure
Defined in:
lib/schnorr_sig/pure.rb

Instance Method Summary collapse

Methods included from Utils

#big2bin, #bin2big, #bin2hex, #binary!, #check!, #hex2bin

Instance Method Details

#bytes(val) ⇒ Object

bytes(val) function signature matches BIP340, returns a binary string



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/schnorr_sig/pure.rb', line 40

def bytes(val)
  case val
  when Integer
    # BIP340: The function bytes(x), where x is an integer,
    # returns the 32-byte encoding of x, most significant byte first.
    big2bin(val)
  when ECDSA::Point
    # BIP340: The function bytes(P), where P is a point,
    # returns bytes(x(P)).
    val.infinity? ? raise(SanityCheck, val.inspect) : big2bin(val.x)
  else
    raise(SanityCheck, val.inspect)
  end
end

#int(x) ⇒ Object

int(x) function signature matches BIP340, returns a bignum (presumably)



37
# File 'lib/schnorr_sig/pure.rb', line 37

def int(x) = bin2big(x)

#keypairObject

generate a new keypair based on random data



121
122
123
124
# File 'lib/schnorr_sig/pure.rb', line 121

def keypair
  sk = random_bytes(KEY)
  [sk, pubkey(sk)]
end

#lift_x(x) ⇒ Object

BIP340: The function lift_x(x), where x is a 256-bit unsigned integer,

returns the point P for which x(P) = x and has_even_y(P),
or fails if x is greater than p-1 or no such point exists.

Input

A large integer, x

Output

ECDSA::Point

Raises:



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/schnorr_sig/pure.rb', line 62

def lift_x(x)
  check!(x, Integer)

  # BIP340: Fail if x >= p
  raise(SanityCheck, "x") if x >= P or x <= 0

  # BIP340: Let c = x^3 + 7 mod p
  c = (x.pow(3, P) + 7) % P

  # BIP340: Let y = c ^ ((p + 1) / 4) mod p
  y = c.pow((P + 1) / 4, P) # use pow to avoid Bignum overflow

  # BIP340: Fail if c != y^2 mod p
  raise(SanityCheck, "c != y^2 mod p") if c != y.pow(2, P)

  # BIP340: Return the unique point P such that:
  #   x(P) = x and y(P) = y    if y mod 2 = 0
  #   y(P) = p - y             otherwise
  GROUP.new_point [x, y.even? ? y : P - y]
end

#point(int) ⇒ Object

int (dot) G, returns ECDSA::Point



27
28
29
# File 'lib/schnorr_sig/pure.rb', line 27

def point(int)
  (GROUP.generator.to_jacobian * int).to_affine # 10x faster via ecdsa_ext
end

#pubkey(sk) ⇒ Object

Input

The secret key, sk: 32 bytes binary

Output

32 bytes binary (represents P.x for point P on the curve)

Raises:



109
110
111
112
113
114
115
116
117
118
# File 'lib/schnorr_sig/pure.rb', line 109

def pubkey(sk)
  binary!(sk, KEY)

  # BIP340: Let d' = int(sk)
  # BIP340: Fail if d' = 0 or d' >= n
  # BIP340: Return bytes(d' . G)
  d0 = int(sk)
  raise(SanityCheck, "d0") if !d0.positive? or d0 >= N
  bytes(point(d0))
end

#random_bytes(count) ⇒ Object

use SecureRandom unless ENV is nonempty



21
22
23
24
# File 'lib/schnorr_sig/pure.rb', line 21

def random_bytes(count)
  nsr = ENV['NO_SECURERANDOM']
  (nsr and !nsr.empty?) ? Random.bytes(count) : SecureRandom.bytes(count)
end

#select_even_y(point, even_val) ⇒ Object

returns even_val or N - even_val



32
33
34
# File 'lib/schnorr_sig/pure.rb', line 32

def select_even_y(point, even_val)
  point.y.even? ? even_val : N - even_val
end

#sign(sk, m, auxrand: nil) ⇒ Object

Input

The secret key, sk:       32 bytes binary
The message, m:           binary / UTF-8 / agnostic
Auxiliary random data, a: 32 bytes binary

Output

The signature, sig:       64 bytes binary

Raises:



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/schnorr_sig/pure.rb', line 136

def sign(sk, m, auxrand: nil)
  a = auxrand.nil? ? random_bytes(B) : auxrand
  binary!(sk, KEY) and check!(m, String) and binary!(a, B)

  # BIP340: Let d' = int(sk)
  # BIP340: Fail if d' = 0 or d' >= n
  d0 = int(sk)
  raise(SanityCheck, "d0") if !d0.positive? or d0 >= N

  # BIP340: Let P = d' . G
  p = point(d0) # this is a point on the elliptic curve
  bytes_p = bytes(p)

  # BIP340: Let d = d' if has_even_y(P), otherwise let d = n - d'
  d = select_even_y(p, d0)

  # BIP340: Let t be the bytewise xor of bytes(d) and hash[BIP0340/aux](a)
  t = d ^ int(tagged_hash('BIP0340/aux', a))

  # BIP340: Let rand = hash[BIP0340/nonce](t || bytes(P) || m)
  nonce = tagged_hash('BIP0340/nonce', bytes(t) + bytes_p + m)

  # BIP340: Let k' = int(rand) mod n
  # BIP340: Fail if k' = 0
  k0 = int(nonce) % N
  raise(SanityCheck, "k0") if !k0.positive?

  # BIP340: Let R = k' . G
  r = point(k0) # this is a point on the elliptic curve
  bytes_r = bytes(r)

  # BIP340: Let k = k' if has_even_y(R), otherwise let k = n - k'
  k = select_even_y(r, k0)

  # BIP340:
  #   Let e = int(hash[BIP0340/challenge](bytes(R) || bytes(P) || m)) mod n
  e = int(tagged_hash('BIP0340/challenge', bytes_r + bytes_p + m)) % N

  # BIP340: Let sig = bytes(R) || bytes((k + ed) mod n)
  # BIP340: Fail unless Verify(bytes(P), m, sig)
  # BIP340: Return the signature sig
  sig = bytes_r + bytes((k + e * d) % N)
  raise(SanityCheck, "sig did not verify") unless verify?(bytes_p, m, sig)
  sig
end

#soft_verify?(pk, m, sig) ⇒ Boolean

as above but swallow internal errors and return false

Returns:

  • (Boolean)


217
218
219
220
221
222
223
# File 'lib/schnorr_sig/pure.rb', line 217

def soft_verify?(pk, m, sig)
  begin
    verify?(pk, m, sig)
  rescue SanityCheck
    false
  end
end

#tagged_hash(tag, msg) ⇒ Object

see bips.xyz/340#design (Tagged hashes) Input

A tag:            UTF-8 > binary > agnostic
The payload, msg: UTF-8 / binary / agnostic

Output

32 bytes binary


89
90
91
92
93
94
95
96
97
98
99
# File 'lib/schnorr_sig/pure.rb', line 89

def tagged_hash(tag, msg)
  check!(tag, String) and check!(msg, String)
  warn("tag expected to be UTF-8") unless tag.encoding == Encoding::UTF_8

  # BIP340: The function hash[name](x) where x is a byte array
  #         returns the 32-byte hash
  #         SHA256(SHA256(tag) || SHA256(tag) || x)
  #         where tag is the UTF-8 encoding of name.
  tag_hash = Digest::SHA256.digest(tag)
  Digest::SHA256.digest(tag_hash + tag_hash + msg)
end

#verify?(pk, m, sig) ⇒ Boolean

Input

The public key, pk: 32 bytes binary
The message, m:     UTF-8 / binary / agnostic
A signature, sig:   64 bytes binary

Output

Boolean

Returns:

  • (Boolean)

Raises:



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/schnorr_sig/pure.rb', line 188

def verify?(pk, m, sig)
  binary!(pk, KEY) and check!(m, String) and binary!(sig, SIG)

  # BIP340: Let P = lift_x(int(pk))
  p = lift_x(int(pk))

  # BIP340: Let r = int(sig[0:32]) fail if r >= p
  r = int(sig[0..KEY-1])
  raise(SanityCheck, "r >= p") if r >= P

  # BIP340: Let s = int(sig[32:64]); fail if s >= n
  s = int(sig[KEY..-1])
  raise(SanityCheck, "s >= n") if s >= N

  # BIP340:
  #   Let e = int(hash[BIP0340/challenge](bytes(r) || bytes(P) || m)) mod n
  e = bytes(r) + bytes(p) + m
  e = int(tagged_hash('BIP0340/challenge', e)) % N

  # BIP340: Let R = s . G - e . P
  # BIP340: Fail if is_infinite(R)
  # BIP340: Fail if not has_even_y(R)
  # BIP340: Fail if x(R) != r
  # BIP340: Return success iff no prior failure
  big_r = point(s) + p.multiply_by_scalar(e).negate
  !big_r.infinity? and big_r.y.even? and big_r.x == r
end