Kez/python/kez/identity.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

78 lines
2.0 KiB
Python

"""KEZ identifiers: always ``system:identifier`` (Spec §3)."""
from __future__ import annotations
import re
from . import bech32
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
class IdentityError(ValueError):
pass
class Identity:
"""A canonical ``system:identifier`` string."""
__slots__ = ("_raw",)
def __init__(self, raw: str) -> None:
self._raw = raw
@classmethod
def parse(cls, raw: str) -> "Identity":
trimmed = raw.strip()
if not trimmed:
raise IdentityError("empty identity")
# CLI ergonomics: a bare npub normalizes to nostr:npub...
if trimmed.startswith("npub1"):
_validate_npub(trimmed)
return cls(f"nostr:{trimmed}")
colon = trimmed.find(":")
if colon <= 0 or colon == len(trimmed) - 1:
raise IdentityError(f"invalid identity (need scheme:value): {raw!r}")
scheme = trimmed[:colon]
rest = trimmed[colon + 1 :]
if scheme == "nostr":
_validate_npub(rest)
elif scheme == "ed25519":
_validate_ed25519_hex(rest)
return cls(f"{scheme}:{rest}")
@property
def scheme(self) -> str:
return self._raw.split(":", 1)[0]
@property
def value(self) -> str:
return self._raw.split(":", 1)[1]
def __str__(self) -> str:
return self._raw
def __repr__(self) -> str:
return f"Identity({self._raw!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Identity) and other._raw == self._raw
def __hash__(self) -> int:
return hash(self._raw)
def _validate_npub(value: str) -> None:
if not value.startswith("npub1"):
raise IdentityError(f"invalid nostr identifier (expected npub1...): {value!r}")
raw = bech32.decode("npub", value)
if len(raw) != 32:
raise IdentityError("invalid npub: expected 32-byte key")
def _validate_ed25519_hex(value: str) -> None:
if not _HEX64.match(value):
raise IdentityError("invalid ed25519 identifier: expected 64 lowercase hex chars")