Skip to content

spec(appraisal): platform_measurement, what a matching measurement covers (#279) - #477

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
revenue7-eng:spec/platform-measurement-appraisal
Oct 8, 2026
Merged

imran-siddique merged 1 commit into
agentrust-io:mainfrom
revenue7-eng:spec/platform-measurement-appraisal

Conversation

@revenue7-eng

@revenue7-eng revenue7-eng commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

spec(appraisal): platform_measurement, what a matching measurement covers (#279)

Closes the appraisal-outcome part of #279, as agreed in the thread: the three cases are appraisal outcomes, not runtime properties, and today a record can only say appraisal.status: none about them.

What it adds

Spec section 3.1.5 (normative). On a measured-boot platform runtime.measurement is a composite over many layers, and a match against a reference does not say which layers recorded anything, which were appraised, or whether the evidence describes one boot. A verifier now writes, per layer, established or not-established with one of three reasons. The right column is the collapse this section removes, not what it prescribes; under the section all three are not-established, which is neither a pass nor a finding of tampering:

Reason Collapsed, without this section, into
layer-not-measured a pass
measured-not-appraised a pass
evidence-spans-multiple-boots a failure, read as tampering

A layer is established when its log replays from the initial value to the quoted value and nothing else the verifier's policy requires of it is missing; a log that replays accounts for every extension since the last reset, so the boot count matters only when it does not.

Five rules: not-established is never reported as established, and neither an unlisted layer nor any layer of a record without the block is established; a verifier does not report affirming while a layer its policy requires is not established, including when it writes no block at all; a replay mismatch the verifier cannot attribute to one boot is not, on that basis alone, contraindicated; measured-not-appraised is not a finding that appraisal was off; a record carrying a result does not report status: none. Rules 1, 4 and 5 can be checked from the record; rules 2 and 3 bind what a verifier writes, since the record does not carry its policy, and the section says so and tells a relying party that needs the assurance to read the layers itself. A profile that wants a replay mismatch read as a contradiction names how its verifiers establish a single boot; without one, rule 3 applies, and a verifier that overuses the reason loses a diagnosis, never a rejection. Rule 2 constrains status where §3.1.4 does not, and the section says why: a re-execution outcome is about one claim read in its own block, while a required layer not established means the appraisal did not establish what its own policy asked, which is what affirming reports. The section also says what rule 3 costs (on a platform whose quote does not show the registers were reset since the log began, a replay mismatch is never contraindicated on its own) and why it does not open a downgrade (rules 1 and 2 keep not-established from passing, for a reader of the layers and for a reader of status alone).

Placement. appraisal.platform_measurement is a member of appraisal in its own right, as provenance_depth_verified is, not an appraisal.method value, so it sits beside a re-execution result. Its measurement must equal runtime.measurement; that is a rule of the reference model and of conformance, not a JSON Schema constraint, since the schema cannot compare two members of a record (the vectors for it carry "schema_sees": false). On tpm2 a layer is named pcr:<n>, decimal, no leading zeros. How a verifier decides each outcome, and how it establishes a single boot, are left to the platform's profile.

Schema and model. schema/trace-claim.json and the packaged copy (identical bytes), models.py (PlatformMeasurementLayer, PlatformMeasurement, two TrustRecord checks), docs/schema.md, the regenerated v0.3 draft schema. The TPM layer pattern is added to the closed set in _patterns.py; it has no dot and no \d, so only the anchor is adapted, and the JavaScript comparison now samples pcr:0, pcr:10, pcr:01, pcr: and PCR0.

Why in the core rather than a profile. The three reasons are a closed set so that a relying party reads one vocabulary across platforms; how a verifier reaches each one, and how it establishes a single boot, are left to the platform's profile.

Vectors. examples/platform-measurement-appraisal/, 44 signed records:

  • five case-and-twin pairs at pcr:2, each twin differing from its case in that one condition only: a measured layer, an appraised layer, a single-boot log, a listed layer, and a result at all (the last two are rule 1);
  • two records carrying the measurements of published quotes on a board with a discrete TPM, the same two feat(verify): record a caller-supplied appraiser's per-layer report on runtime.measurement (#279, #431) #457 cites, without twins, since a twin would state something untrue about that evidence;
  • at least two rejections per rule, seven codes, and an accepting twin for every rejection (29 to 44) that differs from it only in the member that breaks the rule; 38, the twin of 20, is the record off TPM, with a layer named rtmr:0.

The two sourced vectors cite the published evidence at revenue7-eng/tactiq-os, the same evidence the merged #457 vectors cite; the files are a release asset and a committed record with an offline checker beside it.

The generator and tests/test_platform_measurement_appraisal_vectors.py follow the layout of examples/reproducibility-claim/gen_reproducibility_vectors.py and tests/test_reproducibility_claim_vectors.py (seed-derived key, one code per rule, two vectors per code, the LOCUS check), with twin_of, synthetic and source as in examples/platform-measurement/ from #457. What is new is the reading check below and schema_sees.

Accepting vectors carry expected.reading: what a relying party takes from the record for each layer asked about. tests/test_platform_measurement_appraisal_vectors.py checks that a reader refusing everything fails all five twins, a reader trusting appraisal.status fails all five cases, and a reader taking an unlisted layer as established fails 07 and 23. The set is registered in tests/test_adequacy_all_sets.py.

One existing test rule changed

tests/test_safe_integer_range.py::test_no_object_leaves_an_additional_member_unconstrained admitted additionalProperties: false or the canonicalizableValue reference. layers is a map whose member is one closed object schema. The rule now also admits a map whose every member is itself an object schema, which the same test then holds to the same rule: _object_nodes, unchanged, recurses into every value of a schema node, additionalProperties included, so the member schema is checked as an object in its own right. The alternative was an array of {layer, outcome, reason}, which the schema could not keep free of duplicate layers.

Tests that fail when each rule is switched off

Run locally on this branch, one rule at a time, restoring the files after each run; the switch-off script is not part of the PR. What was disabled is named so the runs can be repeated:

Rule switched off Failing tests
reason present exactly when not-established: the layer schema's allOf, and the check in PlatformMeasurementLayer schema and model verdicts on vectors 11, 12, 13, 14
at least one layer: minProperties on layers, and min_length in PlatformMeasurement schema and model verdicts on 15, 20
reason from the closed set: the reason enum, and its Literal in the model schema and model verdicts on 16, 17
TPM layer name: the last top-level allOf entry, and the tpm2 branch in TrustRecord schema and model verdicts on 18, 19
measurement equals runtime.measurement (model only): the comparison in TrustRecord model verdicts on 21, 22, and test_the_mismatch_vectors_fail_on_the_mismatch_and_nothing_else
status not none with a result: the last allOf entry under appraisal, and the check in Appraisal schema and model verdicts on 25, 26
reason never null: the null check in PlatformMeasurementLayer (the schema's enum refuses null already) model verdict on 27
TPM register from 0 to 23: the bound in the layer-name pattern schema and model verdicts on 28, and the pattern-registry checks

Relation to #457

verify_record's platform_measurement field reports the same three causes with underscores. That field is library output and binds no record; this PR names what a record carries, hyphenated as the spec's other outcomes are. Aligning the library's spelling is left out of this PR.

Compatibility

BREAKING for verifiers: one 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; 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 gains one 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.

Proposed and written by @revenue7-eng in #279; carried by @imran-siddique as sponsoring maintainer.

Read by someone who had not seen the work

Three new sessions with no access to this work read the patch, this description, CONTRIBUTING.md and, for one of them, #279. Two could not reach GitHub or PyPI and read statically; the third applied the patch to a fresh clone of main and ran the four checks, all passing (pytest, ruff, tools/check_dashes.py, mypy). Each read an earlier revision. What they found, and what changed:

  • The table read "fail, read as tampering" as the section's own semantics, against Absence of evidence needs a name on every verifier surface: six threads, one primitive #279's rule that not-established is neither pass nor fail. The column now says it is the collapse being removed, and the text says all three are not-established, which is neither.
  • A verifier could report affirming on a tpm2 record carrying no platform_measurement at all. Rules 1 and 2 now cover a record without the block; vectors 23 and 24 pin it.
  • established had no positive definition, and a log that replays while a single boot is not established seemed to fit no reason. The section now defines established, and says why a replaying log settles the boot count for its layer.
  • Rules 2 and 3 cannot be checked from the record, because the verifier's policy is not in it. The section now says which rules the record can show and which bind the verifier only.
  • status: none beside a result was unconstrained. Rule 5 forbids it; schema, model and vectors 25 and 26 hold it.
  • Why rule 2 touches status when §3.1.4 does not was unexplained. The section now says.
  • "Cannot establish a single boot" could become an escape hatch. A profile that wants a mismatch read as a contradiction now names how a single boot is established, and overusing the reason costs a diagnosis, never a rejection.
  • The reason's name is narrower than its definition. The section now says it is named for its most common cause.
  • The measurement equality read as a schema guarantee, and the switch-off runs could not be reproduced from the diff. The description now says the first is a model and conformance rule, and names what was disabled for each run.
  • A schema $comment named the wrong check. Fixed.
  • Whether the relaxed test_safe_integer_range rule still recurses into map members. It does; see above.

They also raised the missing organizational sponsor, which this description states, and judged the per-layer not-established with a reason to fit #279.

@revenue7-eng
revenue7-eng requested review from a team, lywinged and rajnisht7 as code owners October 7, 2026 11:35

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@revenue7-eng this is what #279 asked for, and I am carrying it as sponsoring maintainer under CONTRIBUTING. Put that in the CHANGELOG entry in place of "no organizational sponsor", as #364 did. The open points, in one pass:

Rule 2 stays, labelled breaking. Its last clause bars affirming when the verifier's policy requires a layer and it writes no block. That is a new duty on every verifier that reports affirming after a §3.3 step 4 reference match, so the CHANGELOG entry should say BREAKING and name exactly that. v0.2 is a Draft, so the 30-day period in GOVERNANCE does not apply; the statement of what breaks does.

Twins for the rejections. None of the 14 rejection vectors (11 to 22, 25, 26) has a committed passing twin. The nearest accepting vector differs in 3 to 5 fields, and only 21 and 22 get one inside the test. Each needs a twin that differs only in the rule it tests. Add one accepting vector off TPM as well, so a non-TPM layer name is shown to pass.

Rebase. CHANGELOG.md conflicts with #476; keeping both entries resolves it.

Two nits, your call: an explicit reason: null passes the model and fails the schema, and pcr: has no upper bound.

The generator reproduces all 26 vectors byte for byte, and the v0.2 schema change moves no verdict across the 643 existing documents. Merging once the three land.

…vers (agentrust-io#279)

Section 3.1.5: a verifier records, per layer of a measured-boot platform,
established or not-established with one of three reasons:
layer-not-measured, measured-not-appraised, evidence-spans-multiple-boots.
An unlisted layer 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; a record carrying a result does
not report status none.

BREAKING: a verifier that reports affirming after a section 3.3 step 4
reference match, while its policy requires a layer, must now establish that
layer in appraisal.platform_measurement. Records do not change: the schema
gains one optional member of appraisal.

Schema and packaged copy, reference model, docs/schema.md and the v0.3
draft carry it. Forty-four signed vectors in
examples/platform-measurement-appraisal/: five case-and-twin pairs, two
records with published measurements, at least two rejections per rule, and
an accepting twin for every rejection.

Proposed and written by @revenue7-eng in agentrust-io#279; carried by @imran-siddique
as sponsoring maintainer.

Signed-off-by: Andrey Lazarev <lazarev@tactiqedge.com>
@revenue7-eng
revenue7-eng force-pushed the spec/platform-measurement-appraisal branch from 5494686 to ca6a13c Compare October 8, 2026 02:30
@revenue7-eng

Copy link
Copy Markdown
Contributor Author

Thank you for carrying it. All three are in, rebased on main, as one commit:

  • CHANGELOG. The sponsor line now reads as spec(claims): reproducibility claim for deterministic re-execution (#249) #364's does. Rule 2 has its own entry marked BREAKING: a verifier that reports affirming after a section 3.3 step 4 reference match, while its policy requires a layer, must establish that layer in appraisal.platform_measurement; affirming with the block absent, or with a required layer not established or not listed, is no longer conformant. Records do not change.
  • Twins. 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. A new test checks that each twin differs from its rejection only under the member that code names. 38, the twin of 20, is the accepting vector off TPM, on intel-tdx with a layer named rtmr:0.
  • Rebase. Both CHANGELOG entries kept.

I took both nits. An explicit reason: null now fails the model as it fails the schema (27 and its twin 43), and pcr: is bounded to 0 to 23, the registers a TPM 2.0 PC Client platform defines (28 and its twin 44, plus pcr:23 and pcr:24 in the JavaScript pattern comparison). Each new rule fails its vectors when switched off; the table in the description has the two new rows.

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three are in and both nits taken. Carrying as sponsoring maintainer.

@imran-siddique
imran-siddique merged commit 9b0cf09 into agentrust-io:main Oct 8, 2026
14 of 15 checks passed
@revenue7-eng
revenue7-eng deleted the spec/platform-measurement-appraisal branch October 8, 2026 04:26
opento-suggestions added a commit to opento-suggestions/trace-spec that referenced this pull request Oct 12, 2026
…measurement

agentrust-io#477 added appraisal.platform_measurement.measurement after this branch
was cut. Its pattern is the same ^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$
as the other digest fields, and its row named no algorithm, so merging
main reddened the guard this branch adds: the field and both algorithms
were named, and the counterfactual test failed because its baseline was
no longer clean.

The row now takes the form the other corrected rows use. That makes
thirteen digest fields, of which seven rows were narrower than the
pattern, and the CHANGELOG entry carries those counts. The commit
message of 7ca1d25 says twelve and six, which was true of the tree it
was cut from.

Reverting this row alone reds the guard and names that field alone.
sha512: is refused by its pattern, as by the other twelve.

Editorial: no schema, wire-format or normative change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: opento-suggestions <opentosuggestionsofficial@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants