"""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)