From 17496f79671f35b56a62c694981115d6edaab48b Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Tue, 16 Jun 2026 17:46:19 -0700 Subject: [PATCH 1/2] feat: v0.2 -- accept DID URIs in TR-ENV-003 subject check Updates tr_env.py, test_level0.py, and test_tr_env.py to accept both spiffe:// and did: subject URIs. Adds did:key and did:mesh passing tests. Aligns with agentrust-io/trace-spec#35 and trace-spec v0.2.0. Co-Authored-By: Claude Sonnet 4.6 --- src/trace_tests/modules/tr_env.py | 6 +++--- tests/test_level0.py | 6 +++--- tests/unit/test_tr_env.py | 14 +++++++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/trace_tests/modules/tr_env.py b/src/trace_tests/modules/tr_env.py index 85200b5..2da4d33 100644 --- a/src/trace_tests/modules/tr_env.py +++ b/src/trace_tests/modules/tr_env.py @@ -45,10 +45,10 @@ def check(trace: dict[str, Any], max_age_seconds: int = DEFAULT_MAX_AGE_SECONDS) findings.append(Finding("TR-ENV-002", Status.FAIL, f"iat must be a Unix timestamp >= {_IAT_MIN}, got {iat!r}")) subject = trace.get("subject", "") - if isinstance(subject, str) and subject.startswith("spiffe://"): - findings.append(Finding("TR-ENV-003", Status.PASS, "subject is a SPIFFE URI")) + if isinstance(subject, str) and subject.startswith(("spiffe://", "did:")): + findings.append(Finding("TR-ENV-003", Status.PASS, f"subject is a valid workload identity URI ({subject!r})")) else: - findings.append(Finding("TR-ENV-003", Status.FAIL, f"subject must start with 'spiffe://', got {subject!r}")) + findings.append(Finding("TR-ENV-003", Status.FAIL, f"subject must be a SPIFFE URI (spiffe://) or DID URI (did:), got {subject!r}")) cnf = trace.get("cnf") if isinstance(cnf, dict) and isinstance(cnf.get("jwk"), dict) and "kty" in cnf["jwk"]: diff --git a/tests/test_level0.py b/tests/test_level0.py index 6ab3c91..5fbcfce 100644 --- a/tests/test_level0.py +++ b/tests/test_level0.py @@ -2,7 +2,7 @@ import pytest import jsonschema -SPIFFE_RE = re.compile(r"^spiffe://") +SUBJECT_RE = re.compile(r"^(spiffe://|did:)") DIGEST_RE = re.compile(r"^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$") VALID_PLATFORMS = { "intel-tdx", "amd-sev-snp", "nvidia-h100", "nvidia-blackwell", @@ -24,8 +24,8 @@ def test_iat_is_positive_integer(self, valid_level0): assert isinstance(valid_level0["iat"], int) assert valid_level0["iat"] >= 1700000000 - def test_subject_is_spiffe_uri(self, valid_level0): - assert SPIFFE_RE.match(valid_level0["subject"]) + def test_subject_is_valid_workload_identity_uri(self, valid_level0): + assert SUBJECT_RE.match(valid_level0["subject"]) def test_runtime_platform_is_registered(self, valid_level0): assert valid_level0["runtime"]["platform"] in VALID_PLATFORMS diff --git a/tests/unit/test_tr_env.py b/tests/unit/test_tr_env.py index 138aa8f..80eee58 100644 --- a/tests/unit/test_tr_env.py +++ b/tests/unit/test_tr_env.py @@ -60,7 +60,19 @@ def test_iat_beyond_custom_max_age_fails(): assert any(f.code == "TR-ENV-002" for f in failed) -def test_non_spiffe_subject_fails(): +def test_did_subject_passes(): + trace = {**_VALID, "subject": "did:key:z6MkhaXgBZDvotzL8oCYaXeFuJArwvX6mDMsKTJVjtN7R"} + findings = check(trace) + assert all(f.passed() for f in findings), [f for f in findings if not f.passed()] + + +def test_did_mesh_subject_passes(): + trace = {**_VALID, "subject": "did:mesh:spiffe://factory.example/agent/material-movement/dev"} + findings = check(trace) + assert all(f.passed() for f in findings), [f for f in findings if not f.passed()] + + +def test_non_spiffe_non_did_subject_fails(): trace = {**_VALID, "subject": "https://example.org/agent"} codes = {f.code for f in check(trace) if f.failed()} assert "TR-ENV-003" in codes From e4d6ddf7bb70b26e4af7f558d8055e888a3dc26e Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Tue, 16 Jun 2026 21:21:38 -0700 Subject: [PATCH 2/2] feat: v0.2 -- embedded signature verification, loader fix, new unit tests - tr_sig.py: verify Ed25519 embedded signatures in plain trace records (sign_record() output) - loader.py: remove "signature" from partial envelope markers so signed plain records load - test_tr_sig.py: add test_plain_trace_embedded_ed25519_signature_passes and tamper test - README.md: v0.2 badge, "What changed in v0.2" section - pyproject.toml: bump version to 0.2.0 Co-Authored-By: Claude Sonnet 4.6 --- README.md | 13 ++++++-- pyproject.toml | 2 +- src/trace_tests/loader.py | 4 ++- src/trace_tests/modules/tr_sig.py | 31 ++++++++++++------- tests/unit/test_tr_sig.py | 49 +++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 45e75ab..d9a2865 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -[![TRACE Spec](https://img.shields.io/badge/TRACE-Spec_v0.1-0ea5e9)](https://github.com/agentrust-io/trace-spec) +[![TRACE Spec](https://img.shields.io/badge/TRACE-Spec_v0.2-0ea5e9)](https://github.com/agentrust-io/trace-spec) [![Tests](https://img.shields.io/badge/Conformance_Tests-7_modules-green)]() [![Discord](https://dcbadge.limes.pink/api/server/9JWNpH7E?style=flat)](https://discord.gg/9JWNpH7E) # TRACE Conformance Test Suite -Conformance tests for TRACE v0.1 - Trust, Runtime Attestation, and Compliance Evidence. An implementation producing Trust Records must pass all tests in the applicable level before using the "TRACE-conformant" mark. +Conformance tests for TRACE v0.2 - Trust, Runtime Attestation, and Compliance Evidence. An implementation producing Trust Records must pass all tests in the applicable level before using the "TRACE-conformant" mark. If you are building a gateway, agent runtime, or orchestration layer that produces TRACE records, run this suite against your output to verify conformance before claiming TRACE compliance. @@ -45,9 +45,16 @@ Each test case includes: Error codes follow the form `TR--` (e.g., `TR-ENV-001`: missing `eat_profile`). +## What changed in v0.2 + +- **DID subject support**: `subject` now accepts `did:` URIs in addition to `spiffe://`. TR-ENV-003 passes for both. +- **Embedded signature verification**: plain TRACE records signed with `agentrust-trace sign_record()` (Ed25519 embedded `signature` field) are now cryptographically verified at all levels. Previously marked UNVERIFIED. +- **SLSA Level 0**: `build_provenance.slsa_level: 0` is now valid (software-only / development records). +- **Software-only platform**: `runtime.platform: "software-only"` accepted at Level 0. + ## Status -Test suite v0.1, in development. The TRACE spec publishes at Confidential Computing Summit, June 23 2026, and the test suite will be usable at that point. The certification program is on a separate timeline, launching 2027. +Test suite v0.2. The TRACE spec published at Confidential Computing Summit, June 23 2026. The certification program is on a separate timeline, launching 2027. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 84c4f3b..c42e9c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentrust-trace-tests" -version = "0.1.0" +version = "0.2.0" description = "TRACE conformance test suite" readme = "README.md" license = { text = "Apache-2.0" } diff --git a/src/trace_tests/loader.py b/src/trace_tests/loader.py index fec6936..db4f0ba 100644 --- a/src/trace_tests/loader.py +++ b/src/trace_tests/loader.py @@ -44,7 +44,9 @@ def load_record(path: str) -> tuple[dict[str, Any], str]: # Envelope-only keys present without cmcp_version: this is a partial/stripped # cmcp envelope, not a canonical TRACE record. Reject rather than silently # downgrading to the weaker plain-trace verification path. - partial_markers = sorted(k for k in ("trace", "gateway", "signature") if k in data) + # Note: "signature" alone is allowed -- plain TRACE records may carry an + # embedded Ed25519 signature field (agentrust-trace sign_record() output). + partial_markers = sorted(k for k in ("trace", "gateway") if k in data) if partial_markers: raise LoadError( f"Record contains cmcp envelope field(s) {partial_markers} but no 'cmcp_version'; " diff --git a/src/trace_tests/modules/tr_sig.py b/src/trace_tests/modules/tr_sig.py index f39ca57..4657a8f 100644 --- a/src/trace_tests/modules/tr_sig.py +++ b/src/trace_tests/modules/tr_sig.py @@ -89,14 +89,15 @@ def check_cmcp_runtime(record: dict[str, Any]) -> list[Finding]: def check(trace: dict[str, Any], record: dict[str, Any], fmt: str, level: int = 0) -> list[Finding]: """Return TR-SIG findings. *record* is the full raw dict, *trace* is the extracted TRACE fields. - *level* is the conformance level being checked. Plain trace records carry no - verifiable signature, so they FAIL at level >= 1 and are reported UNVERIFIED - (never PASS) at level 0. + *level* is the conformance level being checked. + + Plain trace records with an embedded ``signature`` field are verified with Ed25519 + (agentrust-trace ``sign_record()`` output). Plain trace records without a signature + field FAIL at level >= 1 and are UNVERIFIED at level 0. """ if fmt == "cmcp-runtime": return check_cmcp_runtime(record) - # Plain trace format: no signature can be cryptographically verified. findings: list[Finding] = [] jwk = trace.get("cnf", {}).get("jwk", {}) kty = jwk.get("kty") @@ -113,17 +114,25 @@ def check(trace: dict[str, Any], record: dict[str, Any], fmt: str, level: int = f"TR-SIG-004: unsupported key type {kty!r}; expected one of {sorted(_SUPPORTED_KTY)}", )) - if level >= 1: + sig = trace.get("signature", "") + if sig and kty == "OKP" and crv == _ED25519_CRV and jwk.get("x"): + body = _canonical_json({k: v for k, v in trace.items() if k != "signature"}) + ok, msg = _verify_ed25519(jwk["x"], sig, body) + status = Status.PASS if ok else Status.FAIL + findings.append(Finding("TR-SIG-005", status, msg)) + elif sig: + findings.append(Finding( + "TR-SIG-005", Status.FAIL, + "TR-SIG-005: signature field present but key type is not OKP/Ed25519 or cnf.jwk.x is missing", + )) + elif level >= 1: findings.append(Finding( - "TR-SIG-005", - Status.FAIL, - f"TR-SIG-005: plain trace records carry no verifiable signature; " - f"Level {level} requires cryptographic signature verification (use a signed envelope, e.g. cmcp-runtime)", + "TR-SIG-005", Status.FAIL, + f"TR-SIG-005: no signature present; Level {level} requires cryptographic verification", )) else: findings.append(Finding( - "TR-SIG-005", - Status.UNVERIFIED, + "TR-SIG-005", Status.UNVERIFIED, "TR-SIG-005: no signature present; this record is NOT cryptographically verified", )) diff --git a/tests/unit/test_tr_sig.py b/tests/unit/test_tr_sig.py index e01b268..7a7ad91 100644 --- a/tests/unit/test_tr_sig.py +++ b/tests/unit/test_tr_sig.py @@ -102,3 +102,52 @@ def test_trace_format_missing_kty_fails(): trace = {"cnf": {"jwk": {}}} findings = check(trace, trace, "trace") assert any(f.failed() for f in findings) + + +def test_plain_trace_embedded_ed25519_signature_passes(): + """plain TRACE record signed with agentrust-trace sign_record() gets TR-SIG-005 PASS.""" + priv = Ed25519PrivateKey.generate() + pub = priv.public_key() + x = _b64url(pub.public_bytes_raw()) + + trace: dict = { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": 1748000000, + "subject": "did:mesh:spiffe://example.org/agent/test", + "runtime": {"platform": "software-only", "measurement": "sha256:" + "a" * 64}, + "policy": {"bundle_hash": "sha256:" + "b" * 64, "enforcement_mode": "enforce"}, + "data_class": "internal", + "cnf": {"jwk": {"kty": "OKP", "crv": "Ed25519", "x": x}}, + "signature": "", + } + body = _canonical_json({k: v for k, v in trace.items() if k != "signature"}) + trace["signature"] = _b64url(priv.sign(body)) + + findings = check(trace, trace, "trace") + sig_findings = [f for f in findings if f.code == "TR-SIG-005"] + assert sig_findings, "TR-SIG-005 finding expected" + assert all(f.passed() for f in sig_findings), sig_findings + + +def test_plain_trace_tampered_embedded_signature_fails(): + priv = Ed25519PrivateKey.generate() + pub = priv.public_key() + x = _b64url(pub.public_bytes_raw()) + + trace: dict = { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": 1748000000, + "subject": "did:mesh:spiffe://example.org/agent/test", + "runtime": {"platform": "software-only", "measurement": "sha256:" + "a" * 64}, + "policy": {"bundle_hash": "sha256:" + "b" * 64, "enforcement_mode": "enforce"}, + "data_class": "internal", + "cnf": {"jwk": {"kty": "OKP", "crv": "Ed25519", "x": x}}, + "signature": "", + } + body = _canonical_json({k: v for k, v in trace.items() if k != "signature"}) + trace["signature"] = _b64url(priv.sign(body)) + trace["iat"] = 1748000001 # tamper after signing + + findings = check(trace, trace, "trace") + sig_findings = [f for f in findings if f.code == "TR-SIG-005"] + assert any(f.failed() for f in sig_findings), sig_findings