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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -45,9 +45,16 @@ Each test case includes:

Error codes follow the form `TR-<MODULE>-<NNN>` (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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
4 changes: 3 additions & 1 deletion src/trace_tests/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'; "
Expand Down
6 changes: 3 additions & 3 deletions src/trace_tests/modules/tr_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down
31 changes: 20 additions & 11 deletions src/trace_tests/modules/tr_sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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",
))

Expand Down
6 changes: 3 additions & 3 deletions tests/test_level0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion tests/unit/test_tr_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_tr_sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading