From 001cd3076b52ceddcf4aa02bc774f8687b19ad2c Mon Sep 17 00:00:00 2001 From: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:35:28 -0500 Subject: [PATCH 1/2] fix(conformance): catch RecursionError/TypeError and walk tuples in canonical_claim_bytes - Catch RecursionError and TypeError from json.dumps in trace_tests.inclusion.canonical_claim_bytes and raise InclusionError so tr_anc.check reports a TR-ANC-002 FAIL finding instead of escaping runner.run when a receipt is supplied (#517). - Walk tuple alongside list in trace_tests.inclusion._outside_profile so tuples carrying floats or out-of-range integers cannot bypass the anchor-leaf profile check. - Include tr_apr and appraisal.status in test_modules_never_raise and exercise tr_anc.check with a well-formed receipt across field mutations and deep/non-JSON/tuple claims. Closes #517 Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- conformance/src/trace_tests/inclusion.py | 17 +++- conformance/tests/test_modules_never_raise.py | 74 +++++++++++++++- conformance/tests/unit/test_tr_anc.py | 87 ++++++++++++++++++- 3 files changed, 169 insertions(+), 9 deletions(-) diff --git a/conformance/src/trace_tests/inclusion.py b/conformance/src/trace_tests/inclusion.py index d421119a..ff6303e2 100644 --- a/conformance/src/trace_tests/inclusion.py +++ b/conformance/src/trace_tests/inclusion.py @@ -55,7 +55,7 @@ def _outside_profile(value: Any) -> str | None: return f"non-integer number {v!r}" elif isinstance(v, dict): stack.extend(v.values()) - elif isinstance(v, list): + elif isinstance(v, (list, tuple)): stack.extend(v) return None @@ -69,7 +69,16 @@ def canonical_claim_bytes(claim: dict[str, Any]) -> bytes: raise InclusionError( f"claim is outside the anchor-leaf profile (registry-anchor-v1 section 1): {excluded}" ) - return json.dumps(claim, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("ascii") + try: + return json.dumps( + claim, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("ascii") + except RecursionError: + raise InclusionError("claim nests too deeply to serialize") from None + except TypeError as exc: + raise InclusionError( + f"claim contains a value that is not JSON serializable: {exc}" + ) from exc def decode_hash(value: object) -> bytes: @@ -128,7 +137,9 @@ def parse_receipt(receipt: object) -> tuple[int, list[bytes], int, bytes]: if not isinstance(receipt, dict): raise InclusionError(f"receipt must be a JSON object, got {type(receipt).__name__}") - missing = [k for k in ("leaf_index", "audit_path", "leaf_count", "merkle_root") if k not in receipt] + missing = [ + k for k in ("leaf_index", "audit_path", "leaf_count", "merkle_root") if k not in receipt + ] if missing: raise InclusionError(f"receipt is missing required field(s): {', '.join(missing)}") diff --git a/conformance/tests/test_modules_never_raise.py b/conformance/tests/test_modules_never_raise.py index 84dc1c69..041bf8e6 100644 --- a/conformance/tests/test_modules_never_raise.py +++ b/conformance/tests/test_modules_never_raise.py @@ -29,15 +29,15 @@ import pytest -from trace_tests.modules import tr_anc, tr_env, tr_pol, tr_rte, tr_sca, tr_sig, tr_txn +from trace_tests.modules import tr_anc, tr_apr, tr_env, tr_pol, tr_rte, tr_sca, tr_sig, tr_txn from trace_tests.result import Finding, Status from trace_tests.runner import run VECTORS = pathlib.Path(__file__).resolve().parent / "vectors" MODULES = { - "tr_env": tr_env, "tr_sig": tr_sig, "tr_pol": tr_pol, "tr_rte": tr_rte, - "tr_txn": tr_txn, "tr_anc": tr_anc, "tr_sca": tr_sca, + "tr_env": tr_env, "tr_sig": tr_sig, "tr_pol": tr_pol, "tr_apr": tr_apr, + "tr_rte": tr_rte, "tr_txn": tr_txn, "tr_anc": tr_anc, "tr_sca": tr_sca, } #: Values a record can carry where an object or a string is expected. `True` is here @@ -65,6 +65,7 @@ (("runtime", "platform"), "TR-RTE-001"), (("build_provenance", "slsa_level"), "TR-SCA-001"), (("cnf", "jwk", "kty"), "TR-SIG-004"), + (("appraisal", "status"), "TR-APR-001"), ) @@ -151,7 +152,7 @@ def test_no_module_raises_on_a_record_whose_fields_are_malformed(name: str) -> N @pytest.mark.parametrize( ("path", "expected_code"), NESTED_DISCRIMINANTS, - ids=["policy", "runtime", "provenance", "signing-key"], + ids=["policy", "runtime", "provenance", "signing-key", "appraisal"], ) @pytest.mark.parametrize("junk", (["unexpected"], {"unexpected": True}), ids=("array", "object")) def test_runner_fails_nested_discriminators_instead_of_raising( @@ -306,3 +307,68 @@ def test_the_cmcp_path_does_not_raise_on_a_malformed_envelope() -> None: assert not raised, ( "tr_sig.check on a cmcp envelope raised or returned nothing:\n " + "\n ".join(raised) ) + + +_RECEIPT: dict[str, Any] = { + "leaf_index": 0, + "audit_path": [], + "leaf_count": 1, + "merkle_root": "sha256:" + "0" * 64, +} + + +def _nested(depth: int = 2000) -> list[Any]: + value: list[Any] = [] + for _ in range(depth): + value = [value] + return value + + +def test_tr_anc_with_receipt_never_raises_on_malformed_or_deep_records() -> None: + """``_call`` above invokes ``tr_anc.check`` with ``receipt=None``, which returns + before ``verify_inclusion`` / ``canonical_claim_bytes`` runs. Exercise the receipt + path across every field mutation and on deep, non-JSON, and tuple-wrapped claims. + """ + raised: list[str] = [] + for label, record in _mutations(): + candidate = copy.deepcopy(record) + if not label.startswith("transparency"): + candidate["transparency"] = "https://registry.example.org/claim/1" + try: + findings = tr_anc.check(candidate, receipt=dict(_RECEIPT)) + except Exception as exc: # noqa: BLE001 - the point is that nothing escapes + raised.append(f"{label} -> {type(exc).__name__}: {exc}") + continue + if not findings: + raised.append(f"{label} -> returned no findings at all") + + for label, extra in ( + ("deeply-nested", _nested(2000)), + ("non-json-object", object()), + ("tuple-float", (1.5,)), + ("tuple-unsafe-int", (2**53,)), + ): + trace: dict[str, Any] = { + "transparency": "https://registry.example.org/claim/1", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "ext": extra, + } + }, + } + try: + findings = tr_anc.check(trace, receipt=dict(_RECEIPT)) + except Exception as exc: # noqa: BLE001 - the point is that nothing escapes + raised.append(f"{label} -> {type(exc).__name__}: {exc}") + continue + if not any(f.code == "TR-ANC-002" and f.status is Status.FAIL for f in findings): + raised.append(f"{label} -> expected TR-ANC-002 FAIL, got {findings!r}") + + assert not raised, ( + "tr_anc.check with a receipt raised or missed TR-ANC-002 FAIL:\n " + + "\n ".join(raised) + ) + diff --git a/conformance/tests/unit/test_tr_anc.py b/conformance/tests/unit/test_tr_anc.py index d6dde3bb..928a3fef 100644 --- a/conformance/tests/unit/test_tr_anc.py +++ b/conformance/tests/unit/test_tr_anc.py @@ -1,6 +1,26 @@ """Unit tests for TR-ANC module.""" +from typing import Any + +import pytest + +from trace_tests.inclusion import InclusionError, canonical_claim_bytes from trace_tests.modules.tr_anc import check +from trace_tests.result import Status + +_RECEIPT: dict[str, Any] = { + "leaf_index": 0, + "audit_path": [], + "leaf_count": 1, + "merkle_root": "sha256:" + "0" * 64, +} + + +def _nested(depth: int = 2000) -> list[Any]: + value: list[Any] = [] + for _ in range(depth): + value = [value] + return value def test_https_transparency_passes_the_shape_check_only(): @@ -17,8 +37,14 @@ def test_https_transparency_passes_the_shape_check_only(): def test_http_transparency_fails(): - failed = [f for f in check({"transparency": "http://scitt.example.org/receipts/abc123"}) if f.failed()] - assert any(f.code == "TR-ANC-001" for f in failed), "plain http transparency URI must be rejected; https only" + failed = [ + f + for f in check({"transparency": "http://scitt.example.org/receipts/abc123"}) + if f.failed() + ] + assert any(f.code == "TR-ANC-001" for f in failed), ( + "plain http transparency URI must be rejected; https only" + ) def test_missing_transparency_fails(): @@ -47,3 +73,60 @@ def test_unparseable_transparency_uri_fails(): failed = [f for f in check({"transparency": "https://["}) if f.failed()] assert any(f.code == "TR-ANC-001" for f in failed) assert any("could not parse" in f.message for f in failed) + + +def test_deeply_nested_claim_raises_inclusion_error_and_fails_tr_anc_002(): + claim = { + "transparency": "https://registry.example.org/claim/1", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "ext": _nested(2000), + } + }, + } + with pytest.raises(InclusionError, match="nests too deeply"): + canonical_claim_bytes(claim) + + findings = check(claim, receipt=dict(_RECEIPT)) + by_code = {f.code: f for f in findings} + assert by_code["TR-ANC-002"].status is Status.FAIL + assert "nests too deeply" in by_code["TR-ANC-002"].message + + +def test_non_json_serializable_claim_raises_inclusion_error_and_fails_tr_anc_002(): + claim = { + "transparency": "https://registry.example.org/claim/1", + "extra": object(), + } + with pytest.raises(InclusionError, match="not JSON serializable"): + canonical_claim_bytes(claim) + + findings = check(claim, receipt=dict(_RECEIPT)) + by_code = {f.code: f for f in findings} + assert by_code["TR-ANC-002"].status is Status.FAIL + assert "not JSON serializable" in by_code["TR-ANC-002"].message + + +@pytest.mark.parametrize( + ("claim", "expected"), + [ + ({"a": (1.5,)}, "non-integer number"), + ({"a": (2**53,)}, "outside the safe-integer range"), + ], + ids=["tuple-float", "tuple-unsafe-int"], +) +def test_tuple_outside_anchor_profile_raises_inclusion_error_and_fails_tr_anc_002( + claim: dict[str, Any], expected: str +): + with pytest.raises(InclusionError, match=expected): + canonical_claim_bytes(claim) + + trace = {"transparency": "https://registry.example.org/claim/1", **claim} + findings = check(trace, receipt=dict(_RECEIPT)) + by_code = {f.code: f for f in findings} + assert by_code["TR-ANC-002"].status is Status.FAIL + assert expected in by_code["TR-ANC-002"].message + From fda8c205a0b42efc8ee75d0ef22c08afa0b35866 Mon Sep 17 00:00:00 2001 From: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:25:52 -0500 Subject: [PATCH 2/2] test(conformance): use 100_000 nesting depth for RecursionError on Python 3.12+ In Python 3.12+ on Linux, C-extension json.dumps checks the C stack via _PyOS_CheckStack rather than sys.getrecursionlimit(), so depth 2000 does not trigger RecursionError. Use depth 100_000 (matching test_untrusted_input_hardening.py). Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- conformance/tests/test_modules_never_raise.py | 4 ++-- conformance/tests/unit/test_tr_anc.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conformance/tests/test_modules_never_raise.py b/conformance/tests/test_modules_never_raise.py index 041bf8e6..f7e506de 100644 --- a/conformance/tests/test_modules_never_raise.py +++ b/conformance/tests/test_modules_never_raise.py @@ -317,7 +317,7 @@ def test_the_cmcp_path_does_not_raise_on_a_malformed_envelope() -> None: } -def _nested(depth: int = 2000) -> list[Any]: +def _nested(depth: int = 100_000) -> list[Any]: value: list[Any] = [] for _ in range(depth): value = [value] @@ -343,7 +343,7 @@ def test_tr_anc_with_receipt_never_raises_on_malformed_or_deep_records() -> None raised.append(f"{label} -> returned no findings at all") for label, extra in ( - ("deeply-nested", _nested(2000)), + ("deeply-nested", _nested(100_000)), ("non-json-object", object()), ("tuple-float", (1.5,)), ("tuple-unsafe-int", (2**53,)), diff --git a/conformance/tests/unit/test_tr_anc.py b/conformance/tests/unit/test_tr_anc.py index 928a3fef..e1d5dd84 100644 --- a/conformance/tests/unit/test_tr_anc.py +++ b/conformance/tests/unit/test_tr_anc.py @@ -16,7 +16,7 @@ } -def _nested(depth: int = 2000) -> list[Any]: +def _nested(depth: int = 100_000) -> list[Any]: value: list[Any] = [] for _ in range(depth): value = [value] @@ -83,7 +83,7 @@ def test_deeply_nested_claim_raises_inclusion_error_and_fails_tr_anc_002(): "kty": "OKP", "crv": "Ed25519", "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - "ext": _nested(2000), + "ext": _nested(100_000), } }, }