Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions conformance/src/trace_tests/inclusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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)}")

Expand Down
74 changes: 70 additions & 4 deletions conformance/tests/test_modules_never_raise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
)


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 = 100_000) -> 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(100_000)),
("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)
)

87 changes: 85 additions & 2 deletions conformance/tests/unit/test_tr_anc.py
Original file line number Diff line number Diff line change
@@ -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 = 100_000) -> list[Any]:
value: list[Any] = []
for _ in range(depth):
value = [value]
return value


def test_https_transparency_passes_the_shape_check_only():
Expand All @@ -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():
Expand Down Expand Up @@ -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(100_000),
}
},
}
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

Loading