Module: SchnorrSig

Defined in:
lib/schnorr_sig/fast.rb,
lib/schnorr_sig/pure.rb,
lib/schnorr_sig/util.rb

Overview

This implementation is based on the BIP340 spec: bips.xyz/340 re-open SchnorrSig to add more functions, errors, and constants

Defined Under Namespace

Classes: BoundsError, EncodingError, Error, InfinityPoint, InputError, SanityCheck, SizeError, TypeError, VerifyFail

Constant Summary collapse

CONTEXT =
Secp256k1::Context.create
FORCE_32_BYTE_MSG =
true
GROUP =
ECDSA::Group::Secp256k1
P =

smaller than 256**32

GROUP.field.prime
N =

smaller than P

GROUP.order
B =

32

GROUP.byte_length

Class Method Summary collapse

Class Method Details

.big2bin(bignum) ⇒ Object

convert a giant integer to a binary string



30
31
32
33
# File 'lib/schnorr_sig/util.rb', line 30

def self.big2bin(bignum)
  # much faster than ECDSA::Format -- thanks ParadoxV5
  hex2bin(bignum.to_s(16).rjust(B * 2, '0'))
end

.bin2big(str) ⇒ Object

likely returns a Bignum, larger than a 64-bit hardware integer



25
26
27
# File 'lib/schnorr_sig/util.rb', line 25

def self.bin2big(str)
  bin2hex(str).to_i(16)
end

.bin2hex(str) ⇒ Object

convert a binary string to a lowercase hex string



36
37
38
# File 'lib/schnorr_sig/util.rb', line 36

def self.bin2hex(str)
  str.unpack1('H*')
end

.bytes(val) ⇒ Object

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



36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/schnorr_sig/pure.rb', line 36

def self.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(InfinityPoint, va.inspect) : big2bin(val.x)
  else
    raise(SanityCheck, val.inspect)
  end
end

.bytestring!(str, size) ⇒ Object

true or raise

Raises:



18
19
20
21
22
# File 'lib/schnorr_sig/util.rb', line 18

def self.bytestring!(str, size)
  string!(str)
  raise(EncodingError, str.encoding) unless str.encoding == Encoding::BINARY
  str.bytesize == size or raise(SizeError, str.bytesize)
end

.dot_group(val) ⇒ Object

val (dot) G, returns ECDSA::Point



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

def self.dot_group(val)
  # ecdsa_ext uses jacobian projection: 10x faster than GROUP.generator * val
  (GROUP.generator.to_jacobian * val).to_affine
end

.hex2bin(hex) ⇒ Object

convert a hex string to a binary string



41
42
43
# File 'lib/schnorr_sig/util.rb', line 41

def self.hex2bin(hex)
  [hex].pack('H*')
end

.intObject



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

alias_method :int, :bin2big

.integer!(i) ⇒ Object

true or raise



8
9
10
# File 'lib/schnorr_sig/util.rb', line 8

def self.integer!(i)
  i.is_a?(Integer) or raise(TypeError, i.class)
end

.key_pair(sk = nil) ⇒ Object

Input

(The secret key, sk: 32 bytes binary)

Output

Secp256k1::KeyPair


36
37
38
39
40
41
42
43
# File 'lib/schnorr_sig/fast.rb', line 36

def self.key_pair(sk = nil)
  if sk
    bytestring!(sk, 32)
    CONTEXT.key_pair_from_private_key(sk)
  else
    CONTEXT.generate_key_pair
  end
end

.keypairObject

generate a new keypair based on random data



49
50
51
52
# File 'lib/schnorr_sig/fast.rb', line 49

def self.keypair(sk = nil)
  kp = self.key_pair(sk)
  [kp.private_key.data, kp.xonly_public_key.serialized]
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:



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/schnorr_sig/pure.rb', line 160

def self.lift_x(x)
  integer!(x)

  # BIP340: Fail if x >= p
  raise(BoundsError, "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

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



58
59
60
# File 'lib/schnorr_sig/fast.rb', line 58

def self.pubkey(sk)
  keypair(sk)[1]
end

.secure_keypairObject

as above, but using SecureRandom



203
204
205
206
# File 'lib/schnorr_sig/pure.rb', line 203

def self.secure_keypair
  sk = SecureRandom.bytes(B)
  [sk, pubkey(sk)]
end

.select_even_y(point, even_val) ⇒ Object

returns even_val or N - even_val



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

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

.sign(sk, m, a = Random.bytes(B)) ⇒ 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:



15
16
17
18
19
# File 'lib/schnorr_sig/fast.rb', line 15

def self.sign(sk, m)
  bytestring!(sk, 32) and string!(m)
  m = m[0..31].ljust(32, ' ') if FORCE_32_BYTE_MSG
  CONTEXT.sign_schnorr(key_pair(sk), m).serialized
end

.signature(str) ⇒ Object

Input

The signature, str: 64 bytes binary

Output

Secp256k1::SchnorrSignature


66
67
68
69
# File 'lib/schnorr_sig/fast.rb', line 66

def self.signature(str)
  bytestring!(str, 64)
  Secp256k1::SchnorrSignature.from_data(str)
end

.string!(str) ⇒ Object

true or raise



13
14
15
# File 'lib/schnorr_sig/util.rb', line 13

def self.string!(str)
  str.is_a?(String) or raise(TypeError, str.class)
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


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

def self.tagged_hash(tag, msg)
  string!(tag) and string!(msg)
  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:



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

def self.verify?(pk, m, sig)
  bytestring!(pk, 32) and string!(m) and bytestring!(sig, 64)
  signature(sig).verify(m, Secp256k1::XOnlyPublicKey.from_data(pk))
end