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).
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
"""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)
|