Add a Python port of the KEZ CLI under python/, mirroring the Rust and Node implementations command-for-command and byte-for-byte: - Pure-Python JCS (RFC 8785), BIP-340 Schnorr, and Bech32; cryptography for Ed25519 and zstandard for the compact zstd framing. - Full CLI: identity new, claim create/dns, verify file, and sigchain add/revoke/show/export. Wire Python into crosstest.sh with 35 new scenarios covering Python against both Rust and Node, in every direction, across all wire formats, both key types, DNS proofs, and sigchains (incl. JSONL byte parity). All 55 scenarios pass. Update root README and .gitignore for the new implementation.
152 lines
4.5 KiB
Python
152 lines
4.5 KiB
Python
"""BIP-340 Schnorr signatures over secp256k1 (pure Python).
|
|
|
|
This is the reference implementation from BIP-340 (public domain), used for the
|
|
``nostr-secp256k1-schnorr-sha256-jcs`` suite. We sign with an all-zero auxiliary
|
|
randomness value, matching the Rust ``sign_schnorr_no_aux_rand`` and the Node
|
|
``schnorr.sign(digest, sk, ZERO_AUX)`` calls — so every implementation produces
|
|
byte-identical, deterministic signatures.
|
|
|
|
We use a pure-Python implementation because the native ``coincurve``/``secp256k1``
|
|
bindings fail to build on bleeding-edge CPython. Signing/verifying short
|
|
fixed-size digests is well within pure-Python performance for a CLI tool.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
|
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
G = (
|
|
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
|
|
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
|
|
)
|
|
|
|
Point = tuple[int, int] | None
|
|
|
|
|
|
def tagged_hash(tag: str, msg: bytes) -> bytes:
|
|
tag_hash = hashlib.sha256(tag.encode()).digest()
|
|
return hashlib.sha256(tag_hash + tag_hash + msg).digest()
|
|
|
|
|
|
def is_infinite(P: Point) -> bool:
|
|
return P is None
|
|
|
|
|
|
def x(P: Point) -> int:
|
|
assert P is not None
|
|
return P[0]
|
|
|
|
|
|
def y(P: Point) -> int:
|
|
assert P is not None
|
|
return P[1]
|
|
|
|
|
|
def point_add(P1: Point, P2: Point) -> 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: Point, scalar: int) -> Point:
|
|
R: Point = None
|
|
for i in range(256):
|
|
if (scalar >> 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_from_point(P: Point) -> bytes:
|
|
return bytes_from_int(x(P))
|
|
|
|
|
|
def lift_x(b: bytes) -> Point:
|
|
x_ = int.from_bytes(b, byteorder="big")
|
|
if x_ >= p:
|
|
return None
|
|
y_sq = (pow(x_, 3, p) + 7) % p
|
|
y_ = pow(y_sq, (p + 1) // 4, p)
|
|
if pow(y_, 2, p) != y_sq:
|
|
return None
|
|
return (x_, y_ if y_ & 1 == 0 else p - y_)
|
|
|
|
|
|
def int_from_bytes(b: bytes) -> int:
|
|
return int.from_bytes(b, byteorder="big")
|
|
|
|
|
|
def has_even_y(P: Point) -> bool:
|
|
assert P is not None
|
|
return y(P) % 2 == 0
|
|
|
|
|
|
def pubkey_gen(seckey: bytes) -> bytes:
|
|
"""Return the 32-byte x-only public key for a 32-byte secret key."""
|
|
d0 = int_from_bytes(seckey)
|
|
if not (1 <= d0 <= n - 1):
|
|
raise ValueError("schnorr: secret key out of range")
|
|
P = point_mul(G, d0)
|
|
assert P is not None
|
|
return bytes_from_point(P)
|
|
|
|
|
|
def sign(msg: bytes, seckey: bytes, aux_rand: bytes = b"\x00" * 32) -> bytes:
|
|
"""Produce a 64-byte BIP-340 Schnorr signature over ``msg``."""
|
|
d0 = int_from_bytes(seckey)
|
|
if not (1 <= d0 <= n - 1):
|
|
raise ValueError("schnorr: secret key out of range")
|
|
P = point_mul(G, d0)
|
|
assert P is not None
|
|
d = d0 if has_even_y(P) else n - d0
|
|
t = (d ^ int_from_bytes(tagged_hash("BIP0340/aux", aux_rand))).to_bytes(32, "big")
|
|
k0 = int_from_bytes(tagged_hash("BIP0340/nonce", t + bytes_from_point(P) + msg)) % n
|
|
if k0 == 0:
|
|
raise ValueError("schnorr: nonce generation failed")
|
|
R = point_mul(G, k0)
|
|
assert R is not None
|
|
k = k0 if has_even_y(R) else n - k0
|
|
e = (
|
|
int_from_bytes(
|
|
tagged_hash("BIP0340/challenge", bytes_from_point(R) + bytes_from_point(P) + msg)
|
|
)
|
|
% n
|
|
)
|
|
sig = bytes_from_point(R) + ((k + e * d) % n).to_bytes(32, "big")
|
|
if not verify(msg, bytes_from_point(P), sig):
|
|
raise ValueError("schnorr: produced an invalid signature")
|
|
return sig
|
|
|
|
|
|
def verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool:
|
|
"""Verify a 64-byte BIP-340 Schnorr signature ``sig`` over ``msg``."""
|
|
if len(pubkey) != 32 or len(sig) != 64:
|
|
return False
|
|
P = lift_x(pubkey)
|
|
r = int_from_bytes(sig[0:32])
|
|
s = int_from_bytes(sig[32:64])
|
|
if P is None or r >= p or s >= n:
|
|
return False
|
|
e = (
|
|
int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n
|
|
)
|
|
R = point_add(point_mul(G, s), point_mul(P, n - e))
|
|
if R is None or not has_even_y(R) or x(R) != r:
|
|
return False
|
|
return True
|