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:
105
workspace/packages/interop/tests/test_algorithm_matrix.py
Normal file
105
workspace/packages/interop/tests/test_algorithm_matrix.py
Normal 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)
|
||||
Reference in New Issue
Block a user