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:
2026-04-12 07:39:41 +02:00
parent 3a139dfc7e
commit 37859beef6
10 changed files with 1053 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
"""Algorithm compatibility matrix between ACT and ECT.
Facts pinned here:
- Both specs share the JWS/ES256 signing primitive.
- ECT is ES256-only; ACT also supports EdDSA.
- ECT's typ gate rejects cross-type compact tokens even when the
signature algorithm matches — that separation is deliberate.
"""
from __future__ import annotations
import pytest
from act.errors import ACTValidationError
from ect.verify import verify as ect_verify, VerifyOptions
class TestAlgorithmMatrix:
def test_es256_act_record_signature_primitive_verifies(
self, act_record_builder, dual_resolver
):
"""ACT Record signed ES256 feeds through ECT verify up to the typ gate.
Signature primitive is wire-compatible; typ=act+jwt causes
ECT to refuse. Documents "sig-compatible, typ-rejected".
"""
_, ect_resolver = dual_resolver
compact, _ = act_record_builder(alg="ES256")
opts = VerifyOptions(resolve_key=ect_resolver)
with pytest.raises(ValueError, match="invalid typ parameter"):
ect_verify(compact, opts)
def test_eddsa_act_record_rejected_by_ect(
self, act_record_builder, ed25519_keypair
):
"""ACT Record signed EdDSA — ECT parser rejects at alg gate."""
priv, _ = ed25519_keypair
compact, _ = act_record_builder(alg="EdDSA", priv=priv)
# ect.parse (used indirectly via verify) rejects non-ES256.
# verify() checks typ first, but parse() would reject alg first;
# either way the token is rejected — record which gate fires.
opts = VerifyOptions(resolve_key=lambda kid: None)
with pytest.raises(ValueError) as exc:
ect_verify(compact, opts)
# The verify() path checks typ before alg. We accept either
# error message as valid "rejection by ECT" evidence.
assert (
"invalid typ parameter" in str(exc.value)
or "expected ES256" in str(exc.value)
)
def test_ect_compact_rejected_by_act_decoder(self, ect_payload_builder):
"""Mirror direction: ECT compact → ACT decoder rejects on typ."""
from act.token import decode_jws
compact, _ = ect_payload_builder()
with pytest.raises(ACTValidationError, match="typ"):
decode_jws(compact)
def test_es256_primitive_is_wire_compatible_at_raw_sig_level(
self, ect_payload_builder, es256_keypair
):
"""The raw ES256 signature over an ECT compact verifies with
act.crypto.verify using the ECT public key.
This is the layer that is actually portable: both refimpls
sign/verify over the same RFC 7518 §3.4 raw r||s format.
"""
from act.crypto import verify as act_verify
from act.token import _b64url_decode
priv, pub = es256_keypair
compact, _ = ect_payload_builder(priv=priv)
parts = compact.split(".")
signing_input = f"{parts[0]}.{parts[1]}".encode("ascii")
signature = _b64url_decode(parts[2])
# Should not raise.
act_verify(pub, signature, signing_input)
def test_compatibility_matrix_assertion(
act_record_builder, ed25519_keypair, dual_resolver
):
"""Pin the whole matrix in one assertion block.
Matrix entries:
(ACT/ES256) × ECT-verify -> sig-compatible-but-typ-rejected
(ACT/EdDSA) × ECT-verify -> rejected (alg or typ)
"""
_, ect_resolver = dual_resolver
opts = VerifyOptions(resolve_key=ect_resolver)
# ACT/ES256 × ECT-verify
act_es256, _ = act_record_builder(alg="ES256")
with pytest.raises(ValueError, match="invalid typ parameter"):
ect_verify(act_es256, opts)
# ACT/EdDSA × ECT-verify
priv, _ = ed25519_keypair
act_eddsa, _ = act_record_builder(alg="EdDSA", priv=priv)
with pytest.raises(ValueError):
ect_verify(act_eddsa, opts)