Files
bips/bip-0461/gen_test_vectors.py
T

194 lines
7.3 KiB
Python
Raw Normal View History

"""Generate the low-R grinding test vectors."""
import csv
import os
import sys
from reference import (
bytes_from_int,
G,
deterministic_sign,
grind_extra,
is_low_r,
n,
normalize_low_s,
p,
point_add,
point_mul,
pubkey_gen,
sign,
verify,
)
FILENAME_SIGN_TEST = os.path.join(sys.path[0], 'sign_test_vectors.csv')
FILENAME_VERIFY_TEST = os.path.join(sys.path[0], 'verify_test_vectors.csv')
# (secret key, message hash, comment)
SIGN_CASES = [
(0x12bef4612f311c38408e662304f22ef64792e80561b0a8adecd2074687e12bf1, 0xec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff, "grinding stops at counter 7"),
(0x4889e538d453c1b6834f66d70274c770e689210505c34fc5caad70fa29c8ad99, 2**256 - 1, "msghash%n=2^256-1-n"),
(0x4889e538d453c1b6834f66d70274c770e689210505c34fc5caad70fa29c8ad99, n, "msghash%n=0"),
]
# Secret keys outside 1..n-1, for which signing must fail.
INVALID_SECKEY_CASES = [
(0, "sk=0"),
(n, "sk=n"),
(p, "sk=p, which is above n"),
]
def pubkey_hex(P):
"""Serialize a public key in SEC 1 format: uncompressed, or the single byte 00
for the point at infinity."""
if P is None:
return "00"
return "04" + f"{P[0]:064x}" + f"{P[1]:064x}"
def sig_hex(sig):
return (bytes_from_int(sig[0]) + bytes_from_int(sig[1])).hex()
def signed_cases():
"""Record the signature and grind counter of each case."""
cases = []
for sk, msg, comment in SIGN_CASES:
seckey, msghash = bytes_from_int(sk), bytes_from_int(msg)
for count in range(2**32):
sig = sign(seckey, msghash, grind_extra(count))
if is_low_r(sig):
break
# `sign` returns s unnormalized; normalization happens once after grinding
sig = normalize_low_s(sig)
assert deterministic_sign(seckey, msghash) == sig
cases.append((sk, msg, comment, sig, count))
return cases
def sign_vectors(cases):
"""Generate the valid and invalid signing vectors."""
for sk, msg, comment, sig, _ in cases:
assert is_low_r(sig)
yield {
"seckey": f"{sk:064x}",
"msghash": f"{msg:064x}",
"sig": sig_hex(sig),
"comment": comment,
}
msg = cases[0][1] # nonzero, so a missing key check can't loop forever on s = 0
for sk, comment in INVALID_SECKEY_CASES:
raised = False
try:
deterministic_sign(bytes_from_int(sk), bytes_from_int(msg))
except ValueError:
raised = True
assert raised
# An empty signature means signing must fail for this secret key.
yield {
"seckey": f"{sk:064x}",
"msghash": f"{msg:064x}",
"sig": "",
"comment": comment,
}
def verify_row(P, msg, sig, result, comment):
assert verify(sig, P, bytes_from_int(msg)) is result
return {
"pubkey": pubkey_hex(P),
"msghash": f"{msg:064x}",
"sig": sig_hex(sig),
"result": "TRUE" if result else "FALSE",
"comment": comment,
}
def verify_vectors(cases):
"""Generate the valid and invalid verification vectors."""
for sk, msg, _, sig, _ in cases:
yield verify_row(pubkey_gen(bytes_from_int(sk)), msg, sig, True, "")
sk, msg = cases[0][:2]
yield verify_row(pubkey_gen(bytes_from_int(sk)), msg, cases[1][3], False,
"signature made with a different key and message")
sk, msg, _, sig, count = cases[0]
P = pubkey_gen(bytes_from_int(sk))
r, s = sig
# This case required grinding for a low-r, so we can inject an earlier
# counter to get a valid high-r signature, which should verify.
assert count > 0
high_r = normalize_low_s(sign(bytes_from_int(sk), bytes_from_int(msg), grind_extra(0)))
assert not is_low_r(high_r)
yield verify_row(P, msg, high_r, True,
"high-r signature: low-r is a signing policy, not a validity rule")
# s outside 1..n-1, and r = s = 0.
yield verify_row(P, msg, (r, 0), False, "s=0")
yield verify_row(P, msg, (r, n), False, "s=n")
yield verify_row(P, msg, (0, 0), False,
"r = s = 0; a verifier missing the zero checks can accept this for "
"any key and message, as in CVE-2022-21449")
# Pubkey which will verify under the given `msghash`/`sig` pair if it receives
# no explicit on-curve check.
Q = (2, 3)
assert Q[1] ** 2 % p != (Q[0] ** 3 + 7) % p
assert point_mul(Q, 6) is None
u2 = 1
forged_R = point_mul(Q, u2)
assert forged_R is not None
forged_r = forged_R[0] % n
forged_s = (forged_r * pow(u2, -1, n)) % n
assert 1 <= forged_r <= n - 1 and 1 <= forged_s <= n - 1
yield verify_row(Q, 0, (forged_r, forged_s), False,
"public key not on the curve, with a signature that verifies "
"if the on-curve check is skipped")
x_one = (1, pow(1 + 7, (p + 1) // 4, p))
y_one = (pow((1 - 7) % p, (p + 2) // 9, p), 1)
for key, wide_key, comment in [
(x_one, (x_one[0] + p, x_one[1]),
"public key x = p+1; accepted if coordinates are reduced mod p instead of rejected"),
(y_one, (y_one[0], y_one[1] + p),
"public key y = p+1; accepted if coordinates are reduced mod p instead of rejected"),
]:
assert key[1] ** 2 % p == (key[0] ** 3 + 7) % p and max(wide_key) < 2**256
key_R = point_add(G, key)
assert key_R is not None
key_r = key_R[0] % n
assert verify((key_r, key_r), key, bytes_from_int(key_r))
yield verify_row(wide_key, key_r, (key_r, key_r), False, comment)
yield verify_row(None, msg, sig, False, "public key is the point at infinity")
# Vector that produces R as the point at infinity.
infinite_r_msg = (-r * sk) % n
assert (infinite_r_msg + r * sk) % n == 0 and infinite_r_msg != 0
yield verify_row(P, infinite_r_msg, sig, False, "u1*G + u2*P is the point at infinity")
# Vector with x(R) greater than n, unreachable in practice (n + 2, since
# x = n gives r = 0).
big_x = n + 2
big_y = pow((big_x ** 3 + 7) % p, (p + 1) // 4, p)
assert big_y ** 2 % p == (big_x ** 3 + 7) % p
big_R = (big_x, big_y)
big_P = point_add(big_R, (G[0], p - G[1]))
assert big_P is not None and big_x % n == 2
yield verify_row(big_P, 2, (2, 2), True,
"x(R) = n+2; fails if x(R) is compared to r without reducing mod n")
# The same signature with r or s moved out of range.
yield verify_row(big_P, 2, (2 + n, 2), False,
"r = n+2; accepted if r is reduced mod n instead of rejected")
yield verify_row(big_P, 2, (2, 2 + n), False,
"s = n+2; accepted if s is reduced mod n instead of rejected")
def gen_all(fil, fields, vectors):
writer = csv.DictWriter(fil, fields)
writer.writeheader()
for row in vectors:
writer.writerow(row)
if __name__ == "__main__":
cases = signed_cases()
print(f"Generating {FILENAME_SIGN_TEST}...")
with open(FILENAME_SIGN_TEST, "w", newline='', encoding="utf-8") as fil_sign:
gen_all(fil_sign, ["seckey", "msghash", "sig", "comment"],
sign_vectors(cases))
print(f"Generating {FILENAME_VERIFY_TEST}...")
with open(FILENAME_VERIFY_TEST, "w", newline='', encoding="utf-8") as fil_verify:
gen_all(fil_verify, ["pubkey", "msghash", "sig", "result", "comment"], verify_vectors(cases))