diff --git a/docs/tutorials/hardware-attestation-platforms.md b/docs/tutorials/hardware-attestation-platforms.md new file mode 100644 index 0000000..c57b48f --- /dev/null +++ b/docs/tutorials/hardware-attestation-platforms.md @@ -0,0 +1,204 @@ +# Hardware Attestation Platforms + +Understand what the `runtime.measurement` field contains for each TEE platform, what it proves, and how a verifier uses it. + +## What you'll learn + +- What `runtime.platform` and `runtime.measurement` mean for each supported platform +- Why `software-only` is only safe for development and testing +- What measurement values prove about the code that signed the record +- How a verifier checks a measurement against a Reference Integrity Manifest +- What the `agentrust-trace` library does and does not do with measurements + +## Prerequisites + +```bash +pip install agentrust-trace +``` + +--- + +## The measurement Field + +Every TRACE Trust Record carries a `runtime` object with two required fields: + +```json +{ + "runtime": { + "platform": "amd-sev-snp", + "measurement": "sha384:c9e4b1d2e3f4a5b6..." + } +} +``` + +`measurement` is a digest that identifies the exact binary that ran inside the TEE at the moment the signing key was generated. The TEE hardware computes and seals this value; software running outside the TEE cannot forge it. + +This is what makes hardware-attested TRACE records meaningful: the signing key was generated inside the measured enclave, so the measurement in the record is a claim about the code that produced the key. If the measurement matches a known-good reference value, you know the key came from the expected software, unmodified. + +--- + +## software-only + +```python +import time +from agentrust_trace import generate_key, sign_record + +key = generate_key() + +record = { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": int(time.time()), + "subject": "spiffe://dev.example.org/agent/local-test", + "runtime": { + "platform": "software-only", + "measurement": "sha256:" + "0" * 64, + }, + # ... other required fields ... +} +``` + +`platform: "software-only"` means no TEE is present. The measurement is conventionally all-zero bytes. This platform value exists so a development record can never be mistaken for a hardware-backed record by a consumer that inspects `runtime.platform`. + +Use `software-only` only in development and testing. A production verifier should reject records with this platform: + +```python +def check_platform(record: dict) -> None: + platform = record["runtime"]["platform"] + if platform == "software-only": + raise ValueError("software-only records are not accepted in production") +``` + +--- + +## TPM2 + +```json +{ + "runtime": { + "platform": "tpm2", + "measurement": "sha256:", + "rim_uri": "https://vendor.example.org/rim/firmware-1.2.pem" + } +} +``` + +For TPM2, `measurement` is a PCR (Platform Configuration Register) digest. TPM PCR banks accumulate measurements of firmware, bootloader, kernel, and application code during the boot sequence. The value in `measurement` reflects the state of specific PCR banks at the time the key was generated. + +Which PCR banks are included depends on the deployment configuration. A verifier checks the measurement by fetching the Reference Integrity Manifest (RIM) at `runtime.rim_uri` and comparing the expected PCR values against the measurement. + +--- + +## AMD SEV-SNP + +```json +{ + "runtime": { + "platform": "amd-sev-snp", + "measurement": "sha384:", + "rim_uri": "https://kdsintf.amd.com/vcek/v1/Milan/...", + "firmware_version": "1.51.00" + } +} +``` + +For AMD SEV-SNP, `measurement` is the `MEASUREMENT` field from the SNP attestation report. AMD's Secure Nested Paging hardware computes this value over the initial memory contents of the confidential VM: firmware, kernel, initrd, and the guest application image. The measurement is sealed by the hardware before any guest code runs. + +The RIM is the AMD Key Distribution Service (KDS) URL for the Versioned Chip Endorsement Key (VCEK) or VLEK. A verifier fetches the platform attestation report from KDS, verifies the AMD root certificate chain, and confirms the `MEASUREMENT` field matches the expected value for the known-good image. + +The `firmware_version` field helps correlate against published AMD firmware RIMs. + +--- + +## Intel TDX + +```json +{ + "runtime": { + "platform": "intel-tdx", + "measurement": "sha384:", + "rim_uri": "https://api.trustedservices.intel.com/tdx/certification/v4/..." + } +} +``` + +For Intel TDX, `measurement` is the `MRTD` (Measurement of the TD) field from the TDX TD Report. Intel TDX measures the initial TD memory (TDVF firmware, kernel, and workload image) into `MRTD` during TD build. This value cannot be changed after TD launch. + +The RIM endpoint is Intel Trust Authority (ITA). A verifier fetches the TD Quote (via `tdx-attest` or a platform attestation proxy), verifies the Intel root certificate chain, and confirms the `MRTD` value matches the reference for the expected image. + +TDX reports also carry `RTMR` (Runtime Measurement Registers) for post-launch measurements. TRACE v0.1 binds only `MRTD` in the `measurement` field; `RTMR` values are outside the current scope. + +--- + +## NVIDIA H100 + +```json +{ + "runtime": { + "platform": "nvidia-h100", + "measurement": "sha384:", + "rim_uri": "https://nras.attestation.nvidia.com/v3/attestation/..." + } +} +``` + +For NVIDIA H100 (Confidential Computing mode), `measurement` is the attestation report digest from the NVIDIA Remote Attestation Service (NRAS). NVIDIA's hardware attestation chain covers the GPC firmware, the driver, and the GPU workload configuration. + +A verifier fetches the attestation certificate from NRAS, verifies the NVIDIA root certificate chain, and confirms the digest corresponds to an approved GPU firmware and driver version. + +--- + +## How a Verifier Checks a Measurement + +The `agentrust-trace` Python library carries the `measurement` field and makes it available in the signed record. It does not perform hardware measurement verification. That is the TEE platform's responsibility and requires platform-specific tooling. + +A complete verifier for hardware-attested records does three things: + +1. Verify the TRACE record signature with `verify_record()` (this library). +2. Fetch the platform attestation report for the stated `rim_uri`. +3. Confirm the `measurement` in the TRACE record matches the expected value in the RIM. + +Step 3 proves the key that signed the TRACE record was generated by the expected software running inside the attested enclave. Without step 3, you know the record was not tampered with after signing, but you do not know whether the signing key came from legitimate code. + +```python +from agentrust_trace import verify_record, validate_json + +# Step 1: verify the TRACE record structure and signature +validate_json(record) +verify_record(record) + +# Step 2 + 3: platform-specific — outside the scope of agentrust-trace +# For cMCP-issued records, use cmcp-verify which handles the full chain: +# from cmcp_verify import verify_trace_claim +# verify_trace_claim(record) +``` + +--- + +## Platform Enum Reference + +The `RuntimeInfo` model accepts exactly these platform values: + +| Value | Attestation root | +|---|---| +| `software-only` | None — development only | +| `tpm2` | TPM PCR digest | +| `amd-sev-snp` | AMD SEV-SNP MEASUREMENT field | +| `intel-tdx` | Intel TDX MRTD field | +| `nvidia-h100` | NVIDIA NRAS attestation digest | +| `nvidia-blackwell` | NVIDIA Blackwell confidential computing | +| `aws-nitro` | AWS Nitro Enclave attestation document | +| `arm-cca` | Arm CCA Realm Measurement | +| `google-confidential-space` | Google Confidential Space measurement | + +Records with any other platform string will fail schema validation. + +--- + +## Summary + +The `runtime.measurement` field identifies the binary that generated the signing key, as measured by the TEE hardware. Each platform computes this differently: PCR digest for TPM2, `MEASUREMENT` for AMD SEV-SNP, `MRTD` for Intel TDX, and an NRAS digest for NVIDIA H100. The `agentrust-trace` library carries this field in the signed record; verifying the measurement against the TEE platform's attestation chain is a separate step that requires platform-specific tooling. For cMCP-issued records, the `cmcp-verify` library handles the full chain. + +Related tutorials: + +- [Sign your first trust record](signing-your-first-trust-record.md) +- [Integration with cMCP](integrating-with-cmcp.md) diff --git a/docs/tutorials/integrating-with-cmcp.md b/docs/tutorials/integrating-with-cmcp.md new file mode 100644 index 0000000..72f22d7 --- /dev/null +++ b/docs/tutorials/integrating-with-cmcp.md @@ -0,0 +1,177 @@ +# Integration with cMCP + +Understand how TRACE trust records are generated by Confidential MCP (cMCP) and how a downstream verifier checks them. + +## What you'll learn + +- When and how cMCP generates a TRACE trust record +- How the TEE-sealed Ed25519 key ties the record to the hardware measurement +- What the CRYPTO-001 nonce binding is and why it matters +- Where to find the TRACE record written by cMCP +- How to pass the record to `cmcp-verify` for full policy and audit chain verification +- The division between `agentrust-trace` (sign/verify the record structure) and `cmcp-verify` (full chain verification) + +## Prerequisites + +```bash +pip install agentrust-trace +# For full cMCP verification: +pip install cmcp-verify +``` + +--- + +## How cMCP Issues a TRACE Record + +cMCP runs inside a TEE (Intel TDX, AMD SEV-SNP, or NVIDIA H100). At startup, it generates an Ed25519 signing key inside the enclave. The private key never leaves the measured TEE. This is different from a software-only key, which any process with access to the filesystem could read. + +At the end of each MCP session, cMCP: + +1. Collects the session evidence: model identity, tool transcript, data classes, policy state +2. Constructs a TRACE Trust Record dict with all required fields +3. Calls `sign_record(record, key)` using the TEE-sealed key +4. Writes the signed record to the path in `CMCP_TRACE_OUTPUT_PATH` + +The resulting record is a standard TRACE v0.1 JSON object. You can read it with any JSON parser and verify its signature with `agentrust-trace`. + +--- + +## The TEE-Sealed Signing Key + +The key used to sign cMCP TRACE records is generated inside the TEE at startup. Its public half appears in every record as `cnf.jwk`. + +```json +{ + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "" + } + }, + "runtime": { + "platform": "amd-sev-snp", + "measurement": "sha384:" + } +} +``` + +The `cnf.jwk` and `runtime.measurement` together make a claim: "the key that signed this record was generated by the code identified by this measurement, running inside the stated TEE." A verifier that trusts the measurement trusts the key, and by extension trusts the record. + +Verifying just the Ed25519 signature (with `verify_record()`) confirms the record was not modified after signing. Verifying the full chain (with `cmcp-verify`) additionally confirms the signing key came from the stated TEE measurement. + +--- + +## The CRYPTO-001 Nonce Binding + +cMCP implements the CRYPTO-001 binding defined in the TRACE spec. When the TEE generates its signing key, it also generates a TEE nonce. The first 32 bytes of the TEE nonce equal the RFC 7638 JWK Thumbprint of the signing key. + +This binding ties the nonce to the key: a verifier that receives the TEE attestation report (which includes the nonce) can confirm the nonce was derived from the same key that signed the TRACE record. Substituting the key in `cnf.jwk` would require also forging the nonce in the hardware attestation report, which is not possible without compromising the TEE silicon. + +The `agentrust-trace` library does not implement CRYPTO-001 nonce verification. That check is part of `cmcp-verify`. + +--- + +## Where to Find the TRACE Record + +cMCP writes the signed TRACE record to the path set in the `CMCP_TRACE_OUTPUT_PATH` environment variable. In a typical deployment: + +```bash +export CMCP_TRACE_OUTPUT_PATH=/var/run/cmcp/session.trace.json +``` + +After the session closes, the file at that path contains the signed TRACE record. + +```python +import json + +with open("/var/run/cmcp/session.trace.json") as f: + record = json.load(f) + +print(record["subject"]) # SPIFFE URI for the agent +print(record["appraisal"]) # {"status": "affirming", ...} +print(record["cnf"]["jwk"]) # public key for verification +``` + +If `CMCP_TRACE_OUTPUT_PATH` is not set, cMCP emits the record to stdout as newline-delimited JSON. + +--- + +## Verify the Record Structure with agentrust-trace + +You can verify the record structure and Ed25519 signature independently of the cMCP hardware chain: + +```python +import json +from agentrust_trace import verify_record, validate_json +from cryptography.exceptions import InvalidSignature + +with open("/var/run/cmcp/session.trace.json") as f: + record = json.load(f) + +# Schema validation +validate_json(record) # raises ValidationError if malformed + +# Signature verification (uses cnf.jwk embedded in the record) +try: + verify_record(record) + print("Ed25519 signature valid") +except InvalidSignature: + print("signature invalid") +``` + +This confirms the record was not tampered with after cMCP signed it. It does not confirm the key came from a legitimate cMCP TEE instance. For that, use `cmcp-verify`. + +--- + +## Full Chain Verification with cmcp-verify + +`cmcp-verify` is a separate package that performs the full TRACE verification chain for cMCP-issued records: + +1. Schema validation (calls `validate_json()` from `agentrust-trace`) +2. Ed25519 signature check (calls `verify_record()` from `agentrust-trace`) +3. TEE attestation report fetch and verification (platform-specific) +4. CRYPTO-001 nonce binding check +5. Policy audit chain verification against the `policy.bundle_hash` +6. Appraisal status evaluation + +```python +from cmcp_verify import verify_trace_claim + +with open("/var/run/cmcp/session.trace.json") as f: + record = json.load(f) + +# verify_trace_claim raises on any failure; returns the verified record on success +verified = verify_trace_claim(record) +print(verified["appraisal"]["status"]) # "affirming" +``` + +`verify_trace_claim()` fetches the TEE attestation report from the RIM URI in `runtime.rim_uri`, so it requires network access to the attestation service for the stated platform. + +--- + +## Division of Responsibility + +| Concern | agentrust-trace | cmcp-verify | +|---|---|---| +| Sign a TRACE record | `sign_record()` | N/A | +| Verify the Ed25519 signature | `verify_record()` | Called internally | +| Validate the JSON schema | `validate_json()`, `iter_errors()` | Called internally | +| TEE measurement verification | Not in scope | Handled | +| CRYPTO-001 nonce binding | Not in scope | Handled | +| Policy audit chain | Not in scope | Handled | +| SCITT transparency receipt | Not in scope | Planned | + +Use `agentrust-trace` directly when you need to sign records or verify the record structure in isolation. Use `cmcp-verify` when you need full assurance that a cMCP-issued record is rooted in a legitimate TEE instance. + +--- + +## Summary + +cMCP generates a TRACE Trust Record at the end of each session, signed with an Ed25519 key that was generated inside the TEE and never exported. The CRYPTO-001 nonce binding ties the key to the TEE attestation report. The signed record is written to `CMCP_TRACE_OUTPUT_PATH`. Use `agentrust-trace` to verify the record structure and Ed25519 signature in isolation. Use `cmcp-verify` for the full chain: hardware attestation, nonce binding, and policy audit verification. + +Related tutorials: + +- [Sign your first trust record](signing-your-first-trust-record.md) +- [Verify a trust record](verifying-a-trust-record.md) +- [Hardware attestation platforms](hardware-attestation-platforms.md) diff --git a/docs/tutorials/signing-your-first-trust-record.md b/docs/tutorials/signing-your-first-trust-record.md new file mode 100644 index 0000000..1cc74de --- /dev/null +++ b/docs/tutorials/signing-your-first-trust-record.md @@ -0,0 +1,190 @@ +# Sign Your First Trust Record + +Generate an Ed25519 signing key and produce a signed TRACE Trust Record that any verifier can check offline. + +## What you'll learn + +- How to generate a signing key and export its public JWK +- Which fields a minimal valid TrustRecord requires +- How `sign_record()` constructs the signature and embeds `cnf.jwk` +- Why RFC 8785 JCS canonical form matters and how the library handles it +- How to verify the signed record with `verify_record()` + +## Prerequisites + +```bash +pip install agentrust-trace +``` + +--- + +## Generate a Key + +`generate_key()` returns an `Ed25519PrivateKey` from the `cryptography` library. Keep the private key secret. Distribute only the public half. + +```python +from agentrust_trace import generate_key, key_to_jwk + +key = generate_key() +jwk = key_to_jwk(key) +print(jwk) +# {'kty': 'OKP', 'crv': 'Ed25519', 'x': ''} +``` + +`key_to_jwk()` returns the public JWK dict in OKP format (RFC 8037). This is the value that will appear in `cnf.jwk` on every record you sign with this key. + +For production use, persist the private key and load it via the `TRACE_PRIVATE_KEY_PEM` environment variable: + +```python +from agentrust_trace import load_signing_key + +# Reads TRACE_PRIVATE_KEY_PEM if set, otherwise generates an ephemeral key +# (ephemeral keys emit a warning and cannot be re-verified after the process exits) +key = load_signing_key() +``` + +--- + +## Construct a Minimal TrustRecord + +Every TRACE Trust Record requires these top-level fields. There are no optional shortcuts for a conformant record. + +```python +import time + +record = { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": int(time.time()), + "subject": "spiffe://trust.example.org/agent/my-agent", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6", + "version": "20251001", + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:" + "0" * 64, + }, + "policy": { + "bundle_hash": "sha256:b2c3d4e5f6a7b8c9" + "0" * 48, + "enforcement_mode": "enforce", + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 1, + "digest": "sha256:e5f6a7b8c9d0e1f2" + "0" * 48, + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example.org", + }, + "transparency": "https://registry.agentrust.io/claim/placeholder", +} +``` + +A few constraints to keep in mind: + +- `subject` must be a SPIFFE URI (`spiffe://...`) or a DID URI (`did:...`). +- `measurement` must be a `sha256:` or `sha384:` digest string. For `software-only` development records, all-zero digests are conventional. +- `enforcement_mode` must be `"enforce"`, `"advisory"`, or `"silent"`. Omitting the field is not valid. +- `appraisal.status` of `"none"` is correct for software-only Level 0 records. Use `"affirming"` for hardware-attested records. + +--- + +## Sign the Record + +Pass the record dict and the private key to `sign_record()`. It returns a new dict with two additional fields: `cnf.jwk` (populated from the key) and `signature`. + +```python +from agentrust_trace import sign_record + +signed = sign_record(record, key) + +print(signed["cnf"]["jwk"]) # {'kty': 'OKP', 'crv': 'Ed25519', 'x': '...'} +print(signed["signature"]) # base64url string, no padding +``` + +The signature covers every field in the record except `signature` itself. `cnf.jwk` is included in the signed payload, which binds the public key to the record content. + +--- + +## What the Signature Covers + +The library signs the canonical byte representation of the record with the `signature` field removed. Canonicalization follows RFC 8785 JSON Canonicalization Scheme (JCS): + +- Object keys sorted in Unicode code-point order (ascending) +- No whitespace between tokens +- Numbers serialized in IEEE 754 double-precision shortest form + +`json.dumps(record, sort_keys=True)` produces a different byte sequence than JCS for Unicode keys whose sort order differs between Python and Unicode code-point order. The library uses `_canonical_bytes()` internally, which calls `json.dumps(..., sort_keys=True, separators=(",", ":"), ensure_ascii=True)`. For plain ASCII keys this matches JCS. Records with non-ASCII keys require a dedicated JCS library; use ASCII-only field names to stay portable. + +The spec (section 3.2.2) requires JCS canonical form. Do not reimplement this by hand. + +--- + +## Verify the Signed Record + +`verify_record()` extracts `cnf.jwk`, recomputes the canonical bytes, and checks the Ed25519 signature. It raises `cryptography.exceptions.InvalidSignature` if the record was tampered with, and returns `None` on success. + +```python +from agentrust_trace import verify_record +from cryptography.exceptions import InvalidSignature + +try: + verify_record(signed) + print("signature valid") +except InvalidSignature: + print("tampered — do not trust this record") +``` + +To confirm that tampered records are rejected: + +```python +import copy + +tampered = copy.deepcopy(signed) +tampered["data_class"] = "public" # change a field after signing + +try: + verify_record(tampered) +except InvalidSignature: + print("correctly rejected") # this branch runs +``` + +--- + +## Validate the Schema + +Signature verification and schema validation are separate steps. A record can have a valid signature but still violate the JSON Schema (for example, a malformed digest string). Call `validate_json()` to check conformance: + +```python +from agentrust_trace import validate_json +import jsonschema + +try: + validate_json(signed) + print("schema valid") +except jsonschema.ValidationError as e: + print(e.message) +``` + +For all violations at once instead of failing on the first: + +```python +from agentrust_trace import iter_errors + +errors = iter_errors(signed) +for e in errors: + print(e.message) +``` + +--- + +## Summary + +You generated an Ed25519 key, built a minimal TRACE Trust Record, signed it with `sign_record()`, and verified it with `verify_record()`. The signature covers all fields except `signature` itself, canonicalized via RFC 8785 JCS. The `cnf.jwk` field embeds the public key so any verifier can check the record offline without a separate key distribution step. + +Next steps: + +- [Verify a trust record received from a third party](verifying-a-trust-record.md) +- [Hardware attestation platforms](hardware-attestation-platforms.md) diff --git a/docs/tutorials/verifying-a-trust-record.md b/docs/tutorials/verifying-a-trust-record.md new file mode 100644 index 0000000..acf77d2 --- /dev/null +++ b/docs/tutorials/verifying-a-trust-record.md @@ -0,0 +1,257 @@ +# Verify a Trust Record + +Check the integrity and schema conformance of a TRACE Trust Record you received from a third party, and decide whether to trust the agent session it describes. + +## What you'll learn + +- What `verify_record()` does internally, step by step +- How to verify against a pinned trusted key instead of the embedded key +- How to run schema validation separately from signature verification +- How to collect all validation errors with `iter_errors()` +- How to interpret `runtime.platform` to distinguish development from hardware-attested records +- What to do when verification fails + +## Prerequisites + +```bash +pip install agentrust-trace +``` + +--- + +## Understand What verify_record() Does + +When you call `verify_record(record)` without a second argument, the library: + +1. Reads `record["signature"]` and base64url-decodes it to raw bytes +2. Reads `record["cnf"]["jwk"]` and reconstructs an `Ed25519PublicKey` from the `x` field +3. Rebuilds the canonical payload: all fields except `signature`, serialized with sorted keys and no whitespace +4. Calls `Ed25519PublicKey.verify(sig_bytes, payload_bytes)` from the `cryptography` library +5. Returns `None` on success, raises `cryptography.exceptions.InvalidSignature` on failure + +The public key is embedded in the record itself via `cnf.jwk`. This means any party can verify the record offline, without contacting the issuer. + +```python +import json +from agentrust_trace import verify_record +from cryptography.exceptions import InvalidSignature + +with open("session.trace.json") as f: + record = json.load(f) + +try: + verify_record(record) + print("signature valid") +except InvalidSignature: + print("signature invalid — record may have been tampered with") +except ValueError as e: + print(f"record malformed: {e}") +``` + +`ValueError` is raised when the record is missing a `signature` field or the `cnf.jwk` cannot be decoded. Treat both as verification failure. + +--- + +## Verify Against a Pinned Public Key + +The embedded `cnf.jwk` proves the record was not tampered with after signing. It does not prove the key is one you should trust. For stricter verification, supply your own trusted public key as the second argument. + +Pass either an `Ed25519PublicKey` object or a JWK dict you obtained out-of-band (for example, from the issuer's published key manifest): + +```python +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +import base64 + +# Trusted JWK obtained from the issuer's key directory +trusted_jwk = { + "kty": "OKP", + "crv": "Ed25519", + "x": "", +} + +# verify_record accepts a JWK dict directly +verify_record(record, trusted_jwk) +``` + +Or with an `Ed25519PublicKey` object: + +```python +x_bytes = base64.urlsafe_b64decode(trusted_jwk["x"] + "==") +pub_key = Ed25519PublicKey.from_public_bytes(x_bytes) + +verify_record(record, pub_key) +``` + +When you supply a key, the library uses it instead of `cnf.jwk`. If the record was signed with a different key, `InvalidSignature` is raised. + +--- + +## Validate the Schema + +Signature verification confirms the record was not modified after signing. It says nothing about whether the record conforms to the TRACE v0.1 schema. A valid signature over a malformed record is still a malformed record. + +Call `validate_json()` to check schema conformance. It raises `jsonschema.ValidationError` on the first violation: + +```python +from agentrust_trace import validate_json +import jsonschema + +try: + validate_json(record) +except jsonschema.ValidationError as e: + print(f"schema violation: {e.message}") + print(f"at path: {list(e.absolute_path)}") +``` + +To collect all violations at once instead of stopping at the first: + +```python +from agentrust_trace import iter_errors + +errors = iter_errors(record) +if errors: + for e in errors: + print(f"{list(e.absolute_path)}: {e.message}") +else: + print("schema valid") +``` + +Run schema validation before trusting any claims in the record. A record that passes both `verify_record()` and `validate_json()` was signed by the stated key and has a well-formed structure. + +--- + +## Check the EAT Profile + +Every TRACE record carries an `eat_profile` field that identifies the spec version. Reject records with an unexpected profile before parsing their claims: + +```python +EXPECTED_PROFILE = "tag:agentrust.io,2026:trace-v0.1" + +if record.get("eat_profile") != EXPECTED_PROFILE: + raise ValueError(f"unexpected eat_profile: {record.get('eat_profile')!r}") +``` + +--- + +## Interpret the Appraisal Status + +After verifying the signature and schema, read `appraisal.status`: + +```python +status = record["appraisal"]["status"] + +if status == "affirming": + # All evidence passed appraisal. Safe to act on the session output. + pass +elif status == "warning": + # Evidence passed with conditions. Review before acting. + pass +elif status == "contraindicated": + # Evidence failed. Treat the session output as untrusted. + raise RuntimeError("appraisal contraindicated — do not process agent output") +elif status == "none": + # No appraisal performed (software-only Level 0 record). + # Acceptable for development; not acceptable for production. + pass +``` + +--- + +## Distinguish Software-Only from Hardware-Attested Records + +The `runtime.platform` field tells you the attestation root. Before trusting a record in a production context, confirm it is not a development record: + +```python +runtime = record["runtime"] + +if runtime["platform"] == "software-only": + # All-zero measurement, no TEE binding. Only accept in dev/test. + raise ValueError("software-only records are not accepted in production") + +# Hardware-attested platforms +HARDWARE_PLATFORMS = { + "intel-tdx", + "amd-sev-snp", + "nvidia-h100", + "nvidia-blackwell", + "aws-nitro", + "arm-cca", + "google-confidential-space", + "tpm2", +} + +if runtime["platform"] not in HARDWARE_PLATFORMS: + raise ValueError(f"unknown platform: {runtime['platform']!r}") + +print(f"platform: {runtime['platform']}") +print(f"measurement: {runtime['measurement']}") +``` + +For hardware-attested records, `runtime.measurement` is a real digest from the TEE. To confirm the key was generated inside the attested enclave, compare `runtime.measurement` against the published Reference Integrity Manifest at `runtime.rim_uri`. See [Hardware attestation platforms](hardware-attestation-platforms.md) for per-platform details. + +--- + +## Complete Verification Sequence + +Put it together for a production verifier: + +```python +import json +from agentrust_trace import verify_record, validate_json, iter_errors +from cryptography.exceptions import InvalidSignature +import jsonschema + +def verify_trust_record(path: str, trusted_jwk: dict | None = None) -> dict: + with open(path) as f: + record = json.load(f) + + # 1. Schema validation first — reject malformed records early + errors = iter_errors(record) + if errors: + messages = [e.message for e in errors] + raise ValueError(f"schema violations: {messages}") + + # 2. Signature verification + try: + verify_record(record, trusted_jwk) + except InvalidSignature: + raise RuntimeError("signature invalid — record tampered or wrong key") + except ValueError as e: + raise RuntimeError(f"record malformed: {e}") + + # 3. Profile check + if record.get("eat_profile") != "tag:agentrust.io,2026:trace-v0.1": + raise ValueError(f"unexpected eat_profile: {record.get('eat_profile')!r}") + + # 4. Appraisal + status = record["appraisal"]["status"] + if status == "contraindicated": + raise RuntimeError("appraisal contraindicated — do not process agent output") + + return record +``` + +--- + +## What to Do When Verification Fails + +If `verify_record()` raises `InvalidSignature`: + +- Do not process the agent output. +- Do not rely on any claim in the record. +- Log the failure with the record's `subject` and `iat` fields for audit purposes. +- Investigate whether the record was modified in transit or the wrong key was used. + +A failed signature means either the record was tampered with after issuance, or it was not signed by the key in `cnf.jwk`. Either way, the record cannot be trusted. + +--- + +## Summary + +You verified a TRACE Trust Record by checking its Ed25519 signature, validating its schema, and interpreting the appraisal status. Signature verification uses the embedded `cnf.jwk` by default; pass a trusted key to pin verification to a specific issuer. Schema validation with `validate_json()` or `iter_errors()` is a separate step that confirms the record structure is well-formed. + +Related tutorials: + +- [Sign your first trust record](signing-your-first-trust-record.md) +- [Hardware attestation platforms](hardware-attestation-platforms.md) +- [Integration with cMCP](integrating-with-cmcp.md) diff --git a/mkdocs.yml b/mkdocs.yml index 3634445..e06dbdd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -150,6 +150,11 @@ nav: - Quickstart: docs/quickstart.md - Verification: docs/verification.md - Schema Reference: docs/schema.md + - Tutorials: + - Sign your first trust record: docs/tutorials/signing-your-first-trust-record.md + - Verify a trust record: docs/tutorials/verifying-a-trust-record.md + - Hardware attestation platforms: docs/tutorials/hardware-attestation-platforms.md + - Integration with cMCP: docs/tutorials/integrating-with-cmcp.md - Specification: spec/trace-v0.1.md - Integration: - AGT: docs/integration/agt.md