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.
78 lines
2.0 KiB
Python
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")
|