mirror of
https://github.com/zap-proto/py.git
synced 2026-10-30 15:04:09 +00:00
Faithful port of github.com/zap-proto/go/cap over the capabilities.zap
v1.1 schema. Signed, attenuable authority tokens that replace bearer JWTs.
- Cap zero-copy view + canonical_bytes (SPEC §3 signed scope) +
id = SHA-256(canonical_bytes || Sig) (SPEC §4). The canonical
construction reads Capability[0..164) verbatim from the wire and
recomputes the caveat section, so the signed bytes are byte-identical
to Go regardless of heap layout.
- Issuance/issue/attenuate enforce the SPEC §2.3 delegation gate at MINT
time (parent needs PERM_ATTENUATE or CapKind.DELEGATE), monotonic
permission narrowing, child.Issuer == parent.Holder, and
downward-only expiry — refuses to build a cap its own verifier rejects.
- Verifier.verify / verify_chain enforce the full §2.3 invariants:
monotonic perms, holder linkage, target invariance, root-has-zero-parent,
the delegation gate at verify time, and FAIL-CLOSED scheme dispatch
(reserved tag 0x00 and unknown tags refused before any hook is
consulted — never a silent downgrade).
- Revocation/revoke/verify_revocation, scheme-aware and fail-closed.
- Schemes: Ed25519 (mandatory bootstrap), ML-DSA-65 (FIPS 204),
secp256k1 ECDSA — all real and end-to-end verified. A missing backend
raises SchemeUnavailable (an UnhandledSchemeError), never a fabricated
signature or a True on unverified bytes.
Cross-language interop is pinned by tests/testdata/cap_go_kat.{hex,json}:
a Go-signed cap (fixed ed25519 seed) decodes in Python, recomputes the
same canonical bytes and CapID (7c94f5897ad74d44...655044ae), and
verifies True; a tampered variant verifies False; and a deterministic
Python re-issue is byte-identical to the Go wire bytes.
Wire/canonical/CapID paths are pure stdlib; signing needs [crypto].
48 cap tests mirror the Go cap_test.go + spec_test.go. Full gate green
(pytest 100, ruff, ruff format, mypy --strict). Version 1.2.0 -> 1.3.0.
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""Cross-language Known-Answer Test — a Go-signed capability verifies in Python.
|
|
|
|
The fixture ``testdata/cap_go_kat.hex`` is the wire bytes of a Capability minted
|
|
and signed by the Go runtime (``github.com/zap-proto/go/cap``) with a FIXED
|
|
Ed25519 seed (32 bytes of 0x42) and fixed inputs. ``cap_go_kat.json`` carries
|
|
the seed, the raw public key, the Go-computed canonical bytes, and the
|
|
Go-computed CapID.
|
|
|
|
This is the interop proof. The same canonical bytes, the same CapID, and a
|
|
Go-produced signature that verifies under the Python verifier together
|
|
demonstrate that the two runtimes share one wire and one signing scope. A
|
|
byte-for-byte re-issue in Python (Ed25519 is deterministic per RFC 8032) closes
|
|
the loop in the other direction.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from zap import cap
|
|
|
|
pytest.importorskip("cryptography")
|
|
|
|
_TESTDATA = Path(__file__).parent / "testdata"
|
|
|
|
|
|
def _load_kat() -> dict[str, str]:
|
|
with (_TESTDATA / "cap_go_kat.json").open() as f:
|
|
data: dict[str, str] = json.load(f)
|
|
# The .hex fixture is the authoritative wire bytes; assert the JSON agrees.
|
|
hex_wire = (_TESTDATA / "cap_go_kat.hex").read_text().strip()
|
|
assert hex_wire == data["wire_hex"], "fixture .hex and .json disagree on wire bytes"
|
|
return data
|
|
|
|
|
|
def test_go_signed_cap_canonical_bytes_match() -> None:
|
|
kat = _load_kat()
|
|
c = cap.wrap(bytes.fromhex(kat["wire_hex"]))
|
|
assert c.canonical_bytes() == bytes.fromhex(kat["canonical_hex"])
|
|
|
|
|
|
def test_go_signed_cap_id_matches() -> None:
|
|
kat = _load_kat()
|
|
c = cap.wrap(bytes.fromhex(kat["wire_hex"]))
|
|
assert c.id().hex() == kat["capid_hex"]
|
|
|
|
|
|
def test_go_signed_cap_verifies_in_python() -> None:
|
|
kat = _load_kat()
|
|
c = cap.wrap(bytes.fromhex(kat["wire_hex"]))
|
|
pub = bytes.fromhex(kat["pubkey_hex"])
|
|
issuer_hash = bytes.fromhex(kat["issuer_hash"])
|
|
assert c.issuer == issuer_hash
|
|
assert c.issuer == cap.hash32(pub)
|
|
|
|
def issuer_key(h: bytes) -> bytes:
|
|
assert h == issuer_hash
|
|
return pub
|
|
|
|
v = cap.Verifier(issuer_key=issuer_key)
|
|
v.verify(c, 1700000001) # no raise => Go-signed cap verifies in Python
|
|
|
|
|
|
def test_go_signed_cap_tampered_variant_fails() -> None:
|
|
kat = _load_kat()
|
|
pub = bytes.fromhex(kat["pubkey_hex"])
|
|
v = cap.Verifier(issuer_key=lambda _h: pub)
|
|
|
|
raw = bytearray(bytes.fromhex(kat["wire_hex"]))
|
|
root_off = int.from_bytes(raw[8:12], "little")
|
|
# Flip a permission bit inside the signed header.
|
|
raw[root_off + cap._codec.CAPABILITY_PERMISSIONS_OFF] ^= 0x01
|
|
tampered = cap.wrap(bytes(raw))
|
|
with pytest.raises(cap.SigMismatchError):
|
|
v.verify(tampered, 1700000001)
|
|
|
|
|
|
def test_python_reissue_is_byte_identical_to_go() -> None:
|
|
"""Deterministic re-issue in Python yields the exact Go wire bytes.
|
|
|
|
Ed25519 is deterministic (RFC 8032), so identical inputs + identical seed
|
|
must produce identical signatures and therefore identical wire bytes —
|
|
the strongest interop guarantee, both directions.
|
|
"""
|
|
kat = _load_kat()
|
|
signer = cap.Ed25519Signer.from_seed(bytes.fromhex(kat["seed_hex"]))
|
|
assert signer.public_key_bytes() == bytes.fromhex(kat["pubkey_hex"])
|
|
|
|
target = bytes(range(32))
|
|
holder = bytes((0x80 + i) & 0xFF for i in range(32))
|
|
inp = cap.Issuance(
|
|
kind=int(cap.CapKind.IAM_SESSION),
|
|
target=target,
|
|
holder=holder,
|
|
permissions=cap.PERM_ATTENUATE | 0x0F,
|
|
parent=b"\x00" * 32,
|
|
issued_at=1700000000,
|
|
expires_at=2000000000,
|
|
caveats=[
|
|
cap.Caveat(int(cap.CaveatKind.MAX_AMOUNT), struct.pack("<Q", 1000000)),
|
|
cap.Caveat(int(cap.CaveatKind.RATE_LIMIT), struct.pack("<II", 60, 10)),
|
|
cap.Caveat(int(cap.CaveatKind.IP_CIDR), b"10.0.0.0/8"),
|
|
],
|
|
)
|
|
c = cap.issue(inp, signer)
|
|
assert c.raw == bytes.fromhex(kat["wire_hex"])
|
|
assert c.id().hex() == kat["capid_hex"]
|