diff --git a/CHANGELOG.md b/CHANGELOG.md index 8225cd6e..a237a3b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,35 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR. `no_check_performed` when no revocation store is supplied from `verified` when the supplied store was checked and clean for this call. Revoked-key refusal is unchanged. No new revocation source or bundle support is introduced. +- **BREAKING: a verifier that reports `affirming` after a section 3.3 step 4 reference + match, while its appraisal policy requires a layer, must now establish that layer in + `appraisal.platform_measurement` (section 3.1.5, rule 2, #279).** Reporting `affirming` + with the block absent, or with a required layer not established or not listed, is no + longer conformant. Records do not change: the schema change is one new optional member + of `appraisal`, and no existing document changes verdict. v0.2 is a Draft, so the + 30-day period in GOVERNANCE does not apply. +- **`appraisal.platform_measurement`: what a matching measurement covers, layer by layer + (section 3.1.5, #279).** A matching composite `runtime.measurement` does not say which + layers recorded anything, which were appraised before they ran, or whether the evidence + describes one boot, and the record could say only `appraisal.status: none` about it. A + verifier now writes, per layer, `established` or `not-established` with one of three + reasons: `layer-not-measured`, `measured-not-appraised` or `evidence-spans-multiple-boots`. + The first two are what a digest comparison reports as a pass; the third is what a replay + check reports as tampering. A layer the result does not list is not established, nor is + any layer of a record without the block; a replay mismatch the verifier cannot attribute + to one boot is not, on that basis alone, `contraindicated`; and a record carrying a result + does not report `status: none`. The block is a member of `appraisal` in its own right, + not an `appraisal.method` value, so it sits beside a re-execution result; its `measurement` + equals `runtime.measurement`, a `reason` is never `null`, and on `tpm2` a layer is named + `pcr:` for a register from 0 to 23. Schema, packaged copy, reference model and + `docs/schema.md` carry it; the v0.3 draft schema is regenerated. + `tests/test_safe_integer_range.py` now admits a map whose every member is one object + schema, which it then holds to the same rule. Forty-four signed vectors in + `examples/platform-measurement-appraisal/`: five case-and-twin pairs, two records carrying + measurements from published quotes on a board with a discrete TPM, at least two + rejections per rule, and an accepting twin for every rejection that differs from it only + in the rule it tests. Proposed and written by @revenue7-eng in #279; carried by + @imran-siddique as sponsoring maintainer. - **`verify_record` reports what a caller-supplied appraiser found about `runtime.measurement`, per layer (#279 platform-measurement row, #431).** A matching composite measurement says diff --git a/docs/schema.md b/docs/schema.md index 06c81c29..a8d593c0 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -207,6 +207,7 @@ Verifier judgment on the evidence in this record. | `provenance_depth_verified` | string | no | Depth this verifier actually ran: `surface`, `builder` or `transitive` | | `method` | string | no | The method this appraisal used. A closed set, because a verifier keys on it; this version defines `re-execution`, the result of re-running the record's `reproducibility` claim. `status` is untouched by it: the outcome is not folded into the EAR set | | `re_execution` | object | when `method` is `re-execution` | The re-execution result, described below. Present exactly when `method` is `re-execution`, and absent otherwise | +| `platform_measurement` | object | no | Per-layer appraisal of `runtime.measurement`, described below. A member of `appraisal` in its own right, not a `method` value, so it can sit next to a re-execution result | ### `appraisal.re_execution` members {#trace-field-appraisal-re-execution} @@ -219,6 +220,22 @@ The result of re-running a `reproducibility` claim, attributed to the party name | `reason` | string | when `outcome` is `not-attempted` | Why no outcome could be reported. A named absence and a generic one are different findings | | `verifier_code_identity` | string | no | Digest of the verifier's own implementation. Self-asserted and of no weight singly; a correlation key across results, since two verifiers at different implementations disagreeing over one closure is verifier drift | +### `appraisal.platform_measurement` members {#trace-field-appraisal-platform-measurement} + +What a matching `runtime.measurement` covers, layer by layer, attributed to the party named as `verifier`. A composite that matches its reference does not say which layers recorded anything, which were appraised before they ran, or whether the evidence describes one boot; this block says it. `status` is untouched by it, except that a verifier does not report `affirming` while a layer its policy requires is not established or not listed, and a record carrying this block does not report `none`. Spec section 3.1.5. + +| Field | Type | Required | Description | +|---|---|---|---| +| `measurement` | string | **yes** | The digest this appraisal is about. Must equal `runtime.measurement`: a result about another measurement is not about this record | +| `layers` | object | **yes** | At least one member, keyed by the platform's name for the layer. On `tpm2` the key is `pcr:` and a register number from 0 to 23 in decimal without leading zeros (`pcr:0`, `pcr:23`). Each member carries `outcome` and, when not established, `reason`. A layer not listed is not established, and a record without this block establishes no layer | + +Each member of `layers`: + +| Field | Type | Required | Description | +|---|---|---|---| +| `outcome` | string | **yes** | `established`, or `not-established`, which is never reported as `established` | +| `reason` | string | when `outcome` is `not-established` | `layer-not-measured`: the layer holds no measurement (its initial value, or on a TPM a separator and nothing else). `measured-not-appraised`: the measurements replay to the quoted value and nothing in the evidence shows they were appraised before they ran. `evidence-spans-multiple-boots`: the evidence does not establish that the quote and the event log describe the same single boot, and the log does not replay to the quoted value; not reported as tampering on that basis alone | + ## `transparency` {#trace-field-transparency} diff --git a/examples/README.md b/examples/README.md index deba0cc8..5538cca8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,12 @@ checking any archived record, including these. result: two vectors per rule and five accepting records. Each file is a test-vector envelope; the record is under `record`, and the digests the claim carries recompute from the JSON under `context`. See that directory's README. +- `platform-measurement-appraisal/`: 44 signed Trust Records that pin the shape rules + spec section 3.1.5 states for `appraisal.platform_measurement`, two vectors per rule, + and what a relying party reads off an accepting record, layer by layer: five + case-and-twin pairs, an accepting twin for every rejection, and two records carrying + published measurements. Each file is a + test-vector envelope; the record is under `record`. See that directory's README. - `canonicalization-boundary/`: three signed Trust Records that separate an RFC 8785-conformant canonicalizer from `json.dumps(sort_keys=True)`, which §3.2.2 requires and names as insufficient. Each file is a test-vector envelope; diff --git a/examples/platform-measurement-appraisal/01-layer-not-measured.json b/examples/platform-measurement-appraisal/01-layer-not-measured.json new file mode 100644 index 00000000..8241a120 --- /dev/null +++ b/examples/platform-measurement-appraisal/01-layer-not-measured.json @@ -0,0 +1,86 @@ +{ + "name": "layer-not-measured", + "description": "Synthetic. pcr:2 holds no measurement, so a match there confirms only that nothing was recorded. A reader that takes the matching composite as a pass reads it as established; it is not.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:e09f00095803b62c87420c503a6eed83f30be752bfc3b34cb48dde3d92e33d2a" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:e09f00095803b62c87420c503a6eed83f30be752bfc3b34cb48dde3d92e33d2a", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + }, + "pcr:3": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "established" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "established" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "3DMAoBm5T6Qv_h_f2G8kJn-uIun6BNnG1PN3Nn9VgfnAYXEmDWKI1xlC0MQOmFumxhXJaocQfst3vZkouAZQBA" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/02-layer-not-measured-twin.json b/examples/platform-measurement-appraisal/02-layer-not-measured-twin.json new file mode 100644 index 00000000..f0521851 --- /dev/null +++ b/examples/platform-measurement-appraisal/02-layer-not-measured-twin.json @@ -0,0 +1,85 @@ +{ + "name": "layer-not-measured-twin", + "description": "Twin of 01: the same record with a measured layer at pcr:2, so the layer is established. The only difference is that one condition.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "01", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:e09f00095803b62c87420c503a6eed83f30be752bfc3b34cb48dde3d92e33d2a" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:e09f00095803b62c87420c503a6eed83f30be752bfc3b34cb48dde3d92e33d2a", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "established" + }, + "pcr:3": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "established" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "established" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "5IcGR5xaMxWsj9WhK4Na-MsXY7VI-NfzfB27TJU_RgruvBAn0e43M8C1xLtVRDT47oa_BSgLAEtTnmync0dvDg" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/03-measured-not-appraised.json b/examples/platform-measurement-appraisal/03-measured-not-appraised.json new file mode 100644 index 00000000..4f0a555d --- /dev/null +++ b/examples/platform-measurement-appraisal/03-measured-not-appraised.json @@ -0,0 +1,86 @@ +{ + "name": "measured-not-appraised", + "description": "Synthetic. pcr:2's measurements replay to the quoted value and nothing in the evidence shows they were appraised before they ran. Not a finding that appraisal was off.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:2642b489b77585eb684f423df157d47ed4ed5fefaf87d88e75464a9ee6e5745a" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:2642b489b77585eb684f423df157d47ed4ed5fefaf87d88e75464a9ee6e5745a", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "not-established", + "reason": "measured-not-appraised" + }, + "pcr:3": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "established" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "established" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "YwT0zJpR6Ax4h4TL76e5Qn8z3CyMUYir17WR86sEiyG02ivicREoYcvXpy_euxVhB0NB69_zFszyrUULMhKtDA" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "measured-not-appraised" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/04-measured-not-appraised-twin.json b/examples/platform-measurement-appraisal/04-measured-not-appraised-twin.json new file mode 100644 index 00000000..36f3336a --- /dev/null +++ b/examples/platform-measurement-appraisal/04-measured-not-appraised-twin.json @@ -0,0 +1,85 @@ +{ + "name": "measured-not-appraised-twin", + "description": "Twin of 03: the same record with an appraised layer at pcr:2, so the layer is established. The only difference is that one condition.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "03", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:2642b489b77585eb684f423df157d47ed4ed5fefaf87d88e75464a9ee6e5745a" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:2642b489b77585eb684f423df157d47ed4ed5fefaf87d88e75464a9ee6e5745a", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "established" + }, + "pcr:3": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "established" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "established" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Iy_MyUzaouhYAll2JNPFub0C2wD26vOUX6KJ7Tiu7TJdc1HO8BcFJXjUOSklJKmHuEk62RlTkTkeJeWkgffKBw" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/05-evidence-spans-multiple-boots.json b/examples/platform-measurement-appraisal/05-evidence-spans-multiple-boots.json new file mode 100644 index 00000000..96050c6a --- /dev/null +++ b/examples/platform-measurement-appraisal/05-evidence-spans-multiple-boots.json @@ -0,0 +1,86 @@ +{ + "name": "evidence-spans-multiple-boots", + "description": "Synthetic. the log does not replay to the quoted value and the evidence does not establish one boot. Reported as not established, not as tampering: appraisal.status is warning, not contraindicated, under rule 3.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:57394086d5967087eacce55f21145747248822863aba51f1abcad909de0a7671" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:57394086d5967087eacce55f21145747248822863aba51f1abcad909de0a7671", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "not-established", + "reason": "evidence-spans-multiple-boots" + }, + "pcr:3": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "established" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "established" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "azMxIDLTiuMXGCathoWSiJxzvh0I3d-tLKBoN_a94QPtMj9RKpYP-xJwZ_3TICIekwRxg-duMZL2owfdIRwVAQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "evidence-spans-multiple-boots" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/06-evidence-spans-multiple-boots-twin.json b/examples/platform-measurement-appraisal/06-evidence-spans-multiple-boots-twin.json new file mode 100644 index 00000000..dd1de192 --- /dev/null +++ b/examples/platform-measurement-appraisal/06-evidence-spans-multiple-boots-twin.json @@ -0,0 +1,85 @@ +{ + "name": "evidence-spans-multiple-boots-twin", + "description": "Twin of 05: the same record with a single-boot log at pcr:2, so the layer is established. The only difference is that one condition.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "05", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:57394086d5967087eacce55f21145747248822863aba51f1abcad909de0a7671" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:57394086d5967087eacce55f21145747248822863aba51f1abcad909de0a7671", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "established" + }, + "pcr:3": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "established" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "established" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "la2N4Umnd1rQGqqpFRyXuPKNwpOlCotPvV2IhN4wwdvw5mTYfc7PuKdT8oMtPhbGrwIDj5CFY5tdlViBafrcDg" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/07-layer-not-listed.json b/examples/platform-measurement-appraisal/07-layer-not-listed.json new file mode 100644 index 00000000..6c4b3201 --- /dev/null +++ b/examples/platform-measurement-appraisal/07-layer-not-listed.json @@ -0,0 +1,61 @@ +{ + "name": "layer-not-listed", + "description": "Synthetic. The result lists pcr:0 and pcr:4 only. A reader asking about pcr:2 reads not-established with the reader's own reason not-listed: a layer the result does not list is not established either.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:9ee1ca75a5754aa3e42838b864ccd344e86b27154664d35f482eb37cf6cbeabe" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:9ee1ca75a5754aa3e42838b864ccd344e86b27154664d35f482eb37cf6cbeabe", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "XIjQUHm5gX7RSPLxIbukkcXqdQTJihiXaYM64vWlqCR4Uqu9ZGkLCI1YEPMB2agCxhZDlQ_oHd7XzCRXYzEJBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "not-listed" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/08-layer-not-listed-twin.json b/examples/platform-measurement-appraisal/08-layer-not-listed-twin.json new file mode 100644 index 00000000..63b5fd1c --- /dev/null +++ b/examples/platform-measurement-appraisal/08-layer-not-listed-twin.json @@ -0,0 +1,64 @@ +{ + "name": "layer-not-listed-twin", + "description": "Twin of 07: the same record with pcr:2 listed and established. The only difference is that one layer.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "07", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:9ee1ca75a5754aa3e42838b864ccd344e86b27154664d35f482eb37cf6cbeabe" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:9ee1ca75a5754aa3e42838b864ccd344e86b27154664d35f482eb37cf6cbeabe", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "established" + }, + "pcr:4": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "6xEWbr4yKByVTvp6H2qNRdv2x2Ok8ullRCkYyuy5MHtfD-MtxLbcP8cXt0DH1S-V1qkS2yqdC8CApgD5Pbg4CQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/09-rc13-layer-not-measured.json b/examples/platform-measurement-appraisal/09-rc13-layer-not-measured.json new file mode 100644 index 00000000..c3b1dd35 --- /dev/null +++ b/examples/platform-measurement-appraisal/09-rc13-layer-not-measured.json @@ -0,0 +1,96 @@ +{ + "name": "rc13-layer-not-measured", + "description": "Real measurement: the v2.1.0-rc13 release reference over PCR 0 to 9. PCR 2, 3, 5 and 7 hold a single separator and nothing else, so a digest comparison passes them while nothing was measured there. PCR 10 is outside the quote's selection and is not listed, so a reader asking about it reads not-listed.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "source": "https://github.com/revenue7-eng/tactiq-os/releases/tag/v2.1.0-rc13 (VERIFY-L3-rc13.md and l3-evidence-rc13.tar.gz)", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:2ee6f2d22d3b8e704d1ee8119220aed62a5caff4624c5804b8185debdff1f745" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:2ee6f2d22d3b8e704d1ee8119220aed62a5caff4624c5804b8185debdff1f745", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:1": { + "outcome": "established" + }, + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + }, + "pcr:3": { + "outcome": "not-established", + "reason": "layer-not-measured" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "not-established", + "reason": "layer-not-measured" + }, + "pcr:6": { + "outcome": "established" + }, + "pcr:7": { + "outcome": "not-established", + "reason": "layer-not-measured" + }, + "pcr:8": { + "outcome": "established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "0pfDTgY9jaGpeMi_DLQB_oSG7ybETM-rSlsB_NHes-sVxhc83-ZPxmiDlZhTy0d8gelc64fhPGaOKkMg9s4yBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + }, + "pcr:4": { + "outcome": "established" + }, + "pcr:10": { + "outcome": "not-established", + "reason": "not-listed" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/10-dev-measured-not-appraised.json b/examples/platform-measurement-appraisal/10-dev-measured-not-appraised.json new file mode 100644 index 00000000..8d5c43a4 --- /dev/null +++ b/examples/platform-measurement-appraisal/10-dev-measured-not-appraised.json @@ -0,0 +1,67 @@ +{ + "name": "dev-measured-not-appraised", + "description": "Real measurement: a cold-start quote over PCR 0 to 12 from a development image. Four IMA entries in PCR 11 and 12 replay to the quoted values; the policy that leaves them unappraised is not in the log. Only the layers the appraiser reported are carried. The quote was taken by hand with tpm2_quote under the board's registered attestation key; the image's attestation agent quotes PCR 0 to 9 only, so PCR 11 and 12 are not in its envelope.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "source": "https://github.com/revenue7-eng/tactiq-os/blob/main/measurements/tpm-quotes-dev-20261002.md (cold.* files and check.py beside it)", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:266c3432e78cde16a9e9e63c479c4d07aad80ac049b25d655bfd897fb3b19746" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:266c3432e78cde16a9e9e63c479c4d07aad80ac049b25d655bfd897fb3b19746", + "layers": { + "pcr:11": { + "outcome": "not-established", + "reason": "measured-not-appraised" + }, + "pcr:12": { + "outcome": "not-established", + "reason": "measured-not-appraised" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "ooA9EzRSKou8UGQPJdT-k2uQW95xVJnjFeGkFKkyt2U3Rr0XL9hcd4tzVrhKkJy35-MOxCWNbJywSe-rsun4Bg" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:11": { + "outcome": "not-established", + "reason": "measured-not-appraised" + }, + "pcr:12": { + "outcome": "not-established", + "reason": "measured-not-appraised" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/11-not-established-without-reason.json b/examples/platform-measurement-appraisal/11-not-established-without-reason.json new file mode 100644 index 00000000..a2f9b2dc --- /dev/null +++ b/examples/platform-measurement-appraisal/11-not-established-without-reason.json @@ -0,0 +1,54 @@ +{ + "name": "not-established-without-reason", + "description": "pcr:2 is not-established and names no reason. The three reasons are different findings; one without its reason is a generic absence.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "uXyG_f0DDnfs-2fE8cPmMTHrfOgmsbGdXnMSsjj7uVXbjPmSqQUURKFbB1OqkhhOyFivSoSVGWyFHKi16anPAQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "not_established_without_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/12-not-established-without-reason-among-established.json b/examples/platform-measurement-appraisal/12-not-established-without-reason-among-established.json new file mode 100644 index 00000000..743e3fba --- /dev/null +++ b/examples/platform-measurement-appraisal/12-not-established-without-reason-among-established.json @@ -0,0 +1,60 @@ +{ + "name": "not-established-without-reason-among-established", + "description": "pcr:5 is not-established with no reason, among layers that are established.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "not-established" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "mYTM4fKpc-HJAcoTI1W4x_Kv7Qk_7-BaOhF2QsGvETPPcFdowcoVQ3MVHJBQFFzOczD2yuVeXI1zOoR0NAdMDw" + }, + "expected": { + "outcome": "reject", + "codes": [ + "not_established_without_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/13-established-with-a-reason.json b/examples/platform-measurement-appraisal/13-established-with-a-reason.json new file mode 100644 index 00000000..25ddeb9a --- /dev/null +++ b/examples/platform-measurement-appraisal/13-established-with-a-reason.json @@ -0,0 +1,55 @@ +{ + "name": "established-with-a-reason", + "description": "pcr:2 is established and carries a reason. An established layer has none.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "established", + "reason": "layer-not-measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "o_GgyHJCb8JJq7sAQAEqXc_QkgCCAscDIAESht7rVR9oiPQ4qrBJr5AKZXP4nLy_v6V6EggjlQe1ldTqMyibDQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "established_with_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/14-established-with-a-multiple-boots-reason.json b/examples/platform-measurement-appraisal/14-established-with-a-multiple-boots-reason.json new file mode 100644 index 00000000..442a79eb --- /dev/null +++ b/examples/platform-measurement-appraisal/14-established-with-a-multiple-boots-reason.json @@ -0,0 +1,55 @@ +{ + "name": "established-with-a-multiple-boots-reason", + "description": "pcr:0 is established and carries evidence-spans-multiple-boots.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established", + "reason": "evidence-spans-multiple-boots" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "mljfoODL8Vwsy_9fRzz8FVQKSd09da3g2EUS-N_hlXEMf0xh48RPeTC2r5We7GcpooMEQNqX0PBjijQO_FwWDQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "established_with_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/15-layers-empty.json b/examples/platform-measurement-appraisal/15-layers-empty.json new file mode 100644 index 00000000..fb59d6d0 --- /dev/null +++ b/examples/platform-measurement-appraisal/15-layers-empty.json @@ -0,0 +1,50 @@ +{ + "name": "layers-empty", + "description": "layers is an empty object. A result naming no layer establishes nothing.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": {} + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "JUpYHhfAp2DzcnpeESfQIYvOSex1qBiNMWFycV4E-ChOrBf5tW5-X_rEHOwRUH9qlqp6JjY2DNUY3JqEugOvAQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "layers_empty" + ] + } +} diff --git a/examples/platform-measurement-appraisal/16-reason-outside-the-closed-set.json b/examples/platform-measurement-appraisal/16-reason-outside-the-closed-set.json new file mode 100644 index 00000000..262494a4 --- /dev/null +++ b/examples/platform-measurement-appraisal/16-reason-outside-the-closed-set.json @@ -0,0 +1,55 @@ +{ + "name": "reason-outside-the-closed-set", + "description": "pcr:2 names the reason not-listed, which is the reader's finding about an unlisted layer and not a reason a record carries.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "not-listed" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "ijyoytY8gJfZxpSYJ04u9k5rq1IRm-_EfCm-6kYFHVlwi4pYD9K6s2CBOfA3mJc2ttl1e0s-MJUyaDSxiXeoAQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "unknown_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/17-reason-spelled-with-underscores.json b/examples/platform-measurement-appraisal/17-reason-spelled-with-underscores.json new file mode 100644 index 00000000..05947684 --- /dev/null +++ b/examples/platform-measurement-appraisal/17-reason-spelled-with-underscores.json @@ -0,0 +1,55 @@ +{ + "name": "reason-spelled-with-underscores", + "description": "pcr:2 names layer_not_measured, the library's spelling from #457. The record's reasons are hyphenated.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer_not_measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "R_vVx05G3hk6XG2bWkyQ1dlTophJhcfogKrHD9I3sqb0CpONkgbiBhC5yllvz8XdO_jFKl4z9AMmCOnuJQLpCg" + }, + "expected": { + "outcome": "reject", + "codes": [ + "unknown_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/18-tpm-layer-with-a-leading-zero.json b/examples/platform-measurement-appraisal/18-tpm-layer-with-a-leading-zero.json new file mode 100644 index 00000000..34e4cce9 --- /dev/null +++ b/examples/platform-measurement-appraisal/18-tpm-layer-with-a-leading-zero.json @@ -0,0 +1,54 @@ +{ + "name": "tpm-layer-with-a-leading-zero", + "description": "On tpm2 the layer pcr:02 has a leading zero; two verifiers would name one register two ways.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:02": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "8khEcAhGfR-4EMdMBeRiwwDLVlbGgUglfrJ8scmDAnnVTj7XksrLDBY9wxdImzwMGEGIlfCXL8pElQZhjupYDw" + }, + "expected": { + "outcome": "reject", + "codes": [ + "tpm_layer_name" + ] + } +} diff --git a/examples/platform-measurement-appraisal/19-tpm-layer-in-another-spelling.json b/examples/platform-measurement-appraisal/19-tpm-layer-in-another-spelling.json new file mode 100644 index 00000000..015fe3a8 --- /dev/null +++ b/examples/platform-measurement-appraisal/19-tpm-layer-in-another-spelling.json @@ -0,0 +1,54 @@ +{ + "name": "tpm-layer-in-another-spelling", + "description": "On tpm2 the layer is named PCR2.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "PCR2": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "uP8-rwzClmj_VPdu2-zgRrwO3SSDPLihEA-Bemea5xeyJwFqRDvum_LBBBmTo67spFfx2AlPztpj2gQYL-8FBw" + }, + "expected": { + "outcome": "reject", + "codes": [ + "tpm_layer_name" + ] + } +} diff --git a/examples/platform-measurement-appraisal/20-layers-empty-off-tpm.json b/examples/platform-measurement-appraisal/20-layers-empty-off-tpm.json new file mode 100644 index 00000000..9a419b58 --- /dev/null +++ b/examples/platform-measurement-appraisal/20-layers-empty-off-tpm.json @@ -0,0 +1,50 @@ +{ + "name": "layers-empty-off-tpm", + "description": "layers is an empty object on a non-TPM platform. The rule does not depend on the platform.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "intel-tdx", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": {} + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "mfMQl3npaie1Fxz9Pwddr3SgNpeH5UJd7x3KrE86egxLY_OrMywXCjlJ_coO3NwzmTx2yIeT8dejm2NMiFeyCA" + }, + "expected": { + "outcome": "reject", + "codes": [ + "layers_empty" + ] + } +} diff --git a/examples/platform-measurement-appraisal/21-result-about-another-measurement.json b/examples/platform-measurement-appraisal/21-result-about-another-measurement.json new file mode 100644 index 00000000..cde895f5 --- /dev/null +++ b/examples/platform-measurement-appraisal/21-result-about-another-measurement.json @@ -0,0 +1,55 @@ +{ + "name": "result-about-another-measurement", + "description": "platform_measurement.measurement differs from runtime.measurement: the result is about other evidence, not this record. Held by the reference model only: the schema cannot compare two members of the record.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:57d75a1f518dcb33ce27b84a7b308b10865887e2946ca5852f01aed810fde2b4", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "HOm8kkf6YtycRPNfQTiODH7u8JKh3W_sx791svN3lk-oxQgZbBs6pyoJR_-pF2NnIFqdS7P4PZ5v8P6I-ndCAg" + }, + "expected": { + "outcome": "reject", + "codes": [ + "measurement_mismatch" + ], + "schema_sees": false + } +} diff --git a/examples/platform-measurement-appraisal/22-result-about-a-measurement-one-byte-off.json b/examples/platform-measurement-appraisal/22-result-about-a-measurement-one-byte-off.json new file mode 100644 index 00000000..744e7c0a --- /dev/null +++ b/examples/platform-measurement-appraisal/22-result-about-a-measurement-one-byte-off.json @@ -0,0 +1,55 @@ +{ + "name": "result-about-a-measurement-one-byte-off", + "description": "platform_measurement.measurement differs from runtime.measurement in its last byte. Held by the reference model only: the schema cannot compare two members of the record.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7500", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "iHpo2TlKO-N-lTkN0OvTK1AfdMoMongEAfuXh-ROw_qNyQ8W8WxIPFCzlr50DnvbNVtBY2Kki7abeKl1fxBwBQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "measurement_mismatch" + ], + "schema_sees": false + } +} diff --git a/examples/platform-measurement-appraisal/23-no-result.json b/examples/platform-measurement-appraisal/23-no-result.json new file mode 100644 index 00000000..2bbc1040 --- /dev/null +++ b/examples/platform-measurement-appraisal/23-no-result.json @@ -0,0 +1,50 @@ +{ + "name": "no-result", + "description": "Synthetic. The record carries no appraisal.platform_measurement at all. A reader asking about pcr:2 reads not-established with the reader's reason not-listed: a record without the block establishes no layer, whatever its status says.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:56bb3d160a6956c49d9cf1204822eb0962bcb2ab54ccffd1c192aff87cc3c1ca" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "g-CsV_o_NLlBgQp8FQtcm919vs20bum0mHaJ6ravUFJG7mYTPkvnHTUzQ6ByDtTnvtaPUQDWtwQKrkxH6yOOBA" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "not-listed" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/24-no-result-twin.json b/examples/platform-measurement-appraisal/24-no-result-twin.json new file mode 100644 index 00000000..1b75836f --- /dev/null +++ b/examples/platform-measurement-appraisal/24-no-result-twin.json @@ -0,0 +1,58 @@ +{ + "name": "no-result-twin", + "description": "Twin of 23: the same record with a result that lists pcr:2 as established. The only difference is the result.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "23", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:56bb3d160a6956c49d9cf1204822eb0962bcb2ab54ccffd1c192aff87cc3c1ca" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:56bb3d160a6956c49d9cf1204822eb0962bcb2ab54ccffd1c192aff87cc3c1ca", + "layers": { + "pcr:2": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "AWHPCpigz9DuVjqCMPNFQyRGYyMJAuVqI9ZlqqyTYRWa3C-QYPz_IHiAh8Gsn-MTEp_7fR_d3-Hvjalv3MkGBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/25-status-none-with-a-result.json b/examples/platform-measurement-appraisal/25-status-none-with-a-result.json new file mode 100644 index 00000000..2a7ec371 --- /dev/null +++ b/examples/platform-measurement-appraisal/25-status-none-with-a-result.json @@ -0,0 +1,54 @@ +{ + "name": "status-none-with-a-result", + "description": "appraisal.status is none while the record carries a per-layer result. A verifier that wrote the result performed an appraisal.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "my963YhRhvhPfSpa59gWI1-n6GLvxF2yZPqiOMiMb43qGX6cJPg77Lg6B4SAD3kS2_VpnhVsi1UOrFCKfmUtDQ" + }, + "expected": { + "outcome": "reject", + "codes": [ + "status_none_with_result" + ] + } +} diff --git a/examples/platform-measurement-appraisal/26-status-none-with-a-not-established-result.json b/examples/platform-measurement-appraisal/26-status-none-with-a-not-established-result.json new file mode 100644 index 00000000..ef534929 --- /dev/null +++ b/examples/platform-measurement-appraisal/26-status-none-with-a-not-established-result.json @@ -0,0 +1,55 @@ +{ + "name": "status-none-with-a-not-established-result", + "description": "appraisal.status is none while the result reports pcr:2 not-established. A finding per layer is a finding, not the absence of one.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "none", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "GpQxOBOs0IwQ7RGLAjEAs-QOAJxG3vcJfYVpWoI4J3AZrR2IzeIkmlmW_Ufkq7TsL6sjKxam22fkRKnbaNMZAw" + }, + "expected": { + "outcome": "reject", + "codes": [ + "status_none_with_result" + ] + } +} diff --git a/examples/platform-measurement-appraisal/27-reason-null.json b/examples/platform-measurement-appraisal/27-reason-null.json new file mode 100644 index 00000000..8edefd81 --- /dev/null +++ b/examples/platform-measurement-appraisal/27-reason-null.json @@ -0,0 +1,55 @@ +{ + "name": "reason-null", + "description": "pcr:0 is established and carries reason null. A null reason is neither absent nor one of the three reasons.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established", + "reason": null + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "YXUsnxGU_Ivi6waPotvhAkMF79cmVn3ivptk3FMwGdW4HhSzPpS38pdYwmnUCQ6FM7nb_5OgaPsMOymNYU4xAw" + }, + "expected": { + "outcome": "reject", + "codes": [ + "unknown_reason" + ] + } +} diff --git a/examples/platform-measurement-appraisal/28-tpm-layer-past-the-last-register.json b/examples/platform-measurement-appraisal/28-tpm-layer-past-the-last-register.json new file mode 100644 index 00000000..80bc1d96 --- /dev/null +++ b/examples/platform-measurement-appraisal/28-tpm-layer-past-the-last-register.json @@ -0,0 +1,54 @@ +{ + "name": "tpm-layer-past-the-last-register", + "description": "On tpm2 the layer pcr:24 names a register past pcr:23, the last a TPM 2.0 PC Client platform defines.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:24": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "MOZOE9qebu4pB_-ncqZMORfLDVQja-htwKrArHoY_F0_isPYx19XZUadERrHqodvzQcZCnstbfdINpWZ2_zuCw" + }, + "expected": { + "outcome": "reject", + "codes": [ + "tpm_layer_name" + ] + } +} diff --git a/examples/platform-measurement-appraisal/29-not-established-without-reason-twin.json b/examples/platform-measurement-appraisal/29-not-established-without-reason-twin.json new file mode 100644 index 00000000..1c316527 --- /dev/null +++ b/examples/platform-measurement-appraisal/29-not-established-without-reason-twin.json @@ -0,0 +1,60 @@ +{ + "name": "not-established-without-reason-twin", + "description": "Twin of 11: the same record with the member that breaks not_established_without_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "11", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "2IYl-dPpuIonbajljzJ3TagTyTunwRaY4-3PHVFEQmsODh1Amm-egy_papEZcFM9dbGgVghAis5w5XpaJ1YiDQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/30-not-established-without-reason-among-established-twin.json b/examples/platform-measurement-appraisal/30-not-established-without-reason-among-established-twin.json new file mode 100644 index 00000000..155f85cd --- /dev/null +++ b/examples/platform-measurement-appraisal/30-not-established-without-reason-among-established-twin.json @@ -0,0 +1,66 @@ +{ + "name": "not-established-without-reason-among-established-twin", + "description": "Twin of 12: the same record with the member that breaks not_established_without_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "12", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + }, + "pcr:5": { + "outcome": "not-established", + "reason": "measured-not-appraised" + }, + "pcr:9": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Wfo5w-A99P4aZEmgILuLSYFV7M5LZM_1JVkfBmLCTbVxE8JPpWWw6diWLUm0rfX_3QbkkO0PQq2Zrz8G5JYOAQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:5": { + "outcome": "not-established", + "reason": "measured-not-appraised" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/31-established-with-a-reason-twin.json b/examples/platform-measurement-appraisal/31-established-with-a-reason-twin.json new file mode 100644 index 00000000..463afee1 --- /dev/null +++ b/examples/platform-measurement-appraisal/31-established-with-a-reason-twin.json @@ -0,0 +1,58 @@ +{ + "name": "established-with-a-reason-twin", + "description": "Twin of 13: the same record with the member that breaks established_with_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "13", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "yVJ0K61zjdh9vIMwTz6nH6ifkwtQCQo64ggtdpGEQsMzuFqnVI9IGNw92Hfg8DOZAcjQ5NKjqJbeTjXwxfvRBA" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/32-established-with-a-multiple-boots-reason-twin.json b/examples/platform-measurement-appraisal/32-established-with-a-multiple-boots-reason-twin.json new file mode 100644 index 00000000..dc0e50ad --- /dev/null +++ b/examples/platform-measurement-appraisal/32-established-with-a-multiple-boots-reason-twin.json @@ -0,0 +1,58 @@ +{ + "name": "established-with-a-multiple-boots-reason-twin", + "description": "Twin of 14: the same record with the member that breaks established_with_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "14", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Utu6k2pDJC2VFts6o6U7PHbyZalFSNgfRB6I-nimVbxBXsbP1PReWCoWOKTlcC6Y1UeIonQfnGpl6RuUnXRgBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/33-layers-empty-twin.json b/examples/platform-measurement-appraisal/33-layers-empty-twin.json new file mode 100644 index 00000000..01919c20 --- /dev/null +++ b/examples/platform-measurement-appraisal/33-layers-empty-twin.json @@ -0,0 +1,58 @@ +{ + "name": "layers-empty-twin", + "description": "Twin of 15: the same record with the member that breaks layers_empty put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "15", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Utu6k2pDJC2VFts6o6U7PHbyZalFSNgfRB6I-nimVbxBXsbP1PReWCoWOKTlcC6Y1UeIonQfnGpl6RuUnXRgBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/34-reason-outside-the-closed-set-twin.json b/examples/platform-measurement-appraisal/34-reason-outside-the-closed-set-twin.json new file mode 100644 index 00000000..79627399 --- /dev/null +++ b/examples/platform-measurement-appraisal/34-reason-outside-the-closed-set-twin.json @@ -0,0 +1,60 @@ +{ + "name": "reason-outside-the-closed-set-twin", + "description": "Twin of 16: the same record with the member that breaks unknown_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "16", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "2IYl-dPpuIonbajljzJ3TagTyTunwRaY4-3PHVFEQmsODh1Amm-egy_papEZcFM9dbGgVghAis5w5XpaJ1YiDQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/35-reason-spelled-with-underscores-twin.json b/examples/platform-measurement-appraisal/35-reason-spelled-with-underscores-twin.json new file mode 100644 index 00000000..7148fa54 --- /dev/null +++ b/examples/platform-measurement-appraisal/35-reason-spelled-with-underscores-twin.json @@ -0,0 +1,60 @@ +{ + "name": "reason-spelled-with-underscores-twin", + "description": "Twin of 17: the same record with the member that breaks unknown_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "17", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "2IYl-dPpuIonbajljzJ3TagTyTunwRaY4-3PHVFEQmsODh1Amm-egy_papEZcFM9dbGgVghAis5w5XpaJ1YiDQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/36-tpm-layer-with-a-leading-zero-twin.json b/examples/platform-measurement-appraisal/36-tpm-layer-with-a-leading-zero-twin.json new file mode 100644 index 00000000..572ed1b9 --- /dev/null +++ b/examples/platform-measurement-appraisal/36-tpm-layer-with-a-leading-zero-twin.json @@ -0,0 +1,58 @@ +{ + "name": "tpm-layer-with-a-leading-zero-twin", + "description": "Twin of 18: the same record with the member that breaks tpm_layer_name put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "18", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "yVJ0K61zjdh9vIMwTz6nH6ifkwtQCQo64ggtdpGEQsMzuFqnVI9IGNw92Hfg8DOZAcjQ5NKjqJbeTjXwxfvRBA" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/37-tpm-layer-in-another-spelling-twin.json b/examples/platform-measurement-appraisal/37-tpm-layer-in-another-spelling-twin.json new file mode 100644 index 00000000..0e45278f --- /dev/null +++ b/examples/platform-measurement-appraisal/37-tpm-layer-in-another-spelling-twin.json @@ -0,0 +1,58 @@ +{ + "name": "tpm-layer-in-another-spelling-twin", + "description": "Twin of 19: the same record with the member that breaks tpm_layer_name put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "19", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "yVJ0K61zjdh9vIMwTz6nH6ifkwtQCQo64ggtdpGEQsMzuFqnVI9IGNw92Hfg8DOZAcjQ5NKjqJbeTjXwxfvRBA" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/38-layers-empty-off-tpm-twin.json b/examples/platform-measurement-appraisal/38-layers-empty-off-tpm-twin.json new file mode 100644 index 00000000..8313a4ee --- /dev/null +++ b/examples/platform-measurement-appraisal/38-layers-empty-off-tpm-twin.json @@ -0,0 +1,58 @@ +{ + "name": "layers-empty-off-tpm-twin", + "description": "Twin of 20: the same record with the member that breaks layers_empty put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "20", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "intel-tdx", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "rtmr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "V3cs8O3s64516Ifyr6X-ooQFkCs9munMo9Jg45wJB-5uA7WzyLU2HBAf7YCNGPTk2KwhsjUAVc9hG368WuidAg" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "rtmr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/39-result-about-another-measurement-twin.json b/examples/platform-measurement-appraisal/39-result-about-another-measurement-twin.json new file mode 100644 index 00000000..50fdfb40 --- /dev/null +++ b/examples/platform-measurement-appraisal/39-result-about-another-measurement-twin.json @@ -0,0 +1,58 @@ +{ + "name": "result-about-another-measurement-twin", + "description": "Twin of 21: the same record with the member that breaks measurement_mismatch put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "21", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Utu6k2pDJC2VFts6o6U7PHbyZalFSNgfRB6I-nimVbxBXsbP1PReWCoWOKTlcC6Y1UeIonQfnGpl6RuUnXRgBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/40-result-about-a-measurement-one-byte-off-twin.json b/examples/platform-measurement-appraisal/40-result-about-a-measurement-one-byte-off-twin.json new file mode 100644 index 00000000..9027320e --- /dev/null +++ b/examples/platform-measurement-appraisal/40-result-about-a-measurement-one-byte-off-twin.json @@ -0,0 +1,58 @@ +{ + "name": "result-about-a-measurement-one-byte-off-twin", + "description": "Twin of 22: the same record with the member that breaks measurement_mismatch put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "22", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Utu6k2pDJC2VFts6o6U7PHbyZalFSNgfRB6I-nimVbxBXsbP1PReWCoWOKTlcC6Y1UeIonQfnGpl6RuUnXRgBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/41-status-none-with-a-result-twin.json b/examples/platform-measurement-appraisal/41-status-none-with-a-result-twin.json new file mode 100644 index 00000000..cfe3f786 --- /dev/null +++ b/examples/platform-measurement-appraisal/41-status-none-with-a-result-twin.json @@ -0,0 +1,58 @@ +{ + "name": "status-none-with-a-result-twin", + "description": "Twin of 25: the same record with the member that breaks status_none_with_result put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "25", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Utu6k2pDJC2VFts6o6U7PHbyZalFSNgfRB6I-nimVbxBXsbP1PReWCoWOKTlcC6Y1UeIonQfnGpl6RuUnXRgBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/42-status-none-with-a-not-established-result-twin.json b/examples/platform-measurement-appraisal/42-status-none-with-a-not-established-result-twin.json new file mode 100644 index 00000000..8df7a3f0 --- /dev/null +++ b/examples/platform-measurement-appraisal/42-status-none-with-a-not-established-result-twin.json @@ -0,0 +1,60 @@ +{ + "name": "status-none-with-a-not-established-result-twin", + "description": "Twin of 26: the same record with the member that breaks status_none_with_result put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "26", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "2IYl-dPpuIonbajljzJ3TagTyTunwRaY4-3PHVFEQmsODh1Amm-egy_papEZcFM9dbGgVghAis5w5XpaJ1YiDQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:2": { + "outcome": "not-established", + "reason": "layer-not-measured" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/43-reason-null-twin.json b/examples/platform-measurement-appraisal/43-reason-null-twin.json new file mode 100644 index 00000000..f083ee62 --- /dev/null +++ b/examples/platform-measurement-appraisal/43-reason-null-twin.json @@ -0,0 +1,58 @@ +{ + "name": "reason-null-twin", + "description": "Twin of 27: the same record with the member that breaks unknown_reason put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "27", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:0": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "Utu6k2pDJC2VFts6o6U7PHbyZalFSNgfRB6I-nimVbxBXsbP1PReWCoWOKTlcC6Y1UeIonQfnGpl6RuUnXRgBQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:0": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/44-tpm-layer-past-the-last-register-twin.json b/examples/platform-measurement-appraisal/44-tpm-layer-past-the-last-register-twin.json new file mode 100644 index 00000000..27ce041d --- /dev/null +++ b/examples/platform-measurement-appraisal/44-tpm-layer-past-the-last-register-twin.json @@ -0,0 +1,58 @@ +{ + "name": "tpm-layer-past-the-last-register-twin", + "description": "Twin of 28: the same record with the member that breaks tpm_layer_name put right. The only difference is that member.", + "spec": "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers", + "synthetic": true, + "twin_of": "28", + "record": { + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1785000000, + "subject": "spiffe://edge.example/device/bench-001", + "model": { + "provider": "anthropic", + "model_id": "claude-sonnet-4-6" + }, + "runtime": { + "platform": "tpm2", + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545" + }, + "policy": { + "bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202" + }, + "appraisal": { + "status": "warning", + "verifier": "https://verifier.example/platform-measurement", + "platform_measurement": { + "measurement": "sha256:81f605e80d610a786f4169c593480e077cfab5850eb2332d09c08a1bccac7545", + "layers": { + "pcr:23": { + "outcome": "established" + } + } + } + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "shaEEVtRR8nWCpp--gRa2Hu4AbHYpKKTdF3EuE5COU8" + } + }, + "signature": "d6ztCMUZb7Jcvuqo29MjV0ZQaLiRDC3lBtLnDOALPdroCX-1y65R6iAJGFdsj3GLfIahI6bwvbPNf_Wa4qzbAQ" + }, + "expected": { + "outcome": "accept", + "codes": [], + "reading": { + "pcr:23": { + "outcome": "established" + } + } + } +} diff --git a/examples/platform-measurement-appraisal/README.md b/examples/platform-measurement-appraisal/README.md new file mode 100644 index 00000000..171d6ac5 --- /dev/null +++ b/examples/platform-measurement-appraisal/README.md @@ -0,0 +1,66 @@ +# Platform-measurement appraisal vectors + +Forty-four signed Trust Records for `appraisal.platform_measurement`, spec section 3.1.5, +raised in [#279](https://github.com/agentrust-io/trace-spec/issues/279). Each file is a +test-vector envelope: the record is under `record`, the verdict under `expected`. + +## What is pinned + +**Shape.** An accepting vector is a record the schema and the reference model both take. +A rejecting vector carries the one code of the rule it breaks: + +| Code | Rule | +|---|---| +| `not_established_without_reason` | a `not-established` layer names one of the three reasons | +| `established_with_reason` | an `established` layer carries no reason | +| `layers_empty` | a result names at least one layer | +| `unknown_reason` | the reason is one of `layer-not-measured`, `measured-not-appraised`, `evidence-spans-multiple-boots`, and not `null` | +| `tpm_layer_name` | on `tpm2` a layer is `pcr:` and a register number from 0 to 23 in decimal without leading zeros | +| `measurement_mismatch` | `platform_measurement.measurement` equals `runtime.measurement` | +| `status_none_with_result` | a record carrying a result does not report `appraisal.status: none` | + +`measurement_mismatch` compares two members of the record, which a JSON Schema cannot do. +Its vectors carry `"schema_sees": false`: the schema accepts them and the reference model +refuses them. + +**Reading.** On every accepting vector `expected.reading` names, for each layer it asks +about, what a relying party takes from the record. A listed layer reads as its own +outcome; a layer the result does not list reads `not-established` with the reason +`not-listed`. `not-listed` is the reader's finding, not one of the record's reasons, and +a record carrying it is rejected (`unknown_reason`). + +## Pairs + +`02`, `04`, `06`, `08` and `24` are the twins of `01`, `03`, `05`, `07` and `23`: the same +record except one condition at `pcr:2` (a measured layer, an appraised layer, a single-boot +log, a listed layer, a result at all). `23` carries no `appraisal.platform_measurement`: +a record without the block establishes no layer. A reader that refuses everything fails +the twins; a reader that takes `appraisal.status` at its word fails the cases; a reader +that takes an unlisted layer as established fails `07` and `23`. Every pair is synthetic (`"synthetic": true`). + +`05` carries `appraisal.status: "warning"`, not `contraindicated`: a replay mismatch the +verifier cannot attribute to one boot is not, on that basis alone, reported as tampering. + +Every rejection, `11` to `22` and `25` to `28`, has an accepting twin, `29` to `44` in the +same order: the rejected record with the one member that breaks the rule put right, and +nothing else changed. A verifier that refuses everything fails all sixteen. `38`, the twin +of `20`, is the record on a non-TPM platform, with a layer named `rtmr:0`: the TPM naming +rule binds `tpm2` only. + +## Where the real measurements come from + +`09` and `10` carry the `pcrDigest` of published quotes on a physical board with a +discrete TPM (TactiQ OS on Rock 5A, Infineon SLB9670), with the evidence and an offline +checker at the URL in `source`: the v2.1.0-rc13 release reference, in which PCR 2, 3, 5 +and 7 hold a single separator only, and a development-image cold-start quote, in which +IMA measurements in PCR 11 and 12 replay and nothing shows they were appraised. That +quote was taken by hand with `tpm2_quote` under the board's registered attestation key; +the image's attestation agent quotes PCR 0 to 9 only, so PCR 10 to 12 are not in its +envelope. Neither +has a twin, because a twin would state something untrue about that evidence. The two-boot +case exists only in the synthetic pair: the development loader on that board resets the +TPM before measuring, so the board did not produce the case. + +Regenerate with +`PYTHONPATH=src python examples/platform-measurement-appraisal/gen_platform_measurement_appraisal_vectors.py`; +the set reproduces byte-for-byte. diff --git a/examples/platform-measurement-appraisal/gen_platform_measurement_appraisal_vectors.py b/examples/platform-measurement-appraisal/gen_platform_measurement_appraisal_vectors.py new file mode 100644 index 00000000..a7c3fe11 --- /dev/null +++ b/examples/platform-measurement-appraisal/gen_platform_measurement_appraisal_vectors.py @@ -0,0 +1,416 @@ +"""Generate the platform-measurement appraisal vectors (spec section 3.1.5, #279). + +Each vector is one signed Trust Record whose `appraisal.platform_measurement` says, layer +by layer, what a matching `runtime.measurement` covers. Two things are pinned. + +**The shape rules the schema and the reference model hold.** An accepting vector is a +record both validators take; a rejecting vector carries the one code of the rule it +breaks. One rule is held by the reference model only, because it compares two members +of the record and a JSON Schema cannot: `measurement_mismatch`, where the result is +about a measurement other than `runtime.measurement`. Those vectors say so with +`"schema_sees": false`. + +**What a relying party reads off an accepting record.** `expected.reading` names, for +each layer the vector asks about, the outcome a relying party must take from the record, +and the reason when the layer is not established. A layer the result does not list is +read as `not-established` with the reason `not-listed`, which is not one of the record's +three reasons: it is the reader's own finding, rule 1 of the section. + +**Pairs.** Every case appears in a vector that produces it and in a twin, `twin_of` that +vector, which carries the same record except the one condition: a single-boot log, an +appraised layer, a measured layer, a listed layer, a result at all. A reader that refuses everything +reads `not-established` everywhere and fails every twin; a reader that takes +`appraisal.status` at its word, or that reads an unlisted layer as established, fails +the cases. Every pair is synthetic, `"synthetic": true`: its measurement and its +per-layer result are made up to exercise one case, not taken from a platform. + +**Where real measurements come from.** Two accepting vectors carry the `pcrDigest` of +published quotes on a physical board with a discrete TPM (TactiQ OS on Rock 5A, Infineon +SLB9670), and say so in `source`; neither has a twin, because a twin would have to state +something untrue about that evidence: + +- `09` is the v2.1.0-rc13 release reference over PCR 0 to 9, in which PCR 2, 3, 5 and 7 + hold a single separator and nothing else: `layer-not-measured`. +- `10` is the cold-start quote of 2026-10-02 over PCR 0 to 12, in which four IMA entries + in PCR 11 and 12 replay to the quoted values and the policy that leaves them + unappraised is not in the log: `measured-not-appraised`. The quote was taken by hand + with `tpm2_quote` under the board's registered attestation key; the image's attestation + agent quotes PCR 0 to 9 only. + +The two-boot case exists only in the synthetic pair: the development loader on that board +resets the TPM before measuring, so the board did not produce the case. These are the +measurements the platform-measurement vectors of #457 carry, now written into the record +as section 3.1.5 names them. + +The key derives from one published seed so the set regenerates byte-for-byte. Files are +written as bytes with LF line endings. +""" + +from __future__ import annotations + +import base64 +import copy +import hashlib +import json +from pathlib import Path +from typing import Any + +import rfc8785 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +OUT = Path("examples/platform-measurement-appraisal") +V0_2 = "tag:agentrust-io.com,2026:trace-v0.2" +SPEC = "spec/trace-v0.2.md#315-appraisalplatform_measurement-what-a-matching-measurement-covers" + +SEED = hashlib.sha256(b"trace-spec platform-measurement-appraisal fixture key").digest() + +#: Fixed issue time. Nothing in this set turns on the clock. +IAT = 1785000000 + +SUBJECT = "spiffe://edge.example/device/bench-001" +VERIFIER = "https://verifier.example/platform-measurement" + +#: The two real measurements: `pcrDigest` of published quotes. +RC13 = "sha256:2ee6f2d22d3b8e704d1ee8119220aed62a5caff4624c5804b8185debdff1f745" +DEV_COLD = "sha256:266c3432e78cde16a9e9e63c479c4d07aad80ac049b25d655bfd897fb3b19746" +SOURCE_RC13 = ( + "https://github.com/revenue7-eng/tactiq-os/releases/tag/v2.1.0-rc13 " + "(VERIFY-L3-rc13.md and l3-evidence-rc13.tar.gz)" +) +SOURCE_DEV = ( + "https://github.com/revenue7-eng/tactiq-os/blob/main/measurements/" + "tpm-quotes-dev-20261002.md (cold.* files and check.py beside it)" +) + +PCRS_0_9 = [f"pcr:{i}" for i in range(10)] + + +def key() -> Ed25519PrivateKey: + return Ed25519PrivateKey.from_private_bytes(hashlib.sha256(SEED + b"|producer").digest()) + + +def b64u(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode() + + +def jwk() -> dict[str, str]: + raw = key().public_key().public_bytes( + encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw + ) + return {"kty": "OKP", "crv": "Ed25519", "x": b64u(raw)} + + +def synthetic(label: str) -> str: + """A measurement with no platform behind it, derived from the seed by label.""" + return "sha256:" + hashlib.sha256(SEED + b"|" + label.encode()).hexdigest() + + +def established() -> dict[str, str]: + return {"outcome": "established"} + + +def not_established(reason: str) -> dict[str, str]: + return {"outcome": "not-established", "reason": reason} + + +def record(*, measurement: str, status: str, layers: dict[str, Any] | None, + result_measurement: str | None = None, platform: str = "tpm2") -> dict[str, Any]: + """A schema-valid record carrying the appraisal given. Minimal otherwise. + + ``layers=None`` writes no ``appraisal.platform_measurement`` at all. + """ + body: dict[str, Any] = { + "eat_profile": V0_2, + "iat": IAT, + "subject": SUBJECT, + "model": {"provider": "anthropic", "model_id": "claude-sonnet-4-6"}, + "runtime": {"platform": platform, "measurement": measurement}, + "policy": {"bundle_hash": "sha256:" + "aa" * 32, "enforcement_mode": "enforce"}, + "data_class": "internal", + "build_provenance": {"slsa_level": 0, "digest": synthetic("build")}, + "appraisal": { + "status": status, + "verifier": VERIFIER, + "platform_measurement": { + "measurement": result_measurement or measurement, + "layers": layers, + }, + }, + "cnf": {"jwk": jwk()}, + } + if layers is None: + del body["appraisal"]["platform_measurement"] + return sign(body) + + +def sign(body: dict[str, Any]) -> dict[str, Any]: + body = {k: v for k, v in body.items() if k != "signature"} + body["signature"] = b64u(key().sign(rfc8785.dumps(body))) + return body + + +def vector(n: int, name: str, description: str, *, rec: dict[str, Any], + codes: list[str] | None = None, reading: dict[str, Any] | None = None, + twin_of: int | None = None, source: str | None = None, + schema_sees: bool = True) -> tuple[str, dict[str, Any]]: + codes = codes or [] + expected: dict[str, Any] = {"outcome": "reject" if codes else "accept", "codes": codes} + if reading is not None: + expected["reading"] = reading + if not schema_sees: + expected["schema_sees"] = False + doc: dict[str, Any] = {"name": name, "description": description, "spec": SPEC} + if source is not None: + doc["source"] = source + else: + doc["synthetic"] = True + if twin_of is not None: + doc["twin_of"] = f"{twin_of:02d}" + doc["record"] = rec + doc["expected"] = expected + return f"{n:02d}-{name}.json", doc + + +def read(outcome: str, reason: str | None = None) -> dict[str, str]: + return {"outcome": outcome} if reason is None else {"outcome": outcome, "reason": reason} + + +def with_layer(layers: dict[str, Any], name: str, value: Any) -> dict[str, Any]: + out = copy.deepcopy(layers) + out[name] = value + return out + + +def main() -> None: + out: list[tuple[str, dict[str, Any]]] = [] + base = {p: established() for p in PCRS_0_9} + + # Pairs. Each case on pcr:2 with every other layer established, then its twin. + pairs = [ + ("layer-not-measured", "a measured layer", + "pcr:2 holds no measurement, so a match there confirms only that nothing was " + "recorded. A reader that takes the matching composite as a pass reads it as " + "established; it is not."), + ("measured-not-appraised", "an appraised layer", + "pcr:2's measurements replay to the quoted value and nothing in the evidence " + "shows they were appraised before they ran. Not a finding that appraisal was off."), + ("evidence-spans-multiple-boots", "a single-boot log", + "the log does not replay to the quoted value and the evidence does not establish " + "one boot. Reported as not established, not as tampering: appraisal.status is " + "warning, not contraindicated, under rule 3."), + ] + n = 1 + for reason, twin_condition, what in pairs: + m = synthetic(reason) + out.append(vector(n, reason, f"Synthetic. {what}", + rec=record(measurement=m, status="warning", + layers=with_layer(base, "pcr:2", not_established(reason))), + reading={"pcr:2": read("not-established", reason)})) + out.append(vector(n + 1, f"{reason}-twin", + f"Twin of {n:02d}: the same record with {twin_condition} at pcr:2, so the " + "layer is established. The only difference is that one condition.", + rec=record(measurement=m, status="warning", layers=base), + reading={"pcr:2": read("established")}, twin_of=n)) + n += 2 + + # Unlisted layer, rule 1: absence is not a pass. + m = synthetic("not-listed") + out.append(vector(7, "layer-not-listed", + "Synthetic. The result lists pcr:0 and pcr:4 only. A reader asking about pcr:2 " + "reads not-established with the reader's own reason not-listed: a layer the " + "result does not list is not established either.", + rec=record(measurement=m, status="warning", + layers={"pcr:0": established(), "pcr:4": established()}), + reading={"pcr:2": read("not-established", "not-listed")})) + out.append(vector(8, "layer-not-listed-twin", + "Twin of 07: the same record with pcr:2 listed and established. The only " + "difference is that one layer.", + rec=record(measurement=m, status="warning", + layers={"pcr:0": established(), "pcr:2": established(), + "pcr:4": established()}), + reading={"pcr:2": read("established")}, twin_of=7)) + + # Real measurements, unpaired. + rc13 = {p: (not_established("layer-not-measured") + if p in ("pcr:2", "pcr:3", "pcr:5", "pcr:7") else established()) + for p in PCRS_0_9} + out.append(vector(9, "rc13-layer-not-measured", + "Real measurement: the v2.1.0-rc13 release reference over PCR 0 to 9. PCR 2, 3, " + "5 and 7 hold a single separator and nothing else, so a digest comparison passes " + "them while nothing was measured there. PCR 10 is outside the quote's selection " + "and is not listed, so a reader asking about it reads not-listed.", + rec=record(measurement=RC13, status="warning", layers=rc13), + reading={"pcr:2": read("not-established", "layer-not-measured"), + "pcr:4": read("established"), + "pcr:10": read("not-established", "not-listed")}, + source=SOURCE_RC13)) + dev = {"pcr:11": not_established("measured-not-appraised"), + "pcr:12": not_established("measured-not-appraised")} + out.append(vector(10, "dev-measured-not-appraised", + "Real measurement: a cold-start quote over PCR 0 to 12 from a development image. " + "Four IMA entries in PCR 11 and 12 replay to the quoted values; the policy that " + "leaves them unappraised is not in the log. Only the layers the appraiser " + "reported are carried. The quote was taken by hand with tpm2_quote under the " + "board's registered attestation key; the image's attestation agent quotes PCR 0 " + "to 9 only, so PCR 11 and 12 are not in its envelope.", + rec=record(measurement=DEV_COLD, status="warning", layers=dev), + reading={"pcr:11": read("not-established", "measured-not-appraised"), + "pcr:12": read("not-established", "measured-not-appraised")}, + source=SOURCE_DEV)) + + # Rejections: one code per rule, at least two vectors per code. Each rejection has an + # accepting twin, written after all of them, that differs from it only in the rule the + # rejection tests: the same record with the one member that breaks the rule put right. + m = synthetic("rejections") + twins: list[tuple[int, str, str, dict[str, Any], dict[str, Any]]] = [] + + def reject(n: int, name: str, description: str, code: str, *, layers: Any, + fixed: dict[str, Any], twin_reading: dict[str, Any], + platform: str = "tpm2", status: str = "warning", + result_measurement: str | None = None, schema_sees: bool = True) -> None: + out.append(vector(n, name, description, + rec=record(measurement=m, status=status, layers=layers, platform=platform, + result_measurement=result_measurement), + codes=[code], schema_sees=schema_sees)) + twin = {"measurement": m, "status": status, "layers": layers, + "platform": platform, "result_measurement": result_measurement} + twin.update(fixed) + twins.append((n, name, code, twin, twin_reading)) + + reject(11, "not-established-without-reason", + "pcr:2 is not-established and names no reason. The three reasons are different " + "findings; one without its reason is a generic absence.", + "not_established_without_reason", + layers={"pcr:2": {"outcome": "not-established"}}, + fixed={"layers": {"pcr:2": not_established("layer-not-measured")}}, + twin_reading={"pcr:2": read("not-established", "layer-not-measured")}) + reject(12, "not-established-without-reason-among-established", + "pcr:5 is not-established with no reason, among layers that are established.", + "not_established_without_reason", + layers={"pcr:0": established(), "pcr:5": {"outcome": "not-established"}, + "pcr:9": established()}, + fixed={"layers": {"pcr:0": established(), + "pcr:5": not_established("measured-not-appraised"), + "pcr:9": established()}}, + twin_reading={"pcr:5": read("not-established", "measured-not-appraised")}) + reject(13, "established-with-a-reason", + "pcr:2 is established and carries a reason. An established layer has none.", + "established_with_reason", + layers={"pcr:2": {"outcome": "established", "reason": "layer-not-measured"}}, + fixed={"layers": {"pcr:2": established()}}, + twin_reading={"pcr:2": read("established")}) + reject(14, "established-with-a-multiple-boots-reason", + "pcr:0 is established and carries evidence-spans-multiple-boots.", + "established_with_reason", + layers={"pcr:0": {"outcome": "established", + "reason": "evidence-spans-multiple-boots"}}, + fixed={"layers": {"pcr:0": established()}}, + twin_reading={"pcr:0": read("established")}) + reject(15, "layers-empty", + "layers is an empty object. A result naming no layer establishes nothing.", + "layers_empty", + layers={}, fixed={"layers": {"pcr:0": established()}}, + twin_reading={"pcr:0": read("established")}) + reject(16, "reason-outside-the-closed-set", + "pcr:2 names the reason not-listed, which is the reader's finding about an unlisted " + "layer and not a reason a record carries.", + "unknown_reason", + layers={"pcr:2": not_established("not-listed")}, + fixed={"layers": {"pcr:2": not_established("layer-not-measured")}}, + twin_reading={"pcr:2": read("not-established", "layer-not-measured")}) + reject(17, "reason-spelled-with-underscores", + "pcr:2 names layer_not_measured, the library's spelling from #457. The record's " + "reasons are hyphenated.", + "unknown_reason", + layers={"pcr:2": not_established("layer_not_measured")}, + fixed={"layers": {"pcr:2": not_established("layer-not-measured")}}, + twin_reading={"pcr:2": read("not-established", "layer-not-measured")}) + reject(18, "tpm-layer-with-a-leading-zero", + "On tpm2 the layer pcr:02 has a leading zero; two verifiers would name one register " + "two ways.", + "tpm_layer_name", + layers={"pcr:02": established()}, fixed={"layers": {"pcr:2": established()}}, + twin_reading={"pcr:2": read("established")}) + reject(19, "tpm-layer-in-another-spelling", + "On tpm2 the layer is named PCR2.", + "tpm_layer_name", + layers={"PCR2": established()}, fixed={"layers": {"pcr:2": established()}}, + twin_reading={"pcr:2": read("established")}) + reject(20, "layers-empty-off-tpm", + "layers is an empty object on a non-TPM platform. The rule does not depend on the " + "platform.", + "layers_empty", + layers={}, platform="intel-tdx", + fixed={"layers": {"rtmr:0": established()}}, + twin_reading={"rtmr:0": read("established")}) + for n, (name, description, other) in enumerate([ + ("result-about-another-measurement", + "platform_measurement.measurement differs from runtime.measurement: the result is " + "about other evidence, not this record.", synthetic("other")), + ("result-about-a-measurement-one-byte-off", + "platform_measurement.measurement differs from runtime.measurement in its last " + "byte.", m[:-2] + ("00" if m[-2:] != "00" else "01")), + ], start=21): + reject(n, name, description + " Held by the reference model only: the schema " + "cannot compare two members of the record.", + "measurement_mismatch", + layers={"pcr:0": established()}, result_measurement=other, + fixed={"result_measurement": None}, + twin_reading={"pcr:0": read("established")}, schema_sees=False) + + # No result at all, rule 1: a record without the block establishes no layer. + nr = synthetic("no-result") + out.append(vector(23, "no-result", + "Synthetic. The record carries no appraisal.platform_measurement at all. A reader " + "asking about pcr:2 reads not-established with the reader's reason not-listed: a " + "record without the block establishes no layer, whatever its status says.", + rec=record(measurement=nr, status="warning", layers=None), + reading={"pcr:2": read("not-established", "not-listed")})) + out.append(vector(24, "no-result-twin", + "Twin of 23: the same record with a result that lists pcr:2 as established. " + "The only difference is the result.", + rec=record(measurement=nr, status="warning", layers={"pcr:2": established()}), + reading={"pcr:2": read("established")}, twin_of=23)) + + # Rule 5: a written result is an appraisal, so status is not none. + reject(25, "status-none-with-a-result", + "appraisal.status is none while the record carries a per-layer result. A verifier " + "that wrote the result performed an appraisal.", + "status_none_with_result", + layers={"pcr:0": established()}, status="none", fixed={"status": "warning"}, + twin_reading={"pcr:0": read("established")}) + reject(26, "status-none-with-a-not-established-result", + "appraisal.status is none while the result reports pcr:2 not-established. A finding " + "per layer is a finding, not the absence of one.", + "status_none_with_result", + layers={"pcr:2": not_established("layer-not-measured")}, status="none", + fixed={"status": "warning"}, + twin_reading={"pcr:2": read("not-established", "layer-not-measured")}) + reject(27, "reason-null", + "pcr:0 is established and carries reason null. A null reason is neither absent nor " + "one of the three reasons.", + "unknown_reason", + layers={"pcr:0": {"outcome": "established", "reason": None}}, + fixed={"layers": {"pcr:0": established()}}, + twin_reading={"pcr:0": read("established")}) + reject(28, "tpm-layer-past-the-last-register", + "On tpm2 the layer pcr:24 names a register past pcr:23, the last a TPM 2.0 PC " + "Client platform defines.", + "tpm_layer_name", + layers={"pcr:24": established()}, fixed={"layers": {"pcr:23": established()}}, + twin_reading={"pcr:23": read("established")}) + + # The accepting twin of every rejection, in the order the rejections were written. + for i, (n, name, code, twin, twin_reading) in enumerate(twins, start=29): + out.append(vector(i, f"{name}-twin", + f"Twin of {n:02d}: the same record with the member that breaks {code} put " + "right. The only difference is that member.", + rec=record(**twin), reading=twin_reading, twin_of=n)) + OUT.mkdir(parents=True, exist_ok=True) + for name, doc in out: + (OUT / name).write_bytes((json.dumps(doc, indent=2) + "\n").encode()) + + +if __name__ == "__main__": + main() diff --git a/schema/trace-claim-v0.3-draft.json b/schema/trace-claim-v0.3-draft.json index 04ebc620..776e77b3 100644 --- a/schema/trace-claim-v0.3-draft.json +++ b/schema/trace-claim-v0.3-draft.json @@ -608,6 +608,81 @@ } ], "additionalProperties": false + }, + "platform_measurement": { + "type": "object", + "description": "Per-layer appraisal of the platform measurement, attributed to appraisal.verifier. In a record signed only by its producer this attribution is the producer's report. A member of appraisal in its own right, not an appraisal.method value, so it can sit next to a re-execution result. measurement must equal runtime.measurement, a cross-field rule the reference model enforces. Spec section 3.1.5.", + "required": [ + "measurement", + "layers" + ], + "properties": { + "measurement": { + "type": "string", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$", + "description": "The digest this appraisal is about. Must equal runtime.measurement: a result about another measurement is not about this record." + }, + "layers": { + "type": "object", + "minProperties": 1, + "description": "Keyed by the platform's name for the layer; on tpm2, pcr: followed by a register number from 0 to 23 in decimal without leading zeros. A result naming no layer establishes nothing.", + "additionalProperties": { + "type": "object", + "description": "One layer's outcome. Spec section 3.1.5. reason is required when outcome is not-established and absent when it is established.", + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "type": "string", + "enum": [ + "established", + "not-established" + ], + "description": "established: the evidence establishes this layer. not-established: it does not, for the reason given; never reported as established, and a layer the result does not list is not established either." + }, + "reason": { + "type": "string", + "enum": [ + "layer-not-measured", + "measured-not-appraised", + "evidence-spans-multiple-boots" + ], + "description": "layer-not-measured: the layer holds no measurement, its value is the initial value or, on a TPM, a separator and nothing else. measured-not-appraised: the measurements replay to the quoted value and nothing in the evidence establishes that what ran was appraised before it ran. evidence-spans-multiple-boots: the evidence does not establish that the quote and the event log describe the same single boot, and the log does not replay to the quoted value." + } + }, + "allOf": [ + { + "$comment": "A not-established layer carries its reason; an established one carries none.", + "if": { + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "const": "not-established" + } + } + }, + "then": { + "required": [ + "reason" + ] + }, + "else": { + "not": { + "required": [ + "reason" + ] + } + } + } + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false } }, "allOf": [ @@ -635,6 +710,23 @@ ] } } + }, + { + "$comment": "Section 3.1.5, rule 5: a verifier that writes platform_measurement has performed an appraisal, so status is not none. Mirrors the check in the reference model.", + "if": { + "required": [ + "platform_measurement" + ] + }, + "then": { + "properties": { + "status": { + "not": { + "const": "none" + } + } + } + } } ], "additionalProperties": false @@ -825,6 +917,49 @@ } } } + }, + { + "$comment": "Section 3.1.5: on a TPM a layer is named pcr: and a register number from 0 to 23, in decimal without leading zeros. Mirrors the layer-name check in the reference model.", + "if": { + "required": [ + "runtime", + "appraisal" + ], + "properties": { + "runtime": { + "required": [ + "platform" + ], + "properties": { + "platform": { + "const": "tpm2" + } + } + }, + "appraisal": { + "required": [ + "platform_measurement" + ] + } + } + }, + "then": { + "properties": { + "appraisal": { + "properties": { + "platform_measurement": { + "properties": { + "layers": { + "propertyNames": { + "pattern": "^pcr:([0-9]|1[0-9]|2[0-3])$" + } + } + } + } + } + } + } + } } ] } diff --git a/schema/trace-claim.json b/schema/trace-claim.json index 1466dde9..370f635e 100644 --- a/schema/trace-claim.json +++ b/schema/trace-claim.json @@ -537,6 +537,81 @@ } ], "additionalProperties": false + }, + "platform_measurement": { + "type": "object", + "description": "Per-layer appraisal of the platform measurement, attributed to appraisal.verifier. In a record signed only by its producer this attribution is the producer's report. A member of appraisal in its own right, not an appraisal.method value, so it can sit next to a re-execution result. measurement must equal runtime.measurement, a cross-field rule the reference model enforces. Spec section 3.1.5.", + "required": [ + "measurement", + "layers" + ], + "properties": { + "measurement": { + "type": "string", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$", + "description": "The digest this appraisal is about. Must equal runtime.measurement: a result about another measurement is not about this record." + }, + "layers": { + "type": "object", + "minProperties": 1, + "description": "Keyed by the platform's name for the layer; on tpm2, pcr: followed by a register number from 0 to 23 in decimal without leading zeros. A result naming no layer establishes nothing.", + "additionalProperties": { + "type": "object", + "description": "One layer's outcome. Spec section 3.1.5. reason is required when outcome is not-established and absent when it is established.", + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "type": "string", + "enum": [ + "established", + "not-established" + ], + "description": "established: the evidence establishes this layer. not-established: it does not, for the reason given; never reported as established, and a layer the result does not list is not established either." + }, + "reason": { + "type": "string", + "enum": [ + "layer-not-measured", + "measured-not-appraised", + "evidence-spans-multiple-boots" + ], + "description": "layer-not-measured: the layer holds no measurement, its value is the initial value or, on a TPM, a separator and nothing else. measured-not-appraised: the measurements replay to the quoted value and nothing in the evidence establishes that what ran was appraised before it ran. evidence-spans-multiple-boots: the evidence does not establish that the quote and the event log describe the same single boot, and the log does not replay to the quoted value." + } + }, + "allOf": [ + { + "$comment": "A not-established layer carries its reason; an established one carries none.", + "if": { + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "const": "not-established" + } + } + }, + "then": { + "required": [ + "reason" + ] + }, + "else": { + "not": { + "required": [ + "reason" + ] + } + } + } + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false } }, "allOf": [ @@ -564,6 +639,23 @@ ] } } + }, + { + "$comment": "Section 3.1.5, rule 5: a verifier that writes platform_measurement has performed an appraisal, so status is not none. Mirrors the check in the reference model.", + "if": { + "required": [ + "platform_measurement" + ] + }, + "then": { + "properties": { + "status": { + "not": { + "const": "none" + } + } + } + } } ], "additionalProperties": false @@ -754,6 +846,49 @@ } } } + }, + { + "$comment": "Section 3.1.5: on a TPM a layer is named pcr: and a register number from 0 to 23, in decimal without leading zeros. Mirrors the layer-name check in the reference model.", + "if": { + "required": [ + "runtime", + "appraisal" + ], + "properties": { + "runtime": { + "required": [ + "platform" + ], + "properties": { + "platform": { + "const": "tpm2" + } + } + }, + "appraisal": { + "required": [ + "platform_measurement" + ] + } + } + }, + "then": { + "properties": { + "appraisal": { + "properties": { + "platform_measurement": { + "properties": { + "layers": { + "propertyNames": { + "pattern": "^pcr:([0-9]|1[0-9]|2[0-3])$" + } + } + } + } + } + } + } + } } ] } diff --git a/spec/trace-v0.2.md b/spec/trace-v0.2.md index 25682c91..3262509d 100644 --- a/spec/trace-v0.2.md +++ b/spec/trace-v0.2.md @@ -366,6 +366,69 @@ Example result, in the appraisal of a verifier that re-ran the function and obta **Out of scope.** This section names no journal format, no transcript shape, and no way of storing or resolving a closure; those belong to the profile or annex that describes the function. +#### 3.1.5 `appraisal.platform_measurement`: what a matching measurement covers + + + +On a measured-boot platform `runtime.measurement` is a composite digest over many layers. A verifier that compares it against a reference learns that the composite is the one the reference names. It does not learn which layers recorded anything, which were appraised before they ran, or whether the evidence describes one boot. Three conditions are routinely collapsed into an outcome a completed check would have produced. The last column is that collapse, the error this section removes, not what this section prescribes: + +| Reason | Condition | Collapsed, without this section, into | +|---|---|---| +| `layer-not-measured` | The layer holds no measurement: its value is the initial value, or on a TPM a separator and nothing else. A match confirms only that nothing was recorded. | pass | +| `measured-not-appraised` | The layer's measurements replay exactly to the quoted value, so the evidence establishes what ran. Nothing in the evidence establishes that what ran was appraised before it ran. | pass | +| `evidence-spans-multiple-boots` | The evidence does not establish that the quote and the event log describe the same single boot, and the log does not replay to the quoted value. A platform whose measurement registers survive a warm reboot produces this, and so do a quote and a log taken from different boots, and a truncated log. | a failure, read as tampering | + +The first two collapse into a pass. The third collapses the other way, into a failure. Under this section all three are `not-established`, which is neither: it never passes, and it is not a finding of tampering. All three are decided against evidence the record does not carry (a quote, an event log, a reference), so they are appraisal outcomes and not properties of `runtime`. + +A layer is `established` when the evidence establishes what ran in it: the layer holds measurements, its event log replays from the initial value to the quoted value, and nothing else the verifier's policy requires of that layer is missing. Where the policy requires that what ran was appraised and the evidence does not show it, the layer is `measured-not-appraised`. A log that replays from the initial value to the quoted value accounts for every extension since the register was last reset, so for that layer the number of boots does not change what the evidence establishes; `evidence-spans-multiple-boots` arises only when the log does not replay. The reason is named for its most common cause, a platform whose registers survive a warm reboot, and covers the other causes its definition lists. + +**Placement.** The result is carried as `appraisal.platform_measurement`, a member of `appraisal` in its own right, as `appraisal.provenance_depth_verified` is. It is not an `appraisal.method` value, so a record can carry it next to a re-execution result (§3.1.4). It is attributed to `appraisal.verifier`; in a record signed only by its producer that attribution is the producer's report, as §3.1.4 states. + +| Field | Required | Meaning | +|---|---|---| +| `appraisal.platform_measurement` | no | The result block: `measurement`, the digest the appraisal is about; and `layers`, an object with at least one member, each member carrying `outcome`, one of `established` or `not-established`, and `reason`, required when `outcome` is `not-established` and absent otherwise, one of the three reasons above; a `null` reason is neither present nor absent and is refused. | + +`appraisal.platform_measurement.measurement` MUST equal `runtime.measurement`: a result about another measurement is not about this record, and a verifier MUST NOT treat it as one. Layers are keyed by the platform's name for them. On a TPM the key MUST be `pcr:` followed by a register number from 0 to 23, the registers a TPM 2.0 PC Client platform defines, in decimal without leading zeros (`pcr:0`, `pcr:10`, `pcr:23`); other platforms name their layers in their profile or annex. + +**Rules.** + +1. `not-established` MUST NOT be reported as `established`. A layer the result does not list is not established either, and a record that carries no `appraisal.platform_measurement` establishes no layer: a relying party MUST NOT treat an unlisted layer, or any layer of a record without the block, as `established`. +2. A verifier MUST NOT set `appraisal.status` to `affirming` while any layer its appraisal policy requires is `not-established` or is not listed, and that includes a record in which it writes no `appraisal.platform_measurement` at all. +3. A verifier that finds the event log does not replay to the quoted value, and cannot establish that the evidence describes a single boot, MUST report the affected layers `not-established` with `evidence-spans-multiple-boots`, and MUST NOT on that basis alone set `appraisal.status` to `contraindicated`. Where the verifier does establish that the evidence describes a single boot, the mismatch is evidence that resolves and contradicts the record, and §3.3.1's rule for that case applies. How a verifier establishes that evidence describes a single boot is platform-specific and out of scope here; a platform profile that wants a replay mismatch read as a contradiction names how its verifiers establish a single boot, and without such a means rule 3 applies. The reason is available only when the log does not replay to the quoted value, and a verifier that reaches for it more often than it should loses a diagnosis, not a rejection: the layer is `not-established` either way. +4. `measured-not-appraised` is reported when the evidence does not show that the layer was appraised. It is not a finding that appraisal was off: evidence that is silent about appraisal does not establish either. +5. A verifier that writes `appraisal.platform_measurement` has performed an appraisal and MUST NOT set `appraisal.status` to `none`. + +**What the record can and cannot show.** Rules 1, 4 and 5 are about the record and can be checked from it. Rules 2 and 3 bind what a verifier writes: the record carries the verifier's per-layer findings but not its policy, so a reader cannot tell from the record alone which layers the policy required, and cannot check either rule. A relying party that needs that assurance reads the layers it requires itself, under rule 1, and does not rely on `appraisal.status` for them. + +**Why rule 2 touches `appraisal.status` when §3.1.4 does not.** A re-execution outcome is about one claim the record makes, reproducibility, which a relying party reads in its own block. A required layer that is not established means the appraisal did not establish what its own policy asked of it, and that is what `affirming` reports; leaving `status` free here would let it say the opposite of the layers beneath it. + +**What rule 3 costs.** On a platform whose quote does not show that the measurement registers were reset since the event log began, a verifier cannot establish a single boot, and under rule 3 a replay mismatch on that platform is never `contraindicated` on its own. A TPM's reset counter is an example: on a platform whose TPM is not reset by a warm reboot the counter does not move across one, so it cannot separate the two cases rule 3 is about. This is intended. The alternative reports every unreset platform as tampered, which is the collapse this section exists to remove. + +**Why rule 3 does not open a downgrade.** An adversary who can make a tampered boot look like a two-boot mix moves the affected layers from a failure to `not-established`. Rules 1 and 2 make that worthless: `not-established` is never a pass, and a verifier whose policy requires the layer cannot report `affirming`, so a relying party that reads only `appraisal.status` and one that reads the layers both decline the record. The difference between the two readings matters to diagnosis, not to acceptance. What rule 3 prevents is the opposite error, a benign unreset platform reported as tampered. + +**Why this is not `appraisal.status: none`.** `none` says no appraisal was performed. Here one was, and its finding is per layer. A record that can only say `none` cannot tell a layer that was never measured from a log that failed to replay across two boots, and the reason that distinguishes them is the finding. + +Example result, for a reference composite in which one layer holds a separator only. `warning` here is the verifier's policy choice for a layer it does not require; rule 2 fixes only that `affirming` is unavailable when a required layer is not established. + +```json +"appraisal": { + "status": "warning", + "verifier": "https://verifier.example.org", + "platform_measurement": { + "measurement": "sha256:1f3e5d7c...", + "layers": { + "pcr:0": {"outcome": "established"}, + "pcr:2": {"outcome": "not-established", "reason": "layer-not-measured"}, + "pcr:4": {"outcome": "established"} + } + } +} +``` + +**Relation to the reference library.** `verify_record` already reports a caller-supplied appraiser's per-layer report as its `platform_measurement` field (#457), with the same three causes written `layer_not_measured`, `measured_not_appraised` and `evidence_spans_multiple_boots`. That field is library output and binds no record. This section names the outcomes a record carries. + +**Out of scope.** This section does not say how a verifier decides each outcome, and fixes no quote or event-log format; those belong to the platform's profile or annex. + ### 3.2 Wire format **Envelope: ** EAT (RFC 9711): JWT (JSON, human-readable contexts) or CWT/CBOR-COSE (constrained and high-throughput contexts). diff --git a/src/agentrust_trace/_patterns.py b/src/agentrust_trace/_patterns.py index 52f9a071..dd932a6c 100644 --- a/src/agentrust_trace/_patterns.py +++ b/src/agentrust_trace/_patterns.py @@ -2,7 +2,7 @@ import re -# Only the four pattern forms shipped in TRACE v0.2 are adapted here. This is +# Only the five pattern forms shipped in TRACE v0.2 are adapted here. This is # not a general ECMA-262 translator: a new schema pattern needs explicit review. # None of these forms has a literal/escaped dot or dollar, or a dot in a class. _ECMA_PATTERNS = ( @@ -12,6 +12,8 @@ r"(T(\d+H(\d+M)?(\d+S)?|\d+M(\d+S)?|\d+S))?" r"|T(\d+H(\d+M)?(\d+S)?|\d+M(\d+S)?|\d+S))$"), '^(?:[A-Za-z0-9_-]{1,85}|[A-Za-z0-9_-]{85}[AQgw]|[A-Za-z0-9_-]{87,})$', + # Section 3.1.5: a TPM layer key. No dot, no \\d; only the anchor is adapted. + '^pcr:([0-9]|1[0-9]|2[0-3])$', ) _PYTHON_PATTERNS = { pattern: re.compile( diff --git a/src/agentrust_trace/models.py b/src/agentrust_trace/models.py index 65daa42e..f9f85f84 100644 --- a/src/agentrust_trace/models.py +++ b/src/agentrust_trace/models.py @@ -3,7 +3,7 @@ from typing import Annotated, Any, Literal from functools import partial -from agentrust_trace._patterns import _require_pattern +from agentrust_trace._patterns import _PYTHON_PATTERNS, _require_pattern from pydantic import ( BaseModel, @@ -18,6 +18,10 @@ _DIGEST_RE = r"^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$" +# Section 3.1.5: a TPM layer key. Mirrored in schema/trace-claim.json and its copy, +# and run through the same adapted pattern the schema validator uses. +_TPM_LAYER_RE = r"^pcr:([0-9]|1[0-9]|2[0-3])$" + # A workload identity, not a namespace. A SPIFFE ID names a trust domain *and* a # workload path within it, and a DID names a method and an identifier within that # method, so neither is complete at its prefix: `spiffe://example.org` names a trust @@ -424,6 +428,50 @@ def _outcome_carries_what_makes_it_readable(self) -> ReExecution: return self +class PlatformMeasurementLayer(_TraceModel): + """One layer's outcome in a platform-measurement appraisal. Spec section 3.1.5. + + ``not-established`` carries ``reason``, because the three reasons are + different findings: a layer that holds nothing, a layer measured and never + shown to be appraised, and evidence that does not describe one boot. An + ``established`` layer carries no reason. + """ + + model_config = ConfigDict(extra="forbid") + + outcome: Literal["established", "not-established"] + reason: Literal[ + "layer-not-measured", "measured-not-appraised", "evidence-spans-multiple-boots" + ] | None = None + + @model_validator(mode="after") + def _not_established_names_its_reason(self) -> PlatformMeasurementLayer: + if "reason" in self.model_fields_set and self.reason is None: + # The schema refuses an explicit null; so does the model, so the two agree. + raise ValueError("reason is absent or one of the three reasons; null is neither") + if (self.outcome == "not-established") != (self.reason is not None): + raise ValueError( + "a layer carries reason exactly when its outcome is 'not-established'; " + f"got outcome={self.outcome!r} and reason={self.reason!r}" + ) + return self + + +class PlatformMeasurement(_TraceModel): + """Per-layer appraisal of ``runtime.measurement``. Spec section 3.1.5. + + A member of ``Appraisal`` in its own right, not an ``Appraisal.method`` + value, so it can sit next to a re-execution result. ``measurement`` must + equal ``runtime.measurement``; ``TrustRecord`` holds that, since it spans + two members of the record. A result naming no layer establishes nothing. + """ + + model_config = ConfigDict(extra="forbid") + + measurement: DigestStr + layers: Annotated[dict[str, PlatformMeasurementLayer], Field(min_length=1)] + + class Appraisal(_TraceModel): model_config = ConfigDict(extra="forbid") @@ -437,6 +485,19 @@ class Appraisal(_TraceModel): # untouched by it; the re-execution outcome is not folded into the EAR set. method: Literal["re-execution"] | None = None re_execution: ReExecution | None = None + # Not a method value: it can sit next to a re-execution result. Section 3.1.5. + platform_measurement: PlatformMeasurement | None = None + + @model_validator(mode="after") + def _a_written_result_is_an_appraisal(self) -> Appraisal: + """Section 3.1.5, rule 5: a verifier that writes a per-layer result has + performed an appraisal, so ``status`` is not ``none``.""" + if self.platform_measurement is not None and self.status == "none": + raise ValueError( + "appraisal.status is 'none' while appraisal.platform_measurement is " + "present: a written result is an appraisal" + ) + return self @model_validator(mode="after") def _re_execution_present_exactly_when_the_method_says_so(self) -> Appraisal: @@ -571,3 +632,29 @@ def _origin_cannot_claim_hardware(self) -> TrustRecord: "'software-only'" ) return self + + @model_validator(mode="after") + def _platform_measurement_is_about_this_record(self) -> TrustRecord: + """Section 3.1.5: the per-layer result is about ``runtime.measurement``. + + A result about another measurement is not about this record. On a TPM a + layer is named ``pcr:`` and the register number in decimal without + leading zeros, so two verifiers name the same register the same way. + """ + pm = self.appraisal.platform_measurement + if pm is None: + return self + if pm.measurement != self.runtime.measurement: + raise ValueError( + "appraisal.platform_measurement.measurement must equal " + "runtime.measurement: a result about another measurement is not " + "about this record" + ) + if self.runtime.platform == "tpm2": + bad = sorted(k for k in pm.layers if not _PYTHON_PATTERNS[_TPM_LAYER_RE].search(k)) + if bad: + raise ValueError( + "on tpm2 a layer is named pcr: and a register number from 0 to " + f"23 in decimal without leading zeros; got {bad}" + ) + return self diff --git a/src/agentrust_trace/schema/trace-v0.2.json b/src/agentrust_trace/schema/trace-v0.2.json index 1466dde9..370f635e 100644 --- a/src/agentrust_trace/schema/trace-v0.2.json +++ b/src/agentrust_trace/schema/trace-v0.2.json @@ -537,6 +537,81 @@ } ], "additionalProperties": false + }, + "platform_measurement": { + "type": "object", + "description": "Per-layer appraisal of the platform measurement, attributed to appraisal.verifier. In a record signed only by its producer this attribution is the producer's report. A member of appraisal in its own right, not an appraisal.method value, so it can sit next to a re-execution result. measurement must equal runtime.measurement, a cross-field rule the reference model enforces. Spec section 3.1.5.", + "required": [ + "measurement", + "layers" + ], + "properties": { + "measurement": { + "type": "string", + "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$", + "description": "The digest this appraisal is about. Must equal runtime.measurement: a result about another measurement is not about this record." + }, + "layers": { + "type": "object", + "minProperties": 1, + "description": "Keyed by the platform's name for the layer; on tpm2, pcr: followed by a register number from 0 to 23 in decimal without leading zeros. A result naming no layer establishes nothing.", + "additionalProperties": { + "type": "object", + "description": "One layer's outcome. Spec section 3.1.5. reason is required when outcome is not-established and absent when it is established.", + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "type": "string", + "enum": [ + "established", + "not-established" + ], + "description": "established: the evidence establishes this layer. not-established: it does not, for the reason given; never reported as established, and a layer the result does not list is not established either." + }, + "reason": { + "type": "string", + "enum": [ + "layer-not-measured", + "measured-not-appraised", + "evidence-spans-multiple-boots" + ], + "description": "layer-not-measured: the layer holds no measurement, its value is the initial value or, on a TPM, a separator and nothing else. measured-not-appraised: the measurements replay to the quoted value and nothing in the evidence establishes that what ran was appraised before it ran. evidence-spans-multiple-boots: the evidence does not establish that the quote and the event log describe the same single boot, and the log does not replay to the quoted value." + } + }, + "allOf": [ + { + "$comment": "A not-established layer carries its reason; an established one carries none.", + "if": { + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "const": "not-established" + } + } + }, + "then": { + "required": [ + "reason" + ] + }, + "else": { + "not": { + "required": [ + "reason" + ] + } + } + } + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false } }, "allOf": [ @@ -564,6 +639,23 @@ ] } } + }, + { + "$comment": "Section 3.1.5, rule 5: a verifier that writes platform_measurement has performed an appraisal, so status is not none. Mirrors the check in the reference model.", + "if": { + "required": [ + "platform_measurement" + ] + }, + "then": { + "properties": { + "status": { + "not": { + "const": "none" + } + } + } + } } ], "additionalProperties": false @@ -754,6 +846,49 @@ } } } + }, + { + "$comment": "Section 3.1.5: on a TPM a layer is named pcr: and a register number from 0 to 23, in decimal without leading zeros. Mirrors the layer-name check in the reference model.", + "if": { + "required": [ + "runtime", + "appraisal" + ], + "properties": { + "runtime": { + "required": [ + "platform" + ], + "properties": { + "platform": { + "const": "tpm2" + } + } + }, + "appraisal": { + "required": [ + "platform_measurement" + ] + } + } + }, + "then": { + "properties": { + "appraisal": { + "properties": { + "platform_measurement": { + "properties": { + "layers": { + "propertyNames": { + "pattern": "^pcr:([0-9]|1[0-9]|2[0-3])$" + } + } + } + } + } + } + } + } } ] } diff --git a/tests/test_adequacy_all_sets.py b/tests/test_adequacy_all_sets.py index bd06b4ef..3db406da 100644 --- a/tests/test_adequacy_all_sets.py +++ b/tests/test_adequacy_all_sets.py @@ -138,6 +138,15 @@ def reproducibility_claim() -> list[Vector]: lambda e: e["outcome"], lambda e: list(e.get("codes") or [])) +def platform_measurement_appraisal() -> list[Vector]: + """The platform-measurement appraisal set (spec section 3.1.5). One code per rule + the schema or the reference model holds, at least two vectors per code, and + twenty-eight accepting records: five case-and-twin pairs, a twin for every rejection, + and two from published measurements.""" + return _load("platform-measurement-appraisal", + lambda e: e["outcome"], lambda e: list(e.get("codes") or [])) + + def number_spelling() -> list[Vector]: """The number-spelling set (#247, proposed). Boundaries are its failure codes, one per rule an integer-typed member is held to once it is decided by value: two @@ -159,6 +168,7 @@ def signature_encoding() -> list[Vector]: SETS = { "build-provenance-depth": (build_provenance_depth, _depth_boundary), "reproducibility-claim": (reproducibility_claim, None), + "platform-measurement-appraisal": (platform_measurement_appraisal, None), "revocation-bundle": (revocation_bundle, None), "canonicalization-boundary": (canonicalization_boundary, None), "delegation-link": (delegation_link, None), diff --git a/tests/test_platform_measurement_appraisal_vectors.py b/tests/test_platform_measurement_appraisal_vectors.py new file mode 100644 index 00000000..e2fa5a58 --- /dev/null +++ b/tests/test_platform_measurement_appraisal_vectors.py @@ -0,0 +1,298 @@ +"""The platform-measurement appraisal vectors, held to what they say. Spec section 3.1.5. + +`examples/platform-measurement-appraisal/` pins two things. The shape rules: both +validators, the schema and the reference model, return the verdict each vector expects, +and a rejected vector fails where its code says it does and nowhere else. One rule, +`measurement_mismatch`, compares two members of the record, which a JSON Schema cannot +do; its vectors say `"schema_sees": false`, and the schema is held to accepting them so +that the claim stays true. And the reading: on every accepting vector, `read_layer` +below, the reading rule 1 of the section fixes, returns the outcome the vector names. + +The pairs are what make the reading testable. A reader that refuses everything, one +that takes `appraisal.status` at its word, and one that reads an unlisted layer as +established each fail at least one vector, and the tests at the bottom say which. +""" + +from __future__ import annotations + +import base64 +import json +import pathlib +from collections.abc import Callable +from typing import Any + +import pytest +from pydantic import ValidationError + +from agentrust_trace import TrustRecord, iter_errors +from agentrust_trace.sign import _canonical_bytes, _pubkey_from_jwk + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VECTOR_DIR = ROOT / "examples" / "platform-measurement-appraisal" +VECTORS = sorted(VECTOR_DIR.glob("*.json")) +IDS = [p.stem for p in VECTORS] + +#: Where each code's rejection is reported, and the prefix every error must sit under. +LOCUS: dict[str, tuple[str, str]] = { + "not_established_without_reason": ("appraisal.platform_measurement.layers", + "appraisal.platform_measurement.layers"), + "established_with_reason": ("appraisal.platform_measurement.layers", + "appraisal.platform_measurement.layers"), + "layers_empty": ("appraisal.platform_measurement.layers", + "appraisal.platform_measurement.layers"), + "unknown_reason": ("appraisal.platform_measurement.layers", + "appraisal.platform_measurement.layers"), + "tpm_layer_name": ("appraisal.platform_measurement.layers", + "appraisal.platform_measurement.layers"), + "measurement_mismatch": ("", ""), + "status_none_with_result": ("appraisal.status", "appraisal.status"), +} + +Reading = dict[str, str] +Reader = Callable[[dict[str, Any], str], Reading] + + +def _load(path: pathlib.Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def read_layer(record: dict[str, Any], layer: str) -> Reading: + """Rule 1 of section 3.1.5, as a relying party applies it to an accepted record. + + The layer's own outcome when the result lists it; `not-established` with the + reader's reason `not-listed` when it does not. `appraisal.status` is not consulted: + rule 2 constrains what a verifier writes there, and a reader that keys on the + layers needs nothing from it. + """ + result = record["appraisal"].get("platform_measurement") + if result is None or layer not in result["layers"]: + return {"outcome": "not-established", "reason": "not-listed"} + return dict(result["layers"][layer]) + + +def _accepting() -> list[pathlib.Path]: + return [p for p in VECTORS if _load(p)["expected"]["outcome"] == "accept"] + + +def _reading_failures(reader: Reader) -> list[str]: + failed = [] + for path in _accepting(): + doc = _load(path) + for layer, want in doc["expected"]["reading"].items(): + if reader(doc["record"], layer) != want: + failed.append(f"{path.stem}:{layer}") + return failed + + +def test_the_corpus_did_not_shrink() -> None: + """Pinned, so deleting an awkward vector is a visible act rather than a quiet one.""" + assert len(VECTORS) == 44 + + +def test_every_code_is_carried_by_two_vectors() -> None: + by_code: dict[str, list[str]] = {} + for path in VECTORS: + for code in _load(path)["expected"]["codes"]: + by_code.setdefault(code, []).append(path.stem) + assert set(by_code) == set(LOCUS), "a code without a locus, or a locus without a code" + thin = {code: names for code, names in by_code.items() if len(names) < 2} + assert not thin, f"codes carried by one vector: {thin}" + + +@pytest.mark.parametrize("path", VECTORS, ids=IDS) +def test_the_schema_returns_the_expected_verdict(path: pathlib.Path) -> None: + doc = _load(path) + errors = iter_errors(doc["record"]) + if doc["expected"]["outcome"] == "accept" or doc["expected"].get("schema_sees") is False: + assert not errors, [e.message for e in errors] + return + assert errors, "the schema accepts a record the vector says it refuses" + (code,) = doc["expected"]["codes"] + at, under = LOCUS[code] + paths = {".".join(str(k) for k in e.absolute_path) for e in errors} + assert any(p == at or p.startswith(at + ".") for p in paths), ( + f"{code} is reported at {at}; the schema reported at {sorted(paths)}") + outside = sorted(p for p in paths if not (p == under or p.startswith(under + "."))) + assert not outside, f"{path.stem} is also rejected at {outside}, outside {under}" + + +@pytest.mark.parametrize("path", VECTORS, ids=IDS) +def test_the_model_returns_the_expected_verdict(path: pathlib.Path) -> None: + doc = _load(path) + if doc["expected"]["outcome"] == "accept": + parsed = TrustRecord.model_validate(doc["record"]) + assert parsed.model_dump() == doc["record"] + return + with pytest.raises(ValidationError): + TrustRecord.model_validate(doc["record"]) + + +def test_the_mismatch_vectors_fail_on_the_mismatch_and_nothing_else() -> None: + """The model is the only validator that sees this rule, so it must see this rule: + the same record with the result's measurement set back to runtime.measurement is + accepted.""" + for path in VECTORS: + doc = _load(path) + if doc["expected"]["codes"] != ["measurement_mismatch"]: + continue + record = doc["record"] + with pytest.raises(ValidationError, match="must equal runtime.measurement"): + TrustRecord.model_validate(record) + fixed = json.loads(json.dumps(record)) + fixed["appraisal"]["platform_measurement"]["measurement"] = ( + record["runtime"]["measurement"]) + TrustRecord.model_validate(fixed) + + +@pytest.mark.parametrize("path", VECTORS, ids=IDS) +def test_the_signature_verifies(path: pathlib.Path) -> None: + record = _load(path)["record"] + encoded = record["signature"] + signature = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)) + body = _canonical_bytes({k: v for k, v in record.items() if k != "signature"}) + _pubkey_from_jwk(record["cnf"]["jwk"]).verify(signature, body) + + +@pytest.mark.parametrize("path", _accepting(), ids=[p.stem for p in _accepting()]) +def test_the_reading_is_what_the_vector_names(path: pathlib.Path) -> None: + doc = _load(path) + for layer, want in doc["expected"]["reading"].items(): + assert read_layer(doc["record"], layer) == want, layer + + +def test_every_twin_differs_from_its_case_in_one_layer_only() -> None: + """A twin that differed in more than its condition could not show which difference + the reading keyed on.""" + docs = {p.stem[:2]: _load(p) for p in VECTORS} + twins = [d for d in docs.values() + if "twin_of" in d and docs[d["twin_of"]]["expected"]["outcome"] == "accept"] + assert len(twins) == 5 + for twin in twins: + case = docs[twin["twin_of"]] + a = json.loads(json.dumps({k: v for k, v in case["record"].items() if k != "signature"})) + b = json.loads(json.dumps({k: v for k, v in twin["record"].items() if k != "signature"})) + pa = a["appraisal"].pop("platform_measurement", None) + pb = b["appraisal"].pop("platform_measurement", None) + assert a == b, twin["name"] + if pa is None: + # The case writes no result at all; the twin's result names pcr:2 only. + assert pb is not None and set(pb["layers"]) == {"pcr:2"}, twin["name"] + assert pb["measurement"] == b["runtime"]["measurement"], twin["name"] + else: + la, lb = pa.pop("layers"), pb.pop("layers") + assert pa == pb, twin["name"] + changed = {k for k in la.keys() | lb.keys() if la.get(k) != lb.get(k)} + assert changed == {"pcr:2"}, (twin["name"], changed) + assert case["expected"]["reading"]["pcr:2"]["outcome"] == "not-established" + assert twin["expected"]["reading"]["pcr:2"]["outcome"] == "established" + + +def test_the_real_measurements_are_the_published_ones() -> None: + """The two sourced vectors carry the measurements the #457 vectors carry, which + cite the same published evidence; a typo here would cite evidence that does not + say what the vector says.""" + def measurement(directory: str, stem: str) -> str: + doc = _load(ROOT / "examples" / directory / f"{stem}.json") + record = doc["record"] if "record" in doc else doc["records"][0] + return record["runtime"]["measurement"] + assert measurement("platform-measurement-appraisal", "09-rc13-layer-not-measured") == ( + measurement("platform-measurement", "15-rc13-layer-not-measured")) + assert measurement("platform-measurement-appraisal", "10-dev-measured-not-appraised") == ( + measurement("platform-measurement", "16-dev-measured-not-appraised")) + sourced = [p.stem for p in VECTORS if "source" in _load(p)] + assert sourced == ["09-rc13-layer-not-measured", "10-dev-measured-not-appraised"] + for path in VECTORS: + doc = _load(path) + assert ("source" in doc) != bool(doc.get("synthetic")), path.stem + + +def test_the_reference_reader_reads_every_vector() -> None: + assert not _reading_failures(read_layer) + + +def test_a_reader_that_refuses_everything_fails_the_twins() -> None: + failed = _reading_failures( + lambda record, layer: {"outcome": "not-established", "reason": "not-listed"}) + assert {f.split(":")[0] for f in failed} >= { + "02-layer-not-measured-twin", "04-measured-not-appraised-twin", + "06-evidence-spans-multiple-boots-twin", "08-layer-not-listed-twin", + "24-no-result-twin"} + + +def test_a_reader_that_trusts_the_status_fails_the_cases() -> None: + """Every case carries a status other than contraindicated; a reader that takes a + status short of contraindicated as a pass reads every layer established.""" + def by_status(record: dict[str, Any], layer: str) -> Reading: + if record["appraisal"]["status"] != "contraindicated": + return {"outcome": "established"} + return {"outcome": "not-established", "reason": "not-listed"} + failed = {f.split(":")[0] for f in _reading_failures(by_status)} + assert failed >= {"01-layer-not-measured", "03-measured-not-appraised", + "05-evidence-spans-multiple-boots", "07-layer-not-listed", + "23-no-result"} + + +def test_a_reader_that_takes_an_unlisted_layer_as_established_fails_07_and_23() -> None: + def lenient(record: dict[str, Any], layer: str) -> Reading: + result = record["appraisal"].get("platform_measurement") or {"layers": {}} + return dict(result["layers"].get(layer, {"outcome": "established"})) + failed = {f.split(":")[0] for f in _reading_failures(lenient)} + assert {"07-layer-not-listed", "23-no-result"} <= failed + + +def test_the_two_boot_case_is_not_contraindicated() -> None: + """Rule 3: a replay mismatch the verifier cannot attribute to one boot is not, on + that basis alone, contraindicated.""" + doc = _load(VECTOR_DIR / "05-evidence-spans-multiple-boots.json") + assert doc["record"]["appraisal"]["status"] != "contraindicated" + + +#: The members a rejection's twin may put right, by the code of the rule it breaks. +TWIN_FIX: dict[str, tuple[str, ...]] = { + "not_established_without_reason": ("appraisal.platform_measurement.layers",), + "established_with_reason": ("appraisal.platform_measurement.layers",), + "layers_empty": ("appraisal.platform_measurement.layers",), + "unknown_reason": ("appraisal.platform_measurement.layers",), + "tpm_layer_name": ("appraisal.platform_measurement.layers",), + "measurement_mismatch": ("appraisal.platform_measurement.measurement",), + "status_none_with_result": ("appraisal.status",), +} + + +def _leaves(node: Any, prefix: str = "") -> dict[str, Any]: + if isinstance(node, dict) and node: + out: dict[str, Any] = {} + for key, value in node.items(): + out.update(_leaves(value, f"{prefix}.{key}" if prefix else key)) + return out + return {prefix: node} + + +def test_every_rejection_has_one_twin_that_differs_only_in_its_rule() -> None: + """The twin is the rejected record with the one member that breaks the rule put + right. It is accepted, so a verifier that refuses everything fails it; and nothing + outside that member differs, so the verdicts of the two can only turn on the rule.""" + docs = {p.stem[:2]: _load(p) for p in VECTORS} + rejected = {k for k, d in docs.items() if d["expected"]["outcome"] == "reject"} + twins = {d["twin_of"]: d for d in docs.values() + if "twin_of" in d and d["twin_of"] in rejected} + assert set(twins) == rejected, sorted(rejected - set(twins)) + for key, twin in twins.items(): + case = docs[key] + (code,) = case["expected"]["codes"] + assert twin["expected"]["outcome"] == "accept", twin["name"] + a = _leaves({k: v for k, v in case["record"].items() if k != "signature"}) + b = _leaves({k: v for k, v in twin["record"].items() if k != "signature"}) + changed = {p for p in a.keys() | b.keys() if a.get(p, ...) != b.get(p, ...)} + assert changed, twin["name"] + outside = sorted(p for p in changed + if not any(p == f or p.startswith(f + ".") for f in TWIN_FIX[code])) + assert not outside, (twin["name"], outside) + + +def test_a_non_tpm_layer_name_is_accepted() -> None: + """The TPM naming rule binds tpm2 only; another platform names its layers its own way.""" + doc = _load(VECTOR_DIR / "38-layers-empty-off-tpm-twin.json") + assert doc["record"]["runtime"]["platform"] != "tpm2" + assert list(doc["record"]["appraisal"]["platform_measurement"]["layers"]) == ["rtmr:0"] + assert doc["expected"]["outcome"] == "accept" diff --git a/tests/test_safe_integer_range.py b/tests/test_safe_integer_range.py index 5f06abf0..ec5de004 100644 --- a/tests/test_safe_integer_range.py +++ b/tests/test_safe_integer_range.py @@ -300,6 +300,14 @@ def _object_nodes(node: Any, path: str = "") -> list[tuple[str, dict[str, Any]]] return found +def _is_object_map(node: dict[str, Any]) -> bool: + """A map whose members are all one object schema. That schema declares + ``"type": "object"``, so `_object_nodes` reaches it and the test below holds + it to the same rule: the map is bounded exactly as far as its member is.""" + member = node.get("additionalProperties") + return isinstance(member, dict) and member.get("type") == "object" + + @pytest.mark.parametrize("relative", BOUNDED_SCHEMAS) def test_no_object_leaves_an_additional_member_unconstrained(relative: str) -> None: """Bounding the declared fields is not bounding the record. @@ -311,9 +319,11 @@ def test_no_object_leaves_an_additional_member_unconstrained(relative: str) -> N identical except for `cnf.jwk` carrying 9007199254740992 and 9007199254740993 produced one canonical form, with every declared field inside its bound. - An object either closes to `additionalProperties: false` or holds its - undeclared members to `#/$defs/canonicalizableValue`. Absent is neither, and - absent means true. + An object either closes to `additionalProperties: false`, holds its + undeclared members to `#/$defs/canonicalizableValue`, or is a map whose every + member is itself an object schema, which this test then holds to the same + rule (`appraisal.platform_measurement.layers`, section 3.1.5). Absent is none + of these, and absent means true. """ schema = json.loads((REPO_ROOT / relative).read_text(encoding="utf-8")) objects = _object_nodes(schema) @@ -324,6 +334,7 @@ def test_no_object_leaves_an_additional_member_unconstrained(relative: str) -> N f"{path or '/'}: additionalProperties={node.get('additionalProperties', 'absent')}" for path, node in objects if node.get("additionalProperties", "absent") not in permitted + and not _is_object_map(node) ] assert not loose, ( f"{relative}: objects accepting an unconstrained member: {loose}. A member " diff --git a/tests/test_schema_doc_coverage.py b/tests/test_schema_doc_coverage.py index 26311398..adb48db2 100644 --- a/tests/test_schema_doc_coverage.py +++ b/tests/test_schema_doc_coverage.py @@ -76,6 +76,7 @@ "reproducibility": "## `reproducibility`", "reproducibility.input_closure[]": "### `reproducibility.input_closure` entries", "appraisal.re_execution": "### `appraisal.re_execution` members", + "appraisal.platform_measurement": "### `appraisal.platform_measurement` members", } _INLINE_CODE = re.compile(r"`([^`\n]+)`") diff --git a/tests/test_schema_regex_semantics.py b/tests/test_schema_regex_semantics.py index e5fb3b8c..6eb04e86 100644 --- a/tests/test_schema_regex_semantics.py +++ b/tests/test_schema_regex_semantics.py @@ -125,6 +125,8 @@ def test_shipped_pattern_results_match_javascript(): # The signature pattern constrains the final character at 86 characters only: # 85, a canonical 86, a non-canonical 86, and with the "x" suffix below, 86 and 87. "A" * 85, "A" * 85 + "Q", "A" * 85 + "B", + # Section 3.1.5 layer keys: canonical, leading zero, bare prefix, other spelling. + "pcr:0", "pcr:10", "pcr:23", "pcr:24", "pcr:01", "pcr:", "PCR0", ] cases = [[pattern, value + tail + suffix] for pattern in sorted(set(_patterns(SCHEMA))) diff --git a/tests/test_the_schema_and_the_models_agree.py b/tests/test_the_schema_and_the_models_agree.py index 3236945c..58daeedf 100644 --- a/tests/test_the_schema_and_the_models_agree.py +++ b/tests/test_the_schema_and_the_models_agree.py @@ -274,7 +274,15 @@ def test_no_integer_field_accepts_a_boolean(path: tuple[str, ...], value: bool) "method": "re-execution", "re_execution": {"outcome": "diverged", "observed_digest": "sha256:" + "4" * 64, - "verifier_code_identity": "sha256:" + "5" * 64}}, + "verifier_code_identity": "sha256:" + "5" * 64}, + # Section 3.1.5: a member of appraisal in its own right, so it + # sits next to the re-execution result. Its measurement is the + # record's runtime.measurement, which the model requires. + "platform_measurement": { + "measurement": BASE["runtime"]["measurement"], + "layers": {"pcr:0": {"outcome": "established"}, + "pcr:2": {"outcome": "not-established", + "reason": "layer-not-measured"}}}}, "signature": "abcDEF-_123", }