Files
bips/bip-0459/gen_test_vectors.py
T

606 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Generate test vectors for BIP 459 DahLIAS fully aggregated signatures
for secp256k1.
Usage:
python gen_test_vectors.py
Outputs:
- vectors/test_vectors_nonce_gen.csv (NonceGen with a fixed rand')
- vectors/test_vectors_nonce_agg.csv (NonceAgg: success and failure cases)
- vectors/test_vectors_sign.csv (end-to-end signing with fixed nonces)
- vectors/test_vectors_sign_error.csv (sessions in which Sign must fail)
- vectors/test_vectors_verify.csv (Verify: success and failure cases)
- vectors/test_vectors_partial_sig_verify.csv (PartialSigVerify cases)
- vectors/test_vectors_tweak.csv (plain and X-only key tweaking)
"""
import csv
import os
import sys
from pathlib import Path
from typing import List
sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
from secp256k1lab.secp256k1 import FE, G, GE, Scalar
from secp256k1lab.util import bytes_from_int, int_from_bytes, tagged_hash
from reference import (
NonceGenInternal,
NonceAgg,
Sign,
SigAgg,
Verify,
PartialSigVerify,
TweakSK,
TweakPK,
has_even_y,
n,
FULLAGG_TAG_SIG,
)
VECTORS_DIR = Path(__file__).resolve().parent / "vectors"
NONCE_GEN_VECTORS = VECTORS_DIR / "test_vectors_nonce_gen.csv"
NONCE_AGG_VECTORS = VECTORS_DIR / "test_vectors_nonce_agg.csv"
SIGN_VECTORS = VECTORS_DIR / "test_vectors_sign.csv"
SIGN_ERROR_VECTORS = VECTORS_DIR / "test_vectors_sign_error.csv"
VERIFY_VECTORS = VECTORS_DIR / "test_vectors_verify.csv"
PARTIAL_SIG_VERIFY_VECTORS = VECTORS_DIR / "test_vectors_partial_sig_verify.csv"
TWEAK_VECTORS = VECTORS_DIR / "test_vectors_tweak.csv"
def scalar_from_byte(b: int) -> Scalar:
"""Scalar whose 32-byte big-endian encoding is bytes([b]*32). Safe for 0x01..0xee."""
assert 1 <= b <= 0xee
return Scalar.from_bytes_checked(bytes([b] * 32))
def make_signer(i: int):
"""Deterministic (sk, pk, msg, secnonce, pubnonce). sk is normalized so pk has even y."""
sk = scalar_from_byte(i * 3 + 1)
pk = sk * G
if not has_even_y(pk):
sk = -sk
pk = sk * G
r1 = scalar_from_byte(i * 3 + 2)
r2 = scalar_from_byte(i * 3 + 3)
R1, R2 = r1 * G, r2 * G
msg = bytes([0x80 + i] * 32)
return sk, pk, msg, (r1, r2), (R1, R2)
def make_same_pk_signers():
"""Two signers sharing one key, signing different messages."""
sk = scalar_from_byte(1)
pk = sk * G
if not has_even_y(pk):
sk = -sk
pk = sk * G
msg_a = bytes([0xA0] * 32)
msg_b = bytes([0xA1] * 32)
r1_a, r2_a = scalar_from_byte(11), scalar_from_byte(12)
r1_b, r2_b = scalar_from_byte(13), scalar_from_byte(14)
return [
(sk, pk, msg_a, (r1_a, r2_a), (r1_a * G, r2_a * G)),
(sk, pk, msg_b, (r1_b, r2_b), (r1_b * G, r2_b * G)),
]
def make_tweaked_signer(i: int, tweak_byte: int, is_xonly: bool):
"""Signer with a tweaked key pair, using an odd-y base for X-only tweaks to exercise normalization."""
sk, pk, msg, secnonce, pubnonce = make_signer(i)
if is_xonly:
sk, pk = -sk, -pk
t = scalar_from_byte(tweak_byte)
return TweakSK(sk, t, is_xonly), TweakPK(pk, t, is_xonly), msg, secnonce, pubnonce
def make_duplicate_pm_signers():
"""Two signers with identical (pk, m) but distinct nonces."""
sk = scalar_from_byte(1)
pk = sk * G
if not has_even_y(pk):
sk = -sk
pk = sk * G
msg = bytes([0xC0] * 32)
r1_a, r2_a = scalar_from_byte(20), scalar_from_byte(21)
r1_b, r2_b = scalar_from_byte(22), scalar_from_byte(23)
return [
(sk, pk, msg, (r1_a, r2_a), (r1_a * G, r2_a * G)),
(sk, pk, msg, (r1_b, r2_b), (r1_b * G, r2_b * G)),
]
def serialize_pubnonce(pubnonce) -> bytes:
R1, R2 = pubnonce
return R1.to_bytes_compressed() + R2.to_bytes_compressed()
def serialize_secnonce(secnonce) -> bytes:
r1, r2 = secnonce
return r1.to_bytes() + r2.to_bytes()
def serialize_sig(sig) -> bytes:
R, s = sig
return R.to_bytes_xonly() + s.to_bytes()
def serialize_pk(pk: GE) -> bytes:
return pk.to_bytes_xonly()
def format_list(items: List[bytes], sep: str = ";") -> str:
if not items:
return ""
return sep.join(item.hex().upper() for item in items)
def sign_session(signers):
"""Run NonceAgg -> Sign (per signer) -> SigAgg with deterministic inputs."""
sks = [s[0] for s in signers]
pks = [GE.from_bytes_xonly(s[1].to_bytes_xonly()) for s in signers]
msgs = [s[2] for s in signers]
secnonces = [s[3] for s in signers]
pubnonces = [s[4] for s in signers]
aggnonce = NonceAgg(pubnonces)
psigs = [
Sign(secnonces[i], sks[i], msgs[i], aggnonce, pks, msgs, pubnonces)
for i in range(len(sks))
]
sig = SigAgg(aggnonce, pks, msgs, pubnonces, psigs)
return aggnonce, psigs, sig
def sign_unchecked(signers, msgs, r: Scalar):
"""Aggregate signature from a party holding all secret keys, skipping the message length check."""
R = r * G
if not has_even_y(R):
r = -r
R = r * G
L = b"".join(serialize_pk(s[1]) + m for s, m in zip(signers, msgs))
s = r
for (sk, pk, *_), m in zip(signers, msgs):
d = sk if has_even_y(pk) else -sk
c = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + R.to_bytes_xonly() + serialize_pk(pk) + m))
s = s + c * d
return R, s
def gen_nonce_gen_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index", "rand_", "secret_key", "extra_in",
"expected_secnonce", "expected_pubnonce", "comment",
))
rand_prime = bytes([0x0F] * 32)
def row(idx, sk, extra_in, comment):
secnonce, pubnonce = NonceGenInternal(rand_prime, sk, extra_in)
writer.writerow((
idx, rand_prime.hex().upper(),
sk.to_bytes().hex().upper() if sk is not None else "",
extra_in.hex().upper(),
serialize_secnonce(secnonce).hex().upper(),
serialize_pubnonce(pubnonce).hex().upper(),
comment,
))
sk = scalar_from_byte(0x02)
extra_in = bytes([0x08] * 32)
row(0, sk, extra_in, "Secret key and extra_in")
row(1, sk, b"", "Secret key and no extra_in")
row(2, None, extra_in, "No secret key and extra_in")
row(3, None, b"", "No secret key and no extra_in")
def gen_nonce_agg_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index", "pubnonces", "expected_result", "expected_aggnonce", "comment",
))
def nonce_agg_bytes(pubnonces):
return NonceAgg([(GE.from_bytes_compressed(p[:33]), GE.from_bytes_compressed(p[33:]))
for p in pubnonces])
def row(idx, pubnonces, comment):
aggnonce = serialize_pubnonce(nonce_agg_bytes(pubnonces))
writer.writerow((idx, format_list(pubnonces), "TRUE", aggnonce.hex().upper(), comment))
def error_row(idx, pubnonces, comment):
try:
nonce_agg_bytes(pubnonces)
except ValueError:
pass
else:
raise RuntimeError(f"NonceAgg unexpectedly succeeded: {comment}")
writer.writerow((idx, format_list(pubnonces), "FALSE", "", comment))
signers = [make_signer(i) for i in range(3)]
pubnonces = [serialize_pubnonce(s[4]) for s in signers]
(R1_0, R2_0), (R1_1, R2_1) = signers[0][4], signers[1][4]
row(0, pubnonces[:2], "Two signers")
row(1, pubnonces, "Three signers")
error_row(2, [pubnonces[0], b"\x04" + pubnonces[1][1:]],
"Signer 1's pubnonce first half has prefix 0x04")
error_row(3, [pubnonces[0][:33] + b"\x02" + bytes(32), pubnonces[1]],
"Signer 0's pubnonce second half is not on the curve")
error_row(4, [pubnonces[0][:33] + b"\x02" + bytes_from_int(FE.SIZE + 1), pubnonces[1]],
"Signer 0's pubnonce second half exceeds field size")
error_row(5, [pubnonces[0], serialize_pubnonce((-R1_0, R2_1))],
"Aggregate nonce R1 is infinity")
error_row(6, [pubnonces[0], serialize_pubnonce((R1_1, -R2_0))],
"Aggregate nonce R2 is infinity")
def gen_sign_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index",
"secret_keys",
"messages",
"secnonces",
"expected_aggnonce",
"expected_partial_sigs",
"expected_sig",
"comment",
))
def write_row(idx, signers, comment):
aggnonce, psigs, sig = sign_session(signers)
writer.writerow((
idx,
format_list([s[0].to_bytes() for s in signers]),
format_list([s[2] for s in signers]),
format_list([serialize_secnonce(s[3]) for s in signers]),
serialize_pubnonce(aggnonce).hex().upper(),
format_list([p.to_bytes() for p in psigs]),
serialize_sig(sig).hex().upper(),
comment,
))
pks = [s[1] for s in signers]
msgs = [s[2] for s in signers]
pubnonces = [s[4] for s in signers]
for i in range(len(signers)):
assert PartialSigVerify(psigs[i], pks, msgs, pubnonces, i), \
f"generated psig failed self-verification: {comment}"
assert Verify(pks, msgs, sig), \
f"generated sig failed self-verification: {comment}"
write_row(0, [make_signer(0)], "Single signer")
write_row(1, [make_signer(i) for i in range(2)], "Two signers")
write_row(2, [make_signer(i) for i in range(3)], "Three signers")
write_row(3, [make_signer(i) for i in range(5)], "Five signers")
write_row(4, make_same_pk_signers(), "Same pubkey signs two different messages")
write_row(5, [make_tweaked_signer(0, 0x21, False), make_tweaked_signer(1, 0x22, True)],
"Signers with tweaked keys")
write_row(6, make_duplicate_pm_signers(), "Exact duplicate (pk, m) entries")
def gen_sign_error_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index", "secret_key", "message", "secnonce", "aggnonce",
"pubkeys", "messages", "pubnonces",
"comment",
))
def write_row(idx, signer, pks, msgs, pubnonces, comment, aggnonce=None):
sk, _, msg, secnonce, _ = signer
if aggnonce is None:
aggnonce = serialize_pubnonce(NonceAgg(pubnonces))
try:
R1, R2 = GE.from_bytes_compressed(aggnonce[:33]), GE.from_bytes_compressed(aggnonce[33:])
Sign(secnonce, sk, msg, (R1, R2), pks, msgs, pubnonces)
except ValueError:
pass
else:
raise RuntimeError(f"Sign unexpectedly succeeded: {comment}")
writer.writerow((
idx,
sk.to_bytes().hex().upper(),
msg.hex().upper(),
serialize_secnonce(secnonce).hex().upper(),
aggnonce.hex().upper(),
format_list([serialize_pk(p) for p in pks]),
format_list(msgs),
format_list([serialize_pubnonce(p) for p in pubnonces]),
comment,
))
signers = [make_signer(i) for i in range(3)]
pks = [GE.from_bytes_xonly(s[1].to_bytes_xonly()) for s in signers]
msgs = [s[2] for s in signers]
pubnonces = [s[4] for s in signers]
dup_pubnonces = list(pubnonces)
dup_pubnonces[2] = (pubnonces[2][0], pubnonces[0][1])
write_row(0, signers[0], pks, msgs, dup_pubnonces,
"Signer's R2 appears at two indices")
missing_pubnonces = list(pubnonces)
missing_pubnonces[0] = (pubnonces[0][0], scalar_from_byte(0x63) * G)
write_row(1, signers[0], pks, msgs, missing_pubnonces,
"Signer's R2 appears at no index")
wrong_msgs = list(msgs)
wrong_msgs[0] = bytes([0xEE] * 32)
write_row(2, signers[0], pks, wrong_msgs, pubnonces,
"Message at signer's index was substituted")
wrong_pks = list(pks)
wrong_pks[0] = GE.from_bytes_xonly((scalar_from_byte(0x55) * G).to_bytes_xonly())
write_row(3, signers[0], wrong_pks, msgs, pubnonces,
"Public key at signer's index was substituted")
sk, pk, msg, secnonce, pubnonce = signers[0]
write_row(4, (sk, pk, msg, (Scalar(0), Scalar(0)), pubnonce), pks, msgs, pubnonces,
"Secnonce is all zeros")
write_row(5, (Scalar(0), pk, msg, secnonce, pubnonce), pks, msgs, pubnonces,
"Secret key is zero")
aggnonce = serialize_pubnonce(NonceAgg(pubnonces))
write_row(6, signers[0], pks, msgs, pubnonces,
"Aggregate nonce first half is all zeros", bytes(33) + aggnonce[33:])
write_row(7, signers[0], pks, msgs, pubnonces,
"Aggregate nonce second half is not on the curve", aggnonce[:33] + b"\x02" + bytes(32))
write_row(8, signers[0], pks, msgs, pubnonces,
"Aggregate nonce second half exceeds field size",
aggnonce[:33] + b"\x02" + bytes_from_int(FE.SIZE + 1))
def gen_verify_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index", "pubkeys", "messages", "sig", "expected_result", "comment",
))
idx = 0
signers = [make_signer(i) for i in range(2)]
_, _, sig = sign_session(signers)
pks = [serialize_pk(s[1]) for s in signers]
msgs = [s[2] for s in signers]
sig_bytes = serialize_sig(sig)
writer.writerow((idx, format_list(pks), format_list(msgs),
sig_bytes.hex().upper(), "TRUE",
"Two-signer valid aggregate"))
idx += 1
signers3 = [make_signer(i) for i in range(3)]
_, _, sig3 = sign_session(signers3)
pks3 = [serialize_pk(s[1]) for s in signers3]
msgs3 = [s[2] for s in signers3]
sig3_bytes = serialize_sig(sig3)
writer.writerow((idx, format_list(pks3), format_list(msgs3),
sig3_bytes.hex().upper(), "TRUE",
"Three-signer valid aggregate"))
idx += 1
wrong_msgs = [msgs[0], bytes([0xff] * 32)]
writer.writerow((idx, format_list(pks), format_list(wrong_msgs),
sig_bytes.hex().upper(), "FALSE",
"Wrong message for second signer"))
idx += 1
writer.writerow((idx, format_list([pks[1], pks[0]]), format_list(msgs),
sig_bytes.hex().upper(), "FALSE",
"Pubkey order swapped"))
idx += 1
writer.writerow((idx, format_list([pks[0]]), format_list([msgs[0]]),
sig_bytes.hex().upper(), "FALSE",
"Dropped second signer"))
idx += 1
s_int = int_from_bytes(sig_bytes[32:64])
tampered = sig_bytes[:32] + bytes_from_int(s_int ^ 1)
writer.writerow((idx, format_list(pks), format_list(msgs),
tampered.hex().upper(), "FALSE",
"Tampered s (bit flip)"))
idx += 1
s_zero = sig_bytes[:32] + bytes(32)
writer.writerow((idx, format_list(pks), format_list(msgs),
s_zero.hex().upper(), "FALSE",
"s = 0 (in range, wrong)"))
idx += 1
s_eq_n = sig_bytes[:32] + bytes_from_int(n)
writer.writerow((idx, format_list(pks), format_list(msgs),
s_eq_n.hex().upper(), "FALSE",
"s = n (out of range)"))
idx += 1
s_max = sig_bytes[:32] + bytes([0xff] * 32)
writer.writerow((idx, format_list(pks), format_list(msgs),
s_max.hex().upper(), "FALSE",
"s = 2^256 - 1"))
idx += 1
def find_off_curve_x() -> bytes:
for i in range(1, 256):
cand = bytes([i] * 32)
try:
GE.from_bytes_xonly(cand)
except Exception:
return cand
raise RuntimeError("could not find an off-curve x")
off_curve = find_off_curve_x()
bad_R_sig = off_curve + sig_bytes[32:64]
writer.writerow((idx, format_list(pks), format_list(msgs),
bad_R_sig.hex().upper(), "FALSE",
"R x-coord not on curve"))
idx += 1
writer.writerow((idx, format_list([off_curve, pks[1]]), format_list(msgs),
sig_bytes.hex().upper(), "FALSE",
"Public key not on curve"))
idx += 1
writer.writerow((idx, format_list(pks), format_list(msgs),
sig_bytes[:-1].hex().upper(), "FALSE",
"Signature too short"))
idx += 1
writer.writerow((idx, format_list(pks), format_list(msgs),
(sig_bytes + b"\x00").hex().upper(), "FALSE",
"Signature too long"))
idx += 1
writer.writerow((idx, format_list(pks), format_list([msgs[0]]),
sig_bytes.hex().upper(), "FALSE",
"Pubkey/message count mismatch"))
idx += 1
writer.writerow((idx, "", "",
sig_bytes.hex().upper(), "FALSE",
"Empty signer list"))
idx += 1
for bad_m0, comment in ((msgs[0][:31], "Message too short"), (msgs[0] + b"\x00", "Message too long")):
bad_msgs = [bad_m0, msgs[1]]
bad_sig = serialize_sig(sign_unchecked(signers, bad_msgs, scalar_from_byte(0x70)))
writer.writerow((idx, format_list(pks), format_list(bad_msgs),
bad_sig.hex().upper(), "FALSE", comment))
idx += 1
def gen_partial_sig_verify_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index", "signer_index", "psig",
"pubkeys", "messages", "pubnonces",
"expected_result", "comment",
))
signers = [make_signer(i) for i in range(2)]
aggnonce, psigs, _ = sign_session(signers)
pks_hex = format_list([serialize_pk(s[1]) for s in signers])
msgs_hex = format_list([s[2] for s in signers])
pubnonces_hex = format_list([serialize_pubnonce(s[4]) for s in signers])
def write_row(idx, signer_index, psig_bytes, expected, comment):
writer.writerow((
idx, signer_index, psig_bytes.hex().upper(),
pks_hex, msgs_hex, pubnonces_hex,
expected, comment,
))
idx = 0
write_row(idx, 0, psigs[0].to_bytes(), "TRUE", "Honest partial sig (signer 0)")
idx += 1
write_row(idx, 1, psigs[1].to_bytes(), "TRUE", "Honest partial sig (signer 1)")
idx += 1
write_row(idx, 1, psigs[0].to_bytes(), "FALSE",
"Honest psig verified at the wrong signer_index")
idx += 1
write_row(idx, 0, bytes_from_int(int(psigs[0]) ^ 1), "FALSE",
"Tampered partial sig (bit flip)")
idx += 1
write_row(idx, 0, bytes(32), "FALSE", "psig = 0 (in range, wrong)")
idx += 1
write_row(idx, 0, bytes_from_int(n), "FALSE", "psig = n (out of range)")
idx += 1
write_row(idx, 2, psigs[0].to_bytes(), "FALSE", "signer_index out of range")
idx += 1
fake_aggnonce = (aggnonce[0] + G, aggnonce[1])
psig_wrong = Sign(signers[0][3], signers[0][0], signers[0][2], fake_aggnonce,
[s[1] for s in signers], [s[2] for s in signers],
[s[4] for s in signers])
write_row(idx, 0, psig_wrong.to_bytes(), "FALSE", "psig for a different aggregate nonce")
idx += 1
R1_0, R2_0 = signers[0][4]
_, R2_1 = signers[1][4]
inf_pubnonces = format_list([serialize_pubnonce((R1_0, R2_0)),
serialize_pubnonce((-R1_0, R2_1))])
writer.writerow((idx, 0, psigs[0].to_bytes().hex().upper(),
pks_hex, msgs_hex, inf_pubnonces,
"FALSE", "Aggregate nonce R1 is infinity"))
def gen_tweak_vectors(f):
writer = csv.writer(f)
writer.writerow((
"index", "internal_secret_key", "tweak", "is_xonly", "expected_result",
"expected_secret_key", "expected_pubkey", "comment",
))
def row(idx, sk, t, is_xonly, comment):
sk_out = TweakSK(sk, t, is_xonly)
pk_out = TweakPK(sk * G, t, is_xonly)
assert sk_out * G == pk_out
writer.writerow((
idx, sk.to_bytes().hex().upper(), t.to_bytes().hex().upper(),
"TRUE" if is_xonly else "FALSE", "TRUE",
sk_out.to_bytes().hex().upper(), pk_out.to_bytes_compressed().hex().upper(),
comment,
))
def error_row(idx, sk, t, is_xonly, comment):
for tweak_fn, key in ((TweakSK, sk), (TweakPK, sk * G)):
try:
tweak_fn(key, t, is_xonly)
except ValueError:
continue
raise RuntimeError(f"{tweak_fn.__name__} unexpectedly succeeded: {comment}")
writer.writerow((
idx, sk.to_bytes().hex().upper(), t.to_bytes().hex().upper(),
"TRUE" if is_xonly else "FALSE", "FALSE", "", "",
comment,
))
sk1 = scalar_from_byte(1)
even = sk1 if has_even_y(sk1 * G) else -sk1
odd = -even
t = scalar_from_byte(0x02)
row(0, even, t, False, "Plain tweak of an even-y key")
row(1, odd, t, False, "Plain tweak of an odd-y key")
row(2, even, t, True, "X-only tweak of an even-y key")
row(3, odd, t, True, "X-only tweak of an odd-y key")
error_row(4, odd, -odd, False, "Plain tweak resulting in the point at infinity")
error_row(5, odd, odd, True, "X-only tweak resulting in the point at infinity")
writer.writerow((6, even.to_bytes().hex().upper(), bytes_from_int(n).hex().upper(),
"FALSE", "FALSE", "", "", "Tweak = n (out of range)"))
if __name__ == "__main__":
os.makedirs(VECTORS_DIR, exist_ok=True)
print(f"Generating {NONCE_GEN_VECTORS}")
with open(NONCE_GEN_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_nonce_gen_vectors(f)
print(f"Generating {NONCE_AGG_VECTORS}")
with open(NONCE_AGG_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_nonce_agg_vectors(f)
print(f"Generating {SIGN_VECTORS}")
with open(SIGN_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_sign_vectors(f)
print(f"Generating {SIGN_ERROR_VECTORS}")
with open(SIGN_ERROR_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_sign_error_vectors(f)
print(f"Generating {VERIFY_VECTORS}")
with open(VERIFY_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_verify_vectors(f)
print(f"Generating {PARTIAL_SIG_VERIFY_VECTORS}")
with open(PARTIAL_SIG_VERIFY_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_partial_sig_verify_vectors(f)
print(f"Generating {TWEAK_VECTORS}")
with open(TWEAK_VECTORS, "w", newline="", encoding="utf-8") as f:
gen_tweak_vectors(f)
print("Done.")