From 9ff96996afa95d928ba81d594f7fe761e9861550 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sat, 5 Sep 2026 23:52:18 -0700 Subject: [PATCH 1/3] docs: repair verification tutorials and evidence boundaries Signed-off-by: Imran Siddique --- CHARTER.md | 4 +- CONTRIBUTING.md | 2 +- GOVERNANCE.md | 2 +- Governance/License.md | 2 +- ROADMAP.md | 2 +- docs/crosswalks/acta-decision-receipts.md | 10 +- docs/integration/agt.md | 145 +-------- docs/integration/cmcp.md | 147 +--------- docs/platforms/amd-sev-snp.md | 64 +--- docs/platforms/index.md | 39 +-- docs/platforms/intel-tdx.md | 67 +---- docs/platforms/nvidia-h100.md | 75 +---- docs/trust-levels.md | 119 ++------ docs/tutorials/agt-adapter.md | 201 +++---------- docs/tutorials/anchoring-to-the-registry.md | 25 +- .../hardware-attestation-platforms.md | 222 ++------------ docs/tutorials/integrating-with-cmcp.md | 173 +---------- .../signing-your-first-trust-record.md | 190 +----------- docs/tutorials/verifying-a-trust-record.md | 260 ++-------------- docs/tutorials/verifying-the-audit-chain.md | 277 +++--------------- docs/verification-outcome-statements.md | 2 +- docs/verification.md | 143 +++------ spec/trace-v0.2.md | 2 +- tests/test_docs_quickstart.py | 37 +++ 24 files changed, 345 insertions(+), 1865 deletions(-) diff --git a/CHARTER.md b/CHARTER.md index b2b4a7e6..f139353e 100644 --- a/CHARTER.md +++ b/CHARTER.md @@ -43,9 +43,9 @@ Upon host organization acceptance, governance transitions from the current Proje ## 4. Intellectual Property Policy -All contributions must be made under the terms of [LICENSE](LICENSE). Contributors must sign commits with the Developer Certificate of Origin (DCO). No contribution may incorporate material covered by a patent the contributor is unwilling to license royalty-free to conforming implementations. +All contributions must be made under the terms of [LICENSE](https://github.com/agentrust-io/trace-spec/blob/main/LICENSE). Contributors must sign commits with the Developer Certificate of Origin (DCO). No contribution may incorporate material covered by a patent the contributor is unwilling to license royalty-free to conforming implementations. -Normative specification text and the normative TRACE JSON Schema are licensed under the Community Specification License 1.0. Source code, examples, workflows, and tests are licensed under Apache License 2.0. Documentation other than specification materials is licensed under CC BY 4.0. Earlier specification publications remain available under the licenses stated when they were published. See [LICENSE](LICENSE) and the [license map](Governance/License.md). +Normative specification text and the normative TRACE JSON Schema are licensed under the Community Specification License 1.0. Source code, examples, workflows, and tests are licensed under Apache License 2.0. Documentation other than specification materials is licensed under CC BY 4.0. Earlier specification publications remain available under the licenses stated when they were published. See [LICENSE](https://github.com/agentrust-io/trace-spec/blob/main/LICENSE) and the [license map](Governance/License.md). ## 5. Trademark Policy diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6878b5b4..e21dd09a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,4 +109,4 @@ Maintainer response targets are commitments to you, not minimums. Ping the PR if Anyone who submits a PR, files an issue, or participates in discussion on the repository is a Contributor bound by the license terms. -Specification contributions are made under the [Community Specification License 1.0](Governance/COMMUNITY-SPECIFICATION-LICENSE.md) and the [Community Specification Contributor License Agreement](Governance/CLA.md). Source-code contributions are made under Apache License 2.0. Documentation contributions other than specification text are made under CC BY 4.0. You keep the copyright in your contributions: no contributor is asked to assign copyright to the project. See [LICENSE](LICENSE), the [license map](Governance/License.md), and [General Project Policies](GOVERNANCE.md#general-project-policies). +Specification contributions are made under the [Community Specification License 1.0](Governance/COMMUNITY-SPECIFICATION-LICENSE.md) and the [Community Specification Contributor License Agreement](Governance/CLA.md). Source-code contributions are made under Apache License 2.0. Documentation contributions other than specification text are made under CC BY 4.0. You keep the copyright in your contributions: no contributor is asked to assign copyright to the project. See [LICENSE](https://github.com/agentrust-io/trace-spec/blob/main/LICENSE), the [license map](Governance/License.md), and [General Project Policies](GOVERNANCE.md#general-project-policies). diff --git a/GOVERNANCE.md b/GOVERNANCE.md index a5955424..94e37d08 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -12,7 +12,7 @@ All code contributions to the Project must be made under the Apache License, Ver All documentation (excluding specifications) will be made available under the Creative Commons Attribution 4.0 International license, available at: https://creativecommons.org/licenses/by/4.0. -Specification text published before this policy took effect stays available under the license under which it was published. See [LICENSE](LICENSE) and the [license map](Governance/License.md). +Specification text published before this policy took effect stays available under the license under which it was published. See [LICENSE](https://github.com/agentrust-io/trace-spec/blob/main/LICENSE) and the [license map](Governance/License.md). ## Roles diff --git a/Governance/License.md b/Governance/License.md index 8931b4c9..25552a10 100644 --- a/Governance/License.md +++ b/Governance/License.md @@ -10,7 +10,7 @@ TRACE specification materials are subject to the [Community Specification Licens - the normative TRACE JSON Schema at `schema/trace-claim.json`; and - other files expressly identified as part of a Draft Specification or Approved Specification. -Earlier specification publications remain available under the licenses stated when they were published. See the repository [LICENSE](../LICENSE) for that history. +Earlier specification publications remain available under the licenses stated when they were published. See the repository [LICENSE](https://github.com/agentrust-io/trace-spec/blob/main/LICENSE) for that history. ## Source Code License diff --git a/ROADMAP.md b/ROADMAP.md index b39a85ad..e28783cd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,7 +12,7 @@ Status as of August 2026. Spec **v0.2** is current ([`spec/trace-v0.2.md`](spec/ - **OWASP Agentic AI Top 10 cross-walk**: [`docs/crosswalks/owasp-agentic-top-10.md`](docs/crosswalks/owasp-agentic-top-10.md). - **Acta decision-receipt cross-walk**: [`docs/crosswalks/acta-decision-receipts.md`](docs/crosswalks/acta-decision-receipts.md). - **Producer adapters** for AGT, cMCP, and sandboxed agent runtimes, one code path spanning Level 0 and Level 1. -- **Platform bindings documented** for AMD SEV-SNP, Intel TDX, and NVIDIA H100 ([`docs/platforms/`](docs/platforms/)). This SDK verifies the record; verification of the attestation evidence itself lives in `cmcp` and `agent-manifest`, both of which have been run against genuine hardware quotes. +- **Platform bindings documented** for AMD SEV-SNP, Intel TDX, and NVIDIA H100 ([`docs/platforms/`](docs/platforms/index.md)). This SDK verifies the record; verification of the attestation evidence itself lives in `cmcp` and `agent-manifest`, both of which have been run against genuine hardware quotes. - **Reference implementation.** cMCP enforces Cedar policy inside the TEE and emits signed GatewayClaims carrying `policy`, `data_class`, and `tool_transcript`. ## Next: v0.3 diff --git a/docs/crosswalks/acta-decision-receipts.md b/docs/crosswalks/acta-decision-receipts.md index 8f280c0e..9d3affc5 100644 --- a/docs/crosswalks/acta-decision-receipts.md +++ b/docs/crosswalks/acta-decision-receipts.md @@ -6,7 +6,7 @@ ## Purpose -[Spec section 3.3.2](../../spec/trace-v0.1.md#332-action-receipts-for-embodied-workflows-informative) describes action receipts as evidence that sits below a Trust Record: a per-action signed statement, bound to a call or session, that a verifier checks independently of the core record. The [embodied-workflow fixtures](../../examples/action-receipts/README.md) show one profile of that pattern, for a physical controller (a robot arm, a safety monitor) signing an assertion about a physical action. +[Spec section 3.3.2](../../spec/trace-v0.1.md#332-action-receipts-for-embodied-workflows-informative) describes action receipts as evidence that sits below a Trust Record: a per-action signed statement, bound to a call or session, that a verifier checks independently of the core record. The [embodied-workflow fixtures](https://github.com/agentrust-io/trace-spec/blob/main/examples/action-receipts/README.md) show one profile of that pattern, for a physical controller (a robot arm, a safety monitor) signing an assertion about a physical action. This cross-walk describes a second profile of the same pattern: an **Acta decision receipt**, evidencing a software decision (an AI agent's tool call, decided by a local policy gate before it runs) rather than a physical one. The verification pattern in 3.3.2 is deliberately domain-agnostic; the intent here is to show it composes with an existing, independently specified receipt format without a wire-format change to either side. @@ -43,11 +43,11 @@ Chain semantics (Acta s5.7): `previousReceiptHash` is the bare lowercase hex SHA `decision` is `allow`, `deny`, or `rate_limit`; a `deny` receipt is signed with the same rigor as an `allow`, which is the property [trace-spec#95](https://github.com/agentrust-io/trace-spec/issues/95)'s "valid negative controller outcome" case and this profile's `02-valid-denied.json` fixture both exercise. -Working fixtures: [`examples/action-receipts/acta/`](../../examples/action-receipts/acta/). +Working fixtures: [`examples/action-receipts/acta/`](https://github.com/agentrust-io/trace-spec/tree/main/examples/action-receipts/acta/). ## Field mapping -Field names on the left are exact TRACE terms as used in [spec section 3.3.2](../../spec/trace-v0.1.md#332-action-receipts-for-embodied-workflows-informative) and the [embodied fixture shape](../../examples/action-receipts/README.md#shared-receipt-shape). Field names on the right are exact Acta draft-02 fields. +Field names on the left are exact TRACE terms as used in [spec section 3.3.2](../../spec/trace-v0.1.md#332-action-receipts-for-embodied-workflows-informative) and the [embodied fixture shape](https://github.com/agentrust-io/trace-spec/blob/main/examples/action-receipts/README.md#shared-receipt-shape). Field names on the right are exact Acta draft-02 fields. | TRACE action-receipt obligation | Acta field(s) | Notes | |---|---|---| @@ -78,7 +78,7 @@ The `chain_head` value above is real: it is the recomputable s5.7 envelope hash ## Verifier obligations, restated for this profile -Following the same checks [the embodied fixture README](../../examples/action-receipts/README.md#shared-receipt-shape) lists: +Following the same checks [the embodied fixture README](https://github.com/agentrust-io/trace-spec/blob/main/examples/action-receipts/README.md#shared-receipt-shape) lists: 1. Extract `payload` and compute its JCS-canonical bytes (Acta s4.1). 2. Resolve `signature.kid` through a pinned or manifest-bound key set, not a key carried in the receipt, and check `payload.issuer_id` matches `signature.kid`. @@ -98,7 +98,7 @@ An Acta decision receipt proves that a specific policy decision, over a specific ## Conformance fixtures -Six real fixtures in [`examples/action-receipts/acta/`](../../examples/action-receipts/acta/), generated by an actual Ed25519 signer (generator committed alongside), covering the negative cases raised in [trace-spec#97](https://github.com/agentrust-io/trace-spec/issues/97) and [trace-spec#95](https://github.com/agentrust-io/trace-spec/issues/95): valid accepted, valid denied (negative controller-equivalent outcome), signature/key mismatch (mismatched key committed), broken chain (validly signed, wrong predecessor hash), stale policy digest, and mismatched session binding. Expected outcomes are machine-readable in `expected.json`, and [`tests/test_acta_fixtures.py`](https://github.com/agentrust-io/trace-spec/blob/main/tests/test_acta_fixtures.py) re-verifies every fixture in CI against the draft-02 envelope and the declared positive/negative results, using this repository's existing `rfc8785` and `cryptography` dependencies, so fixture or envelope drift fails the build. +Six real fixtures in [`examples/action-receipts/acta/`](https://github.com/agentrust-io/trace-spec/tree/main/examples/action-receipts/acta/), generated by an actual Ed25519 signer (generator committed alongside), covering the negative cases raised in [trace-spec#97](https://github.com/agentrust-io/trace-spec/issues/97) and [trace-spec#95](https://github.com/agentrust-io/trace-spec/issues/95): valid accepted, valid denied (negative controller-equivalent outcome), signature/key mismatch (mismatched key committed), broken chain (validly signed, wrong predecessor hash), stale policy digest, and mismatched session binding. Expected outcomes are machine-readable in `expected.json`, and [`tests/test_acta_fixtures.py`](https://github.com/agentrust-io/trace-spec/blob/main/tests/test_acta_fixtures.py) re-verifies every fixture in CI against the draft-02 envelope and the declared positive/negative results, using this repository's existing `rfc8785` and `cryptography` dependencies, so fixture or envelope drift fails the build. ## References diff --git a/docs/integration/agt.md b/docs/integration/agt.md index 65558520..34ebe70f 100644 --- a/docs/integration/agt.md +++ b/docs/integration/agt.md @@ -1,139 +1,26 @@ # Integration: AGT -[AGT (Agent Governance Toolkit)](https://github.com/microsoft/agent-governance-toolkit) is the most widely adopted agent governance framework (4,100+ stars, 100+ contributors). It provides Cedar policy enforcement, SPIFFE/SVID identity, and Merkle-chained audit logs for any agent framework. +The [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit) can supply policy and audit evidence to a TRACE producer. `TraceAGTAdapter` maps supplied session inputs into a standalone software-only record; it does not run the agent or independently verify those inputs. -AGT emits TRACE v0.1 Trust Records via `TRACEAuditSink` in its latest release (`agent-governance-toolkit-core` 5.0.0): see [ADR 0032](https://github.com/microsoft/agent-governance-toolkit/blob/main/docs/adr/0032-agt-emits-trace-v01-trust-records.md) for the full design. AGT's `main` branch has already moved to v0.2, but that change has not shipped in a release yet, and the linked ADR still describes the v0.1 design. +## Choose the integration path -## What AGT emits +Use the [adapter tutorial](../tutorials/agt-adapter.md) for a complete local example with synthetic session data. For a real AGT deployment, capture the exact policy bytes, audit entries, chain tip, and identity from the version you run. Check that version's API and output profile before connecting it to the current TRACE verifier. -AGT emits **Level 0 (software-only)** TRACE records. The record is signed with an Ed25519 key held in the operator's key store, not a TEE-bound key. For Level 2 hardware-rooted records, deploy AGT inside [cMCP](cmcp.md) or another TEE runtime. +A record under the superseded v0.1 EAT profile is not accepted by the current v0.2 verifier. Reissue through a compatible producer rather than relabeling signed bytes. -| TRACE field | Source in AGT | -|---|---| -| `subject` | `agent_did` passed to `govern()` | -| `policy.bundle_hash` | SHA-256 of the Cedar policy bundle bytes at session start | -| `policy.enforcement_mode` | Always `enforce` (Phase 1) | -| `tool_transcript.hash` | SHA-256 of the canonical JSON of the Merkle `AuditEntry` list | -| `tool_transcript.call_count` | Count of `AuditEntry` items in the session | -| `runtime.platform` | `software-only` | -| `runtime.measurement` | SHA-256 of the Merkle chain tip | -| `appraisal.status` | `affirming` (Phase 1) | -| `model`, `data_class`, `build_provenance` | Injected from `TraceConfig` | - -## Install - -```bash -pip install agentmesh agentrust-trace -``` - -## Quick start: TraceAGTAdapter - -`TraceAGTAdapter` eliminates the ~50-line field-mapping boilerplate. Install: - -```bash -pip install agentrust-trace -``` - -One-liner upgrade path for any AGT-governed session: - -```python -from agentrust_trace.adapters import TraceAGTAdapter, AGTSessionResult -from agentrust_trace import sign_record, generate_key - -adapter = TraceAGTAdapter( - model_provider="anthropic", - model_id="claude-sonnet-4-6", - model_version="20251001", - build_provenance_digest="sha256:e5f6a7b8...", - transparency="https://registry.agentrust-io.com/claim/...", -) - -session = AGTSessionResult( - agent_did="spiffe://trust.example.org/agent/my-agent", - policy_bundle_bytes=Path("policy.cedar").read_bytes(), - audit_entries=govern_fn.get_audit_entries(), - merkle_chain_tip=govern_fn.chain_tip, -) - -record = adapter.build_trust_record(session) -signed = sign_record(record, generate_key()) # or load_signing_key() for production -``` - -→ Full walkthrough: [TraceAGTAdapter tutorial](../tutorials/agt-adapter.md) - -## Manual wiring (legacy) - -The following is the raw field-mapping approach: kept for reference. Prefer `TraceAGTAdapter` for new integrations. - -```python -from agentmesh.governance import govern, GovernanceConfig -from agentmesh.governance.trace_sink import TraceConfig - -trace_config = TraceConfig( - output_path="session.trace.json", - model_provider="anthropic", - model_id="claude-sonnet-4-6", - model_version="20251001", - data_class="confidential", - build_provenance_slsa_level=2, - build_provenance_digest="sha256:e5f6a7b8...", -) +## Field mapping -config = GovernanceConfig( - policy_path="policy.cedar", - trace=trace_config, -) - -governed_fn = govern( - my_tool, - agent_did="spiffe://trust.example.org/agent/my-agent", - config=config, -) - -result = governed_fn(input) -path = governed_fn.close_session() -print(f"Trust Record written to: {path}") -``` - -## Key management - -Key material is managed entirely by `agentrust_trace.load_signing_key()`. AGT does not hold keys directly. - -```bash -# Set the signing key path via environment variable -export AGENTRUST_TRACE_KEY_PATH=/run/secrets/trace-signing-key.pem -``` - -For production, store the key in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Kubernetes Secret) and inject via environment variable or volume mount. - -## Verifying the emitted record - -```bash -agentrust-trace verify session.trace.json --pubkey trace-key.pem.pub -``` - -## Upgrading to Level 2 (hardware-rooted) - -Deploy your AGT-governed agent inside cMCP. The cMCP runtime: - -1. Measures the Cedar policy bundle into the TEE before any code runs -2. Generates a TEE-bound key for the TRACE record -3. Emits a Level 2 record that supersedes AGT's Level 0 record for the same session - -The two records are linked by a shared `subject` and `tool_transcript.hash`: AGT's record and cMCP's record are mutually verifiable. - -→ [Integration guide: cMCP](cmcp.md) - -## Framework support +| Input | TRACE commitment | +|---|---| +| Agent SPIFFE URI or DID | `subject` | +| Exact policy bytes | SHA-256 in `policy.bundle_hash` | +| Canonical audit-entry list | SHA-256 in `tool_transcript.hash` | +| Entry count, or explicit override | `tool_transcript.call_count` | +| UTF-8 chain-tip string | SHA-256 software commitment in `runtime.measurement` | +| Deployment metadata | Model, classification, and build-provenance declarations | -AGT instruments any Python callable via `govern()`. Confirmed compatible frameworks: +The adapter uses RFC 8785 for transcript canonicalization. It currently accepts a transparency string but does not submit to a registry, and populates an `affirming` appraisal without independently evaluating the session. A producer must accurately set those fields before signing; the tutorial shows how to avoid claiming an appraisal or anchor for synthetic input. -| Framework | Notes | -|---|---| -| LangGraph | Wrap tool nodes with `govern()` | -| CrewAI | Wrap task tools | -| AutoGen | Wrap function tools | -| Mastra | Via adapter contract | -| Raw Python | Direct function wrapping | +## Assurance -For the full integration list, see the [AGT integrations repo](https://github.com/agentrust-io/integrations). +Software signing authenticates the producer's statement when the recipient trusts its key. Hardware evidence requires a separately verified runtime and key binding. Level 2 additionally requires transparency anchoring. Placing an AGT application near a cMCP gateway does not automatically create matching or superseding records; use the [cMCP integration guide](cmcp.md) for that runtime's boundary. diff --git a/docs/integration/cmcp.md b/docs/integration/cmcp.md index dac32f56..1817a413 100644 --- a/docs/integration/cmcp.md +++ b/docs/integration/cmcp.md @@ -1,148 +1,23 @@ # Integration: cMCP -[Confidential MCP (cMCP)](https://github.com/agentrust-io/cmcp) is the reference implementation of TRACE Level 2. It is an MCP gateway that: +[cMCP](https://cmcp.agentrust-io.com/) is a gateway between an MCP client and its tool servers. It evaluates routed calls against Cedar policy, blocks policy denials in enforcing mode, records decisions, and signs a session claim when the session closes. The client and upstream tools remain outside the gateway's enforcement boundary. -1. Intercepts every tool call from any MCP-compatible agent -2. Evaluates Cedar policy inside a hardware TEE (AMD SEV-SNP, Intel TDX, NVIDIA H100) -3. Measures the policy bundle into the TEE before any code runs -4. Signs the TRACE Trust Record with a TEE-bound Ed25519 key -5. Anchors the record in the TRACE transparency registry (optional) +## Start with a local call -The result: a hardware-verifiable governance record for every agent session: signed by silicon, not by an operator process. +Use the [cMCP quick start](https://cmcp.agentrust-io.com/quickstart/) to run an allow/deny example without hardware. Its software-mode evidence does not establish hardware isolation. The [architecture page](https://cmcp.agentrust-io.com/concepts/) shows the request path and trust boundaries. -## Architecture +cMCP only governs traffic routed through it. Other tool connections are not covered by its policy decision or session transcript. -``` - Agent (LangGraph, CrewAI, AutoGen, ...) - │ MCP tool call - ▼ - ┌─────────────────────────────────────┐ - │ cMCP Gateway (TEE boundary) │ - │ ┌────────────────────────────────┐ │ - │ │ Cedar policy evaluation │ │ - │ │ → allow / deny / escalate │ │ - │ └────────────────────────────────┘ │ - │ ┌────────────────────────────────┐ │ - │ │ Tool-call transcript signing │ │ - │ │ TEE-bound key (cnf.jwk) │ │ - │ └────────────────────────────────┘ │ - │ ┌────────────────────────────────┐ │ - │ │ TRACE Level 2 record emission │ │ - │ └────────────────────────────────┘ │ - └─────────────────────────────────────┘ - │ Forwarded tool call - ▼ - MCP Tool Server (outside TEE) -``` +## Verify the correct envelope -## Conformance level +cMCP's `RuntimeClaim` contains nested TRACE fields and runtime-specific evidence. It is not the flat standalone object accepted by `agentrust_trace.verify_record`. Use `cmcp_verify.verify_trace_claim` with independently approved policy and catalog hashes and the required evidence inputs. -cMCP emits **TRACE Level 2** records: +The [verification walkthrough](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/) explains the result states. A software-only or incomplete hardware result must not be promoted to `verified` by the consumer. -| Property | Level 0 (AGT) | Level 2 (cMCP) | -|---|---|---| -| Policy hash | ✓ SHA-256 | ✓ SHA-256, TEE-measured | -| Signing key | Software key | TEE-bound key (never leaves enclave) | -| `runtime.platform` | `software-only` | `amd-sev-snp` / `intel-tdx` / `nvidia-h100` | -| Hardware measurement | ✗ | ✓ `runtime.measurement` | -| Independent verifiability | Key management by operator | Hardware endorsement chain | +## Hardware and transparency -## Quick start +Hardware assurance depends on the configured provider, actual evidence, key binding, expected measurements, and successful appraisal. Deployment on a confidential VM alone is insufficient; consult [hardware validation](https://cmcp.agentrust-io.com/testing/hardware-validation/). -```bash -docker pull ghcr.io/agentrust-io/cmcp:latest +Level 2 additionally requires transparency anchoring. A cMCP session is not automatically Level 2 because it ran on hardware. Nor does it automatically supersede an AGT record or guarantee matching transcript hashes: their producing profiles and envelopes must be reconciled explicitly. -docker run --device /dev/sev \ - -e CEDAR_POLICY_PATH=/policies/my-policy.cedar \ - -e UPSTREAM_MCP_URL=http://my-mcp-server:8080 \ - -p 8443:8443 \ - -v $(pwd)/policies:/policies \ - ghcr.io/agentrust-io/cmcp:latest -``` - -Your agent points at `https://localhost:8443` instead of the upstream MCP server. Zero code change. - -## Cedar policy example - -```cedar -// Allow credit-risk agent to call financial tools: deny if data class is secret -permit( - principal == Agent::"spiffe://trust.example.org/agent/credit-risk", - action == Action::"call_tool", - resource in Tools::"financial" -) -when { - context.data_class != "secret" -}; -``` - -Cedar policies are versioned, code-reviewable, and their SHA-256 hash is bound into the TRACE record at the TEE measurement step: before any code runs. - -## Connect an MCP-compatible agent - -```python -import anthropic - -# Point at cMCP gateway instead of your MCP server -client = anthropic.Anthropic() -response = client.beta.messages.create( - model="claude-sonnet-4-6", - max_tokens=1024, - tools=[{ - "type": "mcp", - "server_url": "https://localhost:8443", # cMCP gateway - }], - messages=[{"role": "user", "content": "Analyze this credit application"}], - betas=["mcp-client-2025-04-04"], -) -``` - -## Retrieve the TRACE record - -After the session, fetch the TRACE record from the gateway: - -```python -import httpx - -record = httpx.get("https://localhost:8443/trace/latest").json() -# → full TRACE v0.2 Trust Record, Level 2, signed by TEE-bound key -``` - -Or let cMCP push it to the transparency registry automatically: - -```yaml -# cmcp.yaml -trace: - emit: true - registry: https://registry.agentrust-io.com - scitt_anchor: true -``` - -## Hardware platform support - -| Platform | Status | -|---|---| -| AMD SEV-SNP | ✓ GA | -| Intel TDX | ✓ GA | -| NVIDIA H100 Confidential | ✓ GA (demonstrated at GTC Berlin) | -| NVIDIA Blackwell | Preview | -| TPM 2.0 (software-only TEE) | ✓ GA: development mode, no memory encryption | -| Azure CVM (SEV-SNP) | ✓ GA | -| GCP Confidential VM (TDX) | ✓ GA | -| AWS Nitro Enclave | Preview | - -## Relationship to AGT - -cMCP embeds AGT. The Cedar policy engine, SPIFFE identity, and Merkle audit chain are AGT. cMCP adds the TEE boundary, hardware key generation, and Level 2 TRACE emission. - -When cMCP emits a Level 2 record for a session, it supersedes any Level 0 record AGT might have emitted for the same session. The two records are linked by shared `subject` and `tool_transcript.hash`. - -## Related - -| What | Where | -|------|-------| -| Trust level guarantees | [Trust Levels](../trust-levels.md) | -| Hardware attestation platforms | [Platforms overview](../platforms/index.md) | -| Step-by-step cMCP integration tutorial | [Integrating with cMCP](../tutorials/integrating-with-cmcp.md) | -| cMCP source and deployment | [agentrust-io/cmcp](https://github.com/agentrust-io/cmcp) | -| AGT integration | [Integration: AGT](agt.md) | +Continue to [trust levels](../trust-levels.md) or [AGT integration](agt.md). diff --git a/docs/platforms/amd-sev-snp.md b/docs/platforms/amd-sev-snp.md index f1dbddf6..fca0d8e4 100644 --- a/docs/platforms/amd-sev-snp.md +++ b/docs/platforms/amd-sev-snp.md @@ -1,65 +1,21 @@ # Platform: AMD SEV-SNP -AMD SEV-SNP (Secure Encrypted Virtualization - Secure Nested Paging) is the primary hardware root of trust for TRACE Level 2 records on AMD-based cloud instances and on-premises servers. +A signed SEV-SNP report supplies evidence about a confidential VM. To use it for TRACE, a verifier must check the report's authenticity, accepted platform state and measurement, freshness, and binding to the record-signing key. A digest copied into JSON is not that verification. -## What AMD SEV-SNP provides +## Measurement and key binding -| Property | Detail | -|---|---| -| Memory encryption | AES-128 per-VM encrypted memory | -| Attestation report | Signed by AMD's VCEK (chip-unique key) | -| Measurement | SHA-384 of the VM image + policy | -| Replay protection | Nonce-based freshness | +The SNP report's `MEASUREMENT` field is a 48-byte launch measurement. It is distinct from guest-supplied `REPORT_DATA`, which a profile can use to bind a key and challenge. See Google's [SEV-SNP ABI implementation](https://pkg.go.dev/github.com/google/go-sev-guest/abi) for the field sizes. -## TRACE fields populated by SEV-SNP +An expected launch measurement must be independently approved. A certificate-distribution endpoint supplies signing collateral; it is not a Reference Integrity Manifest describing the expected workload. Matching a report's measurement to a record also does not establish what that measurement represents without the producing profile and reference values. -```json -{ - "runtime": { - "platform": "amd-sev-snp", - "measurement": "sha384:c9e4b1d2e3f4a5b6...", - "rim_uri": "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain", - "firmware_version": "1.53.0", - "nonce": "ZRVkXG1w..." - } -} -``` +## TRACE representation -- `measurement`: SHA-384 of the SNP attestation report's `measurement` field (the VM image digest) -- `rim_uri`: AMD Key Distribution Service URL for VCEK certificate chain verification -- `firmware_version`: SNP firmware version embedded in the attestation report -- `nonce`: replay-protection nonce from the attestation challenge +Standalone records use `runtime.platform="amd-sev-snp"`, or `azure-cvm-sev-snp` when the producing profile requires that identifier. `runtime.measurement` uses the schema's digest syntax; the producer must define its relationship to the verified report. Do not hash an already-computed measurement again unless the profile explicitly defines that transformation. -## Verification flow +Hardware appraisal supports Level 1. Level 2 adds a separately verified transparency anchor. See [trust levels](../trust-levels.md). -To verify a SEV-SNP TRACE record offline: +## Deployment and verification -1. Parse `runtime.rim_uri` and fetch the VCEK certificate chain from AMD KDS -2. Verify the VCEK chain up to AMD's root CA (publicly available) -3. Verify the SNP attestation report signature using the VCEK certificate -4. Compare `runtime.measurement` against the report's `measurement` field -5. Confirm `cnf.jwk` was generated inside the enclave at that measurement +Cloud support depends on machine family, region, firmware, and guest configuration. For GCP, consult the current [Confidential VM configurations](https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/supported-configurations). C3 is an Intel TDX family; N2D is used for AMD SEV-SNP. -```bash -# cMCP does steps 1-5 automatically and embeds the result in the TRACE record -agentrust-trace verify-hardware session.trace.json \ - --platform amd-sev-snp \ - --check-rim -``` - -## Supported cloud instances - -| Cloud | Instance family | -|---|---| -| Azure | DCasv5, ECasv5, DCadsv5, ECadsv5 | -| GCP | C3 (with AMD SEV-SNP enabled) | -| AWS | Not supported (AWS uses Nitro: separate profile) | -| On-premises | Any server with EPYC Genoa / Bergamo or newer | - -## On-premises deployment - -For on-premises SEV-SNP (e.g., Supermicro H13 with EPYC Genoa), OPAQUE ships the verifier with the platform: same cryptographic guarantees as cloud deployments, no cloud attestation service dependency. See [agentrust-io/cmcp](https://github.com/agentrust-io/cmcp) for the Helm chart. - -## Example record - -See [`examples/amd-sev-snp.json`](https://github.com/agentrust-io/trace-spec/blob/main/examples/amd-sev-snp.json) for a complete TRACE Level 2 record from an AMD SEV-SNP deployment. +For the actual AgenTrust implementation and recorded hardware runs, use [cMCP hardware validation](https://cmcp.agentrust-io.com/testing/hardware-validation/) and its [verification tutorial](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/). The TRACE SDK's signature verifier does not perform this hardware appraisal. diff --git a/docs/platforms/index.md b/docs/platforms/index.md index 68e1c11e..b40464bf 100644 --- a/docs/platforms/index.md +++ b/docs/platforms/index.md @@ -1,34 +1,25 @@ # Attestation Platforms -TRACE Level 1 trust records require a TEE-backed signing key and a hardware measurement in the `runtime` field. The following platforms are supported. +Hardware evidence can support TRACE Level 1. Level 2 additionally requires transparency anchoring; selecting a hardware platform does not establish either level by itself. A recipient must verify the evidence and its binding to the record-signing key. -| Platform | `runtime.platform` value | Measurement source | -|----------|-------------------------|-------------------| -| AMD SEV-SNP | `sev-snp` | Launch measurement (128-bit digest extended to sha256) | -| Intel TDX | `tdx` | MRTD + RTMRs combined measurement | -| NVIDIA H100 | `opaque` | GPU attestation report RIM hash | -| TPM2 | `tpm2` | PCR bank quote (sha256 bank, PCRs 0 to 7) | +## Platform names and evidence -For `software-only` (Level 0) records, no TEE is required and the `runtime.platform` value must be exactly `"software-only"`. +Standalone TRACE records use the names in the [canonical schema](https://github.com/agentrust-io/trace-spec/blob/main/schema/trace-claim.json). Runtime configuration names such as cMCP's `sev-snp`, `tdx`, and `opaque` are not interchangeable with these wire values. -## How platform verification works +| Platform guide | Standalone `runtime.platform` | Evidence to appraise | +|---|---|---| +| [AMD SEV-SNP](amd-sev-snp.md) | `amd-sev-snp` or the profile-specific `azure-cvm-sev-snp` | Signed SNP report, certificate chain, measurement, and key/challenge binding | +| [Intel TDX](intel-tdx.md) | `intel-tdx` | Signed TD quote, collateral, measurement registers, and key/challenge binding | +| [NVIDIA H100](nvidia-h100.md) | `nvidia-h100` | GPU attestation evidence and its explicit binding to the workload and signing key | +| TPM2 | `tpm2` | Quote, selected PCRs, trusted attestation-key provenance, and challenge binding | +| Software | `software-only` | Software signature and producer-defined commitments; no hardware assurance | -At Level 1, the cMCP runtime: +The schema also registers other platform identifiers. Registration is not a claim that this Python SDK collects or appraises evidence for every platform. -1. Requests a fresh quote from the TEE firmware at session start. -2. Submits the quote to the TRACE verifier endpoint, which checks it against the relevant RIM. -3. Embeds the verified measurement in `runtime.measurement` and sets `appraisal.status` to `"affirming"`. -4. Signs the completed record with the TEE-bound key. +## What the SDK checks -The signing key is generated inside the TEE and never leaves the enclave boundary. The `cnf.jwk` in the record carries only the public half. +`agentrust_trace.verify_record` checks the standalone record's schema, profile, signature against a trusted key, freshness, and configured nonce/revocation inputs. It does not collect a hardware quote or turn a platform string into verified hardware evidence. There is no `agentrust-trace verify-hardware` command in this package. -## Platform guides +Use a verifier for the producing runtime and evidence format. For cMCP's distinct `RuntimeClaim` envelope, follow [cMCP verification](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/) and its [hardware-validation record](https://cmcp.agentrust-io.com/testing/hardware-validation/). -- [AMD SEV-SNP](amd-sev-snp.md): setup, measurement format, launch policy -- [Intel TDX](intel-tdx.md): MRTD/RTMR layout, on-premises and cloud deployment -- [NVIDIA H100](nvidia-h100.md): GPU attestation, RIM URI format - -## Related - -- [Trust Levels](../trust-levels.md): what Level 1 guarantees and when to use it -- [Hardware Attestation Platforms tutorial](../tutorials/hardware-attestation-platforms.md): end-to-end walkthrough +Continue to [trust levels](../trust-levels.md) or [interpreting hardware evidence](../tutorials/hardware-attestation-platforms.md). diff --git a/docs/platforms/intel-tdx.md b/docs/platforms/intel-tdx.md index 11eee07e..f55effa1 100644 --- a/docs/platforms/intel-tdx.md +++ b/docs/platforms/intel-tdx.md @@ -1,68 +1,19 @@ # Platform: Intel TDX -Intel TDX (Trust Domain Extensions) provides hardware-isolated Trust Domains (TDs): VMs with encrypted memory, isolated register state, and hardware-signed attestation. TRACE Level 2 on TDX is supported on GCP Confidential VM (N2D-TDX) and select on-premises Intel Xeon Scalable deployments. +Intel TDX isolates a Trust Domain and provides signed quote evidence about its measured state. A TRACE consumer still needs a verifier that checks the quote, accepted trust roots and collateral, expected measurements, freshness, and record-signing-key binding. -## What Intel TDX provides +## Measurement and representation -| Property | Detail | -|---|---| -| Memory encryption | AES-256-XTS per-TD | -| Attestation report | TD Quote, signed by Intel's QE (Quoting Enclave) | -| Measurement | SHA-384 MRTD (TD measurement register) | -| Extensible registers | RTMR0 to 3 for measuring additional components | +Standalone TRACE uses `runtime.platform="intel-tdx"`. MRTD describes the initial Trust Domain measurement; RTMRs can carry additional runtime measurements. The producing profile must say which evidence the record commits to and how the recipient checks it. A generic combination of these registers is not defined by this page. -## TRACE fields populated by TDX +The `runtime.measurement` string is a claim until checked against authenticated evidence and independently approved reference values. Comparing two self-reported digests cannot establish key custody inside a Trust Domain. -```json -{ - "runtime": { - "platform": "intel-tdx", - "measurement": "sha384:a1b2c3d4e5f6a7b8...", - "rim_uri": "https://api.trustedservices.intel.com/tdx/certification/v4/", - "firmware_version": "5.35.1", - "nonce": "dGRhY2Uzz..." - } -} -``` +## Deployment -- `measurement`: SHA-384 of the TDX TD Quote's MRTD field -- `rim_uri`: Intel Trust Authority / PCCS URL for TDX certificate chain -- `firmware_version`: TDX firmware version from the TD Quote header +GCP provides Intel TDX on C3 Confidential VMs; N2D is an AMD family. Availability and supported guest configurations change, so use Google's current [supported configurations](https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/supported-configurations) when provisioning. -## Verification flow +For AgenTrust's collected evidence, verifier behavior, and remaining collateral checks, see [cMCP hardware validation](https://cmcp.agentrust-io.com/testing/hardware-validation/). Follow its [verification tutorial](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/) for the runtime's envelope and trust inputs. -```bash -agentrust-trace verify-hardware session.trace.json \ - --platform intel-tdx \ - --check-rim -``` +## Assurance boundary -The verifier: -1. Fetches the TDX certificate chain from Intel's PCCS or Trust Authority -2. Verifies the TD Quote using Intel's SGX QVL (Quote Verification Library) -3. Compares `runtime.measurement` against the TD Quote MRTD -4. Validates that `cnf.jwk` was generated inside the TD at that measurement - -## Supported cloud instances - -| Cloud | Instance type | -|---|---| -| GCP | C3 Confidential VM (TDX) | -| Azure | DCesv5, ECesv5 (Intel TDX preview) | -| On-premises | Intel Xeon Scalable 4th Gen (Sapphire Rapids) and newer | - -## On-premises deployment - -For on-premises Intel TDX (e.g., Supermicro SYS-121H with Xeon Scalable 4th Gen), the cMCP gateway runs as a TD and uses Intel's PCCS (Platform Certificate Caching Service) or the Intel Trust Authority for attestation verification. No cloud dependency is required: deploy PCCS locally to air-gap the attestation path. See [agentrust-io/cmcp](https://github.com/agentrust-io/cmcp) for the Helm chart. - -```yaml -# cmcp.yaml (on-premises TDX) -attestation: - platform: intel-tdx - pccs_url: https://pccs.internal.example.org:8081 # your local PCCS - rim_cache: /var/cache/trace/rims -``` - -## Example record - -See [`examples/intel-tdx.json`](https://github.com/agentrust-io/trace-spec/blob/main/examples/intel-tdx.json). +Hardware appraisal supports TRACE Level 1; Level 2 additionally requires transparency anchoring. The standalone TRACE SDK does not provide a `verify-hardware` CLI or automatically provision an Intel collateral service. See [trust levels](../trust-levels.md) and [attestation platforms](index.md). diff --git a/docs/platforms/nvidia-h100.md b/docs/platforms/nvidia-h100.md index b69ca9cc..a1170893 100644 --- a/docs/platforms/nvidia-h100.md +++ b/docs/platforms/nvidia-h100.md @@ -1,76 +1,19 @@ # Platform: NVIDIA H100 Confidential Computing -NVIDIA H100 (and Blackwell) GPUs support Confidential Computing: hardware-isolated GPU execution with attestation rooted in NVIDIA's Attestation Root Certificate Authority (NRAS). TRACE Level 2 on NVIDIA CC is the first hardware-verifiable governance record for GPU-based AI inference. +NVIDIA GPU attestation provides evidence about a GPU's identity and firmware state. NVIDIA Remote Attestation Service (NRAS), its Reference Integrity Manifest service, and certificate-status services have separate roles. See [NVIDIA's attestation documentation](https://docs.nvidia.com/attestation/index.html) and [H100 attestation example](https://docs.nvidia.com/attestation/quick-start-guide/latest/attestation-examples/hopper_single_gpu.html). -## What NVIDIA H100 CC provides +## TRACE representation -| Property | Detail | -|---|---| -| Memory protection | GPU memory encrypted and isolated per VM | -| Attestation | NVIDIA RIM Service attestation, signed by NVIDIA NRAS | -| Measurement | GPU firmware + driver measurement | -| Combined attestation | CPU TEE + GPU CC: one unified attestation report | +Standalone TRACE registers `runtime.platform="nvidia-h100"` and `"nvidia-blackwell"`. A registered identifier does not mean this Python SDK collects GPU evidence or verifies an NRAS result. The producer must define how its `runtime.measurement` relates to authenticated GPU evidence. -TRACE on H100 is the first open standard to combine CPU TEE attestation and GPU CC attestation into a single signed governance record. This was demonstrated at GTC Berlin. +The cMCP configuration name `opaque` belongs to that runtime's provider interface; it is not a standalone TRACE platform value. Follow the producing runtime's envelope and verifier documentation rather than substituting names between formats. -## TRACE fields populated by NVIDIA H100 CC +## CPU, GPU, and signing-key binding -```json -{ - "runtime": { - "platform": "nvidia-h100", - "measurement": "sha256:f0e9d8c7b6a5f4e3d2c1b0a9...", - "rim_uri": "https://nras.nvidia.com/rims/H100_SXM5/fw_v551.81", - "firmware_version": "551.81" - } -} -``` +An accepted GPU attestation does not automatically attest the CPU workload, model weights, policy enforcement, or record-signing key. A combined deployment needs explicit evidence linking the relevant components and the signing key under a documented profile. This page does not define a universal combined CPU/GPU digest or an additional `runtime.extensions` wire field. -- `measurement`: Combined CPU+GPU measurement hash -- `rim_uri`: NVIDIA RIM Service URL for firmware Reference Integrity Manifest -- `firmware_version`: NVIDIA GPU driver/firmware version +Hardware appraisal supports Level 1. Level 2 additionally requires transparency anchoring. Read [trust levels](../trust-levels.md) and the producing runtime's [hardware-validation record](https://cmcp.agentrust-io.com/testing/hardware-validation/) before relying on a deployment claim. -## Verification flow +## Getting started -```bash -agentrust-trace verify-hardware session.trace.json \ - --platform nvidia-h100 \ - --check-rim -``` - -1. Fetches the GPU RIM from NVIDIA's RIM Service at `runtime.rim_uri` -2. Verifies firmware measurement against the RIM -3. Verifies the GPU attestation report using NVIDIA NRAS root certificate -4. Validates that the combined CPU+GPU measurement matches `runtime.measurement` -5. Confirms `cnf.jwk` is endorsed by both CPU TEE and GPU CC attestation - -## Combined CPU+GPU attestation - -For maximum assurance, run the agent in a combined AMD SEV-SNP + NVIDIA H100 CC deployment. The TRACE record carries both measurements: - -```json -{ - "runtime": { - "platform": "nvidia-h100", - "measurement": "sha256:combined-cpu-gpu-measurement...", - "rim_uri": "https://nras.nvidia.com/rims/...", - "extensions": { - "cpu_platform": "amd-sev-snp", - "cpu_measurement": "sha384:cpu-only-measurement..." - } - } -} -``` - -## Supported configurations - -| Configuration | Status | -|---|---| -| H100 SXM5 + AMD EPYC (SEV-SNP) | ✓ GA | -| H100 PCIe + Intel Xeon (TDX) | ✓ GA | -| H100 SXM5 + AMD EPYC (bare metal) | Preview | -| NVIDIA Blackwell B200 | Preview | - -## Example record - -See [`examples/nvidia-h100.json`](https://github.com/agentrust-io/trace-spec/blob/main/examples/nvidia-h100.json). +Use NVIDIA's current example for GPU evidence collection and appraisal. For standalone TRACE signing and signature verification, use the [local quick start](../quickstart.md). These are separate checks; this package has no `agentrust-trace verify-hardware` command. diff --git a/docs/trust-levels.md b/docs/trust-levels.md index 4fa615e4..54f851ce 100644 --- a/docs/trust-levels.md +++ b/docs/trust-levels.md @@ -1,118 +1,51 @@ --- -description: TRACE defines three trust levels, each adding a stronger guarantee about the origin and integrity of a trust record, from software signing to hardware attestation. +description: Understand TRACE's software, hardware, and transparency evidence levels and the checks a relying party still needs to perform. --- # Trust Levels -TRACE defines three trust levels. Each level adds a stronger guarantee about the origin and integrity of the trust record. Higher levels require additional infrastructure but enable stronger relying-party policies. +TRACE's conformance suite groups checks into three levels. A level describes required checks; it is not a blanket guarantee that an agent behaved correctly. The recipient supplies trust anchors, evidence, and an acceptance policy. ## Summary -| Level | Name | Key guarantee | Typical use | -|-------|------|--------------|-------------| -| 0 | Software-only | Record is structurally valid and Ed25519-signed | Development, CI, unit testing | -| 1 | TEE attestation | Record is signed by a key that was generated inside a verified TEE | Staging, regulated production | -| 2 | Transparency anchoring | Level 1 plus SCITT transparency log entry | Multi-tenant, cross-org audit chains | +| Level | Adds | What still needs scrutiny | +|---|---|---| +| 0: software | Record structure, signing, policy and appraisal fields | Trusted issuer, truth of producer claims, software key custody | +| 1: hardware evidence | Runtime and build-provenance checks | Actual quote appraisal, expected measurements, key binding, provider-specific limits | +| 2: transparency | Transcript and anchoring checks | Authenticated log/checkpoint, inclusion proof, record binding, completeness of the submitted history | -## Level 0: Software-only +See the [suite's level definitions](https://tests.agentrust-io.com/docs/levels/) for required modules and its [limitations](https://tests.agentrust-io.com/LIMITATIONS/) for what a pass establishes. Record-format checks must not be described as a fresh hardware appraisal unless that evidence was actually verified. -Level 0 records are signed with an Ed25519 key held by the agent process. There is no hardware attestation step. The `runtime.platform` field must be `software-only`. +## Level 0: software-only -**What it proves:** The record has not been tampered with since the agent process signed it. The signing key is trust-on-first-use. +A software-held key signs the record. `software-only` identifies the absence of hardware assurance. A recipient checks the signature against a key obtained through its own trust channel; the key embedded in an incoming record cannot establish its own authority. -**What it does not prove:** The key was generated in a trusted execution environment. An attacker with access to the signing key can forge records. +A valid signature authenticates the key's statement. It does not prove that a policy ran or an action completed. A privileged party holding that key can sign other statements. -**Minimum required fields:** +The measurement can be a software commitment defined by the producer. All-zero is reserved for an example or producer that offers no measurement commitment. Use the [quick start](quickstart.md) for a complete runnable record rather than copying abbreviated field examples. -```json -{ - "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", - "iat": 1750000000, - "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:0000...0000" }, - "policy": { "bundle_hash": "sha256:b2c3...", "enforcement_mode": "enforce" }, - "data_class": "internal", - "build_provenance": { "slsa_level": 1, "digest": "sha256:e5f6..." }, - "appraisal": { "status": "none", "verifier": "https://verifier.example.org" }, - "transparency": "https://registry.agentrust-io.com/claim/placeholder", - "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "" } }, - "signature": "" -} -``` +## Level 1: hardware evidence -`runtime.measurement` is required on every record, including `software-only` ones. Under `software-only`, the field is not a hardware measurement: it is a software commitment defined by the producing profile (for example, a hash over an image digest and policy bundle, or over a chain-tip), and that profile must document its preimage so a verifier can recompute it. All-zero (`sha256:000...000`) is reserved for a producer that has no commitment to offer at all, such as a bare development record with nothing measured; it is not the default for `software-only` in general. The `appraisal.status` of `"none"` is correct when no hardware verifier is in the path. +| Build-provenance field | Schema range | +|---|---| +| `build_provenance.slsa_level` | SLSA Build Level (0-3); verify the supporting provenance separately | ---- - -## Level 1: TEE Attestation - -Level 1 requires that the signing key be generated inside a verified TEE (AMD SEV-SNP, Intel TDX, NVIDIA H100, or TPM2). The `runtime.platform` field must be one of these values, and `runtime.measurement` must be a non-zero hardware measurement (PCR hash, launch measurement, or equivalent). - -**What it proves:** The signing key was generated inside a hardware-isolated enclave whose firmware digest matches the `runtime.measurement`. An attacker who compromises the host OS cannot forge records without breaking the TEE isolation boundary. - -**What it does not prove:** The record was published to a public ledger. Audit chains cannot span organizational boundaries without a shared transparency anchor. - -**Additional required fields over Level 0:** - -| Field | Requirement | -|-------|-------------| -| `runtime.platform` | Must be `tpm2`, `sev-snp`, `tdx`, or `opaque` (not `software-only`) | -| `runtime.measurement` | Non-zero `sha256:` or `sha384:` digest of the TEE launch state | -| `appraisal.status` | Must be `affirming` (verifier has checked the TEE quote) | -| `build_provenance.slsa_level` | Must be present (0-3) | -| `build_provenance.digest` | Must be a valid `sha256:` digest | - -The cMCP runtime handles Level 1 record emission automatically when running in a supported TEE. See [Hardware Attestation Platforms](tutorials/hardware-attestation-platforms.md). - ---- - -## Level 2: Transparency Anchoring - -Level 2 adds a SCITT transparency log entry to a Level 1 record. The `transparency` field must be a resolvable HTTPS URI that returns a valid reference manifest from a SCITT-compatible log. - -**What it proves:** The record has been durably committed to an append-only transparency log that any party can query. This makes post-hoc audit possible without trusting either the agent or its operator. - -**What it does not prove: ** That every field in the record is correct: only that the specific record at the URI has not been altered since it was logged. +A hardware-backed deployment needs authenticated evidence binding the record-signing key to the expected environment. Merely changing `runtime.platform`, copying a nonzero digest, or setting `appraisal.status="affirming"` does not establish that evidence. -**Additional required fields over Level 1:** +Use the standalone platform identifiers from the [schema](https://github.com/agentrust-io/trace-spec/blob/main/schema/trace-claim.json), not a runtime's configuration aliases. Provider guarantees differ: a TPM quote is not equivalent to protecting application memory in a confidential VM. See [attestation platforms](platforms/index.md). -| Field | Requirement | -|-------|-------------| -| `transparency` | Resolvable `https://` URI to a SCITT receipt | +`agentrust_trace.verify_record` does not itself appraise hardware quotes. A producing runtime's verifier must perform the relevant evidence checks. cMCP uses a different envelope; see [cMCP verification](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/). -The [SCITT reference implementation](https://github.com/microsoft/scitt-api-emulator) and the [agentrust SCITT registry](https://registry.agentrust-io.com) are both supported anchors. +## Level 2: transparency anchoring -**Recommended flow:** - -``` -Agent signs Level 1 record - ↓ -Submit record to SCITT transparency log → get receipt URI - ↓ -Append transparency URI to record (does not invalidate signature) - ↓ -Distribute Level 2 record to relying party -``` - -The signature covers all fields except `signature` itself. Appending the `transparency` field after the initial signing step requires re-signing. The recommended pattern is to emit a Level 1 record, submit it for transparency, then emit a new Level 2 record with the receipt URI and a fresh signature. - ---- +Level 2 adds transparency and transcript requirements to the lower levels. A `transparency` URI is a reference, not an inclusion proof. The verifier needs proof bound to the record and a log or checkpoint it independently trusts. Inclusion does not establish that every event was logged or that each claim is true. -## Choosing a level +Changing any signed field, including `transparency`, changes the signature preimage. Re-sign after adding or changing that field. The [registry anchor format](../spec/registry-anchor-v1.md) defines which bytes the anchor commits to; the receipt must match that format and the signed record being checked. -| If you are... | Use level | -|---------------|-----------| -| Building an agent locally or writing tests | 0 | -| Running agents in production but within a single org | 1 | -| Sharing records across organizational boundaries | 2 | -| Meeting a regulated compliance requirement | 1 or 2 (check your framework) | +For the worked sequence, see [anchoring to the registry](tutorials/anchoring-to-the-registry.md). A software-only record does not become hardware-backed simply because it is logged. -Relying parties set the minimum acceptable level in their Cedar policy. Records that fail to meet the required level are rejected at the policy enforcement point before the agent is permitted to act. +## Choosing an acceptance policy -## Related +Decide which issuer, hardware evidence, artifact commitments, freshness, revocation status, and log you require for the operation. A successful signature check is only one input to that decision. These levels do not certify regulatory compliance or replace application authorization. -- [Trust Levels in the test suite](https://tests.agentrust-io.com/docs/levels/) -- [TRACE Specification: Section 4: Trust Levels](../spec/trace-v0.2.md) -- [Hardware Attestation Platforms](tutorials/hardware-attestation-platforms.md) -- [Glossary](glossary.md) +Read [verification protocol](verification.md) for the checks and [limitations](../LIMITATIONS.md) for the remaining boundaries. diff --git a/docs/tutorials/agt-adapter.md b/docs/tutorials/agt-adapter.md index 1cb8b139..1be09c08 100644 --- a/docs/tutorials/agt-adapter.md +++ b/docs/tutorials/agt-adapter.md @@ -1,184 +1,53 @@ -# TraceAGTAdapter: One-line AGT → TRACE upgrade +# Build a TRACE Record from AGT Session Inputs -Replace ~50 lines of manual field wiring with a single `build_trust_record()` call. +Map policy bytes and audit entries into a signed software record. This local example uses synthetic inputs; it does not run AGT, call a model, or appraise hardware. -## What you'll learn +## Setup -- How `TraceAGTAdapter` maps AGT session data to TRACE Trust Record fields -- How to collect the three inputs AGT exposes (`policy_bundle_bytes`, `audit_entries`, `merkle_chain_tip`) -- How to sign and validate the resulting record -- How to upgrade from Level 0 (software-only) to Level 2 (hardware-rooted) inside cMCP - -## Prerequisites - -```bash -pip install agentrust-trace -``` - ---- - -## The problem: 50 lines of boilerplate per project - -Every project that integrates AGT with TRACE has to wire the same field mappings by hand: +Use the source installation from the [quick start](../quickstart.md). Save the complete block below as `adapter_example.py` and run `python adapter_example.py`. ```python -import hashlib, json, time -from agentrust_trace import ( - TrustRecord, ModelInfo, RuntimeInfo, PolicyInfo, - ToolTranscript, BuildProvenance, Appraisal, ConfirmationKey, JWK, -) - -# Hash the Cedar bundle -bundle_bytes = Path("policy.cedar").read_bytes() -bundle_hash = "sha256:" + hashlib.sha256(bundle_bytes).hexdigest() +import hashlib +from agentrust_trace import generate_key, sign_record, verify_record +from agentrust_trace.adapters import AGTSessionResult, TraceAGTAdapter -# Hash the audit entries -entries_json = json.dumps(audit_entries, sort_keys=True, separators=(",", ":")) -transcript_hash = "sha256:" + hashlib.sha256(entries_json.encode()).hexdigest() - -# Hash the Merkle chain tip -measurement = "sha256:" + hashlib.sha256(chain_tip.encode()).hexdigest() - -# Build the record manually -record = TrustRecord( - eat_profile="tag:agentrust-io.com,2026:trace-v0.2", - iat=int(time.time()), - subject=agent_did, - model=ModelInfo(provider="anthropic", model_id="claude-sonnet-4-6", version="20251001"), - runtime=RuntimeInfo(platform="software-only", measurement=measurement), - policy=PolicyInfo(bundle_hash=bundle_hash, enforcement_mode="enforce"), - data_class="confidential", - tool_transcript=ToolTranscript(hash=transcript_hash, call_count=len(audit_entries)), - build_provenance=BuildProvenance(slsa_level=2, digest="sha256:e5f6..."), - appraisal=Appraisal(status="affirming", verifier="https://agentrust-io.com/verify"), - transparency="https://registry.agentrust-io.com/claim/...", - cnf=ConfirmationKey(jwk=JWK(kty="OKP", crv="Ed25519", x="...")), +policy = b'permit(principal, action, resource);' +entries = [{"tool": "demo.read", "decision": "allow"}] +session = AGTSessionResult( + agent_did="spiffe://example.test/agent/demo", + policy_bundle_bytes=policy, + audit_entries=entries, + merkle_chain_tip="0" * 64, ) -``` - -`TraceAGTAdapter` encapsulates all of this. - ---- - -## The solution: TraceAGTAdapter - -```python -from pathlib import Path -from agentrust_trace.adapters import TraceAGTAdapter, AGTSessionResult -from agentrust_trace import sign_record, load_signing_key, TrustRecord - -# 1. Configure once per deployment adapter = TraceAGTAdapter( - model_provider="anthropic", - model_id="claude-sonnet-4-6", - model_version="20251001", - build_provenance_digest="sha256:e5f6a7b8...", - transparency="https://registry.agentrust-io.com/claim/...", + model_provider="example", model_id="synthetic-demo", + build_provenance_digest="sha256:" + "e" * 64, + transparency="https://example.test/unused", ) - -# 2. Collect AGT session data after govern_fn.close_session() -session = AGTSessionResult( - agent_did="spiffe://trust.example.org/agent/my-agent", - policy_bundle_bytes=Path("policy.cedar").read_bytes(), - audit_entries=govern_fn.get_audit_entries(), # list[dict] - merkle_chain_tip=govern_fn.chain_tip, # hex string -) - -# 3. Build and sign record = adapter.build_trust_record(session) -key = load_signing_key() # reads TRACE_PRIVATE_KEY_PEM env var +# The adapter currently requires a URI argument but performs no registration. +record.pop("transparency") +# Synthetic input has not been appraised; do not keep the adapter's default verdict. +record["appraisal"]["status"] = "none" +key = generate_key() +trusted_key = key.public_key() signed = sign_record(record, key) - -# 4. Validate structure before writing -TrustRecord.model_validate(signed) - -import json -Path("session.trace.json").write_text(json.dumps(signed, indent=2)) +verify_record(signed, public_key_or_jwk=trusted_key) +assert signed["runtime"]["platform"] == "software-only" +assert signed["policy"]["bundle_hash"] == "sha256:" + hashlib.sha256(policy).hexdigest() +assert signed["tool_transcript"]["call_count"] == 1 +assert "transparency" not in signed +print("PASS: mapped and signed synthetic session; no hardware appraisal or registry anchor") ``` ---- - -## Field mapping reference - -| TRACE field | Source | -|---|---| -| `subject` | `AGTSessionResult.agent_did` | -| `policy.bundle_hash` | `sha256(policy_bundle_bytes)` | -| `policy.enforcement_mode` | `TraceAGTAdapter(enforcement_mode=...)` (default: `enforce`) | -| `tool_transcript.hash` | `sha256(canonical_json(audit_entries))` | -| `tool_transcript.call_count` | `len(audit_entries)` or `AGTSessionResult.call_count` override | -| `runtime.platform` | Always `software-only` (Level 0) | -| `runtime.measurement` | `sha256(merkle_chain_tip)` | -| `appraisal.status` | Always `affirming` (Phase 1) | -| `model`, `data_class`, `build_provenance` | `TraceAGTAdapter(...)` constructor params | -| `iat`, `appraisal.timestamp` | `AGTSessionResult.iat` (default: current time) | - ---- - -## Collecting the three inputs from AGT - -### `policy_bundle_bytes` - -Read the Cedar bundle from disk immediately after calling `govern()`. The hash must match what the session evaluated against. - -```python -from pathlib import Path - -policy_bundle_bytes = Path(config.policy_path).read_bytes() -``` - -### `audit_entries` - -AGT's `govern()` returns a wrapped callable with `.get_audit_entries()`. Call it after `.close_session()`: - -```python -governed_fn = govern(my_tool, agent_did=agent_did, config=config) -result = governed_fn(input_data) -governed_fn.close_session() - -audit_entries = governed_fn.get_audit_entries() # list of Merkle AuditEntry dicts -``` - -### `merkle_chain_tip` - -The Merkle chain tip is the hash of the last `AuditEntry` in the chain: - -```python -chain_tip = governed_fn.chain_tip # hex string, e.g. "deadbeef..." -``` - ---- - -## Adapting to different enforcement modes - -```python -adapter = TraceAGTAdapter( - ... - enforcement_mode="advisory", # "enforce" | "advisory" | "silent" -) -``` - -`enforce` (default) means policy decisions are binding: tool calls blocked by a `forbid` rule do not execute. `advisory` means decisions are logged but not enforced. The mode appears in `policy.enforcement_mode` in the TRACE record so verifiers know what the policy actually did. - ---- - -## Upgrading to Level 2 (hardware-rooted) - -`TraceAGTAdapter` produces Level 0 records: `runtime.platform` is `software-only` and the signing key is not TEE-bound. For Level 2: - -1. Deploy your AGT-governed agent inside cMCP on an Azure DCasv5 (SEV-SNP) or DCesv6 (TDX) VM, or GCP N2D (SEV-SNP) or C3 (TDX) -2. cMCP measures the Cedar policy bundle into the TEE hardware at startup -3. The cMCP runtime generates a TEE-bound key and emits a Level 2 TRACE record that supersedes the Level 0 record for the same session -4. Both records share `subject` and `tool_transcript.hash` and are mutually verifiable +The nonzero build digest is illustrative metadata, not verified build provenance. The chain tip is synthetic. The example checks mapping and a software signature only. -The Level 0 record from `TraceAGTAdapter` remains valid: it is evidence of policy enforcement at the software layer. The Level 2 record from cMCP adds hardware attestation on top. +## Use real session evidence -→ [Deploy on Azure](https://cmcp.agentrust-io.com/tutorials/deploy-azure/): `Standard_DC2as_v5` (SEV-SNP) or `Standard_DC2es_v6` (TDX) -→ [Deploy on GCP](https://cmcp.agentrust-io.com/tutorials/deploy-gcp/): `n2d-standard-4` (SEV-SNP) or `c3-standard-4` (TDX) -→ Platform detail: [AMD SEV-SNP](../platforms/amd-sev-snp.md) · [Intel TDX](../platforms/intel-tdx.md) +Supply the exact policy bytes used for the session, audit entries as plain dictionaries, the session's chain tip, and its authenticated identity. The adapter hashes the audit list with RFC 8785 and the chain-tip string as UTF-8. Its default call count is the list length; supply `call_count` only when your producing profile defines a different count. ---- +The adapter records the configured enforcement mode; it does not enforce that mode or prove the policy was evaluated. Likewise, its default `affirming` appraisal is not an independent assessment. Set the record's claims to the checks actually performed before signing. -## Summary +## Verify and extend -`TraceAGTAdapter` turns 50 lines of manual field wiring into three calls: configure the adapter once, collect the three AGT session values (`policy_bundle_bytes`, `audit_entries`, `merkle_chain_tip`) after each session, call `build_trust_record()`. The record is structurally valid and ready for `sign_record()` without any additional construction. +Recipients obtain the issuer key independently and use [record verification](verifying-a-trust-record.md). Add hardware evidence through a runtime-specific profile and verifier; adding a platform name is insufficient. For Level 2, also follow the [registry anchor format](../../spec/registry-anchor-v1.md). Re-sign after changing signed fields. diff --git a/docs/tutorials/anchoring-to-the-registry.md b/docs/tutorials/anchoring-to-the-registry.md index 836817e5..f0abc190 100644 --- a/docs/tutorials/anchoring-to-the-registry.md +++ b/docs/tutorials/anchoring-to-the-registry.md @@ -1,6 +1,6 @@ # Anchoring a Trust Record to the TRACE registry -After signing a Trust Record, you can anchor it to the TRACE transparency registry. The anchor proves the record existed at a specific time and has not been altered since, which is tamper evidence that holds even if the operator who produced the record is later compromised. +After signing a Trust Record, you can anchor it to the TRACE transparency registry. An inclusion proof binds the signed record to a batch root. Any claim about when it existed also depends on authenticating and trusting the registry's checkpoint and timestamp. **What you need:** A signed Trust Record (from [Signing your first trust record](signing-your-first-trust-record.md)). @@ -15,7 +15,7 @@ After signing a Trust Record, you can anchor it to the TRACE transparency regist A Trust Record carries a signature from the issuer's key. A verifier holding that key can confirm the record has not been modified, but only if the key is trustworthy. If the issuer is later compromised, an attacker holding the key could forge records backdated to before the compromise. -Anchoring solves this with a different trust root: an append-only log whose history a third party can inspect. Once a record is anchored, its exact bytes are fixed in the log at that timestamp. A verifier recomputes the Merkle root from the record and its proof and compares it to the published entry. No trust in the operator is required, and no call back to the issuer is needed. +Anchoring introduces a separate trust input: the registry entry or checkpoint. A verifier recomputes the Merkle root from the record and proof and compares it to an independently authenticated entry. Accepting a record, proof, and root from the same untrusted sender establishes only internal consistency. Check the registry's append-only history and timestamp policy separately. The normative format is [TRACE Registry Anchor Format v1](../../spec/registry-anchor-v1.md). Read §0 of it before you implement anything: TRACE uses **RFC 8785 (JCS)** to canonicalize a record for *signing* and **sorted-key JSON** to canonicalize it for the *anchor leaf*. Assuming JCS at the leaf produces proofs that never verify, and the failure has no useful diagnostic. @@ -37,7 +37,7 @@ Where present, it identifies the registry entry anchoring the record. At Level 2 ## Step 1: Sign the record -Sign as normal. You do not need a placeholder for `transparency`; leave it unset until you have an anchor. +Sign the final record. Leave `transparency` absent if the registry entry URI is not known yet; distribute the receipt separately. If the registry supports reserving an entry URI, set that URI before signing and submit those exact signed bytes. Do not invent a placeholder URI. ```python import time @@ -105,24 +105,17 @@ The verifier is standard library only and small enough to read in one sitting. R --- -## Step 5: Set `transparency` +## Step 5: Preserve the anchored record -Once anchored, set `transparency` to the entry that anchors your record and re-sign, so the signature covers the anchor reference. - -```python -record["transparency"] = "" -signed_final = sign_record(record, key) -``` - -A verifier retrieving that entry can confirm inclusion without contacting you, which is the whole point. +Keep the signed object unchanged with its proof and registry entry. Adding `transparency` and re-signing creates a different object; the original proof no longer covers it. Submit that new object for anchoring if you change any signed field. A Level 2 workflow needs a registry arrangement that lets the final record name its entry before its bytes are committed. --- ## What this proves, and what it does not -Inclusion proves the exact signed bytes were in the batch at the entry's timestamp, and that they have not changed since. +Inclusion verifies the exact signed object against the supplied batch root. Authenticity and timing depend on the separately trusted registry entry or checkpoint. -It does not validate the signature, and it does not say the record's contents are true. Signature verification against the producer key is a separate step (spec §3.3). A record can be genuinely anchored and still describe something inaccurate; anchoring establishes *when these bytes existed*, and nothing more. +It does not validate the signature or establish that the record's contents are true. Signature verification against a trusted producer key is a separate step (spec §3.3). Inclusion alone does not establish complete logging, a trustworthy timestamp, or an append-only history. --- @@ -130,8 +123,8 @@ It does not validate the signature, and it does not say the record's contents ar | Step | What happens | |---|---| -| Sign the record | `transparency` stays unset until there is an anchor to name | +| Sign the record | Set a reserved entry URI before signing, or leave `transparency` absent | | Submit to staging | The pipeline batches by producer and builds a Merkle tree | | Retrieve the proof | `leaf_index` plus `audit_path`, one per record | | **Verify it yourself** | Recompute the root; exit 0 or exit 1, nothing in between | -| Set `transparency`, re-sign | Signature now covers the anchor reference | +| Preserve the record | Keep the exact signed object covered by the proof | diff --git a/docs/tutorials/hardware-attestation-platforms.md b/docs/tutorials/hardware-attestation-platforms.md index e8bc1742..217cbd75 100644 --- a/docs/tutorials/hardware-attestation-platforms.md +++ b/docs/tutorials/hardware-attestation-platforms.md @@ -1,212 +1,36 @@ -# Hardware Attestation Platforms +# Interpreting Hardware Attestation -Understand what the `runtime.measurement` field contains for each TEE platform, what it proves, and how a verifier uses it. +A platform name and measurement in a signed TRACE record are claims. To treat them as hardware evidence, a recipient needs authenticated attestation evidence, accepted reference values, and a binding to the signing key. This guide explains that distinction; it does not provision a TEE. -## What you'll learn +## Start with the right format -- 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 +Standalone TRACE records use the [canonical schema](https://github.com/agentrust-io/trace-spec/blob/main/schema/trace-claim.json). cMCP emits a distinct `RuntimeClaim` envelope and uses its own verifier. Do not pass that envelope directly to `agentrust_trace.verify_record`. -## 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.com,2026:trace-v0.2", - "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 against a trusted key -validate_json(record) -verify_record(record, trusted_jwk) # trusted_jwk obtained out-of-band - -# Step 2 + 3: platform-specific: outside the scope of agentrust-trace -# For cMCP-issued records, use cmcp-verify which handles the full chain. -# cmcp-verify operates on a RuntimeClaim (the cMCP envelope), not on a -# flat TrustRecord, and requires the expected hashes to verify against: -# -# from cmcp_verify import verify_trace_claim, ApprovedHashes -# approved = ApprovedHashes( -# policy_bundle_hash="sha256:", -# tool_catalog_hash="sha256:", -# ) -# result = verify_trace_claim(claim_json, approved) -# print(result.status.value) # "verified" | "partially_verified" | "unverified" -``` - ---- +| Evidence | What the recipient checks | +|---|---| +| Signed record | Schema, supported profile, trusted signing key, signature, freshness, configured revocation and nonce checks | +| Hardware report or quote | Signature chain and collateral, platform policy, expected measurement, fresh challenge | +| Key binding | The authenticated report binds this record-signing key under the producing profile | +| Policy and transcript | Independently obtained artifacts match the signed commitments; their meaning depends on the producer | +| Transparency | Inclusion proof and an independently trusted log/checkpoint; a URL alone is insufficient | -## Platform Enum Reference +## Software-only records -The `RuntimeInfo` model accepts exactly these platform values: +`runtime.platform="software-only"` carries no hardware assurance. Its measurement can be a producer-defined software commitment; all-zero is appropriate only when no commitment is offered. Software-signed records can support uses whose trust policy accepts software key custody. They must not satisfy a requirement for verified hardware evidence. -| 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 | +## Hardware-specific evidence -Records with any other platform string will fail schema validation. +- [AMD SEV-SNP](../platforms/amd-sev-snp.md): distinguish the launch measurement from guest-supplied report data. +- [Intel TDX](../platforms/intel-tdx.md): interpret MRTD and RTMRs under the producing profile. +- [NVIDIA H100](../platforms/nvidia-h100.md): GPU appraisal does not automatically establish CPU workload or signing-key identity. +- TPM2: a quote and trusted attestation key can establish measured-state evidence. A TPM is not a general-purpose enclave for the application; it does not by itself protect agent process memory from the host OS. See [cMCP's TPM security model](https://cmcp.agentrust-io.com/spec/tpm-security-model/). ---- +## What the TRACE SDK does -## Summary +`verify_record` verifies the standalone signed object. It does not collect or appraise hardware quotes, fetch a Reference Integrity Manifest, or independently substantiate `appraisal.status`. There is no `verify-hardware` command in this package. -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. +For cMCP evidence, use its [verification tutorial](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/), approved policy/catalog hashes, and required attestation inputs. For a local software example, use [quick start](../quickstart.md). -Related tutorials: +## Report the checks performed -- [Sign your first trust record](signing-your-first-trust-record.md) -- [Integration with cMCP](integrating-with-cmcp.md) +Hardware appraisal corresponds to Level 1; Level 2 adds transparency anchoring. Report unavailable evidence as unavailable, and a contradictory result as a failure. A record that says `affirming` does not authorize an action without the recipient's own acceptance policy. See [trust levels](../trust-levels.md) and [verification outcomes](../verification-outcome-statements.md). diff --git a/docs/tutorials/integrating-with-cmcp.md b/docs/tutorials/integrating-with-cmcp.md index 2d839386..dfd32ffe 100644 --- a/docs/tutorials/integrating-with-cmcp.md +++ b/docs/tutorials/integrating-with-cmcp.md @@ -1,172 +1,21 @@ # Integration with cMCP -Understand how TRACE trust records are generated by Confidential MCP (cMCP) and how a downstream verifier checks them. +Use cMCP's runtime verifier for its signed session claims. The standalone TRACE SDK and cMCP share evidence concepts, but their record envelopes are different. -## What you'll learn +## Walkthrough -- 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` (standalone TrustRecord signing/verification) and `cmcp-verify` (RuntimeClaim full chain verification) +1. Run the [local cMCP quick start](https://cmcp.agentrust-io.com/quickstart/). It shows allowed and denied calls and creates a signed session claim in software mode. +2. Follow [verify a TRACE claim](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/) to supply independently approved policy and catalog hashes and inspect the verification result. +3. Review [hardware validation](https://cmcp.agentrust-io.com/testing/hardware-validation/) before adding hardware requirements. Keep unavailable evidence distinct from an affirmative result. -## Prerequisites +These pages contain the runtime's maintained commands and complete examples. Do not pass a cMCP `RuntimeClaim` directly to `agentrust_trace.validate_json` or `verify_record`; those APIs consume standalone TRACE objects. -```bash -pip install agentrust-trace -# For full cMCP verification. cmcp_verify ships inside the cmcp-runtime -# distribution; there is no separate cmcp-verify package on PyPI. -pip install cmcp-runtime -``` +## Trust boundary ---- +The gateway checks routed tool calls and records its decisions. It does not observe calls that bypass it, prove an upstream tool's internal behavior, or establish physical task completion. Its [architecture](https://cmcp.agentrust-io.com/concepts/) separates the client, gateway, tools, and evidence consumer. -## How cMCP Issues a TRACE Record +The signing key's hardware protection must be established by actual provider evidence and key binding. A development-mode claim remains software evidence. A claimed policy hash must be compared with an independently approved artifact, not copied from the incoming claim into the verifier's allowlist. -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. +## TRACE levels -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. Signs the record internally using the TEE-sealed key -4. Writes the signed `RuntimeClaim` to the path in `CMCP_TRACE_OUTPUT_PATH` - -The resulting file is a `RuntimeClaim`: a cMCP-specific envelope that wraps a TRACE `GatewayTrace` under the `trace` key, alongside gateway metadata and a top-level `signature`. A `RuntimeClaim` is not the same shape as a standalone `TrustRecord`; field access goes through `record["trace"]["subject"]`, `record["trace"]["policy"]`, and so on. - ---- - -## 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 -{ - "cmcp_version": "0.1", - "trace": { - "cnf": { - "jwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "" - } - }, - "runtime": { - "platform": "amd-sev-snp", - "measurement": "sha384:" - } - }, - "signature": "" -} -``` - -The `trace.cnf.jwk` and `trace.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["trace"]["subject"]) # SPIFFE URI for the agent -print(record["trace"]["policy"]) # {"bundle_hash": "sha256:...", "enforcement_mode": "enforce"} -print(record["trace"]["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 - -The `agentrust-trace` library's `validate_json()` and `verify_record()` functions operate on standalone `TrustRecord` objects: the flat schema produced by `sign_record()`. A cMCP `RuntimeClaim` has a different envelope (TRACE fields nested under `trace`, plus top-level `signature`, `gateway`, and `cmcp_version`) and will fail `validate_json()` with schema errors if passed directly. - -To verify a cMCP-issued `RuntimeClaim`, use `cmcp-verify`. It validates the RuntimeClaim envelope directly and handles the full chain. - ---- - -## 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 -import json -from cmcp_verify import verify_trace_claim, ApprovedHashes - -with open("/var/run/cmcp/session.trace.json") as f: - claim_json = json.load(f) - -# ApprovedHashes carries the expected hashes pinned at deployment time. -# Compute policy_bundle_hash from your Cedar policy archive. -# Compute tool_catalog_hash as sha256(json.dumps(catalog, sort_keys=True, separators=(',', ':'))) -# on the sorted-by-tool-name catalog: canonical JSON, not raw file bytes. -approved = ApprovedHashes( - policy_bundle_hash="sha256:", - tool_catalog_hash="sha256:", -) - -result = verify_trace_claim(claim_json, approved) -print(result.status.value) # "verified" | "partially_verified" | "unverified" -``` - -`verify_trace_claim()` fetches the TEE attestation report from the RIM URI in `trace.runtime.rim_uri`, so it requires network access to the attestation service for the stated platform. The returned `VerificationResult` exposes `.verified_fields`, `.unverified_fields`, and `.failure_reason` for detailed inspection. - ---- - -## Division of Responsibility - -| Concern | agentrust-trace | cmcp-verify | -|---|---|---| -| Sign a standalone TrustRecord | `sign_record()` | N/A | -| Verify a standalone TrustRecord (Ed25519 + schema) | `verify_record()`, `validate_json()` | N/A | -| Verify a cMCP RuntimeClaim (full chain) | Not in scope | `verify_trace_claim()` | -| 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 signing or verifying standalone `TrustRecord` objects. Use `cmcp-verify` when working with cMCP-issued `RuntimeClaim` files: the envelope schema is different and `agentrust-trace` functions cannot process it directly. - ---- - -## Summary - -cMCP generates a `RuntimeClaim` at the end of each session, signed with an Ed25519 key that was generated inside the TEE and never exported. TRACE fields are nested under `trace` in the envelope; top-level field access (e.g. `record["subject"]`) will fail, use `record["trace"]["subject"]` instead. The CRYPTO-001 nonce binding ties the key to the TEE attestation report. The signed claim is written to `CMCP_TRACE_OUTPUT_PATH`. Use `cmcp-verify` for all verification of cMCP-issued records, the `RuntimeClaim` envelope is incompatible with the standalone `validate_json()` and `verify_record()` helpers in `agentrust-trace`. - -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) +Hardware appraisal supports Level 1. Level 2 adds a verified transparency anchor. Neither a TEE platform name nor the presence of a registry URL establishes those checks. See [trust levels](../trust-levels.md) and [verification protocol](../verification.md). diff --git a/docs/tutorials/signing-your-first-trust-record.md b/docs/tutorials/signing-your-first-trust-record.md index 4a18e024..d011cf8c 100644 --- a/docs/tutorials/signing-your-first-trust-record.md +++ b/docs/tutorials/signing-your-first-trust-record.md @@ -1,191 +1,23 @@ # Sign Your First Trust Record -Generate an Ed25519 signing key and produce a signed TRACE Trust Record that any verifier can check offline. +Sign a complete standalone TRACE record, then verify it using a separately retained public key. Start with the [quick start](../quickstart.md), which includes the source installation, complete script, expected output, and an edited-record rejection. -## What you'll learn +## What the signature covers -- 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()` +`sign_record(record, key)` adds the public confirmation key and signs the RFC 8785 canonical representation of every field except `signature`. The confirmation key is part of the signed preimage. Changing the subject, policy hash, transcript, appraisal, or transparency reference requires a new signature. -## Prerequisites +`json.dumps(sort_keys=True)` is not a substitute for RFC 8785 canonicalization. Use the SDK signing function so non-ASCII strings and numeric constraints follow the same rules as verification. -```bash -pip install agentrust-trace -``` +## Keep trust separate ---- +Retain the public key when creating your local demo authority. A recipient of a production record obtains its trusted issuer key through an approved channel; the incoming record cannot choose that authority. -## Generate a Key +The embedded `cnf.jwk` supports key binding. It does not remove the need to distribute trust. `verify_record` requires a trusted key by default and performs schema/profile validation as well as signature verification. The explicit embedded-key option checks internal consistency only. -`generate_key()` returns an `Ed25519PrivateKey` from the `cryptography` library. Keep the private key secret. Distribute only the public half. +## Describe the evidence honestly -```python -from agentrust_trace import generate_key, key_to_jwk +The quick-start record contains synthetic claims and software signing. It does not run an agent, enforce policy, produce hardware attestation, or submit to a registry. `appraisal.status="none"` records that no appraisal was performed. Set `affirming` only for an appraisal actually performed by an authorized producer under a stated policy. -key = generate_key() -jwk = key_to_jwk(key) -print(jwk) -# {'kty': 'OKP', 'crv': 'Ed25519', 'x': ''} -``` +A software measurement can be a documented commitment to inputs. Use an all-zero measurement only when no commitment is offered. Do not add a placeholder registry URI and describe it as an anchor. -`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.com,2026:trace-v0.2", - "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.com/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 UTF-16 code-unit order (ascending) -- No whitespace between tokens -- Numbers serialized in IEEE 754 double-precision shortest round-trip form (RFC 8785 §3.2.2.3) -- Strings emitted as raw UTF-8, escaping only the characters RFC 8259 §7 requires - -`_canonical_bytes()` is implemented with the RFC 8785-conformant [`rfc8785`](https://pypi.org/project/rfc8785/) library. `json.dumps(record, sort_keys=True, ensure_ascii=True)` is **not** a substitute: it escapes non-ASCII characters as `\uXXXX`, zero-pads number exponents (`1e-07` vs JCS `1e-7`), and sorts by Unicode code point rather than UTF-16 code unit. Any of these would break cross-implementation verification and allow signature-preserving mutation, so the library is used in both signing and verification. - -The spec (section 3.2.2) requires JCS canonical form. Do not reimplement this by hand. - ---- - -## Verify the Signed Record - -`verify_record()` requires a trusted key, 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. Pass the public JWK of the key you trust: here, the `jwk` from earlier in the tutorial. - -```python -from agentrust_trace import verify_record -from cryptography.exceptions import InvalidSignature - -try: - verify_record(signed, jwk) # jwk is the public key you trust - 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, jwk) -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) +Continue to [verify a received record](verifying-a-trust-record.md), [hardware evidence](hardware-attestation-platforms.md), or [transparency anchoring](anchoring-to-the-registry.md). diff --git a/docs/tutorials/verifying-a-trust-record.md b/docs/tutorials/verifying-a-trust-record.md index 15b4a536..4e28515f 100644 --- a/docs/tutorials/verifying-a-trust-record.md +++ b/docs/tutorials/verifying-a-trust-record.md @@ -1,260 +1,62 @@ # 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 built-in schema validation rejects malformed signed records -- 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 +Verify a standalone TRACE record against a separately trusted issuer key, reject a changed record, and preserve the result of the revocation check. This walkthrough checks signed evidence; it does not authorize an agent action or appraise hardware. ## Prerequisites -```bash -pip install agentrust-trace -``` - ---- - -## Understand What verify_record() Does +Complete the [quick start](../quickstart.md) from a source checkout. It creates `session.trace.json` and `issuer-public.pem`. Run the blocks below from that directory, in one Python script. In production, obtain the issuer key through your own trust configuration rather than accepting a key supplied with an incoming record. -When you call `verify_record(record, trusted_key)` the library: - -1. Requires the exact TRACE v0.2 profile and validates the complete record against its canonical JSON Schema -2. Reads `record["signature"]` and base64url-decodes it to raw bytes -3. Resolves the trusted key you supplied, checking `kty == "OKP"` and `crv == "Ed25519"` before reconstructing an `Ed25519PublicKey` from the `x` field -4. Enforces freshness: rejects records whose `iat` is older than `max_age_seconds` (default 24h) or further in the future than `max_future_skew_seconds` (default 5m), and, if you pass `expected_nonce`, compares it in constant time to `runtime.nonce` -5. Rebuilds the canonical payload with all fields except `signature`, using RFC 8785 (JCS) -6. Calls `Ed25519PublicKey.verify(sig_bytes, payload_bytes)` from the `cryptography` library -7. Returns `None` on success, raises `cryptography.exceptions.InvalidSignature` on a bad signature and `ValueError` on every other rejection - -A trusted key is required. A record cannot authenticate itself with the key it embeds, so `verify_record` will not fall back to `cnf.jwk` unless you explicitly opt in with `allow_embedded_key=True` (which emits a `UserWarning`, since it proves only internal consistency, not authenticity). +## Verify the record ```python import json +from pathlib import Path +from cryptography.hazmat.primitives.serialization import load_pem_public_key from agentrust_trace import verify_record -from cryptography.exceptions import InvalidSignature - -with open("session.trace.json") as f: - record = json.load(f) - -# trusted_jwk obtained out-of-band (see "Verify Against a Pinned Public Key" below) -try: - verify_record(record, trusted_jwk) - print("signature valid") -except InvalidSignature: - print("signature invalid: record may have been tampered with") -except ValueError as e: - print(f"verification failed: {e}") -``` - -`ValueError` is raised when schema conformance fails, no trusted key is supplied, the record is missing a `signature` field, a key or signature cannot be decoded, the JWK type is not Ed25519, or the record is stale. Treat all of these as verification failure. - ---- - -## Verify Against a Pinned Public Key - -The embedded `cnf.jwk` names the key the record claims made its mandatory signature binding. It cannot establish trust by itself. Supply a trusted public key out of band; `verify_record()` requires the embedded and trusted keys to have the same RFC 7638 thumbprint, then verifies the signature with the trusted key. - -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) +trusted_key = load_pem_public_key(Path("issuer-public.pem").read_bytes()) +record = json.loads(Path("session.trace.json").read_text()) +result = verify_record(record, public_key_or_jwk=trusted_key) +assert result.revocation.outcome == "no_check_performed" +print("signature and record checks passed; no revocation check performed") ``` -When you supply a key, the library first requires it to identify the same public key as `cnf.jwk`, ignoring optional JWK metadata such as `kid`, and then uses the trusted key for signature verification. A key mismatch raises `ValueError`; a matching key with an invalid signature raises `InvalidSignature`. - ---- +`verify_record` checks schema and profile, key binding and signature, record age, future clock skew, and any nonce/revocation inputs you configure. It requires a trusted key by default. `allow_embedded_key=True` is an explicit consistency-only option, not issuer authentication. -## Validate the Schema +The default maximum age is 24 hours. If the quick-start record has expired, recreate it. For historical evaluation, pin the evaluation time and retain the trust and revocation evidence used for that decision. -Signature verification confirms the record was not modified after signing. It says nothing about whether the record conforms to the TRACE v0.2 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: +## Reject an edited record ```python -from agentrust_trace import validate_json -import jsonschema +from copy import deepcopy +from cryptography.exceptions import InvalidSignature +changed = deepcopy(record) +changed["subject"] = "spiffe://example.test/agent/changed" 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}") + verify_record(changed, public_key_or_jwk=trusted_key) +except InvalidSignature: + print("edited record rejected") 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.com,2026:trace-v0.2" - -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) -> 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.com,2026:trace-v0.2": - 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 + raise AssertionError("edited record accepted") ``` ---- +The replacement subject is schema-valid, so this case reaches the signature check. A malformed field can fail schema validation earlier and does not exercise the same check. -## What to Do When Verification Fails +## Revocation is a separate outcome -If `verify_record()` raises `InvalidSignature`: +Without a store or bundle, the result reports `no_check_performed`. A supplied bundle that cannot establish status can produce `unverified_for_revocation`; it does not necessarily raise. If your policy requires a current revocation check, inspect the result and refuse to proceed unless that requirement is satisfied. -- 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. +See [checking revocation status](../verification.md#checking-revocation-status) for store and bundle inputs, freshness bounds, and implementation limits. -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. +## Appraisal and hardware claims ---- +A signed `appraisal.status` authenticates a statement about appraisal. This function does not independently verify the hardware report, expected measurement, policy execution, or transcript contents. Do not treat `affirming` or a non-software platform string as sufficient evidence to act. -## Summary +For cMCP's `RuntimeClaim`, use [cmcp-verify](https://cmcp.agentrust-io.com/tutorials/verifying-a-trace-claim/). That envelope is different from standalone TRACE. For the wider distinction, read [hardware evidence](hardware-attestation-platforms.md) and [trust levels](../trust-levels.md). -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. +## Failure handling -Related tutorials: +`InvalidSignature` rejects a signature mismatch. `ValueError` rejects other supported verification failures, such as malformed input, an unsupported profile, an untrusted/mismatched key, stale timestamps, or configured nonce/revocation failures. Do not return a successful verification result when either is raised. -- [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) +The returned verification result still needs the recipient's acceptance policy. A signed statement is not proof of task completion, hardware isolation, or complete audit history. diff --git a/docs/tutorials/verifying-the-audit-chain.md b/docs/tutorials/verifying-the-audit-chain.md index 95a7b4a4..eef19f6d 100644 --- a/docs/tutorials/verifying-the-audit-chain.md +++ b/docs/tutorials/verifying-the-audit-chain.md @@ -1,251 +1,62 @@ -# Verifying the tool call transcript +# Verify the Tool Call Transcript -A TRACE Trust Record commits the evidence of every tool call by hash. This tutorial explains what the `tool_transcript` field contains, how to verify that a received record's transcript hash is consistent with the actual tool calls, and how external execution receipts extend the chain. +Check that a signed record commits to the transcript you received. This establishes integrity of the supplied transcript; it cannot show that the producer recorded every real call. -**What you need:** A Trust Record with a `tool_transcript` field, the matching transcript file, and the issuer's public key. +## Run a complete local example ---- +First save the complete [AGT adapter example](agt-adapter.md) as `adapter_example.py`. It creates synthetic audit entries and a signed software record. Append the following block to that same file, then run `python adapter_example.py`. -## What `tool_transcript` captures - -The `tool_transcript` field in `TrustRecord` has three fields: - -```python -class ToolTranscript(BaseModel): - hash: DigestStr # sha256 or sha384 digest of all tool call content - call_count: int | None # number of calls in this session (optional) - transcript_uri: str | None # where the full transcript can be retrieved -``` - -`hash` is the binding between the Trust Record (which is signed) and the full transcript (which is stored externally). When the Trust Record signature verifies, the `hash` inside it is signed. When the hash matches the transcript you retrieve from `transcript_uri`, you know the transcript has not been altered since the record was signed. - -The full transcript is NOT embedded in the Trust Record: it lives at `transcript_uri`. This keeps records small enough to sign and transmit while still committing all call-level evidence. - ---- - -## Step 1: Retrieve and verify the record signature - -Start by checking the Trust Record signature with the issuer's public key: +This producer hashes RFC 8785 canonical JSON. Other producers can define different transcript encodings; use their documented byte format instead of assuming every TRACE transcript is a JSON array or a hash of an HTTP response. ```python -from agentrust_trace.sign import verify_record, load_key +import hmac +import rfc8785 -with open("issuer_pub.pem", "rb") as f: - public_key = load_key(f.read()) +# The adapter defines these exact canonical bytes as its transcript input. +transcript_bytes = rfc8785.dumps(entries) -with open("trust_record.json") as f: +def check_transcript(record, payload, trusted_public_key): + verify_record(record, public_key_or_jwk=trusted_public_key) + commitment = record.get("tool_transcript") + if commitment is None: + raise ValueError("Record has no transcript commitment") + algorithm, expected = commitment["hash"].split(":", 1) + if algorithm not in {"sha256", "sha384"}: + raise ValueError("Unsupported transcript digest") + actual = hashlib.new(algorithm, payload).hexdigest() + if not hmac.compare_digest(actual, expected): + raise ValueError("Transcript hash mismatch") + # This adapter's profile counts entries in a JSON list. import json - record = json.load(f) - -result = verify_record(record, public_key_or_jwk=public_key) -# raises agentrust_trace.exceptions.VerificationError on failure -# returns True on success -``` - -`verify_record` confirms that the signed content of the record has not been altered. This includes the `tool_transcript.hash` field: if the signature is valid, you have a trusted copy of the hash. - ---- - -## Step 2: Retrieve the transcript - -The full transcript lives at `tool_transcript.transcript_uri`. Retrieve it and hold the raw bytes for hashing: - -```python -import requests - -transcript_uri = record["tool_transcript"]["transcript_uri"] -response = requests.get(transcript_uri, timeout=30) -response.raise_for_status() - -# Hold raw bytes: hash must be computed over the exact bytes served -transcript_bytes = response.content -``` - -!!! warning "Hash bytes, not parsed content" - The `tool_transcript.hash` is computed over the raw bytes of the transcript as stored. Do not decode, re-encode, or reformat before hashing: JSON parsing and re-serialization changes whitespace and key order, which changes the hash. - ---- - -## Step 3: Verify the transcript hash - -Parse the `hash` field to determine the algorithm, then compute and compare: - -```python -import hashlib - -expected = record["tool_transcript"]["hash"] -# expected is a DigestStr: "sha256:" or "sha384:" - -algorithm, expected_hex = expected.split(":", 1) - -if algorithm == "sha256": - computed = hashlib.sha256(transcript_bytes).hexdigest() -elif algorithm == "sha384": - computed = hashlib.sha384(transcript_bytes).hexdigest() + calls = json.loads(payload) + if not isinstance(calls, list): + raise ValueError("Expected a transcript list for this producer") + count = commitment.get("call_count") + if count is not None and len(calls) != count: + raise ValueError("Transcript call count mismatch") + return calls + +assert len(check_transcript(signed, transcript_bytes, trusted_key)) == 1 +print("PASS: transcript matches the signed commitment") +try: + check_transcript(signed, transcript_bytes + b" ", trusted_key) +except ValueError as error: + assert str(error) == "Transcript hash mismatch" + print("PASS: changed transcript rejected") else: - raise ValueError(f"Unsupported digest algorithm: {algorithm}") - -if computed != expected_hex: - raise RuntimeError( - f"Transcript hash mismatch.\n" - f" Expected: {expected}\n" - f" Computed: {algorithm}:{computed}" - ) - -print(f"Transcript verified: {len(transcript_bytes)} bytes, {algorithm}:{computed[:16]}...") + raise RuntimeError("Changed transcript was accepted") ``` -If this check passes, the transcript at `transcript_uri` is byte-for-byte what was hashed when the Trust Record was signed. Combined with the signature check from Step 1, this gives you end-to-end integrity: record → hash → transcript. - ---- - -## Step 4: Inspect individual call records +The final two lines should report a matching transcript and rejection of changed bytes. The verifier's key is retained independently by the demo; a recipient needs its own trusted issuer-key configuration. -The transcript is a JSON array of tool call records. Each entry captures one call: +## Verify a received transcript -```json -[ - { - "call_index": 0, - "tool_name": "read_file", - "input_hash": "sha256:...", - "output_hash": "sha256:...", - "started_at": "2026-06-23T09:14:58Z", - "duration_ms": 142 - } -] -``` - -The inputs and outputs are themselves hashed: the raw argument and response values are not in the transcript by default. This protects sensitive tool arguments while still committing the content: - -```python -import json - -calls = json.loads(transcript_bytes) - -print(f"Total calls: {len(calls)}") -for call in calls: - print(f" [{call['call_index']}] {call['tool_name']}") - print(f" input: {call.get('input_hash', 'not committed')}") - print(f" output: {call.get('output_hash', 'not committed')}") -``` - -Cross-check against `call_count` if it was set in the Trust Record: - -```python -call_count = record["tool_transcript"].get("call_count") -if call_count is not None and len(calls) != call_count: - print(f"Warning: record says {call_count} calls but transcript has {len(calls)}") -``` - ---- - -## External execution receipts - -For high-assurance scenarios, individual calls may carry external execution receipts: signed by a third-party (the caller, an orchestrator, or a notary) rather than the agent that produced the Trust Record. - -The spec (§3.3.2) defines the receipt structure: - -| Field | Description | -|---|---| -| `issuer` | URI identifying the signing party | -| `issuer_key_id` | Key identifier within that party's key set | -| `signature` | Signature over `evidence_hash` | -| `evidence_hash` | Digest of the specific call being attested | -| `evidence_type` | Content type of the evidence (e.g., `application/json`) | -| `linked_call_id` | The call index this receipt binds to | - -To verify a receipt against a specific call: - -```python -def verify_external_receipt(call, receipt, issuer_public_key): - expected_hash = call["input_hash"] # or output_hash depending on what was attested - algorithm, expected_hex = expected_hash.split(":", 1) - - receipt_evidence_hash = receipt["evidence_hash"] - receipt_alg, receipt_hex = receipt_evidence_hash.split(":", 1) - - # The receipt's evidence_hash must match the call's committed hash - if receipt_hex != expected_hex or receipt_alg != algorithm: - raise RuntimeError( - f"Receipt evidence_hash does not match call {call['call_index']}" - ) - - # The signature covers the evidence_hash bytes (algorithm-specific) - # Verify using the issuer's public key from their published key set - # (Key retrieval from issuer URI is application-specific) - # ... - return True -``` - -!!! info "No SDK helper for receipt verification" - The `agentrust_trace` SDK does not include an issuer key resolver or receipt chain verifier. Resolution of `issuer` URIs to public keys is application-specific: typically a DID document or a published JWK Set at a well-known endpoint. - ---- - -## Putting it together - -A complete audit verification run: - -```python -from agentrust_trace.sign import verify_record, load_key -import hashlib -import json -import requests - -def verify_audit_chain(record_path, public_key_path): - with open(public_key_path, "rb") as f: - public_key = load_key(f.read()) - - with open(record_path) as f: - record = json.load(f) - - # Step 1: Verify record signature - verify_record(record, public_key_or_jwk=public_key) - print("Signature: OK") - - tt = record.get("tool_transcript") - if not tt: - print("No tool_transcript: nothing further to verify") - return - - # Step 2: Retrieve transcript - uri = tt.get("transcript_uri") - if not uri: - print("No transcript_uri: cannot retrieve transcript") - return - - transcript_bytes = requests.get(uri, timeout=30).content - - # Step 3: Hash check - algorithm, expected_hex = tt["hash"].split(":", 1) - hashfn = hashlib.sha256 if algorithm == "sha256" else hashlib.sha384 - computed = hashfn(transcript_bytes).hexdigest() - - if computed != expected_hex: - raise RuntimeError(f"Transcript hash mismatch: got {algorithm}:{computed}") - - print(f"Transcript hash: OK ({algorithm}:{computed[:16]}...)") - - # Step 4: Call count - calls = json.loads(transcript_bytes) - call_count = tt.get("call_count") - if call_count is not None: - match = "OK" if len(calls) == call_count else "MISMATCH" - print(f"Call count: {len(calls)}/{call_count} [{match}]") - else: - print(f"Calls in transcript: {len(calls)}") -``` +Use the saved-record key-loading sequence in [verify a trust record](verifying-a-trust-record.md). Obtain transcript bytes through your application's approved artifact channel. `transcript_uri` is optional, so a record does not always provide a download location. Apply your application's URL, response-size, and access controls before fetching a producer-supplied URI. ---- +Verify the signature, compute the digest using the producing profile's encoding, and reject mismatches. If a count is supplied, compare it using that profile's definition of a call. Do not silently treat absent evidence or a count mismatch as successful transcript verification. -## Summary +## Interpret the result -| Step | What it proves | -|---|---| -| `verify_record()` | Record was not altered after signing; `tool_transcript.hash` is trusted | -| Transcript hash check | Transcript bytes are exactly what was hashed at signing time | -| Call count check | Transcript was not truncated | -| External receipt check | Third-party confirms specific call inputs/outputs (optional) | +A matching digest binds these bytes to the trusted key's signed statement. A matching count shows consistency with the declared count. Neither establishes that the producer disclosed every call, that an action completed, or that its outputs are correct. Hashing sensitive input also does not guarantee confidentiality, particularly for predictable values. -The chain of custody runs: hardware/software measurement → signed Trust Record → committed transcript hash → per-call hashes → optional external receipts. Each link is independently verifiable without contacting the operator who produced the record. +External action receipts require their own issuer trust, signature, action binding, and freshness checks. Use the [action-receipt verification guide](../verification.md#action-receipts-and-embodied-workflows) for those checks; a generic transcript hash does not perform them. diff --git a/docs/verification-outcome-statements.md b/docs/verification-outcome-statements.md index a2de542f..1a1a809a 100644 --- a/docs/verification-outcome-statements.md +++ b/docs/verification-outcome-statements.md @@ -26,7 +26,7 @@ The rows are representative, not exhaustive. Each row reads: the outcome a verif |---|---|---|---|---| | **Verified at the required floor.** Signature binding verified, record fresh, `appraisal.status` is `affirming` (or `warning`), and `appraisal.provenance_depth_verified` meets the deployment profile floor. | This record was not altered after issuance, and the checks the verifier ran passed at the recorded depth, under the appraisal policy named in `appraisal.policy_ref`. ([§3.2.2](../spec/trace-v0.2.md), [What verification proves](verification.md#what-verification-proves)) | Not current trust in the signing key unless revocation was actually consulted ([LIMITATIONS](../LIMITATIONS.md): pure offline verification cannot prove non-revocation). Not that the bound policy achieves its intended outcome ([LIMITATIONS](../LIMITATIONS.md): policy correctness is a separate control). Not soundness of the model's behavior ([§2.4](../spec/trace-v0.2.md)). Not success of any physical or business outcome ([§3.3.3](../spec/trace-v0.2.md)). Not a general trust score for the subject. | The record itself; the trusted-key thumbprint used; the freshness bounds applied; if revocation was checked, the bundle identity and `valid_until` - reported as "verified against revocation bundle valid at T" ([§3.2.3](../spec/trace-v0.2.md)); the complete `appraisal`. | Re-verify before material reliance on an aging record; consult current revocation status at decision time; keep the `policy_ref` target resolvable for whatever retention the published statement promises. | | **Honestly downgraded, floor still met.** Evidence the claimed depth needs did not resolve; the verifier stopped lower and recorded the achieved depth in `appraisal.provenance_depth_verified`; nothing contradicted; the achieved depth still meets the configured floor. | Verification reached `builder` (for example); the deeper claim was left unverified because named evidence did not resolve; the unresolved evidence is identified. ([§3.3.1](../spec/trace-v0.2.md), [downgrade column of the depth table](verification.md#verifying-build-provenance-depth)) | The downgrade is not a defect in the record: "a record is not defective because someone else's transparency log is unreachable" ([verification.md](verification.md#verifying-build-provenance-depth)). Equally, nothing is assured at the unexecuted depth - unverified is not silently true ([what each stopping point leaves unknown](build-provenance-depth.md)). | `appraisal.provenance_depth_verified`; the evidence locations attempted and why each did not resolve; the configured floor; the final `appraisal`. | Retry resolution or obtain the evidence out-of-band; where the gap matters to the reader, state it explicitly instead of letting the affirming status carry the weight. | -| **Resolved and contradicted.** Named evidence resolves and refutes the record - attestation subject mismatch, dependency publisher outside the trusted set, digest mismatch. The appraisal fails and is not downgraded to escape the contradiction; `appraisal.status` is `contraindicated`. | The named artifact resolves and contradicts this record's claim at the named depth; the appraisal failed, and the record is treated as untrusted. ([§3.3.1](../spec/trace-v0.2.md), [Step 5 status meanings](verification.md#step-5--appraise-the-claims)) | The statement covers the specific contradiction observed and nothing else: it does not extend to other fields or axes, and appraisal output does not identify which adversary class from [§2.2](../spec/trace-v0.2.md) caused the contradiction. | The contradicting artifacts themselves (attestation bytes, publisher attestations, issuer identities outside the trusted set), the comparisons performed, the final `appraisal`. | Treat as incident input; contact the issuer or operator; corrected records need re-issue, not re-reading. | +| **Resolved and contradicted.** Named evidence resolves and refutes the record - attestation subject mismatch, dependency publisher outside the trusted set, digest mismatch. The appraisal fails and is not downgraded to escape the contradiction; `appraisal.status` is `contraindicated`. | The named artifact resolves and contradicts this record's claim at the named depth; the appraisal failed, and the record is treated as untrusted. ([§3.3.1](../spec/trace-v0.2.md), [Step 5 status meanings](verification.md#step-5-appraise-the-claims)) | The statement covers the specific contradiction observed and nothing else: it does not extend to other fields or axes, and appraisal output does not identify which adversary class from [§2.2](../spec/trace-v0.2.md) caused the contradiction. | The contradicting artifacts themselves (attestation bytes, publisher attestations, issuer identities outside the trusted set), the comparisons performed, the final `appraisal`. | Treat as incident input; contact the issuer or operator; corrected records need re-issue, not re-reading. | | **Unresolvable below the floor.** Achieved `appraisal.provenance_depth_verified` sits below the deployment profile floor, so `appraisal.status` is `contraindicated` by the floor rule. Nothing resolved-and-contradicted. | Verification reached `surface`; this deployment's floor is `transitive`; the record is refused by the floor, with no contradicting evidence found. ([floor rule in §3.3.1](../spec/trace-v0.2.md)) | Unverifiable is not incriminating: "the strictness lives in the floor, not in a finding against the record" ([build provenance depth](build-provenance-depth.md#choosing-a-depth)). Do not report this as though evidence had contradicted the record - the depth vectors encode the two outcomes separately ([verification.md](verification.md#verifying-build-provenance-depth)), and reporting should keep them apart. | The attempted evidence locations and errors; the floor configuration; `appraisal.provenance_depth_verified`; `appraisal.status`. | Obtain the missing evidence, or let the rejection stand; record which of the two happened. | | `policy.enforcement_mode` is `"declared"`: the policy is named and bound into the signed record, and nothing evaluated it. ([§4.3](../spec/trace-v0.2.md)) | The policy this deployment states it operated under is bound into the record by `policy.bundle_hash`; the record asserts no evaluation and no enforcement of any rule. | Reading `"declared"` as evidence that any rule was checked contradicts the field's own definition ([§4.3](../spec/trace-v0.2.md)): `enforce`, `advisory` and `silent` all assert some evaluation, `declared` asserts none, and a verifier appraising for enforcement treats it like an absent enforcement claim. | The record itself - the binding is signature-covered; the external report carries its own note that no evaluation claim is present. | If the assurance need requires evaluated policy, request records produced under `enforce` or `silent`, or evaluate the bound policy out-of-band; policy review remains a separate control ([LIMITATIONS](../LIMITATIONS.md)). | diff --git a/docs/verification.md b/docs/verification.md index b53ed413..d6d9f5d4 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -1,105 +1,53 @@ # Verification Protocol -TRACE Trust Records are independently verifiable offline: no call to the issuer, no API, no trust-me-the-log-is-real. The one thing offline verification cannot establish is that the signing key is *still* trusted; see [Checking revocation status](#checking-revocation-status). +A TRACE verifier authenticates a signed record and evaluates the evidence required by the recipient's policy. Offline verification needs the relevant artifacts and trust inputs already available. It cannot infer missing hardware, revocation, or transparency evidence from the record's assertions. ## Five-step verification -This is the normative protocol from [§3.3 of the spec](../spec/trace-v0.2.md). - -Before interpreting any claim, validate the complete object against the canonical -v0.2 JSON Schema. A valid signature authenticates every byte but does not make an -unknown field, missing required claim, or invalid enum meaningful. The Python -`verify_record()` API performs this schema check automatically and fails closed. +This is an implementation guide to [section 3.3 of the specification](../spec/trace-v0.2.md), which remains authoritative. For a runnable Python example, use [verify a trust record](tutorials/verifying-a-trust-record.md). ### Step 1: Parse the envelope -A TRACE Trust Record is a signed JSON object. The `signature` field contains a base64url-encoded Ed25519 (or ES256/ES384) signature over the canonical JSON of the record with only `signature` removed. The `cnf.jwk` public key remains in the signed pre-image, binding that key to the rest of the record. - -```python -import json, base64 -import rfc8785 # RFC 8785 (JCS) canonicalization -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - -record = json.load(open("session.trace.json")) -sig_bytes = base64.urlsafe_b64decode(record["signature"] + "==") -payload = {k: v for k, v in record.items() if k != "signature"} -payload_bytes = rfc8785.dumps(payload) # JCS canonical bytes, NOT json.dumps -``` - -The pre-image is the RFC 8785 (JCS) canonical form of the record with only `signature` removed. All other top-level fields, including `cnf`, are included. `json.dumps(sort_keys=True)` is **not** JCS-conformant, it diverges for non-ASCII strings and IEEE 754 numbers, so use a JCS library (the spec mandates this in §3.2.2). +Validate the complete standalone record against the canonical schema and supported EAT profile. A cMCP `RuntimeClaim` is a different envelope and requires its runtime-specific verifier. ### Step 2: Resolve the public key -The `cnf.jwk` field embeds the public key. For TEE-issued records, this key is TEE-bound: its private half never leaves the measured enclave. - -Resolve trust out of band and require the trusted key and `cnf.jwk` to have the -same RFC 7638 thumbprint before verification. Checking the signature with a -trusted key while allowing the signed record to name a different confirmation -key breaks the binding required by §3.2.2 and can mislead downstream -proof-of-possession checks. - -```python -from cryptography.hazmat.primitives.serialization import load_der_public_key - -jwk = record["cnf"]["jwk"] -# For ES256/ES384: reconstruct EC key from x/y -# For Ed25519: decode x directly -pub_key = Ed25519PublicKey.from_public_bytes( - base64.urlsafe_b64decode(jwk["x"] + "==") -) -``` +Obtain an approved issuer key through the recipient's own trust configuration. The incoming `cnf.jwk` cannot establish its own authority. The trusted key and signed confirmation key must match under the signature profile. ### Step 3: Verify the signature -```python -pub_key.verify(sig_bytes, payload_bytes) -# Raises InvalidSignature if tampered: silent if valid -print("✓ Signature valid") -``` +Use `agentrust_trace.verify_record(record, public_key_or_jwk=trusted_key)`. It checks the standalone schema, supported profile, key binding, and Ed25519 signature, with configured freshness, nonce, and revocation inputs. It rejects missing trust input by default. The signature covers every field except `signature`, including `cnf` and any `transparency` value, using RFC 8785 canonicalization. -### Step 4: Check the EAT profile +A signature proves a statement came from the trusted key. It does not establish the truth of every claim in that statement. -```python -assert record["eat_profile"] == "tag:agentrust-io.com,2026:trace-v0.2", "Unknown profile" -print("✓ eat_profile correct") -``` +### Step 4: Check the EAT profile -If you verify with `agentrust_trace.verify_record`, this step is enforced for you, -before any cryptographic work: a record whose `eat_profile` is missing, superseded -(the v0.1 identifier), or anything other than `TRACE_PROFILE_V0_2` raises -`ValueError`. The manual assert above is what a from-scratch verifier must do -itself: spec section 2 requires a v0.2 verifier to reject everything but the v0.2 -identifier, and a valid signature over semantics your build does not implement is -not evidence. +The current SDK requires `tag:agentrust-io.com,2026:trace-v0.2`. The superseded v0.1 identifier is rejected. This check is already included in `verify_record`; a custom verifier must also enforce its supported profile. ### Step 5: Appraise the claims -Interpret `appraisal.status` against your policy: +Resolve and verify the evidence your policy requires: hardware reports, expected measurements, policy and transcript artifacts, build provenance, revocation state, and transparency proofs. The record's `appraisal.status` is itself a signed claim, not an independent appraisal performed by `verify_record`. -| Status | Meaning | +| Claimed status | Interpretation | |---|---| -| `affirming` | All evidence passed verifier appraisal | -| `warning` | Evidence passed but with conditions | -| `contraindicated` | Evidence failed: treat as untrusted | -| `none` | No appraisal performed (software-only Level 0) | +| `affirming` | The issuer reports a successful appraisal; verify its authority, evidence, scope, and policy | +| `warning` | The issuer reports conditions that need recipient policy handling | +| `contraindicated` | The issuer reports failed appraisal | +| `none` | No appraisal is claimed | -```python -status = record["appraisal"]["status"] -assert status == "affirming", f"Appraisal failed: {status}" -print(f"✓ Appraisal: {status}") -``` +The recipient decides whether the checks performed satisfy the operation's requirements. A non-software platform name or `affirming` string alone is insufficient. ## Checking revocation status -The five steps above are self-contained: given the record and a trusted key, they run with no network. That is the property TRACE is built for, and it has exactly one gap. A signature is valid forever, so a record signed by a key that was later compromised and revoked still passes every offline step. Nothing inside the record can withdraw the key that signed it. +Signature verification alone cannot discover a later key revocation. Offline appraisal requires cached revocation evidence as well as the record and trusted key. Report which evidence was checked and whether it remains current. -[§3.2.3 of the spec](../spec/trace-v0.2.md) closes that gap without giving up offline verification. Two things are worth knowing before reading the code below. +[§3.2.3 of the spec](../spec/trace-v0.2.md) closes that gap without giving up offline verification. Two things are worth knowing before reading the code below. -**The boundary is a log entry ID, not a time.** The intuitive rule is to reject a record from a revoked key when its `iat` falls after the compromise. A compromised record-signing key also signs `iat`, so whoever holds it backdates the record and the rule passes. §3.2.3 anchors to the SCITT inclusion entry ID instead, because entry IDs are monotonic and bound to the Merkle structure, so ordering survives the compromise of the signing key in a way a timestamp does not. In §3.2.3's words, a record from a revoked key is valid *"if and only if its SCITT inclusion entry ID is less than or equal to `last_valid_entry_id`"*, on the log named in the statement. +**The boundary is a log entry ID, not a time.** The intuitive rule is to reject a record from a revoked key when its `iat` falls after the compromise. A compromised record-signing key also signs `iat`, so whoever holds it backdates the record and the rule passes. §3.2.3 anchors to the SCITT inclusion entry ID instead, because entry IDs are monotonic and bound to the Merkle structure, so ordering survives the compromise of the signing key in a way a timestamp does not. In §3.2.3's words, a record from a revoked key is valid *"if and only if its SCITT inclusion entry ID is less than or equal to `last_valid_entry_id`"*, on the log named in the statement. -**Offline is a state you report, not a check you skip.** Revocation statements are anchored in the same transparency log as the records they govern, and verifiers cache a signed bundle carrying `valid_until`. A verifier offline says what it checked against, "verified against revocation bundle valid at T", rather than reporting an affirming appraisal it did not earn. §3.2.3 states that an expired bundle *"MUST report the record as unverified for revocation rather than as verified"*, and that a verifier with no bundle *"MUST report that it performed no revocation check"*. +**Offline is a state you report, not a check you skip.** Revocation statements are anchored in the same transparency log as the records they govern, and verifiers cache a signed bundle carrying `valid_until`. A verifier offline says what it checked against, "verified against revocation bundle valid at T", rather than reporting an affirming appraisal it did not earn. §3.2.3 states that an expired bundle *"MUST report the record as unverified for revocation rather than as verified"*, and that a verifier with no bundle *"MUST report that it performed no revocation check"*. -A record with no usable inclusion entry ID has no anchor to place it before or after the compromise, so §3.2.3 falls back to binary revocation for it: *"a verifier MUST reject every record signed by the revoked key"*. That fallback is what `verify_record()` implements for both the store and the bundle, and it is the correct behaviour for deployments carrying no receipts. +A record with no usable inclusion entry ID has no anchor to place it before or after the compromise, so §3.2.3 falls back to binary revocation for it: *"a verifier MUST reject every record signed by the revoked key"*. That fallback is what `verify_record()` implements for both the store and the bundle, and it is the correct behaviour for deployments carrying no receipts. `verify_record()` takes a `revocation` store to do this. Pass a container of revoked identifiers, or a callable that performs a live lookup: @@ -129,7 +77,7 @@ Both failure modes raise `ValueError`, including a store that cannot answer: The last row is the honest default. Omitting the store is a legitimate mode, since air-gapped audit of archived records has no other option, but the result means "this record was validly signed by this key", not "this key is still trusted", and the result says so rather than leaving it implied. -`verify_record()` also consumes the bundle format §3.2.3 publishes. Pass `revocation_bundle`, a `TraceRevocationBundle/1.0` object, and `trusted_bundle_keys`, the JWKs whose signatures the caller accepts on a bundle: +`verify_record()` also consumes the bundle format §3.2.3 publishes. Pass `revocation_bundle`, a `TraceRevocationBundle/1.0` object, and `trusted_bundle_keys`, the JWKs whose signatures the caller accepts on a bundle: ```python result = verify_record( @@ -142,23 +90,19 @@ result.revocation.cause # why a supplied bundle could not ground "verified" result.revocation.evidence # what a second verifier needs to reach the same outcome ``` -The three outcomes are §3.2.3's own words, and none of them is an appraisal: where a verifier records an unresolvable check in the record itself is the question [#190](https://github.com/agentrust-io/trace-spec/issues/190) holds open. A bundle is evidence only while both age bounds hold, the issuer's `valid_until` and the caller's `max_bundle_age_seconds` measured from `issued_at`; the tighter bound governs, and an expired outcome names which one tripped. `now` pins the verification moment so the outcome reproduces from retained facts. A bundle that is malformed, signed by a key not in `trusted_bundle_keys`, signed with an algorithm this build cannot verify, dated in the future, or expired under either bound yields `unverified_for_revocation` with the cause named; it does not raise, because inability to check is not evidence of a defect. A statement on the bundle's log naming the trusted key raises, under the fallback above, and it is read before the time checks: the bounds say what the bundle's silence is worth, and an authenticated statement has no expiry of its own. [`examples/revocation-bundle/`](../examples/revocation-bundle/) carries the conformance vectors. +The three outcomes are §3.2.3's own words, and none of them is an appraisal: where a verifier records an unresolvable check in the record itself is the question [#190](https://github.com/agentrust-io/trace-spec/issues/190) holds open. A bundle is evidence only while both age bounds hold, the issuer's `valid_until` and the caller's `max_bundle_age_seconds` measured from `issued_at`; the tighter bound governs, and an expired outcome names which one tripped. `now` pins the verification moment so the outcome reproduces from retained facts. A bundle that is malformed, signed by a key not in `trusted_bundle_keys`, signed with an algorithm this build cannot verify, dated in the future, or expired under either bound yields `unverified_for_revocation` with the cause named; it does not raise, because inability to check is not evidence of a defect. A statement on the bundle's log naming the trusted key raises, under the fallback above, and it is read before the time checks: the bounds say what the bundle's silence is worth, and an authenticated statement has no expiry of its own. [`examples/revocation-bundle/`](https://github.com/agentrust-io/trace-spec/tree/main/examples/revocation-bundle/) carries the conformance vectors. -What neither path does yet is entry-ID-scoped revocation. Both answer "is this key revoked", which is the §3.2.3 fallback, so a key revoked after a long run of legitimate records currently invalidates all of them rather than the ones logged after `last_valid_entry_id`. Carrying the entry ID through `verify_record()` is implementation work tracked in the issue that produced §3.2.3. The bundle path also verifies the bundle signature only, not each statement's own signature against the §3.2.1 hierarchy; that check needs the hierarchy, and it is stated here rather than implied. +What neither path does yet is entry-ID-scoped revocation. Both answer "is this key revoked", which is the §3.2.3 fallback, so a key revoked after a long run of legitimate records currently invalidates all of them rather than the ones logged after `last_valid_entry_id`. Carrying the entry ID through `verify_record()` is implementation work tracked in the issue that produced §3.2.3. The bundle path also verifies the bundle signature only, not each statement's own signature against the §3.2.1 hierarchy; that check needs the hierarchy, and it is stated here rather than implied. ## Verifying hardware-rooted records -For Level 2 records (TEE-issued), additionally verify that the `cnf.jwk` key is bound to the hardware measurement in `runtime`: - -1. Fetch the Reference Integrity Manifest at `runtime.rim_uri` -2. Compare `runtime.measurement` against the RIM -3. Verify that `cnf.jwk` was endorsed by the TEE at that measurement +Hardware appraisal supports Level 1; Level 2 adds transparency anchoring. Verify the report or quote signature and accepted trust chain, its freshness and platform policy, the independently approved measurement, and its binding to the record-signing key. The producing profile defines that binding. -This chain proves the key that signed the TRACE record was generated *inside* the attested enclave, not by an operator process. +`verify_record` does not perform these hardware checks. Comparing a record's digest to an unauthenticated reference or reading `affirming` is not a substitute. See [attestation platforms](platforms/index.md) and the producing runtime's verifier. ## Verifying build provenance depth -The normative rules are defined by [§3.3.1 of the specification](../spec/trace-v0.2.md). +The normative rules are defined by [§3.3.1 of the specification](../spec/trace-v0.2.md). `build_provenance.provenance_depth` declares how far down the supply chain the issuer claims to have walked. A verifier records what it actually checked in `appraisal.provenance_depth_verified`, which is a statement about the verifier, not about the @@ -239,30 +183,13 @@ it states what each depth does not assure. ## CLI verification -```bash -# Install -pip install agentrust-trace - -# Verify a record -agentrust-trace verify session.trace.json --pubkey issuer.pub - -# Verify with hardware check (fetches RIM from AMD/Intel/NVIDIA) -agentrust-trace verify session.trace.json --pubkey issuer.pub --check-hardware - -# Batch verify -agentrust-trace verify *.trace.json --pubkey issuer.pub --summary -``` +The reference SDK exposes a Python API; it does not install an `agentrust-trace` command. Follow the [complete verification script](tutorials/verifying-a-trust-record.md) to load a saved record and independently trusted public key. Hardware appraisal requires a provider-specific verifier and evidence inputs. ## SCITT-anchored records -If `transparency` is set, the record is anchored in an append-only transparency log. Verify the anchor: - -```bash -agentrust-trace verify-scitt session.trace.json \ - --transparency-log https://registry.agentrust-io.com -``` +A `transparency` URI names a claimed log entry. It does not establish inclusion by itself. Retrieve the receipt, verify its binding to the record, and verify the inclusion proof against an independently trusted log or checkpoint. See [anchoring to the registry](tutorials/anchoring-to-the-registry.md) for the reference format and sequence. -A valid SCITT receipt proves the record was included in the log and cannot be retroactively removed or modified. +An authenticated inclusion proof establishes inclusion under that checkpoint. It does not establish the truth of the record's claims, complete logging, or future log availability. ## Action receipts and embodied workflows @@ -300,15 +227,15 @@ external outcome claim. | Claim verified | What it means | |---|---| -| Signature valid | The record was not tampered with after issuance | -| `cnf.jwk` hardware-bound | The signing key was generated inside a measured TEE | -| `policy.bundle_hash` | This exact Cedar policy was in force, not an approximate | -| `tool_transcript.hash` | The audit log is intact and matches the record | -| SCITT receipt valid | The record is in an append-only log: cannot be quietly deleted | +| Signature valid against a trusted key | That key signed the authenticated record bytes | +| Independently appraised hardware/key binding | The accepted evidence binds this key to the environment under the producing profile | +| Policy artifact matches its hash | The supplied artifact matches the signed commitment; execution needs separate evidence | +| Transcript artifact matches its hash | The supplied transcript matches the commitment; completeness is not established by the hash alone | +| Receipt valid against a trusted log/checkpoint | The bound record was included under that checkpoint; availability and completeness remain separate | ## What verification does NOT prove -Verification proves *what happened during the recorded session* under the stated policy, in the stated environment. It does not: +Verification establishes the checks actually performed against the supplied evidence and trust inputs. It does not: - Prove the signing key is still trusted; offline verification cannot prove non-revocation, so pass a `revocation` store - Prove the agent's internal reasoning was sound diff --git a/spec/trace-v0.2.md b/spec/trace-v0.2.md index 2c8dfe9e..4a2d18d7 100644 --- a/spec/trace-v0.2.md +++ b/spec/trace-v0.2.md @@ -7,7 +7,7 @@ | Authors | Imran Siddique, Rishabh Poddar, Aaron Fulkerson (OPAQUE Systems) | | Target announcement | Confidential Computing Summit, San Francisco: 23 June 2026 | | Reference implementation | [agentrust-io/cmcp](https://github.com/agentrust-io/cmcp): Confidential MCP | -| License | Community Specification License 1.0 (see [LICENSE](../LICENSE)) | +| License | Community Specification License 1.0 (see [LICENSE](https://github.com/agentrust-io/trace-spec/blob/main/LICENSE)) | > **Note:** This is a pre-ratification draft. Fields, wire formats, and conformance requirements are subject to change before v1.0. Send feedback to: open an issue on this repository. diff --git a/tests/test_docs_quickstart.py b/tests/test_docs_quickstart.py index ec2d33ad..c41922ee 100644 --- a/tests/test_docs_quickstart.py +++ b/tests/test_docs_quickstart.py @@ -21,3 +21,40 @@ def test_first_record_tutorial(tmp_path): ) assert second.returncode == 0, second.stdout + second.stderr assert "PASS: saved record verified against the retained public key" in second.stdout + + verify_page = page.parent / "tutorials/verifying-a-trust-record.md" + verify_blocks = re.findall( + r"^```python\n(.*?)^```", verify_page.read_text(encoding="utf-8-sig"), re.M | re.S + ) + assert len(verify_blocks) == 2 + received = subprocess.run( + [sys.executable, "-c", "\n".join(verify_blocks)], + cwd=tmp_path, text=True, capture_output=True, + ) + assert received.returncode == 0, received.stdout + received.stderr + assert "no revocation check performed" in received.stdout + assert "edited record rejected" in received.stdout + + +def test_agt_adapter_tutorial(tmp_path): + page = Path(__file__).resolve().parents[1] / "docs/tutorials/agt-adapter.md" + blocks = re.findall(r"^```python\n(.*?)^```", page.read_text(encoding="utf-8-sig"), re.M | re.S) + assert len(blocks) == 1 + result = subprocess.run( + [sys.executable, "-c", blocks[0]], cwd=tmp_path, text=True, capture_output=True + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "PASS: mapped and signed synthetic session" in result.stdout + + transcript_page = page.with_name("verifying-the-audit-chain.md") + transcript_blocks = re.findall( + r"^```python\n(.*?)^```", transcript_page.read_text(encoding="utf-8-sig"), re.M | re.S + ) + assert len(transcript_blocks) == 1 + transcript = subprocess.run( + [sys.executable, "-c", blocks[0] + "\n" + transcript_blocks[0]], + cwd=tmp_path, text=True, capture_output=True, + ) + assert transcript.returncode == 0, transcript.stdout + transcript.stderr + assert "PASS: transcript matches the signed commitment" in transcript.stdout + assert "PASS: changed transcript rejected" in transcript.stdout From 38efda8382f328cd1749b01dce00ae4d86c7a0b2 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sun, 6 Sep 2026 09:46:31 -0700 Subject: [PATCH 2/3] docs: restore measurement requirement and repair section encoding Signed-off-by: Imran Siddique --- docs/trust-levels.md | 2 +- docs/verification.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/trust-levels.md b/docs/trust-levels.md index 54f851ce..07130150 100644 --- a/docs/trust-levels.md +++ b/docs/trust-levels.md @@ -22,7 +22,7 @@ A software-held key signs the record. `software-only` identifies the absence of A valid signature authenticates the key's statement. It does not prove that a policy ran or an action completed. A privileged party holding that key can sign other statements. -The measurement can be a software commitment defined by the producer. All-zero is reserved for an example or producer that offers no measurement commitment. Use the [quick start](quickstart.md) for a complete runnable record rather than copying abbreviated field examples. +`runtime.measurement` is required on every record, including `software-only` ones. Under `software-only`, the field is not a hardware measurement: it is a software commitment defined by the producing profile (for example, a hash over an image digest and policy bundle, or over a chain-tip), and that profile must document its preimage so a verifier can recompute it. All-zero (`sha256:000...000`) is reserved for a producer that has no commitment to offer at all, such as a bare development record with nothing measured; it is not the default for `software-only` in general. The `appraisal.status` of `"none"` is correct when no hardware verifier is in the path. Use the [quick start](quickstart.md) for a complete runnable record rather than copying abbreviated field examples. ## Level 1: hardware evidence diff --git a/docs/verification.md b/docs/verification.md index d6d9f5d4..ead76793 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -41,13 +41,13 @@ The recipient decides whether the checks performed satisfy the operation's requi Signature verification alone cannot discover a later key revocation. Offline appraisal requires cached revocation evidence as well as the record and trusted key. Report which evidence was checked and whether it remains current. -[§3.2.3 of the spec](../spec/trace-v0.2.md) closes that gap without giving up offline verification. Two things are worth knowing before reading the code below. +[§3.2.3 of the spec](../spec/trace-v0.2.md) closes that gap without giving up offline verification. Two things are worth knowing before reading the code below. -**The boundary is a log entry ID, not a time.** The intuitive rule is to reject a record from a revoked key when its `iat` falls after the compromise. A compromised record-signing key also signs `iat`, so whoever holds it backdates the record and the rule passes. §3.2.3 anchors to the SCITT inclusion entry ID instead, because entry IDs are monotonic and bound to the Merkle structure, so ordering survives the compromise of the signing key in a way a timestamp does not. In §3.2.3's words, a record from a revoked key is valid *"if and only if its SCITT inclusion entry ID is less than or equal to `last_valid_entry_id`"*, on the log named in the statement. +**The boundary is a log entry ID, not a time.** The intuitive rule is to reject a record from a revoked key when its `iat` falls after the compromise. A compromised record-signing key also signs `iat`, so whoever holds it backdates the record and the rule passes. §3.2.3 anchors to the SCITT inclusion entry ID instead, because entry IDs are monotonic and bound to the Merkle structure, so ordering survives the compromise of the signing key in a way a timestamp does not. In §3.2.3's words, a record from a revoked key is valid *"if and only if its SCITT inclusion entry ID is less than or equal to `last_valid_entry_id`"*, on the log named in the statement. -**Offline is a state you report, not a check you skip.** Revocation statements are anchored in the same transparency log as the records they govern, and verifiers cache a signed bundle carrying `valid_until`. A verifier offline says what it checked against, "verified against revocation bundle valid at T", rather than reporting an affirming appraisal it did not earn. §3.2.3 states that an expired bundle *"MUST report the record as unverified for revocation rather than as verified"*, and that a verifier with no bundle *"MUST report that it performed no revocation check"*. +**Offline is a state you report, not a check you skip.** Revocation statements are anchored in the same transparency log as the records they govern, and verifiers cache a signed bundle carrying `valid_until`. A verifier offline says what it checked against, "verified against revocation bundle valid at T", rather than reporting an affirming appraisal it did not earn. §3.2.3 states that an expired bundle *"MUST report the record as unverified for revocation rather than as verified"*, and that a verifier with no bundle *"MUST report that it performed no revocation check"*. -A record with no usable inclusion entry ID has no anchor to place it before or after the compromise, so §3.2.3 falls back to binary revocation for it: *"a verifier MUST reject every record signed by the revoked key"*. That fallback is what `verify_record()` implements for both the store and the bundle, and it is the correct behaviour for deployments carrying no receipts. +A record with no usable inclusion entry ID has no anchor to place it before or after the compromise, so §3.2.3 falls back to binary revocation for it: *"a verifier MUST reject every record signed by the revoked key"*. That fallback is what `verify_record()` implements for both the store and the bundle, and it is the correct behaviour for deployments carrying no receipts. `verify_record()` takes a `revocation` store to do this. Pass a container of revoked identifiers, or a callable that performs a live lookup: @@ -77,7 +77,7 @@ Both failure modes raise `ValueError`, including a store that cannot answer: The last row is the honest default. Omitting the store is a legitimate mode, since air-gapped audit of archived records has no other option, but the result means "this record was validly signed by this key", not "this key is still trusted", and the result says so rather than leaving it implied. -`verify_record()` also consumes the bundle format §3.2.3 publishes. Pass `revocation_bundle`, a `TraceRevocationBundle/1.0` object, and `trusted_bundle_keys`, the JWKs whose signatures the caller accepts on a bundle: +`verify_record()` also consumes the bundle format §3.2.3 publishes. Pass `revocation_bundle`, a `TraceRevocationBundle/1.0` object, and `trusted_bundle_keys`, the JWKs whose signatures the caller accepts on a bundle: ```python result = verify_record( @@ -90,9 +90,9 @@ result.revocation.cause # why a supplied bundle could not ground "verified" result.revocation.evidence # what a second verifier needs to reach the same outcome ``` -The three outcomes are §3.2.3's own words, and none of them is an appraisal: where a verifier records an unresolvable check in the record itself is the question [#190](https://github.com/agentrust-io/trace-spec/issues/190) holds open. A bundle is evidence only while both age bounds hold, the issuer's `valid_until` and the caller's `max_bundle_age_seconds` measured from `issued_at`; the tighter bound governs, and an expired outcome names which one tripped. `now` pins the verification moment so the outcome reproduces from retained facts. A bundle that is malformed, signed by a key not in `trusted_bundle_keys`, signed with an algorithm this build cannot verify, dated in the future, or expired under either bound yields `unverified_for_revocation` with the cause named; it does not raise, because inability to check is not evidence of a defect. A statement on the bundle's log naming the trusted key raises, under the fallback above, and it is read before the time checks: the bounds say what the bundle's silence is worth, and an authenticated statement has no expiry of its own. [`examples/revocation-bundle/`](https://github.com/agentrust-io/trace-spec/tree/main/examples/revocation-bundle/) carries the conformance vectors. +The three outcomes are §3.2.3's own words, and none of them is an appraisal: where a verifier records an unresolvable check in the record itself is the question [#190](https://github.com/agentrust-io/trace-spec/issues/190) holds open. A bundle is evidence only while both age bounds hold, the issuer's `valid_until` and the caller's `max_bundle_age_seconds` measured from `issued_at`; the tighter bound governs, and an expired outcome names which one tripped. `now` pins the verification moment so the outcome reproduces from retained facts. A bundle that is malformed, signed by a key not in `trusted_bundle_keys`, signed with an algorithm this build cannot verify, dated in the future, or expired under either bound yields `unverified_for_revocation` with the cause named; it does not raise, because inability to check is not evidence of a defect. A statement on the bundle's log naming the trusted key raises, under the fallback above, and it is read before the time checks: the bounds say what the bundle's silence is worth, and an authenticated statement has no expiry of its own. [`examples/revocation-bundle/`](https://github.com/agentrust-io/trace-spec/tree/main/examples/revocation-bundle/) carries the conformance vectors. -What neither path does yet is entry-ID-scoped revocation. Both answer "is this key revoked", which is the §3.2.3 fallback, so a key revoked after a long run of legitimate records currently invalidates all of them rather than the ones logged after `last_valid_entry_id`. Carrying the entry ID through `verify_record()` is implementation work tracked in the issue that produced §3.2.3. The bundle path also verifies the bundle signature only, not each statement's own signature against the §3.2.1 hierarchy; that check needs the hierarchy, and it is stated here rather than implied. +What neither path does yet is entry-ID-scoped revocation. Both answer "is this key revoked", which is the §3.2.3 fallback, so a key revoked after a long run of legitimate records currently invalidates all of them rather than the ones logged after `last_valid_entry_id`. Carrying the entry ID through `verify_record()` is implementation work tracked in the issue that produced §3.2.3. The bundle path also verifies the bundle signature only, not each statement's own signature against the §3.2.1 hierarchy; that check needs the hierarchy, and it is stated here rather than implied. ## Verifying hardware-rooted records @@ -102,7 +102,7 @@ Hardware appraisal supports Level 1; Level 2 adds transparency anchoring. Verify ## Verifying build provenance depth -The normative rules are defined by [§3.3.1 of the specification](../spec/trace-v0.2.md). +The normative rules are defined by [§3.3.1 of the specification](../spec/trace-v0.2.md). `build_provenance.provenance_depth` declares how far down the supply chain the issuer claims to have walked. A verifier records what it actually checked in `appraisal.provenance_depth_verified`, which is a statement about the verifier, not about the From 5ec6f8aabff84ce34c61189af5de42be509dcf19 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sun, 6 Sep 2026 10:43:02 -0700 Subject: [PATCH 3/3] docs: scope privacy claims to SDK behavior and retained artifacts Signed-off-by: Imran Siddique --- PRIVACY.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 900944ef..392d396a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,9 +1,9 @@ # Privacy -agentrust-trace (the TRACE SDK) collects and transmits no personal data. +The TRACE SDK processes the records, keys, and evidence supplied by the calling application. Records can contain identifiers, artifact locations, or other sensitive metadata chosen by the producer; review those fields before sharing them. -It runs locally as a Python library. It processes only the inputs you give it, entirely on your machine, and sends no telemetry, analytics, or usage data to agentrust-io, OPAQUE, or any third party. There is no account, login, or tracking, and no cookies or background network calls. +Core signing and signature verification run locally. The SDK does not send project telemetry or analytics. Application-provided callbacks, evidence retrieval, and registry submission can involve network services chosen by that application; their handling of data is separate from the local signature operation. -Any network activity is user-initiated: anchoring a record to a SCITT transparency service happens only when you explicitly call it, to the endpoint you configure. +Uninstalling the package does not delete generated records, exported keys, logs, caches, backups, or records already shared with a registry or recipient. Manage those artifacts through your application's retention and deletion procedures. -Uninstalling removes it completely. Questions or corrections: https://github.com/agentrust-io/trace-spec/issues +[Report a correction](https://github.com/agentrust-io/trace-spec/issues).