Kez/python/kez/schnorr.py
Jason Tudisco b1240c13e5 Add Python implementation and cross-test interop
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.
2026-06-01 13:29:45 -06:00

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