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).
This commit is contained in:
123
workspace/packages/interop/tests/test_shared_claims.py
Normal file
123
workspace/packages/interop/tests/test_shared_claims.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user