2024-02-14 11:48:41 +01:00
|
|
|
# Copyright (c) 2019-2020 Pieter Wuille
|
2019-04-22 14:09:55 +02:00
|
|
|
# Distributed under the MIT software license, see the accompanying
|
|
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
2022-10-01 17:35:28 +02:00
|
|
|
"""Test-only secp256k1 elliptic curve protocols implementation
|
2015-08-05 23:47:34 +02:00
|
|
|
|
2019-04-22 14:09:55 +02:00
|
|
|
WARNING: This code is slow, uses bad randomness, does not properly protect
|
|
|
|
keys, and is trivially vulnerable to side channel attacks. Do not use for
|
|
|
|
anything but tests."""
|
2024-02-14 11:48:41 +01:00
|
|
|
import csv
|
|
|
|
import hashlib
|
2021-10-27 21:46:03 +02:00
|
|
|
import hmac
|
2024-02-14 11:48:41 +01:00
|
|
|
import os
|
2019-04-22 14:09:55 +02:00
|
|
|
import random
|
2024-02-14 11:48:41 +01:00
|
|
|
import unittest
|
2019-01-07 10:55:35 +01:00
|
|
|
|
2022-10-01 17:35:28 +02:00
|
|
|
from test_framework import secp256k1
|
|
|
|
|
|
|
|
# Order of the secp256k1 curve
|
|
|
|
ORDER = secp256k1.GE.ORDER
|
|
|
|
|
2024-02-14 11:48:41 +01:00
|
|
|
def TaggedHash(tag, data):
|
|
|
|
ss = hashlib.sha256(tag.encode('utf-8')).digest()
|
|
|
|
ss += ss
|
|
|
|
ss += data
|
|
|
|
return hashlib.sha256(ss).digest()
|
|
|
|
|
2022-10-01 17:35:28 +02:00
|
|
|
|
|
|
|
class ECPubKey:
|
2019-04-22 14:09:55 +02:00
|
|
|
"""A secp256k1 public key"""
|
2015-08-05 23:47:34 +02:00
|
|
|
|
|
|
|
def __init__(self):
|
2019-04-22 14:09:55 +02:00
|
|
|
"""Construct an uninitialized public key"""
|
2022-10-01 17:35:28 +02:00
|
|
|
self.p = None
|
2019-04-22 14:09:55 +02:00
|
|
|
|
|
|
|
def set(self, data):
|
|
|
|
"""Construct a public key from a serialization in compressed or uncompressed format"""
|
2022-10-01 17:35:28 +02:00
|
|
|
self.p = secp256k1.GE.from_bytes(data)
|
|
|
|
self.compressed = len(data) == 33
|
2015-08-05 23:47:34 +02:00
|
|
|
|
2019-04-22 14:09:55 +02:00
|
|
|
@property
|
|
|
|
def is_compressed(self):
|
|
|
|
return self.compressed
|
2015-08-05 23:47:34 +02:00
|
|
|
|
2019-04-22 14:09:55 +02:00
|
|
|
@property
|
|
|
|
def is_valid(self):
|
2022-10-01 17:35:28 +02:00
|
|
|
return self.p is not None
|
2019-04-22 14:09:55 +02:00
|
|
|
|
|
|
|
def get_bytes(self):
|
2022-10-01 17:35:28 +02:00
|
|
|
assert self.is_valid
|
2019-04-22 14:09:55 +02:00
|
|
|
if self.compressed:
|
2022-10-01 17:35:28 +02:00
|
|
|
return self.p.to_bytes_compressed()
|
2019-04-22 14:09:55 +02:00
|
|
|
else:
|
2022-10-01 17:35:28 +02:00
|
|
|
return self.p.to_bytes_uncompressed()
|
2019-04-22 14:09:55 +02:00
|
|
|
|
|
|
|
def verify_ecdsa(self, sig, msg, low_s=True):
|
|
|
|
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
|
|
|
|
|
|
|
|
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
|
|
|
ECDSA verifier algorithm"""
|
2022-10-01 17:35:28 +02:00
|
|
|
assert self.is_valid
|
2019-04-22 14:09:55 +02:00
|
|
|
|
|
|
|
# Extract r and s from the DER formatted signature. Return false for
|
|
|
|
# any DER encoding errors.
|
|
|
|
if (sig[1] + 2 != len(sig)):
|
|
|
|
return False
|
|
|
|
if (len(sig) < 4):
|
|
|
|
return False
|
|
|
|
if (sig[0] != 0x30):
|
|
|
|
return False
|
|
|
|
if (sig[2] != 0x02):
|
|
|
|
return False
|
|
|
|
rlen = sig[3]
|
|
|
|
if (len(sig) < 6 + rlen):
|
|
|
|
return False
|
|
|
|
if rlen < 1 or rlen > 33:
|
|
|
|
return False
|
|
|
|
if sig[4] >= 0x80:
|
|
|
|
return False
|
|
|
|
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
|
|
|
|
return False
|
|
|
|
r = int.from_bytes(sig[4:4+rlen], 'big')
|
|
|
|
if (sig[4+rlen] != 0x02):
|
|
|
|
return False
|
|
|
|
slen = sig[5+rlen]
|
|
|
|
if slen < 1 or slen > 33:
|
|
|
|
return False
|
|
|
|
if (len(sig) != 6 + rlen + slen):
|
|
|
|
return False
|
|
|
|
if sig[6+rlen] >= 0x80:
|
|
|
|
return False
|
|
|
|
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
|
|
|
|
return False
|
|
|
|
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
|
|
|
|
|
|
|
|
# Verify that r and s are within the group order
|
2022-10-01 17:35:28 +02:00
|
|
|
if r < 1 or s < 1 or r >= ORDER or s >= ORDER:
|
2019-04-22 14:09:55 +02:00
|
|
|
return False
|
2022-10-01 17:35:28 +02:00
|
|
|
if low_s and s >= secp256k1.GE.ORDER_HALF:
|
2019-04-22 14:09:55 +02:00
|
|
|
return False
|
|
|
|
z = int.from_bytes(msg, 'big')
|
|
|
|
|
|
|
|
# Run verifier algorithm on r, s
|
2022-10-01 17:35:28 +02:00
|
|
|
w = pow(s, -1, ORDER)
|
|
|
|
R = secp256k1.GE.mul((z * w, secp256k1.G), (r * w, self.p))
|
|
|
|
if R.infinity or (int(R.x) % ORDER) != r:
|
2019-04-22 14:09:55 +02:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2024-02-14 11:48:41 +01:00
|
|
|
def generate_privkey():
|
|
|
|
"""Generate a valid random 32-byte private key."""
|
2022-10-01 17:35:28 +02:00
|
|
|
return random.randrange(1, ORDER).to_bytes(32, 'big')
|
2024-02-14 11:48:41 +01:00
|
|
|
|
2021-10-27 21:46:03 +02:00
|
|
|
def rfc6979_nonce(key):
|
|
|
|
"""Compute signing nonce using RFC6979."""
|
|
|
|
v = bytes([1] * 32)
|
|
|
|
k = bytes([0] * 32)
|
|
|
|
k = hmac.new(k, v + b"\x00" + key, 'sha256').digest()
|
|
|
|
v = hmac.new(k, v, 'sha256').digest()
|
|
|
|
k = hmac.new(k, v + b"\x01" + key, 'sha256').digest()
|
|
|
|
v = hmac.new(k, v, 'sha256').digest()
|
|
|
|
return hmac.new(k, v, 'sha256').digest()
|
|
|
|
|
2022-10-01 17:35:28 +02:00
|
|
|
class ECKey:
|
2019-04-22 14:09:55 +02:00
|
|
|
"""A secp256k1 private key"""
|
2015-08-05 23:47:34 +02:00
|
|
|
|
2019-04-22 14:09:55 +02:00
|
|
|
def __init__(self):
|
|
|
|
self.valid = False
|
|
|
|
|
|
|
|
def set(self, secret, compressed):
|
|
|
|
"""Construct a private key object with given 32-byte secret and compressed flag."""
|
|
|
|
assert(len(secret) == 32)
|
|
|
|
secret = int.from_bytes(secret, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
self.valid = (secret > 0 and secret < ORDER)
|
2019-04-22 14:09:55 +02:00
|
|
|
if self.valid:
|
|
|
|
self.secret = secret
|
|
|
|
self.compressed = compressed
|
|
|
|
|
|
|
|
def generate(self, compressed=True):
|
|
|
|
"""Generate a random private key (compressed or uncompressed)."""
|
2024-02-14 11:48:41 +01:00
|
|
|
self.set(generate_privkey(), compressed)
|
2019-04-22 14:09:55 +02:00
|
|
|
|
|
|
|
def get_bytes(self):
|
|
|
|
"""Retrieve the 32-byte representation of this key."""
|
|
|
|
assert(self.valid)
|
|
|
|
return self.secret.to_bytes(32, 'big')
|
2015-08-05 23:47:34 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_valid(self):
|
2019-04-22 14:09:55 +02:00
|
|
|
return self.valid
|
2015-08-05 23:47:34 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_compressed(self):
|
2019-04-22 14:09:55 +02:00
|
|
|
return self.compressed
|
2015-08-05 23:47:34 +02:00
|
|
|
|
2019-04-22 14:09:55 +02:00
|
|
|
def get_pubkey(self):
|
|
|
|
"""Compute an ECPubKey object for this secret key."""
|
|
|
|
assert(self.valid)
|
|
|
|
ret = ECPubKey()
|
2022-10-01 17:35:28 +02:00
|
|
|
ret.p = self.secret * secp256k1.G
|
2019-04-22 14:09:55 +02:00
|
|
|
ret.compressed = self.compressed
|
|
|
|
return ret
|
|
|
|
|
2021-10-27 21:46:03 +02:00
|
|
|
def sign_ecdsa(self, msg, low_s=True, rfc6979=False):
|
2019-04-22 14:09:55 +02:00
|
|
|
"""Construct a DER-encoded ECDSA signature with this key.
|
|
|
|
|
|
|
|
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
|
|
|
ECDSA signer algorithm."""
|
|
|
|
assert(self.valid)
|
|
|
|
z = int.from_bytes(msg, 'big')
|
2021-10-27 21:46:03 +02:00
|
|
|
# Note: no RFC6979 by default, but a simple random nonce (some tests rely on distinct transactions for the same operation)
|
|
|
|
if rfc6979:
|
|
|
|
k = int.from_bytes(rfc6979_nonce(self.secret.to_bytes(32, 'big') + msg), 'big')
|
|
|
|
else:
|
2022-10-01 17:35:28 +02:00
|
|
|
k = random.randrange(1, ORDER)
|
|
|
|
R = k * secp256k1.G
|
|
|
|
r = int(R.x) % ORDER
|
|
|
|
s = (pow(k, -1, ORDER) * (z + self.secret * r)) % ORDER
|
|
|
|
if low_s and s > secp256k1.GE.ORDER_HALF:
|
|
|
|
s = ORDER - s
|
2019-04-22 14:09:55 +02:00
|
|
|
# Represent in DER format. The byte representations of r and s have
|
|
|
|
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
|
|
|
|
# bytes).
|
|
|
|
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
|
|
|
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
|
|
|
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
2024-02-14 11:48:41 +01:00
|
|
|
|
|
|
|
def compute_xonly_pubkey(key):
|
|
|
|
"""Compute an x-only (32 byte) public key from a (32 byte) private key.
|
|
|
|
|
|
|
|
This also returns whether the resulting public key was negated.
|
|
|
|
"""
|
|
|
|
|
|
|
|
assert len(key) == 32
|
|
|
|
x = int.from_bytes(key, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if x == 0 or x >= ORDER:
|
2024-02-14 11:48:41 +01:00
|
|
|
return (None, None)
|
2022-10-01 17:35:28 +02:00
|
|
|
P = x * secp256k1.G
|
|
|
|
return (P.to_bytes_xonly(), not P.y.is_even())
|
2024-02-14 11:48:41 +01:00
|
|
|
|
|
|
|
def tweak_add_privkey(key, tweak):
|
|
|
|
"""Tweak a private key (after negating it if needed)."""
|
|
|
|
|
|
|
|
assert len(key) == 32
|
|
|
|
assert len(tweak) == 32
|
|
|
|
|
|
|
|
x = int.from_bytes(key, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if x == 0 or x >= ORDER:
|
2024-02-14 11:48:41 +01:00
|
|
|
return None
|
2022-10-01 17:35:28 +02:00
|
|
|
if not (x * secp256k1.G).y.is_even():
|
|
|
|
x = ORDER - x
|
2024-02-14 11:48:41 +01:00
|
|
|
t = int.from_bytes(tweak, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if t >= ORDER:
|
2024-02-14 11:48:41 +01:00
|
|
|
return None
|
2022-10-01 17:35:28 +02:00
|
|
|
x = (x + t) % ORDER
|
2024-02-14 11:48:41 +01:00
|
|
|
if x == 0:
|
|
|
|
return None
|
|
|
|
return x.to_bytes(32, 'big')
|
|
|
|
|
|
|
|
def tweak_add_pubkey(key, tweak):
|
|
|
|
"""Tweak a public key and return whether the result had to be negated."""
|
|
|
|
|
|
|
|
assert len(key) == 32
|
|
|
|
assert len(tweak) == 32
|
|
|
|
|
2022-10-01 17:35:28 +02:00
|
|
|
P = secp256k1.GE.from_bytes_xonly(key)
|
2024-02-14 11:48:41 +01:00
|
|
|
if P is None:
|
|
|
|
return None
|
|
|
|
t = int.from_bytes(tweak, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if t >= ORDER:
|
2024-02-14 11:48:41 +01:00
|
|
|
return None
|
2022-10-01 17:35:28 +02:00
|
|
|
Q = t * secp256k1.G + P
|
|
|
|
if Q.infinity:
|
2024-02-14 11:48:41 +01:00
|
|
|
return None
|
2022-10-01 17:35:28 +02:00
|
|
|
return (Q.to_bytes_xonly(), not Q.y.is_even())
|
2024-02-14 11:48:41 +01:00
|
|
|
|
|
|
|
def verify_schnorr(key, sig, msg):
|
|
|
|
"""Verify a Schnorr signature (see BIP 340).
|
|
|
|
|
|
|
|
- key is a 32-byte xonly pubkey (computed using compute_xonly_pubkey).
|
|
|
|
- sig is a 64-byte Schnorr signature
|
|
|
|
- msg is a 32-byte message
|
|
|
|
"""
|
|
|
|
assert len(key) == 32
|
|
|
|
assert len(msg) == 32
|
|
|
|
assert len(sig) == 64
|
|
|
|
|
2022-10-01 17:35:28 +02:00
|
|
|
P = secp256k1.GE.from_bytes_xonly(key)
|
2024-02-14 11:48:41 +01:00
|
|
|
if P is None:
|
|
|
|
return False
|
|
|
|
r = int.from_bytes(sig[0:32], 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if r >= secp256k1.FE.SIZE:
|
2024-02-14 11:48:41 +01:00
|
|
|
return False
|
|
|
|
s = int.from_bytes(sig[32:64], 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if s >= ORDER:
|
2024-02-14 11:48:41 +01:00
|
|
|
return False
|
2022-10-01 17:35:28 +02:00
|
|
|
e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), 'big') % ORDER
|
|
|
|
R = secp256k1.GE.mul((s, secp256k1.G), (-e, P))
|
|
|
|
if R.infinity or not R.y.is_even():
|
2024-02-14 11:48:41 +01:00
|
|
|
return False
|
2022-10-01 17:35:28 +02:00
|
|
|
if r != R.x:
|
2024-02-14 11:48:41 +01:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False):
|
|
|
|
"""Create a Schnorr signature (see BIP 340)."""
|
|
|
|
|
|
|
|
if aux is None:
|
|
|
|
aux = bytes(32)
|
|
|
|
|
|
|
|
assert len(key) == 32
|
|
|
|
assert len(msg) == 32
|
|
|
|
assert len(aux) == 32
|
|
|
|
|
|
|
|
sec = int.from_bytes(key, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
if sec == 0 or sec >= ORDER:
|
2024-02-14 11:48:41 +01:00
|
|
|
return None
|
2022-10-01 17:35:28 +02:00
|
|
|
P = sec * secp256k1.G
|
|
|
|
if P.y.is_even() == flip_p:
|
|
|
|
sec = ORDER - sec
|
2024-02-14 11:48:41 +01:00
|
|
|
t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big')
|
2022-10-01 17:35:28 +02:00
|
|
|
kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + P.to_bytes_xonly() + msg), 'big') % ORDER
|
2024-02-14 11:48:41 +01:00
|
|
|
assert kp != 0
|
2022-10-01 17:35:28 +02:00
|
|
|
R = kp * secp256k1.G
|
|
|
|
k = kp if R.y.is_even() != flip_r else ORDER - kp
|
|
|
|
e = int.from_bytes(TaggedHash("BIP0340/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg), 'big') % ORDER
|
|
|
|
return R.to_bytes_xonly() + ((k + e * sec) % ORDER).to_bytes(32, 'big')
|
|
|
|
|
2024-02-14 11:48:41 +01:00
|
|
|
|
|
|
|
class TestFrameworkKey(unittest.TestCase):
|
|
|
|
def test_schnorr(self):
|
|
|
|
"""Test the Python Schnorr implementation."""
|
2022-10-01 17:35:28 +02:00
|
|
|
byte_arrays = [generate_privkey() for _ in range(3)] + [v.to_bytes(32, 'big') for v in [0, ORDER - 1, ORDER, 2**256 - 1]]
|
2024-02-14 11:48:41 +01:00
|
|
|
keys = {}
|
|
|
|
for privkey in byte_arrays: # build array of key/pubkey pairs
|
|
|
|
pubkey, _ = compute_xonly_pubkey(privkey)
|
|
|
|
if pubkey is not None:
|
|
|
|
keys[privkey] = pubkey
|
|
|
|
for msg in byte_arrays: # test every combination of message, signing key, verification key
|
2021-02-07 13:29:27 +01:00
|
|
|
for sign_privkey, _ in keys.items():
|
2024-02-14 11:48:41 +01:00
|
|
|
sig = sign_schnorr(sign_privkey, msg)
|
|
|
|
for verify_privkey, verify_pubkey in keys.items():
|
|
|
|
if verify_privkey == sign_privkey:
|
|
|
|
self.assertTrue(verify_schnorr(verify_pubkey, sig, msg))
|
|
|
|
sig = list(sig)
|
|
|
|
sig[random.randrange(64)] ^= (1 << (random.randrange(8))) # damaging signature should break things
|
|
|
|
sig = bytes(sig)
|
|
|
|
self.assertFalse(verify_schnorr(verify_pubkey, sig, msg))
|
|
|
|
|
|
|
|
def test_schnorr_testvectors(self):
|
|
|
|
"""Implement the BIP340 test vectors (read from bip340_test_vectors.csv)."""
|
|
|
|
num_tests = 0
|
2020-10-21 22:06:34 +02:00
|
|
|
vectors_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bip340_test_vectors.csv')
|
|
|
|
with open(vectors_file, newline='', encoding='utf8') as csvfile:
|
2024-02-14 11:48:41 +01:00
|
|
|
reader = csv.reader(csvfile)
|
|
|
|
next(reader)
|
|
|
|
for row in reader:
|
|
|
|
(i_str, seckey_hex, pubkey_hex, aux_rand_hex, msg_hex, sig_hex, result_str, comment) = row
|
|
|
|
i = int(i_str)
|
|
|
|
pubkey = bytes.fromhex(pubkey_hex)
|
|
|
|
msg = bytes.fromhex(msg_hex)
|
|
|
|
sig = bytes.fromhex(sig_hex)
|
|
|
|
result = result_str == 'TRUE'
|
|
|
|
if seckey_hex != '':
|
|
|
|
seckey = bytes.fromhex(seckey_hex)
|
|
|
|
pubkey_actual = compute_xonly_pubkey(seckey)[0]
|
|
|
|
self.assertEqual(pubkey.hex(), pubkey_actual.hex(), "BIP340 test vector %i (%s): pubkey mismatch" % (i, comment))
|
|
|
|
aux_rand = bytes.fromhex(aux_rand_hex)
|
|
|
|
try:
|
|
|
|
sig_actual = sign_schnorr(seckey, msg, aux_rand)
|
|
|
|
self.assertEqual(sig.hex(), sig_actual.hex(), "BIP340 test vector %i (%s): sig mismatch" % (i, comment))
|
|
|
|
except RuntimeError as e:
|
2020-10-16 00:39:09 +02:00
|
|
|
self.fail("BIP340 test vector %i (%s): signing raised exception %s" % (i, comment, e))
|
2024-02-14 11:48:41 +01:00
|
|
|
result_actual = verify_schnorr(pubkey, sig, msg)
|
|
|
|
if result:
|
|
|
|
self.assertEqual(result, result_actual, "BIP340 test vector %i (%s): verification failed" % (i, comment))
|
|
|
|
else:
|
|
|
|
self.assertEqual(result, result_actual, "BIP340 test vector %i (%s): verification succeeded unexpectedly" % (i, comment))
|
|
|
|
num_tests += 1
|
|
|
|
self.assertTrue(num_tests >= 15) # expect at least 15 test vectors
|