Skip to content

Verify a TRACE Claim

Use cmcp_verify to confirm that a TRACE claim produced by cMCP is cryptographically valid, bound to the approved policy and catalog, and backed by a fresh attestation.

What you'll learn

  • How to install and call verify_trace_claim
  • What ApprovedHashes fields are and where the values come from
  • What each field in VerificationResult means
  • The difference between verified, partially_verified, and unverified
  • Where to integrate verification in a pipeline that consumes agent output

Prerequisites

pip install cmcp-runtime   # includes cmcp_verify

Install the verify library

cmcp_verify ships as part of cmcp-runtime. No separate install is needed:

from cmcp_verify import verify_trace_claim, ApprovedHashes

Obtain the approved hashes

The expected hashes must come from artifacts your verifier trusts. The quickstart computes them from local input files before startup. Gateway logs also print hashes, but those logs alone do not establish approval:

[cmcp] policy bundle loaded: sha256:abc123...
[cmcp] catalog loaded: 3 tools, sha256:def456...

In production, these values come from your deployment pipeline: not from the operator. The point of verification is to confirm the runtime loaded what your organization approved, without trusting the operator's assertion. Store the hashes in your CI artifact registry or secrets manager at bundle-build time and retrieve them at verification time.


Call verify_trace_claim

Save this as inspect_claim.py beside the quickstart's claim.json and approved-hashes.json, then run python inspect_claim.py:

import json
from pathlib import Path
from cmcp_verify import verify_trace_claim, ApprovedHashes

claim = json.loads(Path("claim.json").read_text())
hashes = json.loads(Path("approved-hashes.json").read_text())
approved = ApprovedHashes(**hashes)
result = verify_trace_claim(claim, approved)

print(f"Status: {result.status.value}")
print(f"Verified fields: {result.verified_fields}")
print(f"Unverified fields: {result.unverified_fields}")
print(f"Details: {result.details}")

This inspection script prints the result; it is not an acceptance gate. The software quickstart should report partially_verified. Use the consuming-job example below when evidence is required before processing output.

Integration sketch: the function also accepts optional parameters. Replace the key placeholder with a verifier-approved key before running:

result = verify_trace_claim(
    claim_json=claim,
    approved=approved,
    max_attestation_age_seconds=3600,       # default 86400; tighten for short-lived sessions
    trusted_public_key_hex="abcdef...",     # optional: cross-check against a pinned key
)

Verify a TPM claim from the CLI

For a TPM 2.0 claim, supply the attestation-key CA certificates your verifier trusts:

cmcp verify claim.json \
  --policy-hash sha256:abc123... \
  --catalog-hash sha256:def456... \
  --trusted-tpm-ca /etc/cmcp/trust/tpm-ca-roots.pem

The PEM file may contain one or more verifier-approved CA certificates. Keep it in a verifier-controlled trust store; do not obtain the trust bundle from the claim or the runtime that produced the claim. A valid CA bundle is one input to TPM verification, not a substitute for the signed quote and attestation-key evidence carried by the claim.

--trusted-tpm-ca is deliberately TPM-only. It does not configure AMD SEV-SNP or Intel TDX trust anchors, and it does not change how claims from those platforms are evaluated.


Read the VerificationResult

VerificationResult has these fields:

Field Type Description
status VerificationStatus Overall result: "verified", "partially_verified", or "unverified"
verified_fields list[str] Fields that passed their checks
unverified_fields list[str] Fields that failed or could not be checked
failure_reason VerificationError \| None First failure code, or None on full verification
attestation_age_seconds int Seconds since the attestation report was generated
is_attestation_fresh bool True if attestation_age_seconds <= max_attestation_age_seconds
details dict[str, str] Structured detail for individual check failures

verified_fields can include: schema, signature, public_key_binding, policy_bundle.hash, tool_catalog.hash, attestation_freshness, audit_chain, hardware_attestation, trusted_public_key.


Understand partially_verified

partially_verified means some checks passed and at least one failed. The most common reason in a correct deployment is that the gateway ran in software-only mode (CMCP_DEV_MODE=1): hardware attestation cannot be verified, but all cryptographic fields are valid.

Example output for a dev-mode claim:

Status:           partially_verified
Verified fields:  ['schema', 'signature', 'policy_bundle.hash', 'tool_catalog.hash', 'attestation_freshness', 'audit_chain']
Unverified fields:['hardware_attestation']
Attestation age:  8s
Attestation fresh:True
Details:          {'hardware_attestation': 'software-only mode - not hardware-backed'}

hardware_attestation is in unverified_fields but no failure_reason is set for it in isolation: the status rolls up to partially_verified because other fields were verified. A hardware deployment reaches verified only when the required checks pass; moving the process to a TEE alone is insufficient.

unverified (with no verified fields at all) means the claim is either malformed, signature-invalid, or the hashes do not match. Treat this as a hard rejection.


Integrate verification at job start

If a consuming job requires fully verified evidence, reject every other status before processing agent output. partially_verified can include failures beyond missing hardware; freshness alone is not an acceptance rule.

Save the following as accept_claim.py. It uses the same two files as the inspection example. Supply approved-hashes.json through your deployment's trusted artifact channel. This is a result gate; configure any additional platform trust inputs required by your deployment when calling the verifier.

import json
from pathlib import Path
from cmcp_verify import verify_trace_claim, ApprovedHashes


def verify_session_claim(claim_path, approved_path):
    claim = json.loads(Path(claim_path).read_text())
    hashes = json.loads(Path(approved_path).read_text())
    result = verify_trace_claim(claim, ApprovedHashes(**hashes))
    if result.status.value != "verified":
        raise SystemExit(
            f"CLAIM REJECTED: {result.status.value}; "
            f"failed or unchecked: {result.unverified_fields}"
        )
    return claim


if __name__ == "__main__":
    claim = verify_session_claim("claim.json", "approved-hashes.json")
    print(f"Claim verified. Tools called: {claim['gateway']['call_summary']['tools_invoked']}")

Run python accept_claim.py. It must reject the software quickstart record with a nonzero exit and CLAIM REJECTED: partially_verified. A development workflow that permits software evidence needs an explicit, narrower acceptance policy and must preserve that distinction in its output.

Next: Cedar policy walkthrough, TEE attestation, and verification library reference.