mirror of
https://github.com/bitcoin/bips.git
synced 2026-11-09 19:47:16 +00:00
BIP461: add test vectors and reference implementation (#2328)
All vectors were tested against libsecp256k1, and all verification vectors were checked against OpenSSL. A check was also added to the `verify` specification to reject pubkeys with x or y coordinates greater than or equal to `p`.
This commit is contained in:
1 parent
21bfd24d6e
commit
5bbeeae962
7 files changed
+512
No files matched your search
@@ -131,6 +131,8 @@ The algorithm *verify(sig, P, msghash)* is defined as:
|
|||||||
|
|
||||||
- If *is_infinite(P)*:
|
- If *is_infinite(P)*:
|
||||||
- Return *false*
|
- Return *false*
|
||||||
|
- If *x(P) >= p* or *y(P) >= p*:
|
||||||
|
- Return *false*
|
||||||
- If *y(P)<sup>2</sup> ≠ x(P)<sup>3</sup> + 7 mod p*:
|
- If *y(P)<sup>2</sup> ≠ x(P)<sup>3</sup> + 7 mod p*:
|
||||||
- Return *false*
|
- Return *false*
|
||||||
- Let *(r, s) = sig*
|
- Let *(r, s) = sig*
|
||||||
@@ -200,6 +202,13 @@ Grinding requires a nonce derivation that is deterministic in the secret key and
|
|||||||
|
|
||||||
Signatures generated under this specification are normal valid signatures and will verify under existing consensus rules, and thus are fully backward compatible.
|
Signatures generated under this specification are normal valid signatures and will verify under existing consensus rules, and thus are fully backward compatible.
|
||||||
|
|
||||||
|
## Reference Implementation
|
||||||
|
|
||||||
|
For development and testing purposes, we provide a [collection of test vectors in CSV format](bip-0461/),
|
||||||
|
a naive, highly inefficient, and non-constant time [pure Python 3.8 reference implementation of the signing and verification algorithm](bip-0461/reference.py)
|
||||||
|
as well as the [script used to generate the test vectors](bip-0461/gen_test_vectors.py).
|
||||||
|
The reference implementation is for demonstration purposes only and not to be used in production environments.
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
This BIP is licensed under the CC0-1.0 license.
|
This BIP is licensed under the CC0-1.0 license.
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CC0-1.0
|
||||||
|
|
||||||
|
The contents of this directory are provided under the terms of CC0 1.0:
|
||||||
|
|
||||||
|
- CC0 1.0
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"""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))
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
from typing import Tuple, Optional
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
||||||
|
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||||
|
half_order = (n - 1) // 2
|
||||||
|
|
||||||
|
# Points are tuples of X and Y coordinates and the point at infinity is
|
||||||
|
# represented by the None keyword.
|
||||||
|
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
|
||||||
|
|
||||||
|
Point = Tuple[int, int]
|
||||||
|
|
||||||
|
# A signature is the integer pair (r, s)
|
||||||
|
Signature = Tuple[int, int]
|
||||||
|
|
||||||
|
def is_infinite(P: Optional[Point]) -> bool:
|
||||||
|
return P is None
|
||||||
|
|
||||||
|
def x(P: Point) -> int:
|
||||||
|
assert not is_infinite(P)
|
||||||
|
return P[0]
|
||||||
|
|
||||||
|
def y(P: Point) -> int:
|
||||||
|
assert not is_infinite(P)
|
||||||
|
return P[1]
|
||||||
|
|
||||||
|
def int_from_bytes(b: bytes) -> int:
|
||||||
|
return int.from_bytes(b, byteorder="big")
|
||||||
|
|
||||||
|
def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]:
|
||||||
|
if P1 is None:
|
||||||
|
return P2
|
||||||
|
if P2 is None:
|
||||||
|
return P1
|
||||||
|
if (x(P1) == x(P2)) and (y(P1) != y(P2)):
|
||||||
|
return None
|
||||||
|
if P1 == P2:
|
||||||
|
lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p
|
||||||
|
else:
|
||||||
|
lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p
|
||||||
|
x3 = (lam * lam - x(P1) - x(P2)) % p
|
||||||
|
return (x3, (lam * (x(P1) - x3) - y(P1)) % p)
|
||||||
|
|
||||||
|
def point_mul(P: Optional[Point], n: int) -> Optional[Point]:
|
||||||
|
R = None
|
||||||
|
for i in range(256):
|
||||||
|
if (n >> i) & 1:
|
||||||
|
R = point_add(R, P)
|
||||||
|
P = point_add(P, P)
|
||||||
|
return R
|
||||||
|
|
||||||
|
def bytes_from_int(x: int) -> bytes:
|
||||||
|
return x.to_bytes(32, byteorder="big")
|
||||||
|
|
||||||
|
def bytes_le(l: int, x: int) -> bytes:
|
||||||
|
return x.to_bytes(l, byteorder="little")
|
||||||
|
|
||||||
|
def fill_bytes(l: int, x: int) -> bytes:
|
||||||
|
return bytes([x]) * l
|
||||||
|
|
||||||
|
def pubkey_gen(seckey: bytes) -> Point:
|
||||||
|
if len(seckey) != 32:
|
||||||
|
raise ValueError('The secret key must be a 32-byte array.')
|
||||||
|
d0 = int_from_bytes(seckey)
|
||||||
|
if not (1 <= d0 <= n - 1):
|
||||||
|
raise ValueError('The secret key must be an integer in the range 1..n-1.')
|
||||||
|
P = point_mul(G, d0)
|
||||||
|
assert P is not None
|
||||||
|
return P
|
||||||
|
|
||||||
|
def hmac_sha256(key: bytes, data: bytes) -> bytes:
|
||||||
|
return hmac.new(key, data, hashlib.sha256).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def nonce_rfc6979(seckey: bytes, msghash: bytes, extra: bytes, count: int) -> bytes:
|
||||||
|
"""
|
||||||
|
There are two auxiliary inputs here which are both effectively counters, but
|
||||||
|
serve different purposes.
|
||||||
|
|
||||||
|
`extra`: in the event that a high-r signature is produced, we inject `extra` to produce
|
||||||
|
a different nonce that may produce a low-r signature. It is the grind counter's
|
||||||
|
encoding, see `grind_extra`.
|
||||||
|
|
||||||
|
`count`: in the event that the nonce is either invalid *OR* produces an invalid signature
|
||||||
|
(both are cryptographically unreachable events), `sign` increments `count` to produce a
|
||||||
|
different nonce which may (will almost certainly) be valid and produce a valid signature.
|
||||||
|
|
||||||
|
`K` and `V` are the internal state variables of the RFC 6979 generator and carry its
|
||||||
|
names. Both are 32-byte arrays rather than curve points, and `K` is unrelated to the
|
||||||
|
nonce `k`.
|
||||||
|
"""
|
||||||
|
if len(seckey) != 32:
|
||||||
|
raise ValueError('The secret key must be a 32-byte array.')
|
||||||
|
if len(msghash) != 32:
|
||||||
|
raise ValueError('The message hash must be a 32-byte array.')
|
||||||
|
|
||||||
|
# RFC 6979 section 3.2d feeds bits2octets(h1), i.e. the message hash reduced
|
||||||
|
# modulo n, not the raw hash. libsecp256k1 does the same (`msgmod32` in
|
||||||
|
# `nonce_function_rfc6979`). The two differ only when int(msghash) >= n.
|
||||||
|
h = bytes_from_int(int_from_bytes(msghash) % n)
|
||||||
|
|
||||||
|
V = fill_bytes(32, 0x01)
|
||||||
|
K = fill_bytes(32, 0x00)
|
||||||
|
|
||||||
|
K = hmac_sha256(K, V + b'\x00' + seckey + h + extra)
|
||||||
|
V = hmac_sha256(K, V)
|
||||||
|
K = hmac_sha256(K, V + b'\x01' + seckey + h + extra)
|
||||||
|
V = hmac_sha256(K, V)
|
||||||
|
|
||||||
|
V = hmac_sha256(K, V)
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
K = hmac_sha256(K, V + b'\x00')
|
||||||
|
V = hmac_sha256(K, V)
|
||||||
|
V = hmac_sha256(K, V)
|
||||||
|
|
||||||
|
return V
|
||||||
|
|
||||||
|
|
||||||
|
def sign(seckey: bytes, msghash: bytes, extra: bytes) -> Signature:
|
||||||
|
"""See `nonce_rfc6979` for the difference between `extra` and `count`."""
|
||||||
|
if len(seckey) != 32:
|
||||||
|
raise ValueError('The secret key must be a 32-byte array.')
|
||||||
|
if len(msghash) != 32:
|
||||||
|
raise ValueError('The message hash must be a 32-byte array.')
|
||||||
|
|
||||||
|
d = int_from_bytes(seckey)
|
||||||
|
if not (1 <= d <= n - 1):
|
||||||
|
raise ValueError('The secret key must be an integer in the range 1..n-1.')
|
||||||
|
|
||||||
|
for count in range(2**32):
|
||||||
|
nonce = nonce_rfc6979(seckey, msghash, extra, count)
|
||||||
|
k = int_from_bytes(nonce)
|
||||||
|
if not (1 <= k <= n - 1):
|
||||||
|
continue
|
||||||
|
R = point_mul(G, k)
|
||||||
|
assert R is not None
|
||||||
|
r = x(R) % n
|
||||||
|
if r == 0:
|
||||||
|
continue
|
||||||
|
s = (pow(k, -1, n) * (d * r + int_from_bytes(msghash))) % n
|
||||||
|
if s == 0:
|
||||||
|
continue
|
||||||
|
return (r, s)
|
||||||
|
raise RuntimeError('No valid signature found below the attempt counter limit.')
|
||||||
|
|
||||||
|
def verify(sig: Signature, pubkey: Optional[Point], msghash: bytes) -> bool:
|
||||||
|
if len(msghash) != 32:
|
||||||
|
raise ValueError('The message hash must be a 32-byte array.')
|
||||||
|
|
||||||
|
if is_infinite(pubkey):
|
||||||
|
return False
|
||||||
|
assert pubkey is not None
|
||||||
|
|
||||||
|
if x(pubkey) >= p or y(pubkey) >= p:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (y(pubkey) ** 2) % p != (x(pubkey) ** 3 + 7) % p:
|
||||||
|
return False
|
||||||
|
|
||||||
|
(r, s) = sig
|
||||||
|
|
||||||
|
if r <= 0 or r >= n:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if s <= 0 or s >= n:
|
||||||
|
return False
|
||||||
|
|
||||||
|
w = pow(s, -1, n)
|
||||||
|
|
||||||
|
u1 = (w * int_from_bytes(msghash)) % n
|
||||||
|
u2 = (w * r) % n
|
||||||
|
R = point_add(point_mul(G, u1), point_mul(pubkey, u2))
|
||||||
|
|
||||||
|
if is_infinite(R):
|
||||||
|
return False
|
||||||
|
assert R is not None
|
||||||
|
|
||||||
|
if x(R) % n != r:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def grind_extra(i: int) -> bytes:
|
||||||
|
"""The additional data corresponding to the grind counter."""
|
||||||
|
if not (0 <= i <= 2**32 - 1):
|
||||||
|
raise ValueError('The grind counter must be an integer in the range 0..2^32-1.')
|
||||||
|
return b'' if i == 0 else bytes_le(4, i) + fill_bytes(28, 0x00)
|
||||||
|
|
||||||
|
def is_low_r(sig: Signature) -> bool:
|
||||||
|
"""Can also be written as bytes_from_int(r)[0] < 0x80"""
|
||||||
|
(r, _) = sig
|
||||||
|
return r < 2**255
|
||||||
|
|
||||||
|
def normalize_low_s(sig: Signature) -> Signature:
|
||||||
|
(r, s) = sig
|
||||||
|
return (r, n - s) if s > half_order else (r, s)
|
||||||
|
|
||||||
|
def deterministic_sign(seckey: bytes, msghash: bytes) -> Signature:
|
||||||
|
if len(seckey) != 32:
|
||||||
|
raise ValueError('The secret key must be a 32-byte array.')
|
||||||
|
if not (1 <= int_from_bytes(seckey) <= n - 1):
|
||||||
|
raise ValueError('The secret key must be an integer in the range 1..n-1.')
|
||||||
|
|
||||||
|
(r, s) = sign(seckey, msghash, grind_extra(0))
|
||||||
|
i = 1
|
||||||
|
while r >= 2**255:
|
||||||
|
if i >= 2**32:
|
||||||
|
raise RuntimeError('No low-r signature found below the grind counter limit.')
|
||||||
|
(r, s) = sign(seckey, msghash, grind_extra(i))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
(r, s) = normalize_low_s((r, s))
|
||||||
|
|
||||||
|
P = pubkey_gen(seckey)
|
||||||
|
if not verify((r, s), P, msghash):
|
||||||
|
raise RuntimeError('The created signature is invalid.')
|
||||||
|
return (r, s)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Run the low-R grinding test vectors."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import reference
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
def sig_from_hex(s):
|
||||||
|
"""Parse a compact signature into the integer pair (r, s)."""
|
||||||
|
b = bytes.fromhex(s)
|
||||||
|
assert len(b) == 64
|
||||||
|
return (int.from_bytes(b[:32], 'big'), int.from_bytes(b[32:], 'big'))
|
||||||
|
|
||||||
|
def sig_hex(sig):
|
||||||
|
return (reference.bytes_from_int(sig[0]) + reference.bytes_from_int(sig[1])).hex()
|
||||||
|
|
||||||
|
def pubkey_from_hex(s):
|
||||||
|
"""Parse an uncompressed SEC 1 public key into a point."""
|
||||||
|
b = bytes.fromhex(s)
|
||||||
|
if b == b'\x00':
|
||||||
|
return None
|
||||||
|
assert len(b) == 65 and b[0] == 0x04
|
||||||
|
return (int.from_bytes(b[1:33], 'big'), int.from_bytes(b[33:], 'big'))
|
||||||
|
|
||||||
|
def result_from_str(s):
|
||||||
|
assert s in ('TRUE', 'FALSE')
|
||||||
|
return s == 'TRUE'
|
||||||
|
|
||||||
|
with open(FILENAME_SIGN_TEST, newline='', encoding='utf-8') as csvfile:
|
||||||
|
print(f"Running {FILENAME_SIGN_TEST} tests...")
|
||||||
|
reader = csv.DictReader(csvfile)
|
||||||
|
for row in reader:
|
||||||
|
seckey = bytes.fromhex(row['seckey'])
|
||||||
|
msghash = bytes.fromhex(row['msghash'])
|
||||||
|
if row['sig'] == '':
|
||||||
|
# An empty signature means signing must fail for this secret key.
|
||||||
|
raised = False
|
||||||
|
try:
|
||||||
|
reference.deterministic_sign(seckey, msghash)
|
||||||
|
except ValueError:
|
||||||
|
raised = True
|
||||||
|
assert raised
|
||||||
|
continue
|
||||||
|
sig = reference.deterministic_sign(seckey, msghash)
|
||||||
|
assert reference.is_low_r(sig)
|
||||||
|
assert row['sig'] == sig_hex(sig)
|
||||||
|
|
||||||
|
with open(FILENAME_VERIFY_TEST, newline='', encoding='utf-8') as csvfile:
|
||||||
|
print(f"Running {FILENAME_VERIFY_TEST} tests...")
|
||||||
|
reader = csv.DictReader(csvfile)
|
||||||
|
for row in reader:
|
||||||
|
pubkey = pubkey_from_hex(row['pubkey'])
|
||||||
|
msghash = bytes.fromhex(row['msghash'])
|
||||||
|
sig = sig_from_hex(row['sig'])
|
||||||
|
assert reference.verify(sig, pubkey, msghash) == result_from_str(row['result'])
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
seckey,msghash,sig,comment
|
||||||
|
12bef4612f311c38408e662304f22ef64792e80561b0a8adecd2074687e12bf1,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,5454d43ce9b30ae6790ef0b3fe49363d052f130472586d12ac80e9f1d4fae1f743678a8cad1215ffec68092a993909082998c504c5de982aef28717e57338bbc,grinding stops at counter 7
|
||||||
|
4889e538d453c1b6834f66d70274c770e689210505c34fc5caad70fa29c8ad99,ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,60a5d1904124dc3925c4887192306e43bf1d0989f89af7adecf06c8ab03bfdf94c2124bfa724c3dce24be6dab661eb1b38cdc42cfa250b61458b6ef2fc46396c,msghash%n=2^256-1-n
|
||||||
|
4889e538d453c1b6834f66d70274c770e689210505c34fc5caad70fa29c8ad99,fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,2e2b06d3334157fa44689912d33be79bafe30ec4b88e4a7d709f33615a1bb2066611e2fa8f82a2cf7802acf64988a5a002c914cc63e553be801db3997d0924c6,msghash%n=0
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,,sk=0
|
||||||
|
fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,,sk=n
|
||||||
|
fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,,"sk=p, which is above n"
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
pubkey,msghash,sig,result,comment
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,5454d43ce9b30ae6790ef0b3fe49363d052f130472586d12ac80e9f1d4fae1f743678a8cad1215ffec68092a993909082998c504c5de982aef28717e57338bbc,TRUE,
|
||||||
|
04a0429363c22cd4b73db46b180fd05d9c3555c0ab31e01363717e23e26886bd5de3ae91e83e58d166955bf64c826bac4f46bf2b018b730ffd77c6a248e0f3f8cd,ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,60a5d1904124dc3925c4887192306e43bf1d0989f89af7adecf06c8ab03bfdf94c2124bfa724c3dce24be6dab661eb1b38cdc42cfa250b61458b6ef2fc46396c,TRUE,
|
||||||
|
04a0429363c22cd4b73db46b180fd05d9c3555c0ab31e01363717e23e26886bd5de3ae91e83e58d166955bf64c826bac4f46bf2b018b730ffd77c6a248e0f3f8cd,fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,2e2b06d3334157fa44689912d33be79bafe30ec4b88e4a7d709f33615a1bb2066611e2fa8f82a2cf7802acf64988a5a002c914cc63e553be801db3997d0924c6,TRUE,
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,60a5d1904124dc3925c4887192306e43bf1d0989f89af7adecf06c8ab03bfdf94c2124bfa724c3dce24be6dab661eb1b38cdc42cfa250b61458b6ef2fc46396c,FALSE,signature made with a different key and message
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,df12252ec1f17a66cc6b438a79f5e63b0b91eefd90c57c636ce2b8e3401075fe37d01c81ba1f9cfa590885ffce18d8a289363f2c756ae8964c608f991a4b883a,TRUE,"high-r signature: low-r is a signing policy, not a validity rule"
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,5454d43ce9b30ae6790ef0b3fe49363d052f130472586d12ac80e9f1d4fae1f70000000000000000000000000000000000000000000000000000000000000000,FALSE,s=0
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,5454d43ce9b30ae6790ef0b3fe49363d052f130472586d12ac80e9f1d4fae1f7fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,FALSE,s=n
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,FALSE,"r = s = 0; a verifier missing the zero checks can accept this for any key and message, as in CVE-2022-21449"
|
||||||
|
0400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003,0000000000000000000000000000000000000000000000000000000000000000,00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002,FALSE,"public key not on the curve, with a signature that verifies if the on-curve check is skipped"
|
||||||
|
04fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc304218f20ae6c646b363db68605822fb14264ca8d2587fdd6fbc750d587e76a7ee,57d783579d03d9ab67a8aa7ad9b75a66ebca4ebce1b5be71442db1307f9146a8,57d783579d03d9ab67a8aa7ad9b75a66ebca4ebce1b5be71442db1307f9146a857d783579d03d9ab67a8aa7ad9b75a66ebca4ebce1b5be71442db1307f9146a8,FALSE,public key x = p+1; accepted if coordinates are reduced mod p instead of rejected
|
||||||
|
041fe1e5ef3fceb5c135ab7741333ce5a6e80d68167653f6b2b24bcbcfaaaff507fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc30,663c42aaae1ba20a1f06879b77b9ef4de8f7f9a1b9b34a3e0fb2fd7343dc4229,663c42aaae1ba20a1f06879b77b9ef4de8f7f9a1b9b34a3e0fb2fd7343dc4229663c42aaae1ba20a1f06879b77b9ef4de8f7f9a1b9b34a3e0fb2fd7343dc4229,FALSE,public key y = p+1; accepted if coordinates are reduced mod p instead of rejected
|
||||||
|
00,ec888dbfce53624100e889809bce7d3cbe677b56a15968c5964d8d3690f57bff,5454d43ce9b30ae6790ef0b3fe49363d052f130472586d12ac80e9f1d4fae1f743678a8cad1215ffec68092a993909082998c504c5de982aef28717e57338bbc,FALSE,public key is the point at infinity
|
||||||
|
046cb5d4539027cde94700dbc595a8743b20188e45096ec83e6af0cf8e3c7baa428c0f0d7b20be55b9b2d9b888a8be73c2e57c4527a2d76568679b42db199be57f,48f414d53b080c53fbe5a11e5fa5cb30b45d1fbb7d31f8747007ceea633246af,5454d43ce9b30ae6790ef0b3fe49363d052f130472586d12ac80e9f1d4fae1f743678a8cad1215ffec68092a993909082998c504c5de982aef28717e57338bbc,FALSE,u1*G + u2*P is the point at infinity
|
||||||
|
04fa080d6569edae14025c385d49d06a25b116121767026fe837249ff0a5bcbac771aeed5573db3682d4f047d920f14c61b39cfd546ef2de3f4a8cf17c2e7a964d,0000000000000000000000000000000000000000000000000000000000000002,00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002,TRUE,x(R) = n+2; fails if x(R) is compared to r without reducing mod n
|
||||||
|
04fa080d6569edae14025c385d49d06a25b116121767026fe837249ff0a5bcbac771aeed5573db3682d4f047d920f14c61b39cfd546ef2de3f4a8cf17c2e7a964d,0000000000000000000000000000000000000000000000000000000000000002,fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd03641430000000000000000000000000000000000000000000000000000000000000002,FALSE,r = n+2; accepted if r is reduced mod n instead of rejected
|
||||||
|
04fa080d6569edae14025c385d49d06a25b116121767026fe837249ff0a5bcbac771aeed5573db3682d4f047d920f14c61b39cfd546ef2de3f4a8cf17c2e7a964d,0000000000000000000000000000000000000000000000000000000000000002,0000000000000000000000000000000000000000000000000000000000000002fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364143,FALSE,s = n+2; accepted if s is reduced mod n instead of rejected
|
||||||
|
Reference in new issue
Block a user