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
146 changes: 146 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import base64
import json
import pathlib
import time

import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

VECTORS_DIR = pathlib.Path(__file__).parent / "vectors"
SCHEMAS_DIR = pathlib.Path(__file__).parent.parent / "schemas"
Expand All @@ -14,6 +18,59 @@ def load_schema():
return json.loads((SCHEMAS_DIR / "trace-claim.json").read_text())


def _b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()


def _canonical_json(d: dict) -> bytes:
return json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def _build_signed_cmcp_record(*, platform: str = "tpm2", nonce: str | None = None) -> tuple[dict, Ed25519PrivateKey]:
"""Return (record, private_key) for a fully-signed cmcp-runtime claim.

The signature covers the canonical JSON of the envelope with the 'signature'
field absent, matching the verification path in tr_sig.check_cmcp_runtime.
"""
priv = Ed25519PrivateKey.generate()
pub = priv.public_key()
pub_raw = pub.public_bytes_raw()
x = _b64url(pub_raw)
kid = f"test-{pub_raw[:4].hex()}"

iat = int(time.time()) - 30 # fresh but not future-dated

trace: dict = {
"eat_profile": "tag:agentrust.io,2026:trace-v0.1",
"iat": iat,
"subject": "spiffe://cmcp.gateway/session/conformance-test",
"runtime": {
"platform": platform,
"measurement": "sha256:" + "a" * 64,
},
"policy": {
"bundle_hash": "sha256:" + "b" * 64,
"enforcement_mode": "enforce",
},
"data_class": "internal",
"cnf": {"jwk": {"kty": "OKP", "crv": "Ed25519", "x": x, "kid": kid}},
}

if nonce is not None:
trace["runtime"]["nonce"] = nonce

record: dict = {
"cmcp_version": "1.0",
"trace": trace,
"gateway": {"session_id": "conformance-test"},
"signature": "",
}

body = _canonical_json({k: v for k, v in record.items() if k != "signature"})
record["signature"] = _b64url(priv.sign(body))
return record, priv


@pytest.fixture
def schema():
return load_schema()
Expand All @@ -37,3 +94,92 @@ def invalid_missing_runtime():
@pytest.fixture
def invalid_wrong_profile():
return load_vector("invalid_wrong_profile.json")


# ---------------------------------------------------------------------------
# Level 1 fixtures
# ---------------------------------------------------------------------------

_CHALLENGE_NONCE = _b64url(b"level1-conformance-nonce-01")


@pytest.fixture
def challenge_nonce() -> str:
"""A stable base64url nonce that the signed EAT fixture embeds in runtime.nonce."""
return _CHALLENGE_NONCE


@pytest.fixture
def signed_eat_fixture() -> dict:
"""A valid, fully-signed cmcp-runtime envelope for Level 1 conformance tests."""
record, _ = _build_signed_cmcp_record(platform="tpm2", nonce=_CHALLENGE_NONCE)
return record


# ---------------------------------------------------------------------------
# Level 2 fixtures
# ---------------------------------------------------------------------------

# Measurement value used consistently across Level 2 fixtures.
_SW_MEASUREMENT = "sha256:" + "c" * 64


def _build_software_only_record() -> dict:
"""Build a signed cmcp-runtime record with platform='software-only' and a fixed measurement."""
priv = Ed25519PrivateKey.generate()
pub_raw = priv.public_key().public_bytes_raw()
x = _b64url(pub_raw)
kid = f"test-{pub_raw[:4].hex()}"
iat = int(time.time()) - 30

record: dict = {
"cmcp_version": "1.0",
"trace": {
"eat_profile": "tag:agentrust.io,2026:trace-v0.1",
"iat": iat,
"subject": "spiffe://cmcp.gateway/session/level2-test",
"runtime": {
"platform": "software-only",
"measurement": _SW_MEASUREMENT,
},
"policy": {
"bundle_hash": "sha256:" + "b" * 64,
"enforcement_mode": "enforce",
},
"data_class": "internal",
"cnf": {"jwk": {"kty": "OKP", "crv": "Ed25519", "x": x, "kid": kid}},
},
"gateway": {"session_id": "level2-test"},
"signature": "",
}
body = _canonical_json({k: v for k, v in record.items() if k != "signature"})
record["signature"] = _b64url(priv.sign(body))
return record


@pytest.fixture
def trust_record() -> dict:
"""A software-only cmcp-runtime record for Level 2 fixture coverage.

'software-only' is the development platform that carries no hardware TEE
evidence. It is deliberately distinct from real attestation platforms so a
consumer can never mistake it for hardware-backed evidence.
"""
return _build_software_only_record()


@pytest.fixture
def attestation_report(trust_record: dict) -> dict:
"""A synthetic attestation report whose measurement matches trust_record.

For software-only records there is no real TEE report; this fixture captures
the structure that a hardware verifier would produce so Level 2 tests can
exercise field-matching logic without real attestation hardware.
"""
return {
"platform": trust_record["trace"]["runtime"]["platform"],
"measurement": trust_record["trace"]["runtime"]["measurement"],
"freshness_nonce": _b64url(b"level2-freshness-nonce"),
"timestamp": trust_record["trace"]["iat"],
"cnf_key_x": trust_record["trace"]["cnf"]["jwk"]["x"],
}
121 changes: 112 additions & 9 deletions tests/test_level1.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,120 @@
"""Level 1 conformance tests: signed EAT envelope with Ed25519 verification.

A Level 1 record must be a cmcp-runtime envelope carrying a valid Ed25519
signature by the key in trace.cnf.jwk over the canonical JSON body. The runner
module (TR-SIG) is the authoritative implementation; these tests drive it through
representative conformant and non-conformant fixtures to verify it behaves correctly.
"""

import base64
import json

import pytest

from trace_tests.modules.tr_sig import check as tr_sig_check
from trace_tests.result import Status


def _b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()


def _canonical_json(d: dict) -> bytes:
return json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


@pytest.mark.level1
@pytest.mark.skip(reason="Level 1 requires a signed EAT implementation")
class TestLevel1Conformance:
def test_eat_is_cose_sign1(self, signed_eat_bytes):
raise NotImplementedError
def test_eat_is_cose_sign1(self, signed_eat_fixture):
"""The envelope must have cmcp_version and a non-empty signature field.

In the TRACE cMCP profile the cmcp-runtime envelope is the signed EAT
carrier. A present, non-empty 'signature' field is the indicator that
the record was signed rather than merely assembled.
"""
assert "cmcp_version" in signed_eat_fixture, (
"Level 1 record must be a cmcp-runtime envelope (cmcp_version key required)"
)
sig = signed_eat_fixture.get("signature", "")
assert isinstance(sig, str) and len(sig) > 0, (
"Level 1 record must carry a non-empty signature field"
)
assert "trace" in signed_eat_fixture and isinstance(signed_eat_fixture["trace"], dict), (
"Level 1 record must embed a trace object"
)

def test_eat_protected_header_content_type(self, signed_eat_fixture):
"""The trace envelope must declare the expected EAT profile sentinel.

In the cMCP profile the eat_profile field inside trace serves the role
of the COSE protected header content-type: it binds the record to the
TRACE v0.1 specification and prevents cross-profile replay.
"""
trace = signed_eat_fixture["trace"]
assert trace.get("eat_profile") == "tag:agentrust.io,2026:trace-v0.1", (
"trace.eat_profile must be 'tag:agentrust.io,2026:trace-v0.1'"
)

def test_signature_verifies_against_cnf_key(self, signed_eat_fixture):
"""TR-SIG must pass for a validly-signed cmcp-runtime record."""
trace = signed_eat_fixture["trace"]
findings = tr_sig_check(trace, signed_eat_fixture, "cmcp-runtime")
failures = [f for f in findings if f.failed()]
assert not failures, (
f"Valid signed record must pass TR-SIG at Level 1; failures: {failures}"
)
passed = [f for f in findings if f.passed()]
assert passed, "TR-SIG must emit at least one PASS finding for a valid signature"

def test_signature_byte_flipped_fails(self, signed_eat_fixture):
"""A record with a tampered signature must fail TR-SIG.

Flipping a byte in the base64url signature produces an invalid
Ed25519 signature that cannot verify against the embedded cnf.jwk
public key.
"""
import base64

original = signed_eat_fixture["signature"]
# Decode, flip the first byte, re-encode without padding.
raw = base64.urlsafe_b64decode(original + "=" * (4 - len(original) % 4))
tampered = bytes([raw[0] ^ 0xFF]) + raw[1:]
signed_eat_fixture["signature"] = base64.urlsafe_b64encode(tampered).rstrip(b"=").decode()

trace = signed_eat_fixture["trace"]
findings = tr_sig_check(trace, signed_eat_fixture, "cmcp-runtime")
assert any(f.failed() and "TR-SIG-001" in f.code for f in findings), (
"Byte-flipped signature must produce TR-SIG-001 FAIL"
)

def test_cnf_jwk_swapped_key_fails(self, signed_eat_fixture):
"""A record whose cnf.jwk has been replaced with a different key must fail TR-SIG.

The signature was produced by the original private key; verifying it
against a freshly-generated unrelated public key must fail.
"""
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

different_priv = Ed25519PrivateKey.generate()
different_pub_raw = different_priv.public_key().public_bytes_raw()
different_x = _b64url(different_pub_raw)

# Swap x in cnf.jwk while leaving the signature unchanged.
signed_eat_fixture["trace"]["cnf"]["jwk"]["x"] = different_x

def test_eat_protected_header_content_type(self, signed_eat_bytes):
raise NotImplementedError
trace = signed_eat_fixture["trace"]
findings = tr_sig_check(trace, signed_eat_fixture, "cmcp-runtime")
assert any(f.failed() for f in findings), (
"Swapped cnf.jwk public key must cause TR-SIG to fail"
)

def test_signature_verifies_against_cnf_key(self, signed_eat_bytes):
raise NotImplementedError
def test_eat_nonce_matches_challenge(self, signed_eat_fixture, challenge_nonce):
"""The runtime.nonce embedded in the EAT must match the challenge nonce.

def test_eat_nonce_matches_challenge(self, signed_eat_bytes, challenge_nonce):
raise NotImplementedError
Nonce binding prevents replay: a verifier issues a freshness challenge
before the agent signs; the resulting EAT must echo that exact nonce.
"""
trace = signed_eat_fixture["trace"]
assert trace["runtime"].get("nonce") == challenge_nonce, (
"trace.runtime.nonce must match the challenge nonce issued by the verifier"
)
Loading
Loading