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.
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""Bech32 encoding (BIP-173 variant) for nostr nsec/npub strings.
|
|
|
|
Reference implementation adapted from BIP-173 (Pieter Wuille, MIT licensed).
|
|
KEZ uses the original Bech32 checksum constant (not Bech32m), matching the
|
|
nostr NIP-19 convention and the Rust/Node implementations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
|
|
|
|
def _polymod(values: list[int]) -> int:
|
|
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
|
|
chk = 1
|
|
for v in values:
|
|
b = chk >> 25
|
|
chk = ((chk & 0x1FFFFFF) << 5) ^ v
|
|
for i in range(5):
|
|
chk ^= generator[i] if ((b >> i) & 1) else 0
|
|
return chk
|
|
|
|
|
|
def _hrp_expand(hrp: str) -> list[int]:
|
|
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
|
|
|
|
|
def _verify_checksum(hrp: str, data: list[int]) -> bool:
|
|
return _polymod(_hrp_expand(hrp) + data) == 1
|
|
|
|
|
|
def _create_checksum(hrp: str, data: list[int]) -> list[int]:
|
|
values = _hrp_expand(hrp) + data
|
|
polymod = _polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
|
|
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
|
|
|
|
|
def _bech32_encode(hrp: str, data: list[int]) -> str:
|
|
combined = data + _create_checksum(hrp, data)
|
|
return hrp + "1" + "".join(CHARSET[d] for d in combined)
|
|
|
|
|
|
def _bech32_decode(bech: str) -> tuple[str, list[int]]:
|
|
if any(ord(x) < 33 or ord(x) > 126 for x in bech):
|
|
raise ValueError("bech32: invalid character")
|
|
if bech.lower() != bech and bech.upper() != bech:
|
|
raise ValueError("bech32: mixed case")
|
|
bech = bech.lower()
|
|
pos = bech.rfind("1")
|
|
if pos < 1 or pos + 7 > len(bech):
|
|
raise ValueError("bech32: invalid separator position")
|
|
hrp = bech[:pos]
|
|
if any(c not in CHARSET for c in bech[pos + 1 :]):
|
|
raise ValueError("bech32: invalid data character")
|
|
data = [CHARSET.find(c) for c in bech[pos + 1 :]]
|
|
if not _verify_checksum(hrp, data):
|
|
raise ValueError("bech32: bad checksum")
|
|
return hrp, data[:-6]
|
|
|
|
|
|
def _convertbits(data: bytes | list[int], frombits: int, tobits: int, pad: bool) -> list[int]:
|
|
acc = 0
|
|
bits = 0
|
|
ret: list[int] = []
|
|
maxv = (1 << tobits) - 1
|
|
max_acc = (1 << (frombits + tobits - 1)) - 1
|
|
for value in data:
|
|
if value < 0 or (value >> frombits):
|
|
raise ValueError("bech32: invalid value in convertbits")
|
|
acc = ((acc << frombits) | value) & max_acc
|
|
bits += frombits
|
|
while bits >= tobits:
|
|
bits -= tobits
|
|
ret.append((acc >> bits) & maxv)
|
|
if pad:
|
|
if bits:
|
|
ret.append((acc << (tobits - bits)) & maxv)
|
|
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
|
raise ValueError("bech32: invalid padding in convertbits")
|
|
return ret
|
|
|
|
|
|
def encode(hrp: str, payload: bytes) -> str:
|
|
"""Encode raw ``payload`` bytes as a bech32 string under ``hrp``."""
|
|
data = _convertbits(payload, 8, 5, True)
|
|
return _bech32_encode(hrp, data)
|
|
|
|
|
|
def decode(expected_hrp: str, bech: str) -> bytes:
|
|
"""Decode a bech32 string, asserting its HRP and returning the raw bytes."""
|
|
hrp, data = _bech32_decode(bech)
|
|
if hrp != expected_hrp:
|
|
raise ValueError(f"bech32: expected hrp {expected_hrp!r}, got {hrp!r}")
|
|
return bytes(_convertbits(data, 5, 8, False))
|