Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

## [Unreleased]

- **Section 3.1.5: a layer is established by its event log or by a reference value
(#279).** As merged, a layer was `established` only when its event log replayed to the
quoted value, so a quote that equals signed reference values with no log could not be
`affirming` under rule 2, although appraisal against reference values is ordinary
appraisal in RFC 9334. A layer that holds measurements is now established by either
route. The condition that it holds measurements binds both: a layer whose quoted value is
the initial value, or on a TPM the value of a separator alone, is `layer-not-measured`
even when a reference value matches it; a reference that gives only a composite
establishes no layer; and a presented log that does not replay is decided under rule 3
first, whatever the reference says. This narrows the BREAKING entry of #477: a verifier
that reports `affirming` after a reference match is conformant when each layer its policy
requires holds measurements and equals its reference value. Vector `45` in
`examples/platform-measurement-appraisal/` carries the published v2.1.0-rc14 release
measurement: six layers established by reference with no log, four separator-only layers
`layer-not-measured`, status `affirming`. Records do not change. Proposed and written by
@revenue7-eng; carried by @imran-siddique as sponsoring maintainer.

- **Revocation store and `allow_embedded_key` are type-checked.** `verify_record` accepted
a dict or a string as `revocation`: a dict was searched by its field names, so a revoked
key verified, and a string was searched by substring. Both now raise
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"name": "rc14-established-by-reference",
"description": "Real measurement: the attestation agent's quotes over PCR 0 to 9 from the v2.1.0-rc14 release image on slot B, appraised against the signed release reference. The evidence carries no event log. PCR 0, 1, 4, 6, 8 and 9 equal their reference values and hold measurements, so they are established by the reference-value route. PCR 2, 3, 5 and 7 equal the value a separator alone produces: the reference matches them and they are layer-not-measured. The verifier's policy requires PCR 0, 1, 4, 6, 8 and 9, all established, so the status is affirming; the record does not carry that policy.",
"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-rc14 (l3-evidence-rc14.tar.gz, rim-rock5a.json and rim-rock5a.json.p7s; checker scripts/appraise-layers.py in the same repository)",
"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:dc7051b201f970dbf438f9561975bb8801eb394170294417b05c4449e1283438"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "internal",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:00ea48600a3a4fcd35a35cea2296fcbe18cb2c92243ece1a5d1992d0dae41202"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/platform-measurement",
"platform_measurement": {
"measurement": "sha256:dc7051b201f970dbf438f9561975bb8801eb394170294417b05c4449e1283438",
"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": "eyhpnPFEEnAoO2PITJDzwxtIucsp1X4bOXsl-xiDbU-EPR6HBn5u_hAXy_pp6b5gubYdNDd4df35-Ffg508DAQ"
},
"expected": {
"outcome": "accept",
"codes": [],
"reading": {
"pcr:0": {
"outcome": "established"
},
"pcr:2": {
"outcome": "not-established",
"reason": "layer-not-measured"
},
"pcr:9": {
"outcome": "established"
}
}
}
}
30 changes: 21 additions & 9 deletions examples/platform-measurement-appraisal/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Platform-measurement appraisal vectors

Forty-four signed Trust Records for `appraisal.platform_measurement`, spec section 3.1.5,
Forty-five 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`.

Expand Down Expand Up @@ -49,15 +49,27 @@ 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
`09`, `10` and `45` 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
checker at the URL in `source`:

- `09`, the v2.1.0-rc13 release reference, in which PCR 2, 3, 5 and 7 hold a single
separator only.
- `10`, 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.
- `45`, the v2.1.0-rc14 release: the attestation agent's quotes over PCR 0 to 9, appraised
against the signed release reference with no event log. PCR 0, 1, 4, 6, 8 and 9 are
established by the reference-value route of section 3.1.5. PCR 2, 3, 5 and 7 equal the
value a separator alone produces; the reference matches them, and they are
`layer-not-measured`. Every layer the verifier's policy requires is established, so the
status is `affirming`. The record does not say which route established a layer; the
per-layer reference values are in the release's `rim-rock5a.json`, and
`tests/test_platform_measurement_appraisal_vectors.py` holds the vector to them.

None of the three 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
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
**Where real measurements come from.** Three 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:
Expand All @@ -36,6 +36,11 @@
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.
- `45` is the v2.1.0-rc14 release: the attestation agent's quotes over PCR 0 to 9 on slot
B, appraised against the signed release reference with no event log. PCR 0, 1, 4, 6, 8
and 9 are established by the reference-value route; PCR 2, 3, 5 and 7 equal the value a
separator alone produces and are `layer-not-measured` although the reference matches
them. With every layer the policy requires established, the status is `affirming`.

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
Expand Down Expand Up @@ -74,6 +79,7 @@
#: The two real measurements: `pcrDigest` of published quotes.
RC13 = "sha256:2ee6f2d22d3b8e704d1ee8119220aed62a5caff4624c5804b8185debdff1f745"
DEV_COLD = "sha256:266c3432e78cde16a9e9e63c479c4d07aad80ac049b25d655bfd897fb3b19746"
RC14_B = "sha256:dc7051b201f970dbf438f9561975bb8801eb394170294417b05c4449e1283438"
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)"
Expand All @@ -82,6 +88,11 @@
"https://github.com/revenue7-eng/tactiq-os/blob/main/measurements/"
"tpm-quotes-dev-20261002.md (cold.* files and check.py beside it)"
)
SOURCE_RC14 = (
"https://github.com/revenue7-eng/tactiq-os/releases/tag/v2.1.0-rc14 "
"(l3-evidence-rc14.tar.gz, rim-rock5a.json and rim-rock5a.json.p7s; checker "
"scripts/appraise-layers.py in the same repository)"
)

PCRS_0_9 = [f"pcr:{i}" for i in range(10)]

Expand Down Expand Up @@ -407,6 +418,26 @@ def reject(n: int, name: str, description: str, code: str, *, layers: Any,
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))

# Real measurement, unpaired: layers established by a reference value, no event log.
rc14 = {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(45, "rc14-established-by-reference",
"Real measurement: the attestation agent's quotes over PCR 0 to 9 from the "
"v2.1.0-rc14 release image on slot B, appraised against the signed release "
"reference. The evidence carries no event log. PCR 0, 1, 4, 6, 8 and 9 equal "
"their reference values and hold measurements, so they are established by the "
"reference-value route. PCR 2, 3, 5 and 7 equal the value a separator alone "
"produces: the reference matches them and they are layer-not-measured. The "
"verifier's policy requires PCR 0, 1, 4, 6, 8 and 9, all established, so the "
"status is affirming; the record does not carry that policy.",
rec=record(measurement=RC14_B, status="affirming", layers=rc14),
reading={"pcr:0": read("established"),
"pcr:2": read("not-established", "layer-not-measured"),
"pcr:9": read("established")},
source=SOURCE_RC14))

OUT.mkdir(parents=True, exist_ok=True)
for name, doc in out:
(OUT / name).write_bytes((json.dumps(doc, indent=2) + "\n").encode())
Expand Down
14 changes: 11 additions & 3 deletions spec/trace-v0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,18 +371,26 @@ Example result, in the appraisal of a verifier that re-ran the function and obta
#### 3.1.5 `appraisal.platform_measurement`: what a matching measurement covers

<!-- CHANGED: #279 - platform-measurement appraisal: per-layer outcome with a named reason -->
<!-- CHANGED: #279 - a layer is established by its event log or by a reference value, and holds measurements either way -->

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 |
| `measured-not-appraised` | The layer holds measurements and the evidence establishes what ran in it, by its event log or by a reference value. 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.
A layer is `established` when the evidence establishes what ran in it and nothing else the verifier's policy requires of that layer is missing. For a layer that holds measurements, the evidence establishes what ran by either of two routes:

- **Event log.** The layer's event log replays from the initial value to the quoted value.
- **Reference value.** The quoted value equals a reference value for that layer, from a source the verifier's policy trusts and not taken from the evidence under appraisal. This is appraisal against reference values as RFC 9334 describes it. A reference value is per layer: a quoted composite that equals the composite of a reference's per-layer values over the same selection establishes each of those values, and a reference that gives only a composite establishes no layer, because it does not show which layers hold measurements.

A presented event log that does not replay to the quoted value is decided under rule 3 first, and a matching reference value does not then establish the layer. Otherwise, either route requires that the layer holds measurements. A layer whose quoted value is the initial value, or on a TPM the value a separator alone produces, is `layer-not-measured` whichever route matched: a reference value equal to it, like a log that holds a separator and nothing else, confirms only that nothing was recorded. Neither route shows that what ran was appraised before it ran.

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.

Expand Down Expand Up @@ -429,7 +437,7 @@ Example result, for a reference composite in which one layer holds a separator o

**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.
**Out of scope.** This section does not say how a verifier decides each outcome, and fixes no quote, event-log or reference-value format; those belong to the platform's profile or annex.

### 3.2 Wire format

Expand Down
Loading
Loading