mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-02 09:46:52 -05:00
tests: add BIP340 Schnorr signature support to test framework
Add a pure Python implementation of BIP340 signing and verification, tested against the BIP's test vectors.
This commit is contained in:
parent
206fb180ec
commit
3c226639eb
3 changed files with 204 additions and 6 deletions
16
test/functional/test_framework/bip340_test_vectors.csv
Normal file
16
test/functional/test_framework/bip340_test_vectors.csv
Normal file
|
@ -0,0 +1,16 @@
|
|||
index,secret key,public key,aux_rand,message,signature,verification result,comment
|
||||
0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE,
|
||||
1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE,
|
||||
2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE,
|
||||
3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n
|
||||
4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE,
|
||||
5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve
|
||||
6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false
|
||||
7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message
|
||||
8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value
|
||||
9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0
|
||||
10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1
|
||||
11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve
|
||||
12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size
|
||||
13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order
|
||||
14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Copyright (c) 2019-2020 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test-only secp256k1 elliptic curve implementation
|
||||
|
@ -6,10 +6,24 @@
|
|||
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."""
|
||||
import csv
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from .util import modinv
|
||||
|
||||
def TaggedHash(tag, data):
|
||||
ss = hashlib.sha256(tag.encode('utf-8')).digest()
|
||||
ss += ss
|
||||
ss += data
|
||||
return hashlib.sha256(ss).digest()
|
||||
|
||||
def xor_bytes(b0, b1):
|
||||
return bytes(x ^ y for (x, y) in zip(b0, b1))
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
|
@ -68,6 +82,10 @@ class EllipticCurve:
|
|||
inv_3 = (inv_2 * inv) % self.p
|
||||
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
|
||||
|
||||
def has_even_y(self, p1):
|
||||
"""Whether the point p1 has an even Y coordinate when expressed in affine coordinates."""
|
||||
return not (p1[2] == 0 or self.affine(p1)[1] & 1)
|
||||
|
||||
def negate(self, p1):
|
||||
"""Negate a Jacobian point tuple p1."""
|
||||
x1, y1, z1 = p1
|
||||
|
@ -86,13 +104,13 @@ class EllipticCurve:
|
|||
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
|
||||
|
||||
def lift_x(self, x):
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point."""
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point for which the Y coordinate is even."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
v = x_3 + self.a * x + self.b
|
||||
y = modsqrt(v, self.p)
|
||||
if y is None:
|
||||
return None
|
||||
return (x, y, 1)
|
||||
return (x, self.p - y if y & 1 else y, 1)
|
||||
|
||||
def double(self, p1):
|
||||
"""Double a Jacobian tuple p1
|
||||
|
@ -197,7 +215,8 @@ class EllipticCurve:
|
|||
r = self.add(r, p)
|
||||
return r
|
||||
|
||||
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
|
||||
SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977
|
||||
SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7)
|
||||
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
|
||||
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
|
||||
|
@ -223,7 +242,7 @@ class ECPubKey():
|
|||
p = SECP256K1.lift_x(x)
|
||||
# if the oddness of the y co-ord isn't correct, find the other
|
||||
# valid y
|
||||
if (p[1] & 1) != (data[0] & 1):
|
||||
if data[0] & 1:
|
||||
p = SECP256K1.negate(p)
|
||||
self.p = p
|
||||
self.valid = True
|
||||
|
@ -307,6 +326,10 @@ class ECPubKey():
|
|||
return False
|
||||
return True
|
||||
|
||||
def generate_privkey():
|
||||
"""Generate a valid random 32-byte private key."""
|
||||
return random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big')
|
||||
|
||||
class ECKey():
|
||||
"""A secp256k1 private key"""
|
||||
|
||||
|
@ -324,7 +347,7 @@ class ECKey():
|
|||
|
||||
def generate(self, compressed=True):
|
||||
"""Generate a random private key (compressed or uncompressed)."""
|
||||
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
|
||||
self.set(generate_privkey(), compressed)
|
||||
|
||||
def get_bytes(self):
|
||||
"""Retrieve the 32-byte representation of this key."""
|
||||
|
@ -369,3 +392,161 @@ class ECKey():
|
|||
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
|
||||
|
||||
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')
|
||||
if x == 0 or x >= SECP256K1_ORDER:
|
||||
return (None, None)
|
||||
P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, x)]))
|
||||
return (P[0].to_bytes(32, 'big'), not SECP256K1.has_even_y(P))
|
||||
|
||||
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')
|
||||
if x == 0 or x >= SECP256K1_ORDER:
|
||||
return None
|
||||
if not SECP256K1.has_even_y(SECP256K1.mul([(SECP256K1_G, x)])):
|
||||
x = SECP256K1_ORDER - x
|
||||
t = int.from_bytes(tweak, 'big')
|
||||
if t >= SECP256K1_ORDER:
|
||||
return None
|
||||
x = (x + t) % SECP256K1_ORDER
|
||||
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
|
||||
|
||||
x_coord = int.from_bytes(key, 'big')
|
||||
if x_coord >= SECP256K1_FIELD_SIZE:
|
||||
return None
|
||||
P = SECP256K1.lift_x(x_coord)
|
||||
if P is None:
|
||||
return None
|
||||
t = int.from_bytes(tweak, 'big')
|
||||
if t >= SECP256K1_ORDER:
|
||||
return None
|
||||
Q = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, t), (P, 1)]))
|
||||
if Q is None:
|
||||
return None
|
||||
return (Q[0].to_bytes(32, 'big'), not SECP256K1.has_even_y(Q))
|
||||
|
||||
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
|
||||
|
||||
x_coord = int.from_bytes(key, 'big')
|
||||
if x_coord == 0 or x_coord >= SECP256K1_FIELD_SIZE:
|
||||
return False
|
||||
P = SECP256K1.lift_x(x_coord)
|
||||
if P is None:
|
||||
return False
|
||||
r = int.from_bytes(sig[0:32], 'big')
|
||||
if r >= SECP256K1_FIELD_SIZE:
|
||||
return False
|
||||
s = int.from_bytes(sig[32:64], 'big')
|
||||
if s >= SECP256K1_ORDER:
|
||||
return False
|
||||
e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), 'big') % SECP256K1_ORDER
|
||||
R = SECP256K1.mul([(SECP256K1_G, s), (P, SECP256K1_ORDER - e)])
|
||||
if not SECP256K1.has_even_y(R):
|
||||
return False
|
||||
if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def sign_schnorr(key, msg, aux=None):
|
||||
"""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')
|
||||
if sec == 0 or sec >= SECP256K1_ORDER:
|
||||
return None
|
||||
P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, sec)]))
|
||||
if not SECP256K1.has_even_y(P):
|
||||
sec = SECP256K1_ORDER - sec
|
||||
t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big')
|
||||
kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + P[0].to_bytes(32, 'big') + msg), 'big') % SECP256K1_ORDER
|
||||
assert kp != 0
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)]))
|
||||
k = kp if SECP256K1.has_even_y(R) else SECP256K1_ORDER - kp
|
||||
e = int.from_bytes(TaggedHash("BIP0340/challenge", R[0].to_bytes(32, 'big') + P[0].to_bytes(32, 'big') + msg), 'big') % SECP256K1_ORDER
|
||||
return R[0].to_bytes(32, 'big') + ((k + e * sec) % SECP256K1_ORDER).to_bytes(32, 'big')
|
||||
|
||||
class TestFrameworkKey(unittest.TestCase):
|
||||
def test_schnorr(self):
|
||||
"""Test the Python Schnorr implementation."""
|
||||
byte_arrays = [generate_privkey() for _ in range(3)] + [v.to_bytes(32, 'big') for v in [0, SECP256K1_ORDER - 1, SECP256K1_ORDER, 2**256 - 1]]
|
||||
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
|
||||
for sign_privkey, sign_pubkey in keys.items():
|
||||
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
|
||||
with open(os.path.join(sys.path[0], 'test_framework', 'bip340_test_vectors.csv'), newline='', encoding='utf8') as csvfile:
|
||||
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:
|
||||
self.assertFalse("BIP340 test vector %i (%s): signing raised exception %s" % (i, comment, e))
|
||||
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
|
||||
|
|
|
@ -70,6 +70,7 @@ TEST_FRAMEWORK_MODULES = [
|
|||
"address",
|
||||
"blocktools",
|
||||
"muhash",
|
||||
"key",
|
||||
"script",
|
||||
"segwit_addr",
|
||||
"util",
|
||||
|
|
Loading…
Add table
Reference in a new issue