diff --git a/schemas/trace-claim.json b/schemas/trace-claim.json index 46eb9eb..ced6a9e 100644 --- a/schemas/trace-claim.json +++ b/schemas/trace-claim.json @@ -28,7 +28,7 @@ "type": "object", "required": ["platform", "measurement"], "properties": { - "platform": {"type": "string", "enum": ["intel-tdx", "amd-sev-snp", "nvidia-h100", "nvidia-blackwell", "aws-nitro", "arm-cca", "google-confidential-space", "tpm2"]}, + "platform": {"type": "string", "enum": ["intel-tdx", "amd-sev-snp", "nvidia-h100", "nvidia-blackwell", "aws-nitro", "arm-cca", "google-confidential-space", "tpm2", "software-only"]}, "measurement": {"type": "string", "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$"}, "rim_uri": {"type": "string", "format": "uri"}, "nonce": {"type": "string"}, diff --git a/src/trace_tests/modules/tr_rte.py b/src/trace_tests/modules/tr_rte.py index 1be7be1..9686982 100644 --- a/src/trace_tests/modules/tr_rte.py +++ b/src/trace_tests/modules/tr_rte.py @@ -17,11 +17,19 @@ "arm-cca", "google-confidential-space", "tpm2", + "software-only", }) +# Platforms that provide no hardware attestation evidence. Valid only at Level 0. +_DEV_PLATFORMS = frozenset({"software-only"}) -def check(trace: dict[str, Any]) -> list[Finding]: - """Return TR-RTE findings for the runtime / TEE platform claim.""" +def check(trace: dict[str, Any], level: int = 0) -> list[Finding]: + """Return TR-RTE findings for the runtime / TEE platform claim. + + *level* is the conformance level being checked. Development-mode platforms + (e.g. ``software-only``) are accepted at Level 0 but rejected at Level 1+ + because they carry no hardware attestation evidence. + """ findings: list[Finding] = [] runtime = trace.get("runtime") @@ -29,7 +37,16 @@ def check(trace: dict[str, Any]) -> list[Finding]: return [Finding("TR-RTE-001", Status.FAIL, "TR-RTE-001: runtime field is missing or not an object")] platform = runtime.get("platform") - if platform in _VALID_PLATFORMS: + if platform in _DEV_PLATFORMS: + if level == 0: + findings.append(Finding("TR-RTE-001", Status.PASS, f"runtime.platform is registered ({platform!r})")) + else: + findings.append(Finding( + "TR-RTE-001", Status.FAIL, + f"TR-RTE-001: runtime.platform {platform!r} is development-mode and not acceptable for " + f"hardware-attested levels (Level {level} requires a hardware TEE platform)", + )) + elif platform in _VALID_PLATFORMS: findings.append(Finding("TR-RTE-001", Status.PASS, f"runtime.platform is registered ({platform!r})")) else: findings.append(Finding( diff --git a/src/trace_tests/runner.py b/src/trace_tests/runner.py index 8808f62..036dc12 100644 --- a/src/trace_tests/runner.py +++ b/src/trace_tests/runner.py @@ -41,7 +41,7 @@ def run( results["TR-POL"] = tr_pol.check(trace) if "TR-RTE" in active: - results["TR-RTE"] = tr_rte.check(trace) + results["TR-RTE"] = tr_rte.check(trace, level) if "TR-SCA" in active: results["TR-SCA"] = tr_sca.check(trace) diff --git a/tests/test_software_only_platform.py b/tests/test_software_only_platform.py new file mode 100644 index 0000000..02fa494 --- /dev/null +++ b/tests/test_software_only_platform.py @@ -0,0 +1,87 @@ +"""Tests for software-only platform handling in TR-RTE. + +Spec note: trace-spec added `software-only` as a valid `runtime.platform` +value for development and CI use. It carries no hardware attestation evidence +and is therefore only acceptable at Level 0. Level 1+ must reject it with a +clear message that names the reason (development-mode, not hardware-attested) +rather than the generic "unknown platform" error. + +Covers: +- software-only at Level 0 passes TR-RTE-001 +- software-only at Level 1 fails TR-RTE-001 with a message mentioning "development-mode" +- software-only at Level 2 fails TR-RTE-001 with a message mentioning "development-mode" +- software-only is accepted by the JSON Schema (schema enum coverage) +""" + +from __future__ import annotations + +import copy + +import jsonschema +import pytest + +from trace_tests.modules import tr_rte +from trace_tests.result import Status + +_SOFTWARE_ONLY_TRACE = { + "runtime": { + "platform": "software-only", + "measurement": "sha256:" + "a" * 64, + } +} + + +@pytest.mark.parametrize("level", [1, 2]) +def test_software_only_fails_at_level(level): + """software-only must fail TR-RTE-001 at Level 1 and Level 2.""" + findings = tr_rte.check(_SOFTWARE_ONLY_TRACE, level=level) + platform_findings = [f for f in findings if f.code == "TR-RTE-001"] + assert platform_findings, "TR-RTE-001 finding expected" + assert all(f.failed() for f in platform_findings), ( + f"software-only must fail TR-RTE-001 at Level {level}; got {platform_findings}" + ) + + +@pytest.mark.parametrize("level", [1, 2]) +def test_software_only_failure_mentions_development_mode(level): + """Failure message for software-only must mention 'development-mode', not 'unknown'.""" + findings = tr_rte.check(_SOFTWARE_ONLY_TRACE, level=level) + fail_findings = [f for f in findings if f.code == "TR-RTE-001" and f.failed()] + assert fail_findings, f"Expected TR-RTE-001 FAIL at Level {level}" + messages = " ".join(f.message.lower() for f in fail_findings) + assert "development-mode" in messages, ( + f"TR-RTE-001 failure at Level {level} must mention 'development-mode'; " + f"got: {[f.message for f in fail_findings]}" + ) + assert "unknown" not in messages, ( + f"TR-RTE-001 failure at Level {level} must not say 'unknown platform'; " + f"got: {[f.message for f in fail_findings]}" + ) + + +def test_software_only_passes_at_level0(): + """software-only must pass TR-RTE-001 at Level 0.""" + findings = tr_rte.check(_SOFTWARE_ONLY_TRACE, level=0) + platform_findings = [f for f in findings if f.code == "TR-RTE-001"] + assert platform_findings, "TR-RTE-001 finding expected" + assert all(f.passed() for f in platform_findings), ( + f"software-only must pass TR-RTE-001 at Level 0; got {platform_findings}" + ) + + +def test_software_only_default_level_passes(): + """check() with no level argument defaults to Level 0 and passes software-only.""" + findings = tr_rte.check(_SOFTWARE_ONLY_TRACE) + platform_findings = [f for f in findings if f.code == "TR-RTE-001"] + assert platform_findings, "TR-RTE-001 finding expected" + assert all(f.passed() for f in platform_findings), ( + f"software-only must pass TR-RTE-001 at default level; got {platform_findings}" + ) + + +def test_software_only_accepted_by_schema(schema, valid_level0): + """software-only must be a valid enum value in the JSON Schema.""" + record = copy.deepcopy(valid_level0) + record["runtime"]["platform"] = "software-only" + # Must not raise + jsonschema.validate(record, schema)