From cf8e7f73229ff690540e7e1cac153b89bfc6c78c Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Wed, 1 Jul 2026 14:01:09 -0700 Subject: [PATCH] feat(a2a): optional delegation-link block for the A2A profile Add an optional `delegation` block to the Trust Record: `parent_record_hash` (digest of the parent hop's record) and `credential_id` (the delegation credential this hop acted under). A chain of records linked this way forms an offline-verifiable delegation DAG. This is the foundation of the A2A profile; A2A is now stable at v1.x, clearing the prior roadmap blocker. Backward-compatible (MINOR): the field is optional and existing records without it remain valid. Updates the Pydantic model (Delegation), both JSON schemas, the schema docs, CHANGELOG, and ROADMAP. Suite: 78 passed. Closes agentrust-io/ca2a#15 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 160 +++--- ROADMAP.md | 96 ++-- docs/schema.md | 340 ++++++------- schema/trace-claim.json | 550 +++++++++++---------- src/agentrust_trace/__init__.py | 108 ++-- src/agentrust_trace/models.py | 303 ++++++------ src/agentrust_trace/schema/trace-v0.1.json | 550 +++++++++++---------- tests/test_models.py | 333 +++++++------ tests/test_validate.py | 161 +++--- 9 files changed, 1365 insertions(+), 1236 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fc663..d175f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,76 +1,84 @@ -# Changelog - -All notable changes to the TRACE specification will be documented here. - -Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.MINOR.PATCH`: -- **MAJOR**: breaking changes to wire format or required Trust Record fields -- **MINOR**: new optional fields, new platform profiles, new conformance levels -- **PATCH**: editorial fixes, clarifications, non-normative additions - ---- - -## [0.3.0] — 2026-06-30 - -### Security - -- `verify_record` now requires an explicit trusted key. Self-verification from the embedded `cnf.jwk` is no longer the default; use `allow_embedded_key=True` to opt in. -- Verification enforces freshness (`iat` / `max_age_seconds`, default 24h) and an optional `expected_nonce`. JWK `kty` / `crv` are validated. - -### Breaking - -- **BREAKING:** Canonicalization is now RFC 8785 (JCS). Trust records are NOT cross-verifiable with 0.2.0 (the prior `json.dumps` canonicalization was non-conformant). - ---- - -## [0.1.0] — 2026-06-23 - -Initial public draft. Announced at Confidential Computing Summit, San Francisco. - -### Specification - -- Trust Record logical schema (§3.1): `subject`, `model`, `runtime`, `policy`, `data_class`, `tool_transcript`, `build_provenance`, `appraisal`, `transparency`, `cnf` -- Wire format (§3.2): EAT/JWT and CBOR-COSE envelopes; profile URI `tag:agentrust.io,2026:trace-v0.1` -- Signing and key management (§3.2.1): ES256/ES384/EdDSA; four-layer key hierarchy; hash agility; revocation -- Verification protocol (§3.3): five-step offline verification, no issuer callback -- Standards composition (§4): RATS/EAT, SLSA, SPIFFE, SCITT, EAR, MCP, A2A, AIBOM, C2PA -- Hardware roots (§4.2): NVIDIA H100/Blackwell, Intel TDX, AMD SEV-SNP, Azure MAA, GCP Confidential Space, AWS Nitro -- Reference implementation (§5): cMCP Phase 1–3 roadmap - -### Schema - -- `schema/trace-claim.json`: JSON Schema (draft/2020-12) for Trust Record validation - -### Examples - -- `examples/amd-sev-snp.json`: AMD SEV-SNP Trust Record -- `examples/intel-tdx.json`: Intel TDX Trust Record -- `examples/nvidia-h100.json`: NVIDIA H100 Confidential Computing Trust Record - -### Open questions - -Seven open questions requiring community input before v0.2 are documented in §7 of the spec. - ---- - -## [0.2.0] — TBD - -### Specification - -- Extend `subject` field to accept DID URIs (any `did:` method) in addition to SPIFFE SVIDs. - Previously `^spiffe://` only; now `^(spiffe://|did:)`. Additive, backward-compatible. - DID-native runtimes (e.g. AGT `did:mesh:` identities) no longer require a parallel SPIFFE identity. - Closes: microsoft/agent-governance-toolkit ADR-0032, agentrust-io/trace-spec#35. - -### Schema - -- `schema/trace-claim.json`: `subject` pattern updated to `^(spiffe://|did:)`, description updated. - -### Reference Implementation - -- `TrustRecord.subject` pattern updated to `r"^(spiffe://|did:)"`. - ---- - -## Upcoming - -See [ROADMAP.md](ROADMAP.md) for planned changes in v0.2 and v1.0. +# Changelog + +All notable changes to the TRACE specification will be documented here. + +Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.MINOR.PATCH`: +- **MAJOR**: breaking changes to wire format or required Trust Record fields +- **MINOR**: new optional fields, new platform profiles, new conformance levels +- **PATCH**: editorial fixes, clarifications, non-normative additions + +--- + +## [Unreleased] + +### Added + +- `delegation` (optional object): the A2A profile delegation-link block, carrying `parent_record_hash` (digest of the parent hop's Trust Record) and `credential_id` (the delegation credential this hop acted under). A chain of records linked this way forms an offline-verifiable delegation DAG. Backward-compatible: existing records without `delegation` remain valid. This is a MINOR (additive) change and the foundation of the forthcoming A2A profile; A2A is now stable at v1.x, clearing the prior blocker. + +--- + +## [0.3.0] — 2026-06-30 + +### Security + +- `verify_record` now requires an explicit trusted key. Self-verification from the embedded `cnf.jwk` is no longer the default; use `allow_embedded_key=True` to opt in. +- Verification enforces freshness (`iat` / `max_age_seconds`, default 24h) and an optional `expected_nonce`. JWK `kty` / `crv` are validated. + +### Breaking + +- **BREAKING:** Canonicalization is now RFC 8785 (JCS). Trust records are NOT cross-verifiable with 0.2.0 (the prior `json.dumps` canonicalization was non-conformant). + +--- + +## [0.1.0] — 2026-06-23 + +Initial public draft. Announced at Confidential Computing Summit, San Francisco. + +### Specification + +- Trust Record logical schema (§3.1): `subject`, `model`, `runtime`, `policy`, `data_class`, `tool_transcript`, `build_provenance`, `appraisal`, `transparency`, `cnf` +- Wire format (§3.2): EAT/JWT and CBOR-COSE envelopes; profile URI `tag:agentrust.io,2026:trace-v0.1` +- Signing and key management (§3.2.1): ES256/ES384/EdDSA; four-layer key hierarchy; hash agility; revocation +- Verification protocol (§3.3): five-step offline verification, no issuer callback +- Standards composition (§4): RATS/EAT, SLSA, SPIFFE, SCITT, EAR, MCP, A2A, AIBOM, C2PA +- Hardware roots (§4.2): NVIDIA H100/Blackwell, Intel TDX, AMD SEV-SNP, Azure MAA, GCP Confidential Space, AWS Nitro +- Reference implementation (§5): cMCP Phase 1–3 roadmap + +### Schema + +- `schema/trace-claim.json`: JSON Schema (draft/2020-12) for Trust Record validation + +### Examples + +- `examples/amd-sev-snp.json`: AMD SEV-SNP Trust Record +- `examples/intel-tdx.json`: Intel TDX Trust Record +- `examples/nvidia-h100.json`: NVIDIA H100 Confidential Computing Trust Record + +### Open questions + +Seven open questions requiring community input before v0.2 are documented in §7 of the spec. + +--- + +## [0.2.0] — TBD + +### Specification + +- Extend `subject` field to accept DID URIs (any `did:` method) in addition to SPIFFE SVIDs. + Previously `^spiffe://` only; now `^(spiffe://|did:)`. Additive, backward-compatible. + DID-native runtimes (e.g. AGT `did:mesh:` identities) no longer require a parallel SPIFFE identity. + Closes: microsoft/agent-governance-toolkit ADR-0032, agentrust-io/trace-spec#35. + +### Schema + +- `schema/trace-claim.json`: `subject` pattern updated to `^(spiffe://|did:)`, description updated. + +### Reference Implementation + +- `TrustRecord.subject` pattern updated to `r"^(spiffe://|did:)"`. + +--- + +## Upcoming + +See [ROADMAP.md](ROADMAP.md) for planned changes in v0.2 and v1.0. diff --git a/ROADMAP.md b/ROADMAP.md index 23eb9bc..5f430f7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,48 +1,48 @@ -# Roadmap - -## Now — v0.1 draft (June 2026) - -Announced at Confidential Computing Summit, San Francisco, June 23 2026. - -**In scope:** -- Full Trust Record schema: `subject`, `model`, `runtime`, `policy`, `data_class`, `tool_transcript`, `build_provenance`, `appraisal`, `transparency`, `cnf` -- Wire formats: EAT/JWT and CBOR-COSE -- Hardware roots: NVIDIA H100/Blackwell, Intel TDX, AMD SEV-SNP, Azure MAA, GCP Confidential Space, AWS Nitro -- JSON Schema and three hardware examples -- Reference implementation: cMCP Phase 1 (Cedar policy enforcement, TRACE Level 2 emission) - -**Not in v0.1:** MCP profile (normative), A2A profile, vendor platform annexes, OWASP/ATLAS cross-walks, encrypted claims envelope. - -## Next — v0.2 (Q3 2026) - -Driven by founding-member feedback and open questions from §7 of the spec. - -- **MCP profile** — normative claim shape and binding rules for MCP tool-call transcripts (`tool_transcript`); proposed for upstream contribution to MCP spec governance -- **A2A profile** — same, for Google Agent-to-Agent; pending A2A protocol stability -- **Vendor platform annexes** — co-authored informative claim-mapping docs for NVIDIA NRAS, Intel Trust Authority, AMD CoRIM, Azure MAA, GCP Confidential Space -- **OWASP Agentic AI Top 10 cross-walk** — which TRACE claim evidences which control for each of the 10 ASIs -- **MITRE ATLAS cross-walk** — TRACE claim coverage mapped to relevant ATLAS tactics -- **Encrypted claims envelope** — normative profile for JWE / COSE-Encrypt when `data_class` requires confidential transport to verifiers (open question §7 Q5) -- **Reference to IETF AIIP** — coordinate with draft-ritz-aiip and determine disposition (open question §7 Q7) -- **cMCP Phase 2** — policy enforcement and `tool_transcript` binding; first full Trust Records - -## Later — v1.0 standard (2027) - -- TSC governance under CoSAI / Linux Foundation -- All §7 open questions resolved -- Complete conformance certification program -- Post-quantum signature profile (ML-DSA, tracking NIST SP 800-208) -- MCP and A2A profiles ratified and proposed to respective upstream governance bodies -- AAIF-assigned canonical profile URI replacing the provisional v0.1 tag URI -- Multi-language verification libraries (Python, TypeScript, Go, Rust) - -## What TRACE will not do - -- Replace RATS, EAT, SLSA, SPIFFE, SCITT, or MCP — TRACE is a profile of these -- Specify a centralized Trust Record registry — verification is designed to work without one -- Build a TEE platform — hardware support targets open silicon (TDX, SEV-SNP, NVIDIA CC) and any platform that produces RATS-conformant evidence -- Adjudicate model alignment or output correctness — TRACE proves what executed and what was in force; correctness is out of scope - -## Influencing the roadmap - -Open a GitHub issue with the `spec` or `roadmap` label. Contributor and community feedback from the CC Summit period (June–September 2026) has priority for v0.2 scope. +# Roadmap + +## Now — v0.1 draft (June 2026) + +Announced at Confidential Computing Summit, San Francisco, June 23 2026. + +**In scope:** +- Full Trust Record schema: `subject`, `model`, `runtime`, `policy`, `data_class`, `tool_transcript`, `build_provenance`, `appraisal`, `transparency`, `cnf` +- Wire formats: EAT/JWT and CBOR-COSE +- Hardware roots: NVIDIA H100/Blackwell, Intel TDX, AMD SEV-SNP, Azure MAA, GCP Confidential Space, AWS Nitro +- JSON Schema and three hardware examples +- Reference implementation: cMCP Phase 1 (Cedar policy enforcement, TRACE Level 2 emission) + +**Not in v0.1:** MCP profile (normative), A2A profile, vendor platform annexes, OWASP/ATLAS cross-walks, encrypted claims envelope. + +## Next — v0.2 (Q3 2026) + +Driven by founding-member feedback and open questions from §7 of the spec. + +- **MCP profile** — normative claim shape and binding rules for MCP tool-call transcripts (`tool_transcript`); proposed for upstream contribution to MCP spec governance +- **A2A profile** — same, for Google Agent-to-Agent. The optional `delegation` link block (`parent_record_hash` + `credential_id`) has landed in the record as the foundation; A2A is now stable at v1.x, so the normative binding rules are the remaining work +- **Vendor platform annexes** — co-authored informative claim-mapping docs for NVIDIA NRAS, Intel Trust Authority, AMD CoRIM, Azure MAA, GCP Confidential Space +- **OWASP Agentic AI Top 10 cross-walk** — which TRACE claim evidences which control for each of the 10 ASIs +- **MITRE ATLAS cross-walk** — TRACE claim coverage mapped to relevant ATLAS tactics +- **Encrypted claims envelope** — normative profile for JWE / COSE-Encrypt when `data_class` requires confidential transport to verifiers (open question §7 Q5) +- **Reference to IETF AIIP** — coordinate with draft-ritz-aiip and determine disposition (open question §7 Q7) +- **cMCP Phase 2** — policy enforcement and `tool_transcript` binding; first full Trust Records + +## Later — v1.0 standard (2027) + +- TSC governance under CoSAI / Linux Foundation +- All §7 open questions resolved +- Complete conformance certification program +- Post-quantum signature profile (ML-DSA, tracking NIST SP 800-208) +- MCP and A2A profiles ratified and proposed to respective upstream governance bodies +- AAIF-assigned canonical profile URI replacing the provisional v0.1 tag URI +- Multi-language verification libraries (Python, TypeScript, Go, Rust) + +## What TRACE will not do + +- Replace RATS, EAT, SLSA, SPIFFE, SCITT, or MCP — TRACE is a profile of these +- Specify a centralized Trust Record registry — verification is designed to work without one +- Build a TEE platform — hardware support targets open silicon (TDX, SEV-SNP, NVIDIA CC) and any platform that produces RATS-conformant evidence +- Adjudicate model alignment or output correctness — TRACE proves what executed and what was in force; correctness is out of scope + +## Influencing the roadmap + +Open a GitHub issue with the `spec` or `roadmap` label. Contributor and community feedback from the CC Summit period (June–September 2026) has priority for v0.2 scope. diff --git a/docs/schema.md b/docs/schema.md index facbae0..f22bf3e 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -1,165 +1,175 @@ -# Schema Reference - -JSON Schema for the TRACE v0.1 Trust Record. Source: [`schema/trace-claim.json`](https://github.com/agentrust-io/trace-spec/blob/main/schema/trace-claim.json). - -## Top-level fields - -| Field | Type | Required | Description | -|---|---|---|---| -| `eat_profile` | string | **yes** | EAT profile URI. Must be `tag:agentrust.io,2026:trace-v0.1` | -| `iat` | integer | **yes** | Issued-at timestamp (Unix epoch seconds) | -| `subject` | string | **yes** | Workload identity. SPIFFE SVID (`spiffe://`) or DID (`did:`) | -| `model` | object | **yes** | Model artifact binding | -| `runtime` | object | **yes** | Execution environment binding | -| `policy` | object | **yes** | Governance policy binding | -| `data_class` | string | **yes** | Data sensitivity classification | -| `tool_transcript` | object | **yes** | Tool-call audit summary | -| `build_provenance` | object | **yes** | Build-time artifact provenance | -| `appraisal` | object | **yes** | Verifier judgment | -| `transparency` | string | **yes** | SCITT transparency log anchor URI (empty string if not anchored) | -| `cnf` | object | **yes** | Confirmation method — contains the `jwk` signing key | -| `signature` | string | **yes** | Base64url Ed25519 / ES256 / ES384 signature over the record | - -## `model` - -Binds the model artifact used in this session. - -| Field | Type | Required | Description | -|---|---|---|---| -| `provider` | string | **yes** | Model provider (e.g., `anthropic`, `openai`, `meta`) | -| `model_id` | string | **yes** | Model identifier (e.g., `claude-sonnet-4-6`) | -| `version` | string | **yes** | Model version or date stamp | -| `weights_digest` | string | no | SHA-256 digest of model weights artifact | -| `aibom_uri` | string | no | URI to the AI Bill of Materials (SPDX/CycloneDX) | - -## `runtime` - -Binds the execution environment. Platform-specific fields vary by TEE type. - -| Field | Type | Required | Description | -|---|---|---|---| -| `platform` | string | **yes** | One of: `amd-sev-snp`, `intel-tdx`, `nvidia-h100`, `nvidia-blackwell`, `tpm-2.0`, `software-only` | -| `measurement` | string | **yes** | Hardware measurement hash (`sha384:` for SEV-SNP/TDX, `sha256:` for TPM) | -| `rim_uri` | string | no | Reference Integrity Manifest URI for hardware verification | -| `firmware_version` | string | no | TEE firmware version | -| `nonce` | string | no | Freshness nonce — ties this record to a specific attestation challenge | - -## `policy` - -Binds the governance policy in force during this session. - -| Field | Type | Required | Description | -|---|---|---|---| -| `bundle_hash` | string | **yes** | `sha256:` digest of the Cedar policy bundle bytes | -| `enforcement_mode` | string | **yes** | `enforce` or `silent` (advisory) | -| `version` | string | no | Policy bundle version string | -| `policy_uri` | string | no | URI to the policy bundle for inspection | - -## `data_class` - -String. Sensitivity classification applied to the data processed in this session. - -Defined values: `public`, `internal`, `confidential`, `restricted`, `secret`. - -Custom values are allowed and SHOULD follow your organization's data classification policy. - -## `tool_transcript` - -Audit summary of tool invocations during the session. - -| Field | Type | Required | Description | -|---|---|---|---| -| `hash` | string | **yes** | `sha256:` of the canonical JSON of the full `AuditEntry` list | -| `call_count` | integer | **yes** | Number of tool invocations recorded | -| `transcript_uri` | string | no | URI to the full per-call transcript (may be encrypted) | - -## `build_provenance` - -Build-time provenance binding the deployed artifact. - -| Field | Type | Required | Description | -|---|---|---|---| -| `slsa_level` | integer | **yes** | SLSA provenance level (0–3) | -| `builder` | string | **yes** | Builder identity URI (e.g., GitHub Actions SLSA generator) | -| `digest` | string | **yes** | `sha256:` digest of the built artifact | -| `provenance_uri` | string | no | URI to the SLSA provenance document (e.g., Rekor entry) | - -## `appraisal` - -Verifier judgment on the evidence in this record. - -| Field | Type | Required | Description | -|---|---|---|---| -| `status` | string | **yes** | One of: `affirming`, `warning`, `contraindicated`, `none` | -| `verifier` | string | **yes** | URI of the verifier that produced this appraisal | -| `policy_ref` | string | no | URI to the appraisal policy applied | -| `timestamp` | integer | no | Unix epoch seconds when appraisal was performed | - -## `transparency` - -String. URI of the SCITT transparency log entry anchoring this record. Empty string (`""`) if not anchored at issuance — anchoring may happen asynchronously. - -## `cnf` - -Confirmation method. Contains the signing key bound to this record. - -| Field | Type | Description | -|---|---|---| -| `jwk` | object | JWK-format public key used to verify `signature` | - -For TEE-issued records, this key was generated inside the measured enclave and its private half never leaves it. The hardware measurement in `runtime` cryptographically binds this key to the TEE. - -## Wire formats - -TRACE v0.1 supports two wire formats: - -**JSON** (primary): signed JSON object with `signature` as a top-level field. - -**CBOR-COSE** (constrained devices): COSE_Sign1 structure with TRACE claims as the payload. Defined in §3.2 of the spec — deferred to a future profile for constrained-device deployments. - -## Example — AMD SEV-SNP - -```json -{ - "eat_profile": "tag:agentrust.io,2026:trace-v0.1", - "iat": 1750676142, - "subject": "spiffe://trust.example.org/agent/payments-processor/prod", - "model": { - "provider": "anthropic", - "model_id": "claude-sonnet-4-6", - "version": "20251001" - }, - "runtime": { - "platform": "amd-sev-snp", - "measurement": "sha384:c9e4b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6...", - "rim_uri": "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain", - "firmware_version": "1.53.0" - }, - "policy": { - "bundle_hash": "sha256:b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1...", - "enforcement_mode": "enforce", - "version": "1.2.0" - }, - "data_class": "confidential", - "tool_transcript": { - "hash": "sha256:d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3...", - "call_count": 3 - }, - "build_provenance": { - "slsa_level": 2, - "builder": "https://github.com/slsa-framework/slsa-github-generator/...", - "digest": "sha256:e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4..." - }, - "appraisal": { - "status": "affirming", - "verifier": "https://trust-authority.example.org" - }, - "transparency": "https://registry.agentrust.io/claim/trace-2026-06-23T09:15:42Z", - "cnf": { - "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } - }, - "signature": "base64url..." -} -``` - -See the full example files in [`examples/`](https://github.com/agentrust-io/trace-spec/tree/main/examples). +# Schema Reference + +JSON Schema for the TRACE v0.1 Trust Record. Source: [`schema/trace-claim.json`](https://github.com/agentrust-io/trace-spec/blob/main/schema/trace-claim.json). + +## Top-level fields + +| Field | Type | Required | Description | +|---|---|---|---| +| `eat_profile` | string | **yes** | EAT profile URI. Must be `tag:agentrust.io,2026:trace-v0.1` | +| `iat` | integer | **yes** | Issued-at timestamp (Unix epoch seconds) | +| `subject` | string | **yes** | Workload identity. SPIFFE SVID (`spiffe://`) or DID (`did:`) | +| `model` | object | **yes** | Model artifact binding | +| `runtime` | object | **yes** | Execution environment binding | +| `policy` | object | **yes** | Governance policy binding | +| `data_class` | string | **yes** | Data sensitivity classification | +| `tool_transcript` | object | **yes** | Tool-call audit summary | +| `delegation` | object | no | A2A profile: link to the delegating hop's Trust Record | +| `build_provenance` | object | **yes** | Build-time artifact provenance | +| `appraisal` | object | **yes** | Verifier judgment | +| `transparency` | string | **yes** | SCITT transparency log anchor URI (empty string if not anchored) | +| `cnf` | object | **yes** | Confirmation method — contains the `jwk` signing key | +| `signature` | string | **yes** | Base64url Ed25519 / ES256 / ES384 signature over the record | + +## `model` + +Binds the model artifact used in this session. + +| Field | Type | Required | Description | +|---|---|---|---| +| `provider` | string | **yes** | Model provider (e.g., `anthropic`, `openai`, `meta`) | +| `model_id` | string | **yes** | Model identifier (e.g., `claude-sonnet-4-6`) | +| `version` | string | **yes** | Model version or date stamp | +| `weights_digest` | string | no | SHA-256 digest of model weights artifact | +| `aibom_uri` | string | no | URI to the AI Bill of Materials (SPDX/CycloneDX) | + +## `runtime` + +Binds the execution environment. Platform-specific fields vary by TEE type. + +| Field | Type | Required | Description | +|---|---|---|---| +| `platform` | string | **yes** | One of: `amd-sev-snp`, `intel-tdx`, `nvidia-h100`, `nvidia-blackwell`, `tpm-2.0`, `software-only` | +| `measurement` | string | **yes** | Hardware measurement hash (`sha384:` for SEV-SNP/TDX, `sha256:` for TPM) | +| `rim_uri` | string | no | Reference Integrity Manifest URI for hardware verification | +| `firmware_version` | string | no | TEE firmware version | +| `nonce` | string | no | Freshness nonce — ties this record to a specific attestation challenge | + +## `policy` + +Binds the governance policy in force during this session. + +| Field | Type | Required | Description | +|---|---|---|---| +| `bundle_hash` | string | **yes** | `sha256:` digest of the Cedar policy bundle bytes | +| `enforcement_mode` | string | **yes** | `enforce` or `silent` (advisory) | +| `version` | string | no | Policy bundle version string | +| `policy_uri` | string | no | URI to the policy bundle for inspection | + +## `data_class` + +String. Sensitivity classification applied to the data processed in this session. + +Defined values: `public`, `internal`, `confidential`, `restricted`, `secret`. + +Custom values are allowed and SHOULD follow your organization's data classification policy. + +## `tool_transcript` + +Audit summary of tool invocations during the session. + +| Field | Type | Required | Description | +|---|---|---|---| +| `hash` | string | **yes** | `sha256:` of the canonical JSON of the full `AuditEntry` list | +| `call_count` | integer | **yes** | Number of tool invocations recorded | +| `transcript_uri` | string | no | URI to the full per-call transcript (may be encrypted) | + +## `delegation` + +A2A profile. Present when this execution acted on authority delegated by another agent; absent on a root (non-delegated) execution. A chain of records linked this way forms an offline-verifiable delegation DAG: a verifier walks `parent_record_hash` from a leaf record back to the root and confirms each hop acted under a credential in the delegation chain. + +| Field | Type | Required | Description | +|---|---|---|---| +| `parent_record_hash` | string | **yes** | `sha256:`/`sha384:` digest of the parent hop's Trust Record | +| `credential_id` | string | **yes** | Identifier of the delegation credential this hop acted under | + +## `build_provenance` + +Build-time provenance binding the deployed artifact. + +| Field | Type | Required | Description | +|---|---|---|---| +| `slsa_level` | integer | **yes** | SLSA provenance level (0–3) | +| `builder` | string | **yes** | Builder identity URI (e.g., GitHub Actions SLSA generator) | +| `digest` | string | **yes** | `sha256:` digest of the built artifact | +| `provenance_uri` | string | no | URI to the SLSA provenance document (e.g., Rekor entry) | + +## `appraisal` + +Verifier judgment on the evidence in this record. + +| Field | Type | Required | Description | +|---|---|---|---| +| `status` | string | **yes** | One of: `affirming`, `warning`, `contraindicated`, `none` | +| `verifier` | string | **yes** | URI of the verifier that produced this appraisal | +| `policy_ref` | string | no | URI to the appraisal policy applied | +| `timestamp` | integer | no | Unix epoch seconds when appraisal was performed | + +## `transparency` + +String. URI of the SCITT transparency log entry anchoring this record. Empty string (`""`) if not anchored at issuance — anchoring may happen asynchronously. + +## `cnf` + +Confirmation method. Contains the signing key bound to this record. + +| Field | Type | Description | +|---|---|---| +| `jwk` | object | JWK-format public key used to verify `signature` | + +For TEE-issued records, this key was generated inside the measured enclave and its private half never leaves it. The hardware measurement in `runtime` cryptographically binds this key to the TEE. + +## Wire formats + +TRACE v0.1 supports two wire formats: + +**JSON** (primary): signed JSON object with `signature` as a top-level field. + +**CBOR-COSE** (constrained devices): COSE_Sign1 structure with TRACE claims as the payload. Defined in §3.2 of the spec — deferred to a future profile for constrained-device deployments. + +## Example — AMD SEV-SNP + +```json +{ + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": 1750676142, + "subject": "spiffe://trust.example.org/agent/payments-processor/prod", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6", + "version": "20251001" + }, + "runtime": { + "platform": "amd-sev-snp", + "measurement": "sha384:c9e4b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6...", + "rim_uri": "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain", + "firmware_version": "1.53.0" + }, + "policy": { + "bundle_hash": "sha256:b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1...", + "enforcement_mode": "enforce", + "version": "1.2.0" + }, + "data_class": "confidential", + "tool_transcript": { + "hash": "sha256:d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3...", + "call_count": 3 + }, + "build_provenance": { + "slsa_level": 2, + "builder": "https://github.com/slsa-framework/slsa-github-generator/...", + "digest": "sha256:e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4..." + }, + "appraisal": { + "status": "affirming", + "verifier": "https://trust-authority.example.org" + }, + "transparency": "https://registry.agentrust.io/claim/trace-2026-06-23T09:15:42Z", + "cnf": { + "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } + }, + "signature": "base64url..." +} +``` + +See the full example files in [`examples/`](https://github.com/agentrust-io/trace-spec/tree/main/examples). diff --git a/schema/trace-claim.json b/schema/trace-claim.json index db8a2fc..bf506ba 100644 --- a/schema/trace-claim.json +++ b/schema/trace-claim.json @@ -1,266 +1,284 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agentrust.io/schema/trace-v0.1.json", - "title": "TRACE Trust Record", - "description": "A TRACE v0.1 Trust Record — hardware-attested governance evidence for an AI agent execution.", - "type": "object", - "required": [ - "eat_profile", - "iat", - "subject", - "model", - "runtime", - "policy", - "data_class", - "build_provenance", - "appraisal", - "transparency", - "cnf" - ], - "properties": { - "eat_profile": { - "type": "string", - "const": "tag:agentrust.io,2026:trace-v0.1", - "description": "EAT profile URI identifying this as a TRACE v0.1 Trust Record." - }, - "iat": { - "type": "integer", - "description": "Issued-at time as Unix epoch seconds.", - "minimum": 1700000000 - }, - "subject": { - "type": "string", - "description": "Workload identity as a SPIFFE SVID URI or DID URI.", - "pattern": "^(spiffe://|did:)" - }, - "model": { - "type": "object", - "description": "Model identity and provenance.", - "required": ["provider", "model_id"], - "properties": { - "provider": { - "type": "string", - "description": "Model provider (e.g. 'anthropic', 'openai', 'meta')." - }, - "model_id": { - "type": "string", - "description": "Model identifier as used by the provider." - }, - "version": { - "type": "string", - "description": "Model version or snapshot identifier." - }, - "weights_digest": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the model weights. Required for local/confidential-inference deployments.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "aibom_uri": { - "type": "string", - "format": "uri", - "description": "URI to SPDX 3.0 AI Profile or CycloneDX 1.7 ML-BOM for this model." - } - }, - "additionalProperties": false - }, - "runtime": { - "type": "object", - "description": "TEE measurement chain binding the workload to hardware.", - "required": ["platform", "measurement"], - "properties": { - "platform": { - "type": "string", - "enum": [ - "intel-tdx", - "amd-sev-snp", - "nvidia-h100", - "nvidia-blackwell", - "aws-nitro", - "arm-cca", - "google-confidential-space", - "tpm2", - "software-only" - ], - "description": "Hardware platform providing the root of trust. software-only marks development-mode records with no hardware backing; they must never be treated as attested evidence." - }, - "measurement": { - "type": "string", - "description": "Hardware measurement of the workload (e.g. TDX MRTD, SEV measurement, TPM PCR composite).", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "rim_uri": { - "type": "string", - "format": "uri", - "description": "URI to the vendor-published Reference Integrity Manifest for this measurement." - }, - "nonce": { - "type": "string", - "description": "Freshness nonce binding the attestation report to this record (base64url, no padding)." - }, - "firmware_version": { - "type": "string", - "description": "Firmware or microcode version included in the measurement." - } - }, - "additionalProperties": false - }, - "policy": { - "type": "object", - "description": "Policy bundle sealed to the TEE measurement.", - "required": ["bundle_hash", "enforcement_mode"], - "properties": { - "bundle_hash": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the policy bundle in force at execution time.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "enforcement_mode": { - "type": "string", - "enum": ["enforce", "advisory", "silent"], - "default": "enforce", - "description": "How policy decisions were applied: enforce (block on deny), advisory (log and allow), silent (allow and suppress operational logs; the audit chain still records every would-have-denied decision). Gateways MUST default to enforce. A deployment MUST explicitly configure silent mode; it MUST NOT be the default." - }, - "version": { - "type": "string", - "description": "Policy bundle version (semantic versioning recommended)." - }, - "policy_uri": { - "type": "string", - "format": "uri", - "description": "URI to the policy bundle for verification." - } - }, - "additionalProperties": false - }, - "data_class": { - "type": "string", - "description": "Highest-sensitivity data classification of inputs and outputs processed during this execution.", - "examples": ["public", "internal", "confidential", "restricted", "top-secret"] - }, - "tool_transcript": { - "type": "object", - "description": "Bound hash of the MCP/A2A tool-call transcript. OPTIONAL for Phase 1 records; REQUIRED for Phase 2+.", - "required": ["hash"], - "properties": { - "hash": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the full tool-call transcript, bound into the EAT envelope.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "call_count": { - "type": "integer", - "minimum": 0, - "description": "Total number of tool calls in this session." - }, - "transcript_uri": { - "type": "string", - "format": "uri", - "description": "URI to the full transcript on the transparency log." - } - }, - "additionalProperties": false - }, - "build_provenance": { - "type": "object", - "description": "SLSA provenance for the workload (agent code + container image).", - "required": ["slsa_level", "digest"], - "properties": { - "slsa_level": { - "type": "integer", - "minimum": 0, - "maximum": 3, - "description": "SLSA Build Level achieved. Level 0 = software-only (development/staging); Level 2 minimum for TRACE conformance; Level 3 for production mark." - }, - "builder": { - "type": "string", - "description": "SLSA builder URI." - }, - "digest": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the container image or workload binary.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "provenance_uri": { - "type": "string", - "format": "uri", - "description": "URI to the SLSA provenance attestation on a Sigstore/Rekor or compatible log." - } - }, - "additionalProperties": false - }, - "appraisal": { - "type": "object", - "description": "Verifier's EAR appraisal of the evidence (draft-ietf-rats-ar4si).", - "required": ["status", "verifier"], - "properties": { - "status": { - "type": "string", - "enum": ["affirming", "warning", "contraindicated", "none"], - "description": "EAR appraisal status." - }, - "verifier": { - "type": "string", - "format": "uri", - "description": "URI identifying the verifier that produced this appraisal." - }, - "policy_ref": { - "type": "string", - "format": "uri", - "description": "URI to the appraisal policy used." - }, - "timestamp": { - "type": "integer", - "description": "Unix epoch seconds when the appraisal was produced." - } - }, - "additionalProperties": false - }, - "transparency": { - "type": "string", - "format": "uri", - "description": "SCITT receipt URI. The Trust Record is the Signed Statement; this URI resolves to the inclusion proof (Receipt) on the transparency log." - }, - "cnf": { - "type": "object", - "description": "Confirmation key (RFC 8747) — binds the Trust Record to the TEE-held signing key.", - "required": ["jwk"], - "properties": { - "jwk": { - "type": "object", - "description": "JWK (RFC 7517) representing the TEE-sealed public key. Keys must carry actual key material: OKP keys require crv and x; EC keys require crv, x, and y.", - "required": ["kty"], - "properties": { - "kty": {"type": "string"}, - "crv": {"type": "string"}, - "x": {"type": "string"}, - "y": {"type": "string"}, - "kid": {"type": "string"} - }, - "allOf": [ - { - "if": { - "required": ["kty"], - "properties": {"kty": {"const": "OKP"}} - }, - "then": {"required": ["crv", "x"]} - }, - { - "if": { - "required": ["kty"], - "properties": {"kty": {"const": "EC"}} - }, - "then": {"required": ["crv", "x", "y"]} - } - ] - } - }, - "additionalProperties": false - }, - "signature": { - "type": "string", - "description": "OPTIONAL embedded signature: base64url (no padding) signature by the cnf key over the canonical JSON form of the record with this field absent. Every Trust Record MUST be signature-bound per spec section 3.2.2, but enveloped profiles (e.g. JWS, cMCP RuntimeClaim) carry the signature outside the record, so this field is not required by the schema.", - "pattern": "^[A-Za-z0-9_-]+$" - } - }, - "additionalProperties": false -} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agentrust.io/schema/trace-v0.1.json", + "title": "TRACE Trust Record", + "description": "A TRACE v0.1 Trust Record — hardware-attested governance evidence for an AI agent execution.", + "type": "object", + "required": [ + "eat_profile", + "iat", + "subject", + "model", + "runtime", + "policy", + "data_class", + "build_provenance", + "appraisal", + "transparency", + "cnf" + ], + "properties": { + "eat_profile": { + "type": "string", + "const": "tag:agentrust.io,2026:trace-v0.1", + "description": "EAT profile URI identifying this as a TRACE v0.1 Trust Record." + }, + "iat": { + "type": "integer", + "description": "Issued-at time as Unix epoch seconds.", + "minimum": 1700000000 + }, + "subject": { + "type": "string", + "description": "Workload identity as a SPIFFE SVID URI or DID URI.", + "pattern": "^(spiffe://|did:)" + }, + "model": { + "type": "object", + "description": "Model identity and provenance.", + "required": ["provider", "model_id"], + "properties": { + "provider": { + "type": "string", + "description": "Model provider (e.g. 'anthropic', 'openai', 'meta')." + }, + "model_id": { + "type": "string", + "description": "Model identifier as used by the provider." + }, + "version": { + "type": "string", + "description": "Model version or snapshot identifier." + }, + "weights_digest": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the model weights. Required for local/confidential-inference deployments.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "aibom_uri": { + "type": "string", + "format": "uri", + "description": "URI to SPDX 3.0 AI Profile or CycloneDX 1.7 ML-BOM for this model." + } + }, + "additionalProperties": false + }, + "runtime": { + "type": "object", + "description": "TEE measurement chain binding the workload to hardware.", + "required": ["platform", "measurement"], + "properties": { + "platform": { + "type": "string", + "enum": [ + "intel-tdx", + "amd-sev-snp", + "nvidia-h100", + "nvidia-blackwell", + "aws-nitro", + "arm-cca", + "google-confidential-space", + "tpm2", + "software-only" + ], + "description": "Hardware platform providing the root of trust. software-only marks development-mode records with no hardware backing; they must never be treated as attested evidence." + }, + "measurement": { + "type": "string", + "description": "Hardware measurement of the workload (e.g. TDX MRTD, SEV measurement, TPM PCR composite).", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "rim_uri": { + "type": "string", + "format": "uri", + "description": "URI to the vendor-published Reference Integrity Manifest for this measurement." + }, + "nonce": { + "type": "string", + "description": "Freshness nonce binding the attestation report to this record (base64url, no padding)." + }, + "firmware_version": { + "type": "string", + "description": "Firmware or microcode version included in the measurement." + } + }, + "additionalProperties": false + }, + "policy": { + "type": "object", + "description": "Policy bundle sealed to the TEE measurement.", + "required": ["bundle_hash", "enforcement_mode"], + "properties": { + "bundle_hash": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the policy bundle in force at execution time.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "enforcement_mode": { + "type": "string", + "enum": ["enforce", "advisory", "silent"], + "default": "enforce", + "description": "How policy decisions were applied: enforce (block on deny), advisory (log and allow), silent (allow and suppress operational logs; the audit chain still records every would-have-denied decision). Gateways MUST default to enforce. A deployment MUST explicitly configure silent mode; it MUST NOT be the default." + }, + "version": { + "type": "string", + "description": "Policy bundle version (semantic versioning recommended)." + }, + "policy_uri": { + "type": "string", + "format": "uri", + "description": "URI to the policy bundle for verification." + } + }, + "additionalProperties": false + }, + "data_class": { + "type": "string", + "description": "Highest-sensitivity data classification of inputs and outputs processed during this execution.", + "examples": ["public", "internal", "confidential", "restricted", "top-secret"] + }, + "tool_transcript": { + "type": "object", + "description": "Bound hash of the MCP/A2A tool-call transcript. OPTIONAL for Phase 1 records; REQUIRED for Phase 2+.", + "required": ["hash"], + "properties": { + "hash": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the full tool-call transcript, bound into the EAT envelope.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "call_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of tool calls in this session." + }, + "transcript_uri": { + "type": "string", + "format": "uri", + "description": "URI to the full transcript on the transparency log." + } + }, + "additionalProperties": false + }, + "delegation": { + "type": "object", + "description": "A2A profile: links this record to the delegating hop's Trust Record. Present when this execution acted on delegated authority; absent on a root (non-delegated) execution. A chain of these forms an offline-verifiable delegation DAG.", + "required": ["parent_record_hash", "credential_id"], + "properties": { + "parent_record_hash": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the parent hop's Trust Record.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "credential_id": { + "type": "string", + "minLength": 1, + "description": "Identifier of the delegation credential this hop acted under." + } + }, + "additionalProperties": false + }, + "build_provenance": { + "type": "object", + "description": "SLSA provenance for the workload (agent code + container image).", + "required": ["slsa_level", "digest"], + "properties": { + "slsa_level": { + "type": "integer", + "minimum": 0, + "maximum": 3, + "description": "SLSA Build Level achieved. Level 0 = software-only (development/staging); Level 2 minimum for TRACE conformance; Level 3 for production mark." + }, + "builder": { + "type": "string", + "description": "SLSA builder URI." + }, + "digest": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the container image or workload binary.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "provenance_uri": { + "type": "string", + "format": "uri", + "description": "URI to the SLSA provenance attestation on a Sigstore/Rekor or compatible log." + } + }, + "additionalProperties": false + }, + "appraisal": { + "type": "object", + "description": "Verifier's EAR appraisal of the evidence (draft-ietf-rats-ar4si).", + "required": ["status", "verifier"], + "properties": { + "status": { + "type": "string", + "enum": ["affirming", "warning", "contraindicated", "none"], + "description": "EAR appraisal status." + }, + "verifier": { + "type": "string", + "format": "uri", + "description": "URI identifying the verifier that produced this appraisal." + }, + "policy_ref": { + "type": "string", + "format": "uri", + "description": "URI to the appraisal policy used." + }, + "timestamp": { + "type": "integer", + "description": "Unix epoch seconds when the appraisal was produced." + } + }, + "additionalProperties": false + }, + "transparency": { + "type": "string", + "format": "uri", + "description": "SCITT receipt URI. The Trust Record is the Signed Statement; this URI resolves to the inclusion proof (Receipt) on the transparency log." + }, + "cnf": { + "type": "object", + "description": "Confirmation key (RFC 8747) — binds the Trust Record to the TEE-held signing key.", + "required": ["jwk"], + "properties": { + "jwk": { + "type": "object", + "description": "JWK (RFC 7517) representing the TEE-sealed public key. Keys must carry actual key material: OKP keys require crv and x; EC keys require crv, x, and y.", + "required": ["kty"], + "properties": { + "kty": {"type": "string"}, + "crv": {"type": "string"}, + "x": {"type": "string"}, + "y": {"type": "string"}, + "kid": {"type": "string"} + }, + "allOf": [ + { + "if": { + "required": ["kty"], + "properties": {"kty": {"const": "OKP"}} + }, + "then": {"required": ["crv", "x"]} + }, + { + "if": { + "required": ["kty"], + "properties": {"kty": {"const": "EC"}} + }, + "then": {"required": ["crv", "x", "y"]} + } + ] + } + }, + "additionalProperties": false + }, + "signature": { + "type": "string", + "description": "OPTIONAL embedded signature: base64url (no padding) signature by the cnf key over the canonical JSON form of the record with this field absent. Every Trust Record MUST be signature-bound per spec section 3.2.2, but enveloped profiles (e.g. JWS, cMCP RuntimeClaim) carry the signature outside the record, so this field is not required by the schema.", + "pattern": "^[A-Za-z0-9_-]+$" + } + }, + "additionalProperties": false +} diff --git a/src/agentrust_trace/__init__.py b/src/agentrust_trace/__init__.py index 4340845..00dbad2 100644 --- a/src/agentrust_trace/__init__.py +++ b/src/agentrust_trace/__init__.py @@ -1,53 +1,55 @@ -"""agentrust-trace — TRACE Trust Record models, validation, and signing.""" - -from agentrust_trace.adapters import AGTSessionResult, TraceAGTAdapter -from agentrust_trace.models import ( - Appraisal, - BuildProvenance, - ConfirmationKey, - JWK, - ModelInfo, - PolicyInfo, - RuntimeInfo, - ToolTranscript, - TrustRecord, -) -from agentrust_trace.sign import ( - generate_key, - key_to_jwk, - load_key, - load_signing_key, - sign_record, - verify_record, -) -from agentrust_trace.validate import ( - iter_errors, - SCHEMA, - validate_json, -) - -__version__ = "0.2.0" - -__all__ = [ - "__version__", - "AGTSessionResult", - "TraceAGTAdapter", - "Appraisal", - "BuildProvenance", - "ConfirmationKey", - "JWK", - "ModelInfo", - "PolicyInfo", - "RuntimeInfo", - "ToolTranscript", - "TrustRecord", - "SCHEMA", - "iter_errors", - "validate_json", - "generate_key", - "key_to_jwk", - "load_key", - "load_signing_key", - "sign_record", - "verify_record", -] +"""agentrust-trace — TRACE Trust Record models, validation, and signing.""" + +from agentrust_trace.adapters import AGTSessionResult, TraceAGTAdapter +from agentrust_trace.models import ( + Appraisal, + BuildProvenance, + ConfirmationKey, + Delegation, + JWK, + ModelInfo, + PolicyInfo, + RuntimeInfo, + ToolTranscript, + TrustRecord, +) +from agentrust_trace.sign import ( + generate_key, + key_to_jwk, + load_key, + load_signing_key, + sign_record, + verify_record, +) +from agentrust_trace.validate import ( + iter_errors, + SCHEMA, + validate_json, +) + +__version__ = "0.2.0" + +__all__ = [ + "__version__", + "AGTSessionResult", + "TraceAGTAdapter", + "Appraisal", + "BuildProvenance", + "ConfirmationKey", + "Delegation", + "JWK", + "ModelInfo", + "PolicyInfo", + "RuntimeInfo", + "ToolTranscript", + "TrustRecord", + "SCHEMA", + "iter_errors", + "validate_json", + "generate_key", + "key_to_jwk", + "load_key", + "load_signing_key", + "sign_record", + "verify_record", +] diff --git a/src/agentrust_trace/models.py b/src/agentrust_trace/models.py index ca3589d..6cc5559 100644 --- a/src/agentrust_trace/models.py +++ b/src/agentrust_trace/models.py @@ -1,143 +1,160 @@ -from __future__ import annotations - -from typing import Annotated, Literal - -from pydantic import BaseModel, ConfigDict, Field, model_validator - -_DIGEST_RE = r"^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - -DigestStr = Annotated[str, Field(pattern=_DIGEST_RE)] - - -class ModelInfo(BaseModel): - model_config = ConfigDict(extra="forbid") - - provider: str - model_id: str - version: str | None = None - weights_digest: DigestStr | None = None - aibom_uri: str | None = None - - -class RuntimeInfo(BaseModel): - model_config = ConfigDict(extra="forbid") - - platform: Literal[ - "intel-tdx", - "amd-sev-snp", - "nvidia-h100", - "nvidia-blackwell", - "aws-nitro", - "arm-cca", - "google-confidential-space", - "tpm2", - # Development / non-attested mode. Distinct from tpm2 so a dev-mode - # record can never be mistaken for hardware-backed evidence by a - # consumer that only inspects runtime.platform. - "software-only", - ] - measurement: DigestStr - rim_uri: str | None = None - nonce: str | None = None - firmware_version: str | None = None - - -class PolicyInfo(BaseModel): - model_config = ConfigDict(extra="forbid") - - bundle_hash: DigestStr - enforcement_mode: Literal["enforce", "advisory", "silent"] - version: str | None = None - policy_uri: str | None = None - - -class ToolTranscript(BaseModel): - model_config = ConfigDict(extra="forbid") - - hash: DigestStr - call_count: Annotated[int, Field(ge=0)] | None = None - transcript_uri: str | None = None - - -class BuildProvenance(BaseModel): - model_config = ConfigDict(extra="forbid") - - slsa_level: Annotated[int, Field(ge=0, le=3)] - builder: str | None = None - digest: DigestStr - provenance_uri: str | None = None - - -class Appraisal(BaseModel): - model_config = ConfigDict(extra="forbid") - - status: Literal["affirming", "warning", "contraindicated", "none"] - verifier: str - policy_ref: str | None = None - timestamp: int | None = None - - -# Private/secret JWK members (RFC 7517 §4, RFC 7518 §6). A cnf.jwk is a public -# proof-of-possession key and must never carry these. -_JWK_PRIVATE_PARAMS = frozenset({"d", "p", "q", "dp", "dq", "qi", "k"}) - - -class JWK(BaseModel): - # JWK params vary by key type (EC, OKP, RSA) — allow unknown members per RFC 7517 - model_config = ConfigDict(extra="allow") - - kty: str - crv: str | None = None - x: str | None = None - y: str | None = None - kid: str | None = None - - @model_validator(mode="after") - def _require_key_material(self) -> JWK: - """A confirmation key without key material binds nothing (RFC 7518 §6).""" - required_by_kty = {"OKP": ("crv", "x"), "EC": ("crv", "x", "y")} - required = required_by_kty.get(self.kty, ()) - missing = [name for name in required if getattr(self, name) is None] - if missing: - raise ValueError( - f"jwk with kty={self.kty!r} must carry key material: missing {', '.join(missing)}" - ) - # extra="allow" would otherwise silently store private key material. A cnf.jwk - # is a public confirmation key, so reject any private/secret member. - private = sorted(_JWK_PRIVATE_PARAMS.intersection(self.model_extra or {})) - if private: - raise ValueError( - f"jwk must not contain private key material: found {', '.join(private)}. " - "cnf.jwk is a public proof-of-possession key." - ) - return self - - -class ConfirmationKey(BaseModel): - model_config = ConfigDict(extra="forbid") - - jwk: JWK - - -class TrustRecord(BaseModel): - """TRACE v0.1 Trust Record — hardware-attested governance evidence for an AI agent execution.""" - - model_config = ConfigDict(extra="forbid") - - eat_profile: Literal["tag:agentrust.io,2026:trace-v0.1"] - iat: Annotated[int, Field(ge=1700000000)] - subject: Annotated[str, Field(pattern=r"^(spiffe://[^/]+/.+|did:[a-z0-9]+:.+)$")] - model: ModelInfo - runtime: RuntimeInfo - policy: PolicyInfo - data_class: str - tool_transcript: ToolTranscript | None = None - build_provenance: BuildProvenance - appraisal: Appraisal - transparency: Annotated[str, Field(min_length=1)] - cnf: ConfirmationKey - signature: Annotated[str, Field(pattern=r"^[A-Za-z0-9_-]+$")] | None = None - """Optional embedded signature (base64url, no padding) by the cnf key over the - canonical JSON form of the record with this field absent. Every Trust Record must - be signature-bound per spec section 3.2.2; enveloped profiles carry the signature - outside the record instead of in this field.""" +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +_DIGEST_RE = r"^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + +DigestStr = Annotated[str, Field(pattern=_DIGEST_RE)] + + +class ModelInfo(BaseModel): + model_config = ConfigDict(extra="forbid") + + provider: str + model_id: str + version: str | None = None + weights_digest: DigestStr | None = None + aibom_uri: str | None = None + + +class RuntimeInfo(BaseModel): + model_config = ConfigDict(extra="forbid") + + platform: Literal[ + "intel-tdx", + "amd-sev-snp", + "nvidia-h100", + "nvidia-blackwell", + "aws-nitro", + "arm-cca", + "google-confidential-space", + "tpm2", + # Development / non-attested mode. Distinct from tpm2 so a dev-mode + # record can never be mistaken for hardware-backed evidence by a + # consumer that only inspects runtime.platform. + "software-only", + ] + measurement: DigestStr + rim_uri: str | None = None + nonce: str | None = None + firmware_version: str | None = None + + +class PolicyInfo(BaseModel): + model_config = ConfigDict(extra="forbid") + + bundle_hash: DigestStr + enforcement_mode: Literal["enforce", "advisory", "silent"] + version: str | None = None + policy_uri: str | None = None + + +class ToolTranscript(BaseModel): + model_config = ConfigDict(extra="forbid") + + hash: DigestStr + call_count: Annotated[int, Field(ge=0)] | None = None + transcript_uri: str | None = None + + +class Delegation(BaseModel): + """A2A profile: links this record to the record of the delegating hop. + + Present when this execution acted on authority delegated by another agent. + ``parent_record_hash`` is the digest of the parent hop's Trust Record and + ``credential_id`` names the delegation credential this hop acted under, so a + chain of records forms an offline-verifiable delegation DAG. Absent on a + root (non-delegated) execution. + """ + + model_config = ConfigDict(extra="forbid") + + parent_record_hash: DigestStr + credential_id: Annotated[str, Field(min_length=1)] + + +class BuildProvenance(BaseModel): + model_config = ConfigDict(extra="forbid") + + slsa_level: Annotated[int, Field(ge=0, le=3)] + builder: str | None = None + digest: DigestStr + provenance_uri: str | None = None + + +class Appraisal(BaseModel): + model_config = ConfigDict(extra="forbid") + + status: Literal["affirming", "warning", "contraindicated", "none"] + verifier: str + policy_ref: str | None = None + timestamp: int | None = None + + +# Private/secret JWK members (RFC 7517 §4, RFC 7518 §6). A cnf.jwk is a public +# proof-of-possession key and must never carry these. +_JWK_PRIVATE_PARAMS = frozenset({"d", "p", "q", "dp", "dq", "qi", "k"}) + + +class JWK(BaseModel): + # JWK params vary by key type (EC, OKP, RSA) — allow unknown members per RFC 7517 + model_config = ConfigDict(extra="allow") + + kty: str + crv: str | None = None + x: str | None = None + y: str | None = None + kid: str | None = None + + @model_validator(mode="after") + def _require_key_material(self) -> JWK: + """A confirmation key without key material binds nothing (RFC 7518 §6).""" + required_by_kty = {"OKP": ("crv", "x"), "EC": ("crv", "x", "y")} + required = required_by_kty.get(self.kty, ()) + missing = [name for name in required if getattr(self, name) is None] + if missing: + raise ValueError( + f"jwk with kty={self.kty!r} must carry key material: missing {', '.join(missing)}" + ) + # extra="allow" would otherwise silently store private key material. A cnf.jwk + # is a public confirmation key, so reject any private/secret member. + private = sorted(_JWK_PRIVATE_PARAMS.intersection(self.model_extra or {})) + if private: + raise ValueError( + f"jwk must not contain private key material: found {', '.join(private)}. " + "cnf.jwk is a public proof-of-possession key." + ) + return self + + +class ConfirmationKey(BaseModel): + model_config = ConfigDict(extra="forbid") + + jwk: JWK + + +class TrustRecord(BaseModel): + """TRACE v0.1 Trust Record — hardware-attested governance evidence for an AI agent execution.""" + + model_config = ConfigDict(extra="forbid") + + eat_profile: Literal["tag:agentrust.io,2026:trace-v0.1"] + iat: Annotated[int, Field(ge=1700000000)] + subject: Annotated[str, Field(pattern=r"^(spiffe://[^/]+/.+|did:[a-z0-9]+:.+)$")] + model: ModelInfo + runtime: RuntimeInfo + policy: PolicyInfo + data_class: str + tool_transcript: ToolTranscript | None = None + delegation: Delegation | None = None + build_provenance: BuildProvenance + appraisal: Appraisal + transparency: Annotated[str, Field(min_length=1)] + cnf: ConfirmationKey + signature: Annotated[str, Field(pattern=r"^[A-Za-z0-9_-]+$")] | None = None + """Optional embedded signature (base64url, no padding) by the cnf key over the + canonical JSON form of the record with this field absent. Every Trust Record must + be signature-bound per spec section 3.2.2; enveloped profiles carry the signature + outside the record instead of in this field.""" diff --git a/src/agentrust_trace/schema/trace-v0.1.json b/src/agentrust_trace/schema/trace-v0.1.json index c009bd5..b1a2b6b 100644 --- a/src/agentrust_trace/schema/trace-v0.1.json +++ b/src/agentrust_trace/schema/trace-v0.1.json @@ -1,266 +1,284 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agentrust.io/schema/trace-v0.1.json", - "title": "TRACE Trust Record", - "description": "A TRACE v0.1 Trust Record — hardware-attested governance evidence for an AI agent execution.", - "type": "object", - "required": [ - "eat_profile", - "iat", - "subject", - "model", - "runtime", - "policy", - "data_class", - "build_provenance", - "appraisal", - "transparency", - "cnf" - ], - "properties": { - "eat_profile": { - "type": "string", - "const": "tag:agentrust.io,2026:trace-v0.1", - "description": "EAT profile URI identifying this as a TRACE v0.1 Trust Record." - }, - "iat": { - "type": "integer", - "description": "Issued-at time as Unix epoch seconds.", - "minimum": 1700000000 - }, - "subject": { - "type": "string", - "description": "Workload identity as a SPIFFE SVID URI.", - "pattern": "^spiffe://" - }, - "model": { - "type": "object", - "description": "Model identity and provenance.", - "required": ["provider", "model_id"], - "properties": { - "provider": { - "type": "string", - "description": "Model provider (e.g. 'anthropic', 'openai', 'meta')." - }, - "model_id": { - "type": "string", - "description": "Model identifier as used by the provider." - }, - "version": { - "type": "string", - "description": "Model version or snapshot identifier." - }, - "weights_digest": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the model weights. Required for local/confidential-inference deployments.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "aibom_uri": { - "type": "string", - "format": "uri", - "description": "URI to SPDX 3.0 AI Profile or CycloneDX 1.7 ML-BOM for this model." - } - }, - "additionalProperties": false - }, - "runtime": { - "type": "object", - "description": "TEE measurement chain binding the workload to hardware.", - "required": ["platform", "measurement"], - "properties": { - "platform": { - "type": "string", - "enum": [ - "intel-tdx", - "amd-sev-snp", - "nvidia-h100", - "nvidia-blackwell", - "aws-nitro", - "arm-cca", - "google-confidential-space", - "tpm2", - "software-only" - ], - "description": "Hardware platform providing the root of trust. software-only marks development-mode records with no hardware backing; they must never be treated as attested evidence." - }, - "measurement": { - "type": "string", - "description": "Hardware measurement of the workload (e.g. TDX MRTD, SEV measurement, TPM PCR composite).", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "rim_uri": { - "type": "string", - "format": "uri", - "description": "URI to the vendor-published Reference Integrity Manifest for this measurement." - }, - "nonce": { - "type": "string", - "description": "Freshness nonce binding the attestation report to this record (base64url, no padding)." - }, - "firmware_version": { - "type": "string", - "description": "Firmware or microcode version included in the measurement." - } - }, - "additionalProperties": false - }, - "policy": { - "type": "object", - "description": "Policy bundle sealed to the TEE measurement.", - "required": ["bundle_hash", "enforcement_mode"], - "properties": { - "bundle_hash": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the policy bundle in force at execution time.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "enforcement_mode": { - "type": "string", - "enum": ["enforce", "advisory", "silent"], - "default": "enforce", - "description": "How policy decisions were applied: enforce (block on deny), advisory (log and allow), silent (allow and suppress operational logs; the audit chain still records every would-have-denied decision). Gateways MUST default to enforce. A deployment MUST explicitly configure silent mode; it MUST NOT be the default." - }, - "version": { - "type": "string", - "description": "Policy bundle version (semantic versioning recommended)." - }, - "policy_uri": { - "type": "string", - "format": "uri", - "description": "URI to the policy bundle for verification." - } - }, - "additionalProperties": false - }, - "data_class": { - "type": "string", - "description": "Highest-sensitivity data classification of inputs and outputs processed during this execution.", - "examples": ["public", "internal", "confidential", "restricted", "top-secret"] - }, - "tool_transcript": { - "type": "object", - "description": "Bound hash of the MCP/A2A tool-call transcript. OPTIONAL for Phase 1 records; REQUIRED for Phase 2+.", - "required": ["hash"], - "properties": { - "hash": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the full tool-call transcript, bound into the EAT envelope.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "call_count": { - "type": "integer", - "minimum": 0, - "description": "Total number of tool calls in this session." - }, - "transcript_uri": { - "type": "string", - "format": "uri", - "description": "URI to the full transcript on the transparency log." - } - }, - "additionalProperties": false - }, - "build_provenance": { - "type": "object", - "description": "SLSA provenance for the workload (agent code + container image).", - "required": ["slsa_level", "digest"], - "properties": { - "slsa_level": { - "type": "integer", - "minimum": 0, - "maximum": 3, - "description": "SLSA Build Level achieved. Level 2 minimum for TRACE conformance; Level 3 for production mark." - }, - "builder": { - "type": "string", - "description": "SLSA builder URI." - }, - "digest": { - "type": "string", - "description": "SHA-256 or SHA-384 digest of the container image or workload binary.", - "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" - }, - "provenance_uri": { - "type": "string", - "format": "uri", - "description": "URI to the SLSA provenance attestation on a Sigstore/Rekor or compatible log." - } - }, - "additionalProperties": false - }, - "appraisal": { - "type": "object", - "description": "Verifier's EAR appraisal of the evidence (draft-ietf-rats-ar4si).", - "required": ["status", "verifier"], - "properties": { - "status": { - "type": "string", - "enum": ["affirming", "warning", "contraindicated", "none"], - "description": "EAR appraisal status." - }, - "verifier": { - "type": "string", - "format": "uri", - "description": "URI identifying the verifier that produced this appraisal." - }, - "policy_ref": { - "type": "string", - "format": "uri", - "description": "URI to the appraisal policy used." - }, - "timestamp": { - "type": "integer", - "description": "Unix epoch seconds when the appraisal was produced." - } - }, - "additionalProperties": false - }, - "transparency": { - "type": "string", - "format": "uri", - "description": "SCITT receipt URI. The Trust Record is the Signed Statement; this URI resolves to the inclusion proof (Receipt) on the transparency log." - }, - "cnf": { - "type": "object", - "description": "Confirmation key (RFC 8747) — binds the Trust Record to the TEE-held signing key.", - "required": ["jwk"], - "properties": { - "jwk": { - "type": "object", - "description": "JWK (RFC 7517) representing the TEE-sealed public key. Keys must carry actual key material: OKP keys require crv and x; EC keys require crv, x, and y.", - "required": ["kty"], - "properties": { - "kty": {"type": "string"}, - "crv": {"type": "string"}, - "x": {"type": "string"}, - "y": {"type": "string"}, - "kid": {"type": "string"} - }, - "allOf": [ - { - "if": { - "required": ["kty"], - "properties": {"kty": {"const": "OKP"}} - }, - "then": {"required": ["crv", "x"]} - }, - { - "if": { - "required": ["kty"], - "properties": {"kty": {"const": "EC"}} - }, - "then": {"required": ["crv", "x", "y"]} - } - ] - } - }, - "additionalProperties": false - }, - "signature": { - "type": "string", - "description": "OPTIONAL embedded signature: base64url (no padding) signature by the cnf key over the canonical JSON form of the record with this field absent. Every Trust Record MUST be signature-bound per spec section 3.2.2, but enveloped profiles (e.g. JWS, cMCP RuntimeClaim) carry the signature outside the record, so this field is not required by the schema.", - "pattern": "^[A-Za-z0-9_-]+$" - } - }, - "additionalProperties": false -} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agentrust.io/schema/trace-v0.1.json", + "title": "TRACE Trust Record", + "description": "A TRACE v0.1 Trust Record — hardware-attested governance evidence for an AI agent execution.", + "type": "object", + "required": [ + "eat_profile", + "iat", + "subject", + "model", + "runtime", + "policy", + "data_class", + "build_provenance", + "appraisal", + "transparency", + "cnf" + ], + "properties": { + "eat_profile": { + "type": "string", + "const": "tag:agentrust.io,2026:trace-v0.1", + "description": "EAT profile URI identifying this as a TRACE v0.1 Trust Record." + }, + "iat": { + "type": "integer", + "description": "Issued-at time as Unix epoch seconds.", + "minimum": 1700000000 + }, + "subject": { + "type": "string", + "description": "Workload identity as a SPIFFE SVID URI.", + "pattern": "^spiffe://" + }, + "model": { + "type": "object", + "description": "Model identity and provenance.", + "required": ["provider", "model_id"], + "properties": { + "provider": { + "type": "string", + "description": "Model provider (e.g. 'anthropic', 'openai', 'meta')." + }, + "model_id": { + "type": "string", + "description": "Model identifier as used by the provider." + }, + "version": { + "type": "string", + "description": "Model version or snapshot identifier." + }, + "weights_digest": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the model weights. Required for local/confidential-inference deployments.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "aibom_uri": { + "type": "string", + "format": "uri", + "description": "URI to SPDX 3.0 AI Profile or CycloneDX 1.7 ML-BOM for this model." + } + }, + "additionalProperties": false + }, + "runtime": { + "type": "object", + "description": "TEE measurement chain binding the workload to hardware.", + "required": ["platform", "measurement"], + "properties": { + "platform": { + "type": "string", + "enum": [ + "intel-tdx", + "amd-sev-snp", + "nvidia-h100", + "nvidia-blackwell", + "aws-nitro", + "arm-cca", + "google-confidential-space", + "tpm2", + "software-only" + ], + "description": "Hardware platform providing the root of trust. software-only marks development-mode records with no hardware backing; they must never be treated as attested evidence." + }, + "measurement": { + "type": "string", + "description": "Hardware measurement of the workload (e.g. TDX MRTD, SEV measurement, TPM PCR composite).", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "rim_uri": { + "type": "string", + "format": "uri", + "description": "URI to the vendor-published Reference Integrity Manifest for this measurement." + }, + "nonce": { + "type": "string", + "description": "Freshness nonce binding the attestation report to this record (base64url, no padding)." + }, + "firmware_version": { + "type": "string", + "description": "Firmware or microcode version included in the measurement." + } + }, + "additionalProperties": false + }, + "policy": { + "type": "object", + "description": "Policy bundle sealed to the TEE measurement.", + "required": ["bundle_hash", "enforcement_mode"], + "properties": { + "bundle_hash": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the policy bundle in force at execution time.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "enforcement_mode": { + "type": "string", + "enum": ["enforce", "advisory", "silent"], + "default": "enforce", + "description": "How policy decisions were applied: enforce (block on deny), advisory (log and allow), silent (allow and suppress operational logs; the audit chain still records every would-have-denied decision). Gateways MUST default to enforce. A deployment MUST explicitly configure silent mode; it MUST NOT be the default." + }, + "version": { + "type": "string", + "description": "Policy bundle version (semantic versioning recommended)." + }, + "policy_uri": { + "type": "string", + "format": "uri", + "description": "URI to the policy bundle for verification." + } + }, + "additionalProperties": false + }, + "data_class": { + "type": "string", + "description": "Highest-sensitivity data classification of inputs and outputs processed during this execution.", + "examples": ["public", "internal", "confidential", "restricted", "top-secret"] + }, + "tool_transcript": { + "type": "object", + "description": "Bound hash of the MCP/A2A tool-call transcript. OPTIONAL for Phase 1 records; REQUIRED for Phase 2+.", + "required": ["hash"], + "properties": { + "hash": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the full tool-call transcript, bound into the EAT envelope.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "call_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of tool calls in this session." + }, + "transcript_uri": { + "type": "string", + "format": "uri", + "description": "URI to the full transcript on the transparency log." + } + }, + "additionalProperties": false + }, + "delegation": { + "type": "object", + "description": "A2A profile: links this record to the delegating hop's Trust Record. Present when this execution acted on delegated authority; absent on a root (non-delegated) execution. A chain of these forms an offline-verifiable delegation DAG.", + "required": ["parent_record_hash", "credential_id"], + "properties": { + "parent_record_hash": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the parent hop's Trust Record.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "credential_id": { + "type": "string", + "minLength": 1, + "description": "Identifier of the delegation credential this hop acted under." + } + }, + "additionalProperties": false + }, + "build_provenance": { + "type": "object", + "description": "SLSA provenance for the workload (agent code + container image).", + "required": ["slsa_level", "digest"], + "properties": { + "slsa_level": { + "type": "integer", + "minimum": 0, + "maximum": 3, + "description": "SLSA Build Level achieved. Level 2 minimum for TRACE conformance; Level 3 for production mark." + }, + "builder": { + "type": "string", + "description": "SLSA builder URI." + }, + "digest": { + "type": "string", + "description": "SHA-256 or SHA-384 digest of the container image or workload binary.", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" + }, + "provenance_uri": { + "type": "string", + "format": "uri", + "description": "URI to the SLSA provenance attestation on a Sigstore/Rekor or compatible log." + } + }, + "additionalProperties": false + }, + "appraisal": { + "type": "object", + "description": "Verifier's EAR appraisal of the evidence (draft-ietf-rats-ar4si).", + "required": ["status", "verifier"], + "properties": { + "status": { + "type": "string", + "enum": ["affirming", "warning", "contraindicated", "none"], + "description": "EAR appraisal status." + }, + "verifier": { + "type": "string", + "format": "uri", + "description": "URI identifying the verifier that produced this appraisal." + }, + "policy_ref": { + "type": "string", + "format": "uri", + "description": "URI to the appraisal policy used." + }, + "timestamp": { + "type": "integer", + "description": "Unix epoch seconds when the appraisal was produced." + } + }, + "additionalProperties": false + }, + "transparency": { + "type": "string", + "format": "uri", + "description": "SCITT receipt URI. The Trust Record is the Signed Statement; this URI resolves to the inclusion proof (Receipt) on the transparency log." + }, + "cnf": { + "type": "object", + "description": "Confirmation key (RFC 8747) — binds the Trust Record to the TEE-held signing key.", + "required": ["jwk"], + "properties": { + "jwk": { + "type": "object", + "description": "JWK (RFC 7517) representing the TEE-sealed public key. Keys must carry actual key material: OKP keys require crv and x; EC keys require crv, x, and y.", + "required": ["kty"], + "properties": { + "kty": {"type": "string"}, + "crv": {"type": "string"}, + "x": {"type": "string"}, + "y": {"type": "string"}, + "kid": {"type": "string"} + }, + "allOf": [ + { + "if": { + "required": ["kty"], + "properties": {"kty": {"const": "OKP"}} + }, + "then": {"required": ["crv", "x"]} + }, + { + "if": { + "required": ["kty"], + "properties": {"kty": {"const": "EC"}} + }, + "then": {"required": ["crv", "x", "y"]} + } + ] + } + }, + "additionalProperties": false + }, + "signature": { + "type": "string", + "description": "OPTIONAL embedded signature: base64url (no padding) signature by the cnf key over the canonical JSON form of the record with this field absent. Every Trust Record MUST be signature-bound per spec section 3.2.2, but enveloped profiles (e.g. JWS, cMCP RuntimeClaim) carry the signature outside the record, so this field is not required by the schema.", + "pattern": "^[A-Za-z0-9_-]+$" + } + }, + "additionalProperties": false +} diff --git a/tests/test_models.py b/tests/test_models.py index 92f8157..29c6d97 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,165 +1,206 @@ -"""Parse all three canonical examples through TrustRecord.""" - -import json -from pathlib import Path - -import pytest -from pydantic import ValidationError - -from agentrust_trace import TrustRecord - -EXAMPLES_DIR = Path(__file__).parent.parent / "examples" - - -def _load(name: str) -> dict: - # Examples must validate exactly as published: no preprocessing. - return json.loads((EXAMPLES_DIR / name).read_text()) - - -@pytest.mark.parametrize("filename", ["intel-tdx.json", "amd-sev-snp.json", "nvidia-h100.json"]) -def test_example_parses(filename: str) -> None: - record = TrustRecord.model_validate(_load(filename)) - assert record.eat_profile == "tag:agentrust.io,2026:trace-v0.1" - assert record.subject.startswith(("spiffe://", "did:")) - - -def test_intel_tdx_fields() -> None: - record = TrustRecord.model_validate(_load("intel-tdx.json")) - assert record.runtime.platform == "intel-tdx" - assert record.policy.enforcement_mode == "enforce" - assert record.appraisal.status == "affirming" - assert record.tool_transcript is not None - assert record.tool_transcript.call_count == 7 - - -def test_extra_fields_rejected() -> None: - data = _load("intel-tdx.json") - data["unknown_field"] = "should fail" - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -def test_missing_required_field_rejected() -> None: - data = _load("intel-tdx.json") - del data["cnf"] - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -def test_bad_digest_rejected() -> None: - data = _load("intel-tdx.json") - data["runtime"]["measurement"] = "not-a-digest" - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -def test_bad_platform_rejected() -> None: - data = _load("intel-tdx.json") - data["runtime"]["platform"] = "unknown-cloud" - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -# CRYPTO-008 / CRYPTO-009: DigestStr regex enforcement - -def test_digest_uppercase_rejected() -> None: - """CRYPTO-008: uppercase hex must be rejected (sha256: is lowercase-only).""" - data = _load("intel-tdx.json") - data["runtime"]["measurement"] = "sha256:" + "A" * 64 - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -def test_digest_sha256_too_short_rejected() -> None: - """CRYPTO-008/009: sha256 digest shorter than 64 chars must be rejected.""" - data = _load("intel-tdx.json") - data["runtime"]["measurement"] = "sha256:" + "a" * 63 - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -def test_digest_sha256_exact_length_accepted() -> None: - """sha256 digest with exactly 64 lowercase hex chars must be accepted.""" - data = _load("intel-tdx.json") - data["runtime"]["measurement"] = "sha256:" + "a" * 64 - record = TrustRecord.model_validate(data) - assert record.runtime.measurement == "sha256:" + "a" * 64 - - -def test_digest_sha384_exact_length_accepted() -> None: - """sha384 digest with exactly 96 lowercase hex chars must be accepted.""" - data = _load("intel-tdx.json") - data["runtime"]["measurement"] = "sha384:" + "b" * 96 - record = TrustRecord.model_validate(data) - assert record.runtime.measurement == "sha384:" + "b" * 96 - - -def test_digest_sha512_rejected() -> None: - """CRYPTO-009: unsupported algorithm sha512 must be rejected.""" - data = _load("intel-tdx.json") - data["runtime"]["measurement"] = "sha512:" + "a" * 128 - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) - - -# cnf.jwk key material enforcement - - -def test_subject_accepts_did_uri() -> None: - data = _load("intel-tdx.json") - data["subject"] = "did:key:z6MkhaXgBZDvotzL8oCYaXeFuJArwvX6mDMsKTJVjtN7R" - record = TrustRecord.model_validate(data) - assert record.subject.startswith("did:") - - -def test_subject_accepts_did_web() -> None: - data = _load("intel-tdx.json") - data["subject"] = "did:web:example.org:agents:payments-processor" +"""Parse all three canonical examples through TrustRecord.""" + +import json +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from agentrust_trace import TrustRecord + +EXAMPLES_DIR = Path(__file__).parent.parent / "examples" + + +def _load(name: str) -> dict: + # Examples must validate exactly as published: no preprocessing. + return json.loads((EXAMPLES_DIR / name).read_text()) + + +@pytest.mark.parametrize("filename", ["intel-tdx.json", "amd-sev-snp.json", "nvidia-h100.json"]) +def test_example_parses(filename: str) -> None: + record = TrustRecord.model_validate(_load(filename)) + assert record.eat_profile == "tag:agentrust.io,2026:trace-v0.1" + assert record.subject.startswith(("spiffe://", "did:")) + + +def test_intel_tdx_fields() -> None: + record = TrustRecord.model_validate(_load("intel-tdx.json")) + assert record.runtime.platform == "intel-tdx" + assert record.policy.enforcement_mode == "enforce" + assert record.appraisal.status == "affirming" + assert record.tool_transcript is not None + assert record.tool_transcript.call_count == 7 + + +def test_extra_fields_rejected() -> None: + data = _load("intel-tdx.json") + data["unknown_field"] = "should fail" + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_missing_required_field_rejected() -> None: + data = _load("intel-tdx.json") + del data["cnf"] + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_bad_digest_rejected() -> None: + data = _load("intel-tdx.json") + data["runtime"]["measurement"] = "not-a-digest" + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_bad_platform_rejected() -> None: + data = _load("intel-tdx.json") + data["runtime"]["platform"] = "unknown-cloud" + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +# CRYPTO-008 / CRYPTO-009: DigestStr regex enforcement + +def test_digest_uppercase_rejected() -> None: + """CRYPTO-008: uppercase hex must be rejected (sha256: is lowercase-only).""" + data = _load("intel-tdx.json") + data["runtime"]["measurement"] = "sha256:" + "A" * 64 + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_digest_sha256_too_short_rejected() -> None: + """CRYPTO-008/009: sha256 digest shorter than 64 chars must be rejected.""" + data = _load("intel-tdx.json") + data["runtime"]["measurement"] = "sha256:" + "a" * 63 + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_digest_sha256_exact_length_accepted() -> None: + """sha256 digest with exactly 64 lowercase hex chars must be accepted.""" + data = _load("intel-tdx.json") + data["runtime"]["measurement"] = "sha256:" + "a" * 64 + record = TrustRecord.model_validate(data) + assert record.runtime.measurement == "sha256:" + "a" * 64 + + +def test_digest_sha384_exact_length_accepted() -> None: + """sha384 digest with exactly 96 lowercase hex chars must be accepted.""" + data = _load("intel-tdx.json") + data["runtime"]["measurement"] = "sha384:" + "b" * 96 + record = TrustRecord.model_validate(data) + assert record.runtime.measurement == "sha384:" + "b" * 96 + + +def test_digest_sha512_rejected() -> None: + """CRYPTO-009: unsupported algorithm sha512 must be rejected.""" + data = _load("intel-tdx.json") + data["runtime"]["measurement"] = "sha512:" + "a" * 128 + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +# cnf.jwk key material enforcement + + +def test_subject_accepts_did_uri() -> None: + data = _load("intel-tdx.json") + data["subject"] = "did:key:z6MkhaXgBZDvotzL8oCYaXeFuJArwvX6mDMsKTJVjtN7R" + record = TrustRecord.model_validate(data) + assert record.subject.startswith("did:") + + +def test_subject_accepts_did_web() -> None: + data = _load("intel-tdx.json") + data["subject"] = "did:web:example.org:agents:payments-processor" + record = TrustRecord.model_validate(data) + assert record.subject.startswith("did:") + + +def test_subject_rejects_http_scheme() -> None: + data = _load("intel-tdx.json") + data["subject"] = "https://example.org/agent" + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_okp_jwk_without_key_material_rejected() -> None: + """An OKP confirmation key with no crv/x carries no key material and binds nothing.""" + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = {"kty": "OKP"} + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_ec_jwk_without_y_rejected() -> None: + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = {"kty": "EC", "crv": "P-256", "x": "dGVzdA"} + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_okp_jwk_with_key_material_accepted() -> None: + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + } + record = TrustRecord.model_validate(data) + assert record.cnf.jwk.x is not None + + +def test_jwk_with_private_key_material_rejected() -> None: + """A cnf.jwk is a public key; private params (d, p, q, ...) must be rejected (#70).""" + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", # private scalar — must not be stored + } + with pytest.raises(ValidationError): + TrustRecord.model_validate(data) + + +def test_delegation_block_parses() -> None: + data = _load("intel-tdx.json") + data["delegation"] = { + "parent_record_hash": "sha256:" + "a" * 64, + "credential_id": "cred-abc", + } record = TrustRecord.model_validate(data) - assert record.subject.startswith("did:") + assert record.delegation is not None + assert record.delegation.credential_id == "cred-abc" -def test_subject_rejects_http_scheme() -> None: - data = _load("intel-tdx.json") - data["subject"] = "https://example.org/agent" - with pytest.raises(ValidationError): - TrustRecord.model_validate(data) +def test_record_without_delegation_is_valid() -> None: + record = TrustRecord.model_validate(_load("intel-tdx.json")) + assert record.delegation is None -def test_okp_jwk_without_key_material_rejected() -> None: - """An OKP confirmation key with no crv/x carries no key material and binds nothing.""" +def test_delegation_bad_digest_rejected() -> None: data = _load("intel-tdx.json") - data["cnf"]["jwk"] = {"kty": "OKP"} + data["delegation"] = {"parent_record_hash": "not-a-digest", "credential_id": "c"} with pytest.raises(ValidationError): TrustRecord.model_validate(data) -def test_ec_jwk_without_y_rejected() -> None: +def test_delegation_empty_credential_id_rejected() -> None: data = _load("intel-tdx.json") - data["cnf"]["jwk"] = {"kty": "EC", "crv": "P-256", "x": "dGVzdA"} + data["delegation"] = {"parent_record_hash": "sha256:" + "a" * 64, "credential_id": ""} with pytest.raises(ValidationError): TrustRecord.model_validate(data) -def test_okp_jwk_with_key_material_accepted() -> None: - data = _load("intel-tdx.json") - data["cnf"]["jwk"] = { - "kty": "OKP", - "crv": "Ed25519", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - } - record = TrustRecord.model_validate(data) - assert record.cnf.jwk.x is not None - - -def test_jwk_with_private_key_material_rejected() -> None: - """A cnf.jwk is a public key; private params (d, p, q, ...) must be rejected (#70).""" +def test_delegation_extra_field_rejected() -> None: data = _load("intel-tdx.json") - data["cnf"]["jwk"] = { - "kty": "OKP", - "crv": "Ed25519", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", # private scalar — must not be stored + data["delegation"] = { + "parent_record_hash": "sha256:" + "a" * 64, + "credential_id": "c", + "foo": "bar", } with pytest.raises(ValidationError): TrustRecord.model_validate(data) diff --git a/tests/test_validate.py b/tests/test_validate.py index c409ac4..c77edb8 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -1,77 +1,92 @@ -"""validate_json and iter_errors against canonical examples.""" - -import json -from pathlib import Path - -import pytest - -from agentrust_trace import SCHEMA, iter_errors, validate_json - -EXAMPLES_DIR = Path(__file__).parent.parent / "examples" - - -def _load(name: str) -> dict: - # Examples must validate exactly as published: no preprocessing. - return json.loads((EXAMPLES_DIR / name).read_text()) - - -@pytest.mark.parametrize("filename", ["intel-tdx.json", "amd-sev-snp.json", "nvidia-h100.json"]) -def test_examples_pass_json_schema(filename: str) -> None: - validate_json(_load(filename)) - - -def test_iter_errors_empty_on_valid() -> None: - assert iter_errors(_load("intel-tdx.json")) == [] - - -def test_invalid_eat_profile_fails() -> None: - data = _load("intel-tdx.json") - data["eat_profile"] = "wrong-profile" - errors = iter_errors(data) - assert errors, "expected at least one schema error" - - -def test_missing_required_field_fails() -> None: +"""validate_json and iter_errors against canonical examples.""" + +import json +from pathlib import Path + +import pytest + +from agentrust_trace import SCHEMA, iter_errors, validate_json + +EXAMPLES_DIR = Path(__file__).parent.parent / "examples" + + +def _load(name: str) -> dict: + # Examples must validate exactly as published: no preprocessing. + return json.loads((EXAMPLES_DIR / name).read_text()) + + +@pytest.mark.parametrize("filename", ["intel-tdx.json", "amd-sev-snp.json", "nvidia-h100.json"]) +def test_examples_pass_json_schema(filename: str) -> None: + validate_json(_load(filename)) + + +def test_iter_errors_empty_on_valid() -> None: + assert iter_errors(_load("intel-tdx.json")) == [] + + +def test_invalid_eat_profile_fails() -> None: + data = _load("intel-tdx.json") + data["eat_profile"] = "wrong-profile" + errors = iter_errors(data) + assert errors, "expected at least one schema error" + + +def test_missing_required_field_fails() -> None: + data = _load("intel-tdx.json") + del data["subject"] + errors = iter_errors(data) + assert errors + + +def test_schema_is_dict() -> None: + assert isinstance(SCHEMA, dict) + assert SCHEMA.get("title") == "TRACE Trust Record" + + +def test_comment_key_fails() -> None: + """additionalProperties is false: a _comment key must be rejected, including in examples.""" + data = _load("intel-tdx.json") + data["_comment"] = "human note" + errors = iter_errors(data) + assert errors + + +def test_okp_jwk_without_key_material_fails() -> None: + """cnf.jwk must carry key material: OKP requires crv and x.""" + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = {"kty": "OKP"} + errors = iter_errors(data) + assert errors + + +def test_ec_jwk_without_y_fails() -> None: + """cnf.jwk must carry key material: EC requires crv, x, and y.""" + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = {"kty": "EC", "crv": "P-256", "x": "dGVzdA"} + errors = iter_errors(data) + assert errors + + +def test_okp_jwk_with_key_material_passes() -> None: + data = _load("intel-tdx.json") + data["cnf"]["jwk"] = { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + } + assert iter_errors(data) == [] + + +def test_delegation_passes_json_schema() -> None: data = _load("intel-tdx.json") - del data["subject"] - errors = iter_errors(data) - assert errors - - -def test_schema_is_dict() -> None: - assert isinstance(SCHEMA, dict) - assert SCHEMA.get("title") == "TRACE Trust Record" - - -def test_comment_key_fails() -> None: - """additionalProperties is false: a _comment key must be rejected, including in examples.""" - data = _load("intel-tdx.json") - data["_comment"] = "human note" - errors = iter_errors(data) - assert errors - - -def test_okp_jwk_without_key_material_fails() -> None: - """cnf.jwk must carry key material: OKP requires crv and x.""" - data = _load("intel-tdx.json") - data["cnf"]["jwk"] = {"kty": "OKP"} - errors = iter_errors(data) - assert errors - - -def test_ec_jwk_without_y_fails() -> None: - """cnf.jwk must carry key material: EC requires crv, x, and y.""" - data = _load("intel-tdx.json") - data["cnf"]["jwk"] = {"kty": "EC", "crv": "P-256", "x": "dGVzdA"} - errors = iter_errors(data) - assert errors + data["delegation"] = { + "parent_record_hash": "sha256:" + "a" * 64, + "credential_id": "cred-abc", + } + assert iter_errors(data) == [] -def test_okp_jwk_with_key_material_passes() -> None: +def test_delegation_bad_digest_fails_json_schema() -> None: data = _load("intel-tdx.json") - data["cnf"]["jwk"] = { - "kty": "OKP", - "crv": "Ed25519", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - } - assert iter_errors(data) == [] + data["delegation"] = {"parent_record_hash": "nope", "credential_id": "c"} + assert iter_errors(data)