# ACT / ECT Cross-Spec Interop Test Plan **Status**: Draft (Task C4 preparation — planning only, not yet implemented) **Scope**: Python refimpls `ietf-act` (Phase 1/2, 103 tests) and `ietf-ect` (single-phase, 56 tests) **Deliverable**: `packages/interop/tests/test_interop.py` + compatibility matrix docs ## 1. Goals and Non-Goals ### Goals - Empirically document which shared claims round-trip cleanly between refimpls. - Surface real format-level incompatibilities (hash encoding, typ header, algorithm support) rather than assume the spec-level claim overlap implies wire interop. - Produce a user-facing **compatibility matrix** that implementers can rely on when building bridges between Phase 2 ACT Records and ECT payloads. - Provide executable regression tests so future changes to either refimpl cannot silently break the documented interop level without CI noticing. ### Non-Goals - Propose spec unification or new shared claim registries. - Build a lossy translator/bridge between ACT Records and ECT payloads. - Test `typ` cross-acceptance — `act+jwt` vs `exec+jwt` MUST remain distinct token types. - Forge one token type as the other. - Add new crypto backends (e.g., Ed25519 support) to ECT as part of this work. ## 2. Known Shape of the Problem Shared claims (by name): `jti`, `wid`, `iat`, `exp`, `aud`, `exec_act`, `pred`, `inp_hash`, `out_hash`. Confirmed divergences discovered while reading the code: - **Hash encoding mismatch**: ACT `b64url_sha256()` emits plain base64url (e.g. `n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg`). ECT `validate_hash_format()` requires `alg:base64url` form (e.g. `sha-256:...`) and raises on plain b64url. The briefing says this was "recently fixed to match ACT's plain base64url format" but the ECT validator still requires the prefix — plan must include a reproducer. - **Algorithm**: ACT supports `EdDSA` + `ES256`; ECT hard-codes `ES256` (see `ect/verify.py`, line 59, `"ect: expected ES256"`). - **Typ header**: ACT requires `act+jwt`; ECT requires `exec+jwt` (with legacy `wimse-exec+jwt`). Neither accepts the other — and per anti-goals, neither should. - **aud shape**: ACT stores `aud` as `str | list[str]`; ECT normalises to `list[str]` via `_audience_deserialize`. - **Claims unique to ACT**: `sub`, `iss` (required string), `task`, `cap`, `del`, `oversight`, `exec_ts`, `status`, `err`. - **Claims unique to ECT**: `ect_ext`, `inp_classification`, and policy claims inside `ect_ext` (`pol`, `pol_decision`, `compensation_required`). ## 3. Test Categories ### 3.1 Shared claim consistency (`TestSharedClaims`) - `test_jti_format_roundtrips`: UUID-v4 jti accepted by both refimpls; non-UUID jti accepted by ACT (no UUID check) but only by ECT when `validate_uuids=False` (document the asymmetry). - `test_wid_shared_semantics`: same wid value on an ACT Record and an ECT payload — both accept. - `test_iat_exp_numericdate`: identical integer NumericDate accepted by both (ACT uses strict `> 0`, ECT uses `int(claims["iat"])`). - `test_aud_string_vs_list`: string `aud` preserved by ACT, coerced to list by ECT; list form is lossless on both. - `test_exec_act_string_both_sides`: same `exec_act` value (e.g. `read.data`) serialises identically; ACT additionally validates ABNF grammar — test that ECT accepts an ACT-grammar-legal value unchanged. - `test_pred_array_shape`: `pred=[]`, `pred=[jti1]`, `pred=[jti1, jti2]` — both refimpls serialise/deserialise identically. - `test_inp_hash_format_divergence` (**expected xfail/documented**): feed ACT's plain b64url output into ECT validator — expect `ValueError("ect: inp_hash/out_hash must be algorithm:base64url...")`. This pins the incompatibility so a future fix flips the test green. - `test_inp_hash_prefixed_form`: `sha-256:` value accepted by ECT; ACT treats it as opaque string (no validation), roundtrips without error. - `test_out_hash_same_as_inp`: mirror the above for `out_hash`. ### 3.2 Algorithm compatibility (`TestAlgorithmMatrix`) - `test_es256_act_record_signature_verifies_with_ect_key_resolver`: build a Phase 2 ACTRecord, sign with ES256 P-256 key. Feed the compact JWS bytes *and an ECT-shaped resolver* through `ect.verify`. Expect `ValueError("ect: invalid typ parameter")` because typ is `act+jwt`. Document: JWS/ES256 signature layer is compatible, but typ gate prevents verifier reuse as-is. - `test_eddsa_act_record_rejected_by_ect`: Phase 2 ACTRecord signed EdDSA. ECT must reject at alg gate (`"ect: expected ES256"`). Documents the ES256-only limitation. - `test_ect_payload_signature_verifies_with_act_crypto`: sign an ECT payload (ES256), strip to raw JWS, feed signature bytes through `act.crypto.verify` with the ECT public key. Expect success — proves the ES256 primitive is wire-compatible at the raw-sig level. ### 3.3 DAG cross-reference (`TestDagInterop`) - `test_pred_array_referenceable_both_ways`: construct ACT Record with `pred=[ect_jti]` and an ECT payload with `pred=[act_jti]`. Both refimpls accept the arrays structurally (they're opaque strings). - `test_mixed_dag_is_out_of_scope`: document and assert that `ACTStore` only stores ACT records and `ECTStore` only stores ECT payloads; neither is designed to resolve a `pred` jti from the other type. A bridging verifier would have to walk both stores — out of scope for refimpls. - `test_jti_collision_across_types`: the same UUID used as `jti` in an ACT Record and an unrelated ECT payload — both refimpls accept independently; document that jti uniqueness is scoped per-token-type in the refimpls. ### 3.4 Semantic divergence (`TestClaimDivergence`) - `test_ect_ignores_act_only_claims`: ECT `Payload.from_claims` is called on a dict that includes `sub`, `task`, `cap`, `oversight`, `exec_ts`, `status`. Expect: silently ignored (no error, no retention). Document as "ECT is lenient on unknown top-level claims". - `test_act_ignores_ect_only_claims`: feed `ACTRecord.from_claims` a claim dict with `ect_ext`, `inp_classification`. Expect: silently ignored and not retained. - `test_exec_act_not_validated_against_cap_in_ect`: ACT Record with `exec_act="read.data"` and `cap=[{"action":"write.result"}]` → ACT verifier raises `ACTCapabilityError`. Same `exec_act` in an ECT payload with no `cap` → ECT accepts. Documents the cap-validation asymmetry; guards against anyone accidentally copy-pasting cap logic into ECT. - `test_act_requires_status_ect_does_not`: ACTRecord without `status` → `ACTValidationError`. ECT without `status` → accepted. ### 3.5 Anti-goals (encoded as negative tests) - `test_act_jwt_typ_rejected_by_ect`: ACT compact with `typ=act+jwt` fed to `ect.verify` → MUST raise "invalid typ parameter". - `test_exec_jwt_typ_rejected_by_act`: ECT compact with `typ=exec+jwt` fed to `act.decode_jws` → MUST raise `ACTValidationError` on typ check. - `test_no_forgery_as_other_type`: explicit comment-only placeholder asserting we do not re-encode one type as the other; kept as a doc anchor. ## 4. Expected Compatibility Matrix (user-facing) | Layer | Direction | Status | Notes | |---|---|---|---| | ES256 raw signature | ACT ↔ ECT | Compatible | Same JWS/ES256 primitive | | EdDSA signature | ACT → ECT | Incompatible | ECT is ES256-only | | `typ` header | ACT ↔ ECT | Strictly separated | By design | | `jti`, `wid`, `iat`, `exp`, `aud`, `exec_act`, `pred` | Shared | Compatible | Identical wire shapes | | `inp_hash`/`out_hash` | ACT → ECT | **Incompatible today** | ACT emits plain b64url, ECT requires `sha-256:` | | `inp_hash`/`out_hash` | ECT → ACT | Compatible | ACT treats as opaque string | | `cap` / `exec_act` coupling | ACT-only | N/A | ECT does not enforce | | DAG `pred` traversal | Separate stores | Manual bridging required | Refimpls do not cross-resolve | ## 5. Dependencies and Structure Both packages must be importable in a single venv: ``` pip install -e packages/act packages/ect packages/interop[dev] ``` Proposed layout: ``` packages/ act/ … ect/ … interop/ pyproject.toml # declares ietf-act, ietf-ect as deps tests/ __init__.py conftest.py # shared ES256 keypair + resolver fixtures test_interop.py # classes Test{SharedClaims,AlgorithmMatrix,DagInterop,ClaimDivergence,AntiGoals} README.md # published compatibility matrix ``` `conftest.py` exposes fixtures: `es256_keypair`, `act_record_builder`, `ect_payload_builder`, `dual_resolver` (one kid → same ES256 pubkey for both refimpls). ## 6. What the Compatibility Matrix Docs Should Tell Users - **Do** reuse ES256 key material across ACT and ECT deployments — the signing primitive is identical. - **Do not** feed ACT compact tokens to an ECT verifier or vice versa; `typ` gates are deliberate. - **Do** treat `jti`, `wid`, `pred`, `exec_act` as semantically aligned when building cross-type audit logs. - **Do not** rely on `inp_hash`/`out_hash` being portable today — raise a spec issue if portability matters for your deployment. - **Do not** expect ECT to enforce ACT's `cap`/`exec_act` coupling — authorization remains an ACT concern. - **Open question for spec editors**: align hash encoding (plain b64url vs prefixed), and decide whether Ed25519 should be optional-to-support for ECT.