Files
Christian Nennemann 37859beef6 feat: interop test package + session handoff doc
Cross-spec interop validation between ietf-act and ietf-ect:
- new packages/interop/ sibling package (ietf-act-ect-interop)
- 32 tests pass: shared claims, algorithm matrix, DAG structure,
  divergence handling, anti-goals
- documents ES256 raw signature wire-compatibility
- documents airtight typ separation (act+jwt vs exec+jwt)

Hazards surfaced:
- ACTLedger.append() silently accepts ECT Payload via duck-typing
  (both have .jti) — documented in interop README as a production
  hazard requiring external isinstance checks

Session handoff:
- SESSION-2026-04-12.md — snapshot of decisions, artifacts, open
  actions, and next-session starting points

Also: session-end commit of hash-format fix propagation to
packages/ect/ (the fix was applied to the old refimpl location
but did not propagate through the parallel package-move agent).
2026-04-12 07:39:41 +02:00

124 lines
4.3 KiB
Python

"""Shared-claim consistency between ACT and ECT refimpls.
Covers the claims both specs declare (jti, wid, iat, exp, aud,
exec_act, pred, inp_hash, out_hash): verifies that identical wire
values are accepted and round-trip cleanly.
"""
from __future__ import annotations
import base64
import hashlib
import uuid
import pytest
from act.crypto import b64url_sha256
from act.token import ACTRecord
from ect.types import Payload
from ect.validate import validate_hash_format
# --- pred array semantics ----------------------------------------------------
class TestPredArray:
@pytest.mark.parametrize(
"pred",
[
[],
[str(uuid.uuid4())],
[str(uuid.uuid4()), str(uuid.uuid4())],
],
)
def test_pred_roundtrips_on_both(self, act_record_builder, ect_payload_builder, pred):
"""Same pred array shape is serialised identically by both refimpls."""
act_compact, act_rec = act_record_builder(pred=list(pred))
ect_compact, ect_pl = ect_payload_builder(pred=list(pred))
assert act_rec.pred == pred
assert ect_pl.pred == pred
# Decode and compare the on-wire pred arrays.
from act.token import decode_jws
_, act_claims, _, _ = decode_jws(act_compact)
assert act_claims["pred"] == pred
import jwt
ect_claims = jwt.decode(
ect_compact, options={"verify_signature": False, "verify_exp": False}
)
assert ect_claims["pred"] == pred
# --- jti / wid scoping -------------------------------------------------------
class TestJtiWidScoping:
def test_same_uuid_accepted_independently(self, act_record_builder, ect_payload_builder):
"""The same jti UUID is accepted by both refimpls independently.
jti uniqueness is scoped per-store in each refimpl; there is no
shared namespace. This test documents that reusing a jti across
token types is not inherently rejected.
"""
shared_jti = str(uuid.uuid4())
act_compact, _ = act_record_builder(jti=shared_jti)
ect_compact, _ = ect_payload_builder(jti=shared_jti)
assert act_compact and ect_compact
def test_same_wid_accepted_by_both(self, act_record_builder, ect_payload_builder):
"""Identical wid value is preserved on both sides."""
shared_wid = str(uuid.uuid4())
_, act_rec = act_record_builder(wid=shared_wid)
_, ect_pl = ect_payload_builder(wid=shared_wid)
assert act_rec.wid == shared_wid == ect_pl.wid
# --- inp_hash / out_hash format (both now plain base64url) -------------------
class TestHashFormat:
def test_act_b64url_sha256_is_valid_for_ect(self):
"""ACT's b64url_sha256() output MUST pass ECT's validate_hash_format.
The ECT validator was aligned to plain base64url to match ACT —
this test would break if either side ever drifts.
"""
h = b64url_sha256(b"interop-payload")
# Should not raise.
validate_hash_format(h)
def test_ect_rejects_prefixed_hash(self):
"""Prefixed form (sha-256:...) is explicitly rejected by ECT now."""
digest = b64url_sha256(b"interop-payload")
with pytest.raises(ValueError, match="plain base64url"):
validate_hash_format(f"sha-256:{digest}")
def test_act_and_ect_agree_on_hash_shape(self):
"""Identical raw bytes produce the same base64url hash on both sides."""
raw = b"same bytes hashed on both sides"
act_hash = b64url_sha256(raw)
# Recompute independently.
expected = (
base64.urlsafe_b64encode(hashlib.sha256(raw).digest())
.rstrip(b"=")
.decode("ascii")
)
assert act_hash == expected
validate_hash_format(act_hash) # ECT accepts.
# --- exec_act ----------------------------------------------------------------
class TestExecAct:
@pytest.mark.parametrize("value", ["read", "read.data", "write.result"])
def test_exec_act_string_shared(self, act_record_builder, ect_payload_builder, value):
"""ACT-grammar-legal exec_act values are accepted unchanged by ECT."""
_, act_rec = act_record_builder(exec_act=value, cap_actions=[value])
_, ect_pl = ect_payload_builder(exec_act=value)
assert act_rec.exec_act == value == ect_pl.exec_act