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.
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""RFC 8785 JSON Canonicalization Scheme (JCS).
|
|
|
|
This is the heart of cross-implementation interop: signatures are computed over
|
|
the JCS-canonicalized bytes of a payload, so two implementations that agree on
|
|
these bytes produce universally-verifiable signatures.
|
|
|
|
Payloads in KEZ only ever contain strings, integers, booleans, nulls, arrays
|
|
and objects — never floating-point numbers — so we implement the integer-only
|
|
subset of the number rules. A float would be a bug, so we reject it loudly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def _canon_string(s: str) -> str:
|
|
out = ['"']
|
|
for ch in s:
|
|
c = ord(ch)
|
|
if ch == '"':
|
|
out.append('\\"')
|
|
elif ch == "\\":
|
|
out.append("\\\\")
|
|
elif c == 0x08:
|
|
out.append("\\b")
|
|
elif c == 0x09:
|
|
out.append("\\t")
|
|
elif c == 0x0A:
|
|
out.append("\\n")
|
|
elif c == 0x0C:
|
|
out.append("\\f")
|
|
elif c == 0x0D:
|
|
out.append("\\r")
|
|
elif c < 0x20:
|
|
out.append("\\u%04x" % c)
|
|
else:
|
|
out.append(ch)
|
|
out.append('"')
|
|
return "".join(out)
|
|
|
|
|
|
def _canon(value: Any) -> str:
|
|
if value is True:
|
|
return "true"
|
|
if value is False:
|
|
return "false"
|
|
if value is None:
|
|
return "null"
|
|
if isinstance(value, str):
|
|
return _canon_string(value)
|
|
if isinstance(value, bool): # unreachable (handled above) but explicit
|
|
return "true" if value else "false"
|
|
if isinstance(value, int):
|
|
return str(value)
|
|
if isinstance(value, float):
|
|
# KEZ payloads never carry floats; refuse rather than risk a
|
|
# non-canonical number serialization.
|
|
if value.is_integer():
|
|
return str(int(value))
|
|
raise ValueError("JCS: floating-point numbers are not supported in KEZ payloads")
|
|
if isinstance(value, (list, tuple)):
|
|
return "[" + ",".join(_canon(v) for v in value) + "]"
|
|
if isinstance(value, dict):
|
|
# RFC 8785: sort object members by their UTF-16 code-unit sequence.
|
|
items = sorted(value.items(), key=lambda kv: kv[0].encode("utf-16-be"))
|
|
return "{" + ",".join(_canon_string(k) + ":" + _canon(v) for k, v in items) + "}"
|
|
raise TypeError(f"JCS: unsupported type {type(value)!r}")
|
|
|
|
|
|
def canonicalize(value: Any) -> str:
|
|
"""Return the RFC 8785 canonical JSON string for ``value``."""
|
|
return _canon(value)
|
|
|
|
|
|
def canonical_bytes(value: Any) -> bytes:
|
|
"""Return the RFC 8785 canonical JSON bytes (UTF-8) for ``value``."""
|
|
return _canon(value).encode("utf-8")
|