Adversarial Execution Evidence, v0.7
<!-- restatement-banner:begin Every figure inside this block is recomputed by scripts/lint_spec_restatement.py and is a build failure when it is wrong -- the block asserts facts about two files, and the first version of it stated the line count of the file it had not yet joined. Do not edit a number here by hand; re-derive with --repin and read what it reports. These sentinels are the gate's parse boundary and the reason the banner's own MUSTs are not counted as obligations. Removing or moving one is a build failure. --><!-- restatement-banner:end -->THIS DOCUMENT IS NOT THE SPECIFICATION
The authoritative text is
spec/predicates/adversarial-execution-evidence.mdin thein-toto/attestationfork, mirrored byte-identically in theaee-conformancerepository. That document is 2,322 lines, whose sha256 is759d2383e5da36fa509dc335e6159a20b87641b25ebbadcf1676c55d75ffd8b0; this one is 1,193. They are deliberately different documents, and this one is a LOCAL RESTATEMENT maintained alongside the authority — not a copy of it.Do not cite this file as normative. Do not resolve a disagreement in its favour. Where this text and the authoritative text differ, the authoritative text wins and this file is the thing to fix.
Why this banner exists. On 2026-07-30 an audit found this file carries 52 sentences imposing
MUSTand 37 imposingREQUIRED, sits at a path namedspec/v1/, nowhere stated that it was not the specification, and had no gate of any kind checking it against the authority — verified by searchingscripts/,.github/workflows/and.githooks/, which returned zero references to it. A reader had no way to tell it apart from the real thing, and "kept in lockstep by hand" is a promise with nothing enforcing it. The counts are over sentences, taken with this banner and the fenced examples excluded and wrapped paragraphs rejoined first; a sentence carrying both keywords is counted under both.What checks it now.
scripts/lint_spec_restatement.py, against the pin ledgeradversarial-execution-evidence.pins.json. The ledger records the authority by content digest, never by version string or line number, so any edit to the authority makes the pin stale and the gate red until a person re-reads both texts and re-pins. Every normative sentence below is enumerated there: 8 are anchored to a span of the authority that carries that sentence verbatim, and 80 are recorded as unanchored, meaning the authority states the obligation in different prose and no mechanical check establishes that the two say the same thing.The pin does not close the semantic gap, and that is the honest limit of it. For an unanchored sentence this gate proves two things and no more: that the authority has not moved since a person last read it against this text, and that every field name the sentence constrains exists in the authority at all. It does not prove the obligation is one the authority imposes. Read the authority before relying on an unanchored sentence, and read the ledger for which are which.
predicateType: https://in-toto.io/attestation/adversarial-execution-evidence/v0.7
v0.6 makes
basis: substraterows substrate-backed at TWO appraisal gates. GATE 1 (coverage validity, RATS RFC 9334 "Appraisal Policy for Evidence") is a key-independent consumption precondition computed from the carried bytes: references resolve and class-match, every covering observation payload is canonical+jsoncarrying the reserved members withaeeRunBindingequal to the derived run binding,methodis capped by the weakest signedaeeMethod, andbatchRootrecomputes; a violation makes the attestation INVALID. GATE 2 (the per-row evidence tierdeclared/unattested/attested, RATS "Appraisal Policy for Attestation Results") is the one trust-relative step: the covering signatures verify against a consumer-named substrate observation key. The producer's observation vocabulary now travels ON-WIRE (observationVocabulary: labels, caught subset, JCS digest), so theresultrecompute and GATE 1 are pure functions of carried bytes and archived attestations stay verifiable without the producer's documentation. The reduction forresultand the two gates are pinned by the normativerecompute-table.json, which every conforming verifier re-encodes. See the Changelog for the v0.3 -> v0.4 -> v0.5 -> v0.6 breaking-change list.
Adversarial Execution Evidence is a signed, offline-verifiable record of a bounded adversarial
stimulus run against a system under test (SUT) inside an attested containment substrate - most
commonly an MCP server scanned by one mlctl gate <server> run. It carries, in one standalone
in-toto predicate:
- a fused, recomputable
result(pass/pass_indirect/degraded/fail) - a deterministic, severity-independent reduction of the carried predicate that any verifier re-derives offline from the carried rows, the on-wire observation vocabulary, and the coverage maps alone; - the observation environment - digest-pinned references to the substrate, the attack corpus
(with its embedded, digest-committed class manifest), the catch policy, the enforced network
posture, the producer's on-wire
observationVocabulary, and (whenever a row isbasis: substrate) therunEntropyrun-start beacon the run binding folds in; - the coverage bound - which assessment classes were assessed and an explicit disclosure map for every class that was not;
- the per-attack containment rows -
attackResults[], each carrying the observedcontainmentObservedlabel plus the row's ownbasis(vantage),method(directness),actualLayer, andobservationRefsbinding it to the observation records that cover it; - the signed observation evidence -
observationRecords[], each an independently signed record of one interception, the armed vantage the run was observed under, or the seal that the vantage stayed armed to run-end, carried as a standard DSSE envelope{ payload, payloadType, signatures[] }whosepayloadis the base64 (RFC 4648 §4 standard alphabet) of the EXACT canonical+jsonbytes the substrate signed. A verifier verifies the per-record signature overPAE(payloadType, payload)FIRST, then reads the reserved payload members after.
This document specifies the predicate body. The signing envelope, subject binding, and offline-verify mechanics are in signing-envelope.md.
Purpose
This predicate is the active-stimulus sibling to passive runtime monitoring attestations (such as
the in-toto runtime-trace family): where a passive trace records an unbounded, monitor-specific log
of whatever happened, this predicate records a bounded adversarial campaign - a pinned corpus of
attacks dispatched against the SUT - with per-attack rows, a cryptographically committed coverage
bound, and each observation independently signed.
The design goal is that a consumer can recompute the outcome from the attestation alone, with no call back to the producer's infrastructure and no dependency on a document that does not travel with the statement. The outcome is a deterministic function of the carried evidence; the coverage denominator is committed by digest, so the producer cannot assert it unilaterally; the observation vocabulary travels on-wire, so the reduction is not something the producer's docs must supply; and each observation record verifies on its own before it is read. A producer cannot claim more than the evidence supports, and a producer claiming less is detectable, since dropping an inconvenient interception changes the committed batch root.
Unlike Simple Verification Results (SVRs), which communicate opaque, delegated policy decisions, this
predicate's result is a strictly deterministic, severity-independent reduction. No policy threshold,
severity ruleset, or producer-private logic participates in the reduction; a consumer that wants to
apply its own policy emits its own downstream verification summary over this evidence - this predicate
does not carry one. Verdicts (pass/fail against an organization's policy) are deliberately out of
scope; they belong in a downstream summary predicate computed over this evidence.
The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in RFC 2119.
The on-wire JSON shape is pinned by a draft 2020-12 JSON Schema, the SSOT at
spec/schemas/v1/adversarial-execution-evidence.schema.json, a conformant structural profile of
the open standard: it gates the field set, primitive types, and the known member set of the
structural objects (rows, records, environment carry additionalProperties: false), while the
value vocabularies whose violation is valid-but-fail-closed at the recompute gate
(containmentObserved, basis, method) are left OPEN and validated by the verifier, not the
schema - so an out-of-vocabulary or (for basis/method) absent value is structurally valid and
fail-closes at recompute. actualLayer and networkPosture.posture remain closed producer
registries, and the predicate object itself is open so a reserved-prefix aee* or evidenceTier
member is ignored rather than rejected. The soundness clauses below - the
JCS corpus-manifest and observation-vocabulary digest equalities, the attack-level coverage
exhaustion, the result recompute, the two consumption gates (coverage validity + evidence tier), the
per-record DSSE proof-of-observation, the run binding, the RFC-6962 batch root, and the
actualLayer/none coherence - are enforced by the producer's validate() gates AND the offline
verifier rules, NOT by the schema alone; a schema-valid object is not necessarily a sound artifact.
I-JSON safe-integer profile (normative). Every signed surface - the statement payload and every
observation-record payload - is RFC 8785 (JCS) restricted to the RFC 7493 (I-JSON) profile: strings,
integers of magnitude below 2^53, booleans, and null. A duplicate member, a non-integer numeric (a
fractional 1.5, an e-notation 1e+21, a non-finite Infinity/NaN), or an integer at or above
2^53 MUST be rejected by every rail, so producer and verifier in any language derive identical bytes.
Statement-wide strict I-JSON (normative). The whole statement is parsed as strict I-JSON: a duplicate member anywhere in the statement, at any depth and not only inside a covering record payload, makes the statement malformed. A lenient parser that silently keeps the last of a repeated member would let two rails disagree on identical bytes, so a verifier MUST reject a duplicate member statement-wide, fail-closed.
String well-formedness (normative). Strict I-JSON also constrains the bytes of every string. A
verifier MUST reject, statement-wide and fail-closed, any statement in which a string literal is not a
well-formed sequence of Unicode scalar values: the statement MUST be valid UTF-8 with no overlong form
and no surrogate encoded directly in UTF-8 (CESU-8); a \u escape naming a high surrogate MUST be
immediately followed by a \u escape naming a low surrogate, and an unpaired surrogate escape of either
half is malformed; a string MUST NOT contain a raw unescaped character below U+0020; and a \u escape
MUST consist of exactly four hexadecimal digits, with no sign, whitespace, or radix prefix, so that a
reader built on a permissive integer parser does not accept \u+041 where a strict one rejects it. A
lenient decoder does not fail on ill-formed bytes, it substitutes U+FFFD, and every check downstream of
the decode then reads a string the producer never wrote; where a digest is recomputed from decoded
strings rather than compared against carried bytes - which is how the observationVocabulary digest is
defined - a producer could otherwise emit ill-formed bytes, derive the digest over the substituted form,
and obtain a statement one conforming verifier calls valid and another calls malformed. A verifier MUST
therefore apply this check to the raw bytes, before any decoded string is read. The profile also
excludes the Unicode noncharacters - the code points U+FDD0 through U+FDEF, and U+nFFFE and U+nFFFF in
every plane - which RFC 7493 section 2.1 forbids in the same sentence as surrogates. A noncharacter is a
valid scalar value that nothing substitutes for, so unlike an ill-formed sequence it is not a cross-rail
decoding split; it is excluded so that a verifier implementing the RFC 7493 label does not reject a
record another verifier accepts, and it is rejected wherever a string literal appears, at any depth and
in both member-name and value position. This rule governs whether the bytes denote admissible scalar
values; the BMP-only rule below governs which of the remaining scalar values may appear.
Nesting-depth bound (normative). A verifier MUST reject, fail-closed, a statement whose JSON nesting
depth exceeds 128. Nesting depth is the number of arrays and objects open at a given point, counting the
outermost { of the statement as depth 1; scalar values do not increase it. The bound is normative
because it is not a resource limit alone: with no bound stated, implementations pick their own, and two
conforming verifiers then disagree about whether identical bytes are evidence at all over the entire
range between their choices. The counting rule is stated because implementations that increment per
parsed value rather than per open container arrive one level apart from an identical constant. Record
payloads are parsed under the same bound.
BMP-only string profile (normative). On every signed surface, object member names and the
observationVocabulary.labels/caught strings MUST be BMP-only - no supplementary-plane code point,
no surrogate pair. RFC 8785 sorts by UTF-16 code unit, but a verifier that sorts by Unicode code point
orders a name outside the BMP differently (a surrogate-led name sorts first under UTF-16 and last under
code point), so an adversarial producer could mint a payload canonical under one reading and not the
other, splitting covers versus covers-nothing - attestation-valid versus attestation-invalid on
identical bytes. Restricting the sorted strings to the BMP makes UTF-16 and code-point order coincide,
so the divergence is unconstructible; this is the string half of the safe-integer profile above.
Probity's ASCII vocabulary trivially satisfies it. BMP-only is a verifier rejection obligation, not
producer hygiene: a verifier MUST treat a supplementary-plane member name as making the covering
payload cover nothing, and a supplementary-plane vocabulary entry as making the statement malformed,
the same handling as non-canonical bytes. The UTF-16 sort rule (above) remains the defense in depth:
a verifier orders labels/caught by UTF-16 code unit, so even against a rail that skipped this
rejection rule, a non-conforming producer's bundle could not split verifier consensus.
Determinacy is established by vectors (normative honesty). These bounds close the divergences the text can foresee: a stated depth, a fixed sort order, a pinned encoding. They do not close the ones it cannot. Where the text underdetermines a reading and no conformance vector exercises it, two implementations agreeing on that reading is evidence the text is determinate, not proof of it - the reading is untested rather than confirmed, and a third implementation could differ there in silence. Conformance is established by vectors; an agreement no vector has exercised is a candidate for the next vector, not a settled rule.
All predicate field names are lowerCamelCase.
Subject
The subject is the scanned SUT image, by digest - the thing the evidence is about. It is NOT the
digest of the attack corpus; that is carried in observationEnvironment.corpus.digest. Binding the
subject to the scanned image lets an admission controller link the evidence to the exact image it is
admitting. For this predicate subject MUST contain exactly one entry on a statement of any basis; a
statement carrying zero or more than one subject is malformed, regardless of whether any row is
basis: substrate. Its digest MUST carry a sha256 value that is already lowercase 64-hex (the run
binding takes it verbatim).
"subject": [
{ "name": "<scanned-sut-name>", "digest": { "sha256": "<64-hex-digest>" } }
]
Run binding
For any statement carrying at least one basis: substrate row, the run binding digest is the
lowercase 64-hex SHA-256 of the RFC 8785 canonicalization of the object
{
"aeeBindingVersion": "2",
"catchPolicy": "<catchPolicy.digest.sha256>",
"corpus": "<corpus.digest.sha256>",
"networkPosture": "<sha256 of the RFC 8785 canonicalization of the carried networkPosture object>",
"observationVocabulary": "<observationVocabulary.digest.sha256>",
"runEntropy": "<runEntropy.digest.sha256>",
"subject": "<subject[0].digest.sha256>",
"substrate": "<substrate.digest.sha256>"
}
Every input is run configuration, fixed before corpus injection. That is the admission test for any future input, not a coincidence: the arming record carries this digest inside its own signature and is signed before injection, so a value the producer could not know then would make the arming record unsignable, and no outcome of the run can ever appear here.
runEntropy is a run-start value the substrate emits and commits inside the arming record's
signature, so two executions sharing every other input still derive distinct bindings. Values are
taken as written (no case-folding, no null fill), and catchPolicy, corpus, runEntropy,
substrate and subject[0] MUST each carry a lowercase-64-hex sha256. So MUST
networkPosture.digest.sha256, even though version 2 no longer reads it verbatim, because it is
still compared byte for byte against a record's aeePostureDigest. The observationVocabulary
digest is the one binding input with no canonicality rule of its own: the vocabulary
digest-integrity check recomputes it from the carried arrays, so a non-canonical value cannot equal
that recompute, and restating the requirement would add a condition that could never be the one to
fail. A statement whose rows are all basis: artifact derives no binding and need not carry
runEntropy. A verifier derives the digest from the statement alone; no field carries it. Every
substrate-signed observation record commits to the run by carrying this digest as its aeeRunBinding
member.
The binding is anti-splice, not a freshness challenge: a record signed under a different subject,
corpus, catch policy, network posture, observation vocabulary, substrate, or run-start entropy value
cannot be spliced in. It
carries no verifier nonce, and identical-configuration re-runs are distinguished only by the
substrate-emitted runEntropy value, so a consumer that must exclude replay of a genuine record into
a later identical-configuration run does so by rejecting reuse of a runEntropy value it has already
seen. aeeBindingVersion names this construction; exactly ONE is implemented at a time. A future
version that changes the construction (another hash algorithm, additional inputs, multiple subjects)
names a new binding version, and a verifier MUST reject, fail-closed, a binding version it does not
implement rather than attempt more than one construction. An arming record's payload MAY carry an
explicit aeeBindingVersion member declaring its construction; a verifier reads it before deriving
the run binding and rejects it fail-closed (the arming record covers nothing) when the value is a
version it does not implement, distinguishably from a run-binding digest mismatch. An absent member
defaults to the implemented version rather than to a fixed number, which is what keeps the
declaration optional across a version change; the carried value never drives the derivation (a
verifier always derives under the version it implements, so a record declaring the implemented
version but constructed otherwise still fails on the digest), which keeps the digest authoritative.
What version 2 changed, and why neither change costs a byte
Both inputs are material the statement already carries, so the wire is unchanged and no new
comparison exists: each closes through the equality every record's aeeRunBinding is already put to.
Version 1's networkPosture input was the value of that member's own digest.sha256, which left the
posture string beside it outside every signature. The posture configuration this predicate digests
travels nowhere in the statement, so no verifier can check the string against the digest, and a party
holding only the envelope key could swap one posture value for another, change no digest and break no
signature. Version 2 hashes the carried networkPosture object, so the string, its pinned digest
and any further member a producer carries there are all inside the binding. The object the binding
covers is the carried one: adding, removing or editing a networkPosture member after the arming
record is signed derives a binding the producer's own records do not carry.
observationVocabulary was not an input at all. Its caught array decides which labels are caught,
and both the recompute and the coverage validity requirements read it, so a producer that narrows the
caught set after the run turns a caught row into a clean one. Nothing resisted that: the vocabulary's
own digest is verified only against the arrays beside it, so it re-derives for free, and no record's
binding moved. Binding the carried digest closes it, since a narrowed vocabulary derives a different
run binding and every record then fails the comparison.
Version 1 is retired with no alias and no dual-accept window. A statement built under it derives a digest no record carries; a record declaring version 1 explicitly covers nothing.
Predicate
{
"result": "fail",
"observationEnvironment": {
"substrate": {
"name": "<attestation-A-subject-name>",
"digest": { "sha256": "<64-hex-substrate-digest>" }
},
"corpus": {
"name": "mcp-cve-corpus",
"uri": "pkg:probity/mcp-cve-corpus@v1.2",
"digest": { "sha256": "f8c41f617cfeddb97292339c877f66b9afa8c201592d6625bb6e0fc7d172a927" },
"manifest": { "classes": { "CO": ["CO-EXFIL-1"] } }
},
"catchPolicy": { "digest": { "sha256": "<64-hex-JCS-catch-policy-digest>" } },
"networkPosture": {
"posture": "sinkhole",
"digest": { "sha256": "<64-hex-JCS-posture-view-digest>" }
},
"observationVocabulary": {
"digest": { "sha256": "<64-hex-JCS-vocabulary-digest>" },
"labels": ["batch_bound", "canary_bound", "egress_captured", "not_exhibited", "payload_bound", "window_bound"],
"caught": ["batch_bound", "canary_bound", "egress_captured", "payload_bound", "window_bound"]
},
"runEntropy": { "digest": { "sha256": "<64-hex-run-start-value>" } }
},
"coverage": {
"assessedClasses": ["CO"],
"outOfScope": {},
"routedElsewhere": {}
},
"attackResults": [
{
"attackId": "CO-EXFIL-1",
"containmentObserved": "egress_captured",
"basis": "substrate",
"method": "intercepted",
"actualLayer": "policy.egress_sinkhole",
"observationRefs": [0]
}
],
"observationRecords": [
{
"payload": "<base64std(canonical +json bytes the substrate signed)>",
"payloadType": "application/vnd.probity.aee-observation.v1+json",
"signatures": [
{ "keyid": "<hex-key-id>", "sig": "<base64-ed25519-per-record-signature>" }
]
}
],
"batchRoot": "<64-hex-observation-record-batch-merkle-root>",
"doesNotAssert": [
"payload-decryptability without the customer recipient key"
],
"executionEnvelope": { "otelTraceId": "<32-hex-w3c-trace-id>" },
"issuedAt": "2026-06-23T16:08:07Z"
}
The payload above base64-decodes to the exact canonical +json bytes the substrate signed - the
reserved members (aeeRunBinding, aeeKind, aeeMethod) and the readable observation fields live
INSIDE it and are read only after the signature verifies (verify-then-read), never as sibling wire
fields. The corpus.digest.sha256 above is real: it is the SHA-256 of the RFC 8785 (JCS)
canonicalization of the example's embedded manifest object ({"classes":{"CO":["CO-EXFIL-1"]}}),
which is exactly how a verifier re-derives it. The observationVocabulary.digest.sha256 is likewise
the SHA-256 of JCS {"caught":[...],"labels":[...]}. The other digests, payload, keyid, sig,
and batchRoot are <placeholders>.
Field semantics
result (REQUIRED)
One of fail, degraded, pass_indirect, pass (lowercase), ordered fail < degraded <
pass_indirect < pass. The fused outcome, defined as a total, deterministic,
severity-independent function of the carried predicate (rows + carried vocabulary + coverage maps
ONLY - never observationRecords, signature outcomes, or consumer policy), pinned by
recompute-table.json. It is the minimum, under that order, of three
independent conditions - worst-wins, never evaluation order:
- FORCES_FAIL (contributes
fail) - someattackResultsrow carries acontainmentObservedlabel in the carriedobservationVocabulary.caughtset, a label outside the carriedobservationVocabulary.labels(fail-closed), OR a missing or out-of-vocabularybasisormethod(fail-closed, same rule). - COVERAGE_INCOMPLETE (contributes
degraded) -outOfScopeorroutedElsewhereis non-empty. - INDIRECT (contributes
pass_indirect) - some clean row (itscontainmentObservedis in the carried labels and not in the carried caught set, and it is fail-closed on neitherbasisnormethod) declares abasisother thansubstrateor amethodother thanintercepted.
A condition that does not hold contributes pass. pass is coverage-bounded-observed - a statement
about what was assessed, not a guarantee of safety - and pass_indirect makes the same coverage claim
resting on at least one observation that was indirect in vantage (basis: artifact, the executed
artifact's own account of itself) or indirect in time (method: reconstructed, derived after the
event rather than at it).
Why the fourth value exists. Without it the top result is reachable by a statement carrying no
substrate evidence at all. A party holding the enclosing envelope key but not the substrate's
observation key relabels every row clean, moves every row to basis: artifact, and drops
observationRecords, batchRoot and runEntropy - none of which a non-substrate row requires. What
it presents is well formed and used to read pass. Measured over every finding-bearing vector in the
conformance corpus, that statement is byte-identical to one an honest producer with no substrate
vantage emits, so no rule over the carried bytes refuses the first without refusing the second, and
refusing both would remove the producer whose attack classes (prompt injection, tool selection) have
no substrate vantage to observe from. The condition therefore prices both below a live
interception rather than claiming to tell them apart.
Why it is not phrased over the evidence tier. The tier is key-relative, and result MUST be
recomputable from carried bytes alone, so the condition reads the declared basis and method
and nothing the tier reads. The cost is stated rather than hidden: an unattested substrate clean row
still reaches pass, and that is the one rank of the clean-row ordering no byte-pure function can
express. A consumer crediting any basis: substrate row MUST still derive the tier.
The default admission threshold is result == "pass". A consumer MAY accept pass_indirect, and one
that relaxes below pass MUST additionally key on each clean row's basis and method and on that
row's derived evidence tier, because below pass the ordinal stops distinguishing them.
There is deliberately no severity threshold, no policy ruleset, and no free-text reason field: a
verifier MUST be able to recompute result from the bundle alone, and a producer MUST NOT record a
result the recompute does not reproduce. A missing expected observation MUST surface as a coverage
gap (the affected class moves to outOfScope with a reason) - never as an out-of-band degradation -
so the recompute stays total.
observationEnvironment (REQUIRED)
The digest-pinned containment context the evidence was earned under. Five members are REQUIRED
(substrate, corpus, catchPolicy, networkPosture, observationVocabulary); a sixth,
runEntropy, is REQUIRED exactly when any row is basis: substrate. All digests are 64-character
lowercase-hex SHA-256 carried as { "sha256": "<64-hex>" }.
-
substrate- an in-toto ResourceDescriptor carrying the Attestation A subject reference (name+digest) of the attested isolation substrate the SUT ran inside. This lets the evidence inherit the substrate's trust: the substrate SHOULD in turn carry its own attestation (e.g. build provenance for the substrate image). -
corpus- a locally typed reference to the attack corpus, not a ResourceDescriptor (see Descriptor typing below):name(REQUIRED, string) - the corpus name.uri(REQUIRED, string) - a locator, RECOMMENDED as a purl (e.g.pkg:probity/mcp-cve-corpus@v1.2).digest.sha256(REQUIRED) - the SHA-256 of the RFC 8785 (JCS) canonical JSON of the embeddedmanifestobject. NOT a hash of the corpus file bytes and NOT producer-asserted: the pre-image is carried in the bundle, so the digest is re-derivable offline with zero fetch.manifest.classes(REQUIRED, object) - a map from each assessment-class code to the complete array ofattackIds that class defines. AnattackIdMUST NOT appear under more than one class. This is the coverage denominator at attack granularity: dropping a class OR a single attackId changes the manifest hash, which changescorpus.digest.
-
catchPolicy- an in-toto ResourceDescriptor identifying the substrate catch-policy (the JSON that tells the substrate WHAT to catch). Itsdigest.sha256(REQUIRED) is the SHA-256 of the RFC 8785 (JCS) canonical JSON of that policy, so an empty/permissive policy (defenses off) is distinguishable from the enforcing one. -
networkPosture- the substrate-authoritative egress posture the run was contained under, sourced from the substrate daemon, not a runner assertion.posture(REQUIRED, string) - one ofno_network(no egress path exists),allowlist(egress permitted only to a declared destination set),sinkhole(egress is accepted and diverted to a capture endpoint rather than reaching its destination), orunsafe_bypass_egress(egress is unrestricted and uninstrumented). The registry is closed: an absent, non-string or unregistered value makes the statement malformed, fail-closed. A minor version MAY append a value and MUST NOT redefine a registered one. Closed rather than illustrative because a consumer is invited to coherence-check a substrate row's claimed observation against the posture the run was contained under, and no verifier can decide whether an unregistered posture provides an interception path at a boundary - an open registry would leave that check permanently unreachable while appearing to offer it.digest.sha256(REQUIRED) - the SHA-256 of the substrate's non-secret posture view (posture, allowlists, private-IP blocking, interception flags, the host-scoped secret COUNT), canonicalized under RFC 8785 (JCS). It deliberately excludes every secret-bearing field, so it is safe to publish and a secret rotation does not change it while a posture change does.- The run binding folds in the RFC 8785 canonical digest of this whole object, so every member carried here is inside the substrate's signature. That is a reason not to extend the object casually: a producer-specific member changes a signed digest, and one added after the arming record is signed invalidates the producer's own statement.
-
observationVocabulary- the producer's versioned observation label set, carried on-wire so the recompute and GATE 1 are pure functions of carried bytes and archived attestations remain verifiable after the producer's documentation moves or disappears.labels(REQUIRED, array of strings) - the complete array ofcontainmentObservedvalues the producer can emit, sorted ascending by UTF-16 code unit (RFC 8785 sec 3.2.3) with no duplicates. Probity's set is["batch_bound","canary_bound","egress_captured","not_exhibited","payload_bound","window_bound"].caught(REQUIRED, array of strings) - the subset whose observation constitutes a caught containment event, sorted ascending by UTF-16 code unit, a subset oflabels. Probity's set is["batch_bound","canary_bound","egress_captured","payload_bound","window_bound"]- i.e. every label except the cleannot_exhibited.digest.sha256(REQUIRED) - the SHA-256 of the RFC 8785 (JCS) canonicalization of the object{"caught":[...],"labels":[...]}. A statement violating the sort, subset, or digest obligations is malformed.
The producer's published documentation is commentary on the same vocabulary, never a normative input. (The label spellings are Probity producer vocabulary; a different producer carries its own labels on-wire under this same member, and a consumer treats a
containmentObservedlabel outside the carried set as fail-closed.) -
runEntropy.digest.sha256(REQUIRED when any row isbasis: substrate; OMITTED on an artifact-only statement) - the substrate-emitted run-start value folded into the run binding. Its pre-image is the substrate's run-start checkpoint; it is an anti-splice beacon, not a freshness nonce. The pre-image SHOULD additionally fold in a publicly datable value that was unpredictable before its round (a drand round output, or an RFC 9334 Section 10.3 epoch identifier), in addition to, never in place of, the substrate-unique component, fetched at arming time (never cached), with the round reference recoverable via the arming payload. A signature over such a value cannot predate its round, so the arming record gains a proven earliest-possible signing time, a floor;issuedAtstays the asserted ceiling, deliberately not a two-sided proof. The floor bounds recency only where consumer policy couples the folded round to its freshness window (the producer selects the round), and a beacon inside the producer's own trust domain yields no floor against that producer. Public rounds also make independent consumers'runEntropy-reuse observations comparable on a shared time axis.
Descriptor typing (normative). All six members of observationEnvironment are descriptor-shaped
and exactly two of them are
ResourceDescriptors;
the rule separating them is stated here so a later reader inherits it rather than rediscovers the
question. substrate and catchPolicy identify a resource and carry nothing beside that identity, so
they take the framework type. The sha256 REQUIRED on each is a requirement the descriptor
specification explicitly permits a context using the type to impose, and reading a pinned sha256 off
a descriptor is already what this predicate does in its most load-bearing place, since subject
entries are ResourceDescriptors by the in-toto Statement specification and the run binding reads
subject[0].digest.sha256. This profile narrows the type on the wire rather than widening it: the
schema SSOT carries additionalProperties: false on both members, so the descriptor's other optional
members (uri, downloadLocation, mediaType, annotations, content) are NOT admitted here, and
no rule in this document would read them. The open standard admits them on a ResourceDescriptor; this
structural profile does not, and a statement carrying one on either member is malformed here.
The other four members stay locally typed, and the reasons are stated rather than left to inference.
Where a member carries the pre-image its own digest is taken over, that pre-image stays on the
statement's own JSON surface: corpus carries manifest and observationVocabulary carries
labels and caught, and the only descriptor member that could hold either is content, whose value
is base64. Every byte-level rule above is stated over the statement's JSON - the duplicate-member rule
at any depth, the string well-formedness check applied to the raw bytes before any decoded string is
read, the nesting bound of 128, and the BMP restriction on canonical surfaces. Material inside a
base64 member sits outside all four, so carrying a digest pre-image there would open a SECOND
canonicalization boundary inside a signed statement, in a predicate whose whole encoding profile
exists so that two conforming verifiers cannot disagree about identical bytes. Where a member instead
carries further normative material beside an identity it stays locally typed rather than extending a
descriptor with members of its own - networkPosture carries the closed posture registry and
observationVocabulary carries the label sets the recompute reads - which is the shape the in-toto
runtime-trace predicate already uses for monitor. runEntropy is offered as a reading rather than
as a rule: its digest commits to a substrate-emitted run-start value rather than describing a
resource, so a descriptor is the wrong vessel for it.
coverage (REQUIRED)
Which assessment classes were assessed and how the rest were handled. Together with the
digest-committed manifest.classes map, this is the coverage bound the verifier checks by set
equality at the class level AND by attack-level exhaustion.
assessedClasses(REQUIRED, array of strings) - the class codes actually assessed in this run.outOfScope(REQUIRED, object) - a map from a class code to a human-readable reason it was not assessed (empty{}when nothing was out of scope). Its presence makes the rundegraded, neverfail.routedElsewhere(REQUIRED, object) - a map from a class code to the layer or mechanism that covers it instead (empty{}when nothing was routed).
Every class code in assessedClasses, outOfScope, and routedElsewhere MUST be a key of
manifest.classes, the three sets MUST be pairwise disjoint, and their union MUST equal
keys(manifest.classes) exactly. Additionally (attack-level exhaustion), the attackId set of
attackResults MUST equal the union of manifest.classes[c] over every c in assessedClasses.
attackResults (REQUIRED)
One row per corpus attack dispatched. Each row:
-
attackId(REQUIRED, string) - the corpus attack/vector identifier; a member of the digest-committedmanifest.classes. No twoattackResultsrows may carry the sameattackId; a duplicate is malformed. The attack-level exhaustion check set-compares rowattackIds, so a duplicate would collapse silently under set semantics; uniqueness is enforced before that comparison. -
containmentObserved(REQUIRED, string) - a label from the carriedobservationVocabulary.labels. The single normative reading of this value is its membership in the carriedcaughtset; attribution nuance (which caught rung, what evidenced it) is non-normative producer vocabulary that nothing normative reads. Probity's labels:payload_bound- the strongest caught rung: an intercepted payload's commitment matched a corpus-pinned expected artifact hash. (Attribution strength only; non-normative.)window_bound- attributed by the drain-barrier timestamp window.batch_bound- caught during a fuzz campaign, not bound to a specific input.egress_captured- a credential-exfil attempt captured at the sinkhole boundary.canary_bound- attributed by a seeded canary marker surviving in the catch's scrubbed payload.not_exhibited- the clean label: the attack was dispatched but the SUT never exhibited the dangerous behavior, so there was nothing to contain. NOT a containment failure and NOT an evasion. (This is Probity's single clean-label spelling; there is nono_egress/uncontainedalias.)
-
basis(REQUIRED, string) - the observation's vantage, closed vocabulary{substrate, artifact}, fail-closed:substrate: every input the row's claim depends on was obtained at a vantage the executed artifact could neither forge nor suppress (a network boundary, syscall supervision, a hypervisor's read of guest state). An input is artifact-sourced when the claim relies on a channel the artifact can populate arbitrarily without performing the claimed event (its stdout/stderr, exit status, self-emitted logs); an egress capture is NOT artifact-sourced even though the packet bytes were artifact-authored, because the artifact cannot cause the boundary to record an egress without performing one.artifact: at least one input the claim depends on derives from output the executed artifact itself produced.
basisis the vantage of the claim's weakest input: a derived observation inheritsartifactfrom any artifact-sourced input it consumed. A producer MUST NOT declarebasis: substrateon a row it cannot cover under GATE 1 (Coverage validity): such a row is not merely mislabeled, it makes the attestation invalid. -
method(REQUIRED, string) - the observation's directness, closed vocabulary{intercepted, reconstructed}, fail-closed:intercepted: the claim rests on events captured as they occurred. On a clean row, a live capture vantage was armed for the attack and no capture was attributed to it.reconstructed: the claim derives from state examined after the fact (a snapshot-to-snapshot diff), which can miss a transient raised and undone between the states compared.
Like
basis,methodcomposes by weakest input. Post-hoc decode of an event stream captured as it occurred does not demote a row, provided the capture channel was armed for the claimed event class before the event; a row fusing a live capture with after-the-fact state examination isreconstructed.methoddescribes how the observation was made, not how the row was attributed to its attack. Both fields are REQUIRED on every row and both vocabularies are closed: a missing value, or any value outside them, is fail-closed exactly as an out-of-vocabularycontainmentObservedlabel is - the row forcesresulttofailand can support nothing stronger. The retired 0.4 valuessubstrate_observed/artifact_reported/inferredare out-of-vocabulary, with no alias (inferredhas no successor because it conflated the two axes). -
actualLayer(REQUIRED, string) - which enforcement layer acted on the row's containment event, drawn from the registered layer set (vocabulary registry), OR the literal stringnone. A row whosecontainmentObservedis a label-not-in-caught (a clean row: nothing acted) MUST carrynone;noneis also valid on a caught row and there states the event was observed but no enforcement layer acted (a passive tap, a monitor-only vantage), so enforcement role travels here and only here andbasisnever has to encode who could act.actualLayeris REQUIRED on every row; a missing member is a MALFORMED statement (a different altitude than the fail-closed row members the recompute reads), not weak evidence. -
observationRefs(REQUIRED onbasis: substraterows, array of non-negative integers) - indexes intoobservationRecordsbinding this row to the records that cover it. Aninterceptionindex MAY be referenced by more than one row. A producer MUST NOT reference a record from a row whose attack the record's committed payload does not evidence. On a row declaringattribution: pinnedthat obligation is checkable and is checked, by the coverage validity requirement stated above: the corpus declares what the attack's interception commits to and the verifier compares. On a row declaringpairedit remains an obligation outside every gate, because no validity requirement, recompute input or tier evaluation reads it there, and a conforming verifier neither can nor may invent an evidencing heuristic in its place. The line between the two is exactly the line the corpus draws by carrying an expectation or not.arming/sealed/examinationindexes are run-level and MAY be shared by every row earned under them. WhereverobservationRefsis present - on any row, regardless ofbasis, and including rows on which nothing normative reads it - every index MUST be in range forobservationRecords. An out-of-range index is a structural integrity fault that makes the statement malformed, fail-closed and independent of any gate, so a reference that does not resolve is never silently ignored. -
observationSelectors(OPTIONAL, array of strings) - producer-defined tokens positionally parallel toobservationRefs, each naming the sub-observation within a referenced record's payload this row rests on. Token content is producer vocabulary; nothing normative reads it, and selector presence or absence changes no gate outcome.
Consumer strength orderings (non-normative guidance; not part of the recompute). The two axes
bind a consumer's confidence on opposite sides. basis bounds a fail over its supporting rows
(those forcing result to fail): a fail whose supporting rows are all artifact is a weaker
claim than one carrying a substrate row, and a consumer MAY reject it; a fail from a
(substrate, reconstructed) row is still an observation the artifact could not manufacture, weaker
than an interception only in that it was derived after the fact. method bounds a pass: a pass
whose clean rows are all attested at (substrate, intercepted) - each covered by both the run's
arming record and a covering sealed record - makes the strongest absence claim this predicate can
carry; a pass resting on any reconstructed clean row tolerates transients between the observed
states; a pass resting on any artifact clean row, or an unattested substrate clean row, is
self-reported absence, the weakest. Only rows deriving attested occupy the substrate rank; an
unattested substrate row ranks with artifact in both orderings. A consumer MAY reject, NEVER
downgrade: declining to credit a rank is not a rewrite of signed bytes.
Field partition (delta-I: which identity's signature backs which field).
Substrate-covered - through GATE 1 and the evidence tier: basis and method on rows deriving
attested, and the content of every verified observation record. Producer-asserted - backed only by
the enclosing envelope: containmentObserved labels and their attribution nuance, basis/method
on artifact rows, actualLayer, coverage, doesNotAssert, and the assembly of the predicate
itself. The two are not composable: a per-observation property the recompute or the documented
consumer gating reads travels ON the row as a required member (with a closed vocabulary, fail-closed
on missing or unknown values); run-level pins in observationEnvironment never substitute for a
row-level property, and the run-level arming/sealed instruments back a clean row's claim through
the row's own observationRefs without substituting for a row member. Which keys count as substrate
observation keys is consumer key policy; the substrate observation key MUST NOT be accessible to the
subject artifact and SHOULD be held apart from the producer's assembly plane. Where it is so held, the
tier defeats a pipeline with no substrate in the loop, cross-configuration splices, record drops, and
method inflation; where one party holds both keys (the single-root deployment), the tier instead
defeats only a keyless downstream tamperer - a substrate operator who signs false evidence remains
outside this predicate's threat model, as for every self-asserted field.
observationRecords (OPTIONAL)
The signed observation records the containment rows bind to. OPTIONAL: an artifact-only statement
carries none (and then carries no batchRoot). Each entry is a standard
DSSE envelope carrying the EXACT signed bytes of one
observation - there is NO parallel readable object; the readable fields live inside the signed
payload and are read only after the signature verifies. Each entry:
-
payload(REQUIRED, string) - the base64 (RFC 4648 §4 standard alphabet) of the exact canonical+jsonbytes the substrate signed. Any record used to cover abasis: substraterow MUST base64-decode to a JSON object that is canonical per RFC 8785 and valid I-JSON per RFC 7493, carrying these reserved top-level members (a record whose payload is not so parseable, or whose media type is not+json, covers nothing):aeeRunBinding(string) - the run binding digest defined under Run binding.aeeKind(string) - one of:interception- per-event capture; covers caught rows.aeeMethodMUST beintercepted.arming- run-level: a live, cooperation-independent capture vantage was armed for the run before corpus injection. Payload MUST carryarmedAt(under the timestamp profileissuedAtdefines, no later thanissuedAt) andaeePostureDigest(equal to the pinnednetworkPosturedigest);aeeMethodMUST beintercepted. Covers clean rows (with a coveringsealedrecord).sealed- run-level: the vantage stayed armed to run-end. Payload MUST carryaeeStillArmed(boolean, MUST betrueto cover),aeeDropCount(integer; MUST be0, or0 <= aeeDropCount <= aeeDropBoundwhereaeeDropBoundis a producer-declared integer in the SAME payload), andaeePostureDigest(equal to BOTH the pinnednetworkPosturedigest AND every coveringarmingrecord'saeePostureDigest);aeeMethodMUST beintercepted. Covers clean rows (with anarmingrecord).examination- the substrate examined artifact-independent state after the fact.aeeMethodMUST bereconstructed. Covers reconstructed rows.
aeeMethod(string) -interceptedorreconstructed, how the substrate observed, stated inside the signature (it caps the row'smethod, see GATE 1).
An
armingrecord's payload MAY additionally carry three reserved members that chain runs under the same substrate key:aeeRunSeq(a positive safe-range integer),aeePrevRunBinding(the lowercase 64-hex run binding digest of the predecessor run, absent exactly whenaeeRunSeqis1), andaeeChainScope(the population the sequence counts, declared as a duplicate-free array of dimension tokens drawn from the closed vocabulary registered below, sorted in the same canonical order asobservationVocabulary.labels(UTF-16 code-unit order, RFC 8785 section 3.2.3); REQUIRED wheneveraeeRunSeqis present). The chain is always structurally under one substrate key; each token names a further within-key partition attribute already carried elsewhere in the attestation and fixes where a consumer reads that attribute's value. The declared array is the dimension set; the evaluated tuple is the projection of the substrate-key value and each declared token onto its registered attribute value for this run (computed, never carried). The recommended minimum is["subject"]; the empty array is the single global per-key counter that makes every rule below vacuous and leaks the producer's total run volume across customers.The
aeeChainScopevocabulary is closed and each token pins a projection to a value already carried on the wire:subjecttosubject[0].digest.sha256,corpustoobservationEnvironment.corpus.digest, andnetworkPosturetonetworkPosture.digest.sha256. The substrate key is the structural outer axis and is never a token. Values are not carried in the member; a consumer projects each declared token onto its registered field for this run. Minor versions MAY append tokens (each with a pinned projection) and MUST NOT redefine an existing one; an unrecognized token fails closed, as every closed vocabulary in this spec does.Within one attestation these members are syntax-checked in the reserved-member walk and nothing else normative reads them (GATE 1, the recompute, and GATE 2 are unchanged). A violation of the syntax rules - a non-positive or non-integer
aeeRunSeq, a malformedaeePrevRunBinding, a missingaeeChainScopewhen the sequence is present, a non-arrayaeeChainScope, an array carrying a token outside the registered vocabulary, an array not in canonical order (the same canonicality rule asobservationVocabulary.labels: UTF-16 code-unit order, duplicate-free), or any of the three present withoutaeeRunSeq- is handled as any reserved-member violation: the record covers nothing.Across attestations these members are consumer policy over whatever set the producer publishes. A consumer compares each attestation's declared dimension set against the set its policy demands: an equal set is admissible; a strictly finer set (a superset of dimensions) is scope-narrowing, fragmenting every run into a singleton chain so no gap, fork, or duplicate genesis can arise and the chain proves nothing; a strictly coarser set (a subset of dimensions) pools distinct subjects, so a withheld run of the demanded subject is deniable as a sibling's private run and a sibling's run can occupy the withheld sequence position. A consumer that has demanded a scope admits only the equal set, neither finer nor coarser. Among admitted attestations the rules key on the evaluated tuple, not the token set: a skipped
aeeRunSequnder one tuple is a gap; two under one tuple with equalaeeRunSeqare a fork; a sharedaeePrevRunBindingis a shared predecessor; two genesis records (absentaeePrevRunBinding) under one tuple are equivocation of the same grade as a shared predecessor. Keying on the tuple is load-bearing: genesis-per-subject-value is the normal case, and only a second genesis under an identical tuple is a reset. A chain reset is not a fresh start.The members claim ordering under the substrate key, nothing more (commit-before-outcome holds only combined with the run-entropy floor or an external registration receipt); a numeric gap is unexplained absence, never fraud evidence in itself (crashed, private, and discarded runs all produce gaps innocently). Even a contiguous, fork-free, correctly-scoped chain does not prove population completeness: a producer may still mint a dense, gap-free set of passing runs after the fact. Fork consistency among the published set is the ceiling of what any self-contained attestation set establishes; the demand-disclosure yield is that a consumer policy MAY require a contiguous, fork-free chain over the runs offered to it. The external completion is a registration receipt (committing each arming record to an append-only transparency log at run start, e.g. SCITT, RFC 9943, with COSE receipts, RFC 9942), deliberately outside this predicate.
A record violating any constraint of its declared
aeeKindcovers nothing, and the constraints are read on every carried record of a covering kind rather than only on the records some row resolves. A constraint evaluated only where a row points is a constraint whose subject the producer chooses: a substrate signs asealedrecord reporting its moat down, the producer carries that record and points the row at a second seal, and the run reads clean with the record that says otherwise sitting in the statement and insidebatchRoot. A record whoseaeeKindthe consumer does not recognize covers nothing and is otherwise ignored, while still contributing its leaf tobatchRoot- an unrecognized kind can only weaken, never strengthen, a row (candidate future kinds, informatively: a hardware-quote kind binding the vantage to a measured platform, and aregistrationkind carrying a transparency-service receipt over the arming record). Theaeemember prefix is reserved for future versions; everything else in the payload stays producer territory. Reserved members inside a producer-defined signed payload follow the registered-claims lineage (JWT RFC 7519 Section 4; EAT RFC 9711 in the RATS family; OCI annotation prefix reservation; the+jsonrule per RFC 6839 Section 3.1), informative citations locating the pattern, not importing any cited standard's rules, with two deliberate departures: unrecognized or collidingaee*members are fail-closed (they can only weaken coverage, never create it) where JWT ignores unknown claims, and verify-then-read is normative here. What aninterceptionrecord carries is a commitment to an intercepted payload rather than the payload itself, keeping the attestation publishable rather than a sensitive-data store. The Probity producer's concrete observation-payload field profile (the readable fields beside the reserved members) is specified in catch-record.md. -
payloadType(REQUIRED, string) - a producer-defined media type ending in+json. It is the second input to the PAE pre-image, so a differing value changes the signed bytes. The Probity producer emits the constantapplication/vnd.probity.aee-observation.v1+json. -
signatures(REQUIRED, array,minItems: 1) - each element{ "keyid": <hex>, "sig": <base64-ed25519> }. The signature is overPAE(payloadType, b64decode(payload))(DSSE Pre-Authentication Encoding). A record verifies when at least one element validates under a pinned key. Akeyidis an unauthenticated lookup hint that selects WHICH pinned key to try, never the check itself.
Verify-then-read. A verifier processes each covering record as: (1) validate a signatures[]
element over PAE(payloadType, b64decode(payload)) under the pinned substrate observation key; (2)
record = json.loads(b64decode(payload)); (3) assert the decoded payload is its own RFC 8785 (JCS)
canonical form and valid I-JSON; (4) read the reserved members and observation fields from record.
All bindings (run binding, kind constraints, method cap) operate on the DECODED record, never on wire
siblings.
batchRoot (REQUIRED when observationRecords is non-empty)
An RFC 6962 Merkle root over the observation records, SHA-256, with domain-separated hashing: each
leaf is H(0x00 || the record's DSSE PAE bytes), each internal node is H(0x01 || left || right),
the tree built by the RFC 6962 recursive largest-power-of-two split - never by duplicating a
trailing node to pad the leaf count - leaves in observationRecords array order, a single-record
tree's root its leaf hash, and an empty array with no root. Two byte-identical entries make the
attestation invalid (duplicate-leaf REJECT: a record's canonical identity is its leaf hash). Carried
once at the predicate level; a batchRoot that does not recompute over the carried records makes the
attestation invalid, and a batchRoot present with no records is likewise invalid. Because a clean
run's committed set includes its arming and sealed records, absence evidence cannot be dropped
without changing the root.
Fold note (batch-root n=5 non-divergence). An earlier concern held that the naive "promote the unpaired odd node" fold diverges from the RFC 6962 recursive split at five leaves. That premise is FALSE: the two folds were empirically verified byte-identical for every leaf count on all Probity rails. The RFC 6962 recursive split is nonetheless pinned as the single normative construction, and the only v0.6 delta to the batch-root rule is the duplicate-leaf reject plus the PAE-bytes leaf content (
H(0x00 || PAE(payloadType, payload)), where v0.3 hashed the raw canonical bytes); the fold arithmetic itself is unchanged and is NOT rewritten on any rail.
Why "never duplicate" is load-bearing, precisely. CVE-2012-2459 (Bitcoin, 2012, verified against
bitcoin/bitcoin's ownsrc/consensus/merkle.cppcommentary and the Bitcoin Optech writeup) is a Merkle-tree malleability / invalidity-caching netsplit: pad an odd transaction list by duplicating the last entry, so an invalid block (the duplicate double-spends) shares a root with the valid block it was built from, and a node that caches the invalid form by root refuses the valid form until restart. It is not a second-preimage attack on the hash function — that is a separate property, and the one the0x00/0x01domain-separation prefixes above defend. The odd-node rule here defends against duplication-based root collisions specifically: promotion carries the unpaired leaf's digest forward unchanged, so an odd batch of N records and a genuinely-duplicated batch of N+1 records (the N+1th a byte-identical copy of the Nth) never share a root — and the duplicate-leaf REJECT rule above closes the remaining case, an honestly-even batch that happens to carry two identical entries.
doesNotAssert (OPTIONAL)
A machine-readable negative-claims list: short strings stating what this evidence explicitly
CANNOT claim, so a consumer does not over-read it (e.g. a --recipient run states it does not assert
payload-decryptability without the customer key). Advisory: a verifier MUST NOT require it and nothing
in it participates in the recompute or the binding rules. doesNotAssert is the single canonical
spelling; the earlier snake_case does_not_assert is rejected with no alias (two accepted spellings
would mean two canonicalizations for the same content). Absent when the producer asserts no explicit
negative claims.
issuedAt (REQUIRED)
When the producer signed this evidence bundle, carried as the in-toto framework's
Timestamp field type.
REQUIRED in v0.6 (was OPTIONAL in v0.3). Distinct from the observation timestamps inside
observationRecords; an arming record's armedAt MUST be no later than issuedAt. Its purpose is
also consumer-side freshness gating: an admission controller MAY deny evidence older than its
window, and a consumer's policy MAY bound a named substrate observation key with a validity window
checked against issuedAt.
Timestamp profile (normative). Timestamp requires RFC 3339 in the UTC timezone, and this
predicate pins the two choices that type leaves open. A statement is canonicalized and digested as its
bytes, so no verifier may normalize the field before reading it and the admissible set has to be
written down; left open, one rail is quietly stricter than another and the divergence surfaces only
when a statement crosses between them. The date-time separator and the zone designator MUST be
uppercase - never the lowercase t and z that
RFC 3339 also admits - and the zone designator MUST be Z,
+00:00, or -00:00, never a non-zero offset such as +05:00. -00:00 is admitted rather than
excluded because RFC 3339 section 4.3 gives that spelling the meaning that the instant in UTC is known
while the offset to local time is not, which describes where the producer stood and not when it
signed, and the instant is the only thing this predicate reads from the field. A statement whose
issuedAt is absent, is not RFC 3339, or is RFC 3339 outside this profile is malformed. armedAt
carries this same profile, defined here and cited from the arming record so the two fields cannot
drift apart.
executionEnvelope (OPTIONAL)
The tool-call OTel correlation envelope. When present, otelTraceId (REQUIRED within, 32-char
lowercase-hex W3C trace-id) is the only gated key; a cross-runtime emitter MAY attach further
correlation fields. OPTIONAL for producer neutrality.
_ext (OPTIONAL)
The in-toto extension-field convention: a free-form object for producer-scoped extensions that are
deliberately out of core. A verifier MUST NOT require _ext and MUST ignore extensions it does
not recognize; nothing in _ext participates in the recompute or the binding rules. No predicate
member (including any _ext sibling) may begin with the reserved aee prefix.
_ext.run_fidelity (Probity extension, OPTIONAL)
The run-level rollup of the per-observation EvidenceFidelity signals - the aggregate CONFIDENCE and COMPLETENESS of the run's attribution, so a buyer's OPA/rego policy can gate on evidence QUALITY, not just structural validity. It is Probity-namespaced: a generic verifier ignores it; a Probity verifier MAY apply the OPTIONAL recompute rule below. Shape:
clean_drain(bool) - true when the run's final signedquarantine.coverageobservation record closed withstatus == "final"andin_flight == 0 && queued == 0: the arm provably drained every admitted catch, so no attribution was silently lost mid-run.task_resolution_counts(object) - a HISTOGRAM (never a min) over the per-catch owning-task resolution mode:{ "4-tuple": n, "dst-fallback": n, "dst-only": n }, counted across the run'svmi.attributionobservation records.verity_device_pairing(string, OPTIONAL) - the run-level dm-verity device-pairing confidence (verity-exact-root-digestorverity-heuristic-size), riding the final signedquarantine.coveragerecord'scoverage.verity_device_pairing; present ONLY on a verity-anchored run. Ahost-block-sourced (copied-up) ModelBOM entry is authoritative over this run scalar for its own attribution (see ml-bom.md).
All three fields are RE-DERIVABLE from the signed observationRecords alone. Optional recompute
rule (Probity verifier). _ext.run_fidelity is NEVER trusted as asserted: a Probity verifier
RE-DERIVES all three from the verified observation records (which ride inside observationRecords,
covered by batchRoot) and asserts equality, so a producer cannot inflate its own evidence quality.
An absent _ext.run_fidelity is sound; a PRESENT one that does not match the recompute is rejected.
Verification (third-party, offline, two gates)
Two expectations are consumer policy, resolved outside the attestation and never read from it: which
keys count as substrate observation keys (GATE 2's input), and which corpus and substrate this
consumer expects. A consumer MUST pin, out of band, the expectedCorpusDigest and
expectedSubstrateDigest for the deployment it is admitting into (a bundle's internal exhaustion is
meaningless unless the consumer pins WHICH corpus and substrate it was earned under) and assert them
at the consumer-policy step after GATE 2. The anchor comparison is deliberately not a validity gate:
GATE 0, GATE 1, and the recompute are functions of carried bytes alone and hold identically for every
consumer, while the expected corpus and substrate differ per consumer. An anchor-mismatched
attestation is valid evidence about the wrong context, and it is NOT ADMITTED, exactly as an
attestation whose covering signatures fail is not admitted. Every verification surface exposes one
consumer-facing admission result, admitted = validity AND tier-policy satisfaction AND
anchors-satisfied-when-supplied, and binds its exit status to admitted, never to bare validity, so
a result-only consumer cannot read a valid-but-wrong-context attestation as admissible. The exit
binding applies whenever a consumer policy or anchors are supplied; a bare conformance replay with
no policy binds to validity alone; with no policy there is no admission decision to bind. After the
envelope checks in signing-envelope.md, a verifier - with zero network
access - proceeds through GATE 0, GATE 1, the recompute equality, and GATE 2 (the four byte-pure
steps first, then the trust-relative stage).
GATE 0 - statement well-formedness
The parsing rules and field shapes: the predicate is its own RFC 8785 (JCS) canonical form and valid
I-JSON (no duplicate members, no unsafe integer); the closed registries (actualLayer,
networkPosture.posture) validate, while containmentObserved/basis/method vocabulary membership
is deferred to the recompute gate (fail-closed on an out-of-vocabulary or absent value), not gated
here; subject has exactly one entry with a lowercase-64-hex sha256; each run-binding digest input
read verbatim is lowercase 64-hex, and so is networkPosture.digest.sha256, which version 2 of the
binding no longer reads verbatim but which is still compared byte for byte against a record's
aeePostureDigest; issuedAt is present and inside the timestamp profile; a
basis: substrate statement carries runEntropy; and actualLayer is present on every row (a
missing member is malformed here, not a fail-closed row). Coverage integrity is checked here:
canonicalize manifest under JCS, hash, assert
equality with corpus.digest.sha256; assert the class sets are pairwise-disjoint and set-equal to
keys(manifest.classes); and, at ATTACK granularity, assert the attackId set of attackResults
equals the union of manifest.classes[c] over c in assessedClasses. Canonicalize
{"caught":[...],"labels":[...]} and assert equality with observationVocabulary.digest.sha256, and
assert labels/caught are sorted ascending by UTF-16 code unit, dupe-free, and caught a subset of labels.
GATE 1 - coverage validity (RATS "Appraisal Policy for Evidence")
A key-independent consumption precondition computed from carried bytes (reads record payloads, but
never signatures or consumer policy). A consumer that consumes result, credits any row, or applies
either strength ordering MUST evaluate GATE 1 first; on failure the attestation is INVALID and its
result MUST NOT be consumed - the same handling as any malformed statement. For every basis: substrate row:
observationRefsis non-empty and every index is in range forobservationRecords.- Class-match: a caught
interceptedrow references at least oneinterceptionrecord; areconstructedrow references at least oneexaminationrecord; a cleaninterceptedrow references at least onearmingrecord AND at least one coveringsealedrecord. A row fail-closed oncontainmentObserved,basis, ormethodcannot class-match and is therefore INVALID. - Every referenced payload parses as a canonical
+jsonI-JSON object carrying the reserved members, and itsaeeRunBindingequals the run binding derived from this statement. - The row's
methodis no stronger than the weakest signedaeeMethodacross its COVERING records (records that cover nothing do not participate in the cap). batchRootrecomputes overobservationRecords(RFC 6962, domain-separated, duplicate-leaf reject) - a statement-level check run whenever records are present.- Every carried record that binds to this run and whose
aeeKindnames a covering kind (interception,arming,sealed,examination) satisfies every constraint of that kind, whether or not any row resolves an index to it - a statement-level check, and the universal partner of the class-match rule above, which asks only whether a covering record is present and so says nothing about the failing ones carried beside it. The kinds registered as covering nothing and the kinds a verifier does not recognize are unaffected: neither carries a constraint that could be violated.
Recompute equality
Re-derive result by the recompute-table.json reduction over the rows, the
carried vocabulary, and the coverage maps, and assert it equals the recorded result. This is a
function of the carried predicate alone; it never reads observationRecords, signature outcomes, or
consumer policy, so a result that varied with the consumer's trust anchors would not be recomputable.
GATE 2 - evidence tier (RATS "Appraisal Policy for Attestation Results")
Given a valid attestation, a consumer MUST - before crediting any basis: substrate row or applying
either strength ordering - derive a per-row evidence tier: a basis: artifact row (and a row
fail-closed on basis) is declared; a basis: substrate row is attested when every covering
record's signature verifies against a key the consumer's policy names as a substrate observation key,
and unattested otherwise. No TOFU: a consumer with no policy-pinned substrate root MUST treat
every basis: substrate row as unattested and MUST NOT infer the substrate root from the predicate.
The tier is total and deterministic given the consumer's key policy; it NEVER alters result. A
carried predicate member named evidenceTier, or any predicate-level member beginning with the
reserved prefix aee, MUST be ignored and MUST NOT alter the derivation.
Finally the consumer-policy step: when the consumer supplies anchors (it MUST for admission
decisions), assert corpus.digest.sha256 == expectedCorpusDigest and substrate.digest.sha256 == expectedSubstrateDigest; a mismatch fails admitted, never validity. Confirm the subject digest
is the SUT image the verifier intends to admit and surface posture + coverage + the per-row tier for
the consumer's admission policy. The surfaced report carries both the byte-pure facts (validity,
result) and the consumer-relative facts (tier, anchor comparison) with admitted as their
conjunction. No live execution environment and no network access are required.
Named non-claims
- COV-1 (coverage-bounded-observed). A
passis a statement about what was assessed under the pinned corpus and catch policy; it makes no general safety claim. - COV-2 (run-population completeness is out-of-wire). A clean
(substrate, intercepted)row'spassis a claim about the run THIS attestation carries - that a live vantage was armed and stayed armed and no interception was attributed to the row - never a claim about a run population. Nothing in this predicate proves that other runs of the same configuration did not occur or were not withheld; run-population completeness (via a run ledger or monotonic counter) is a consumer or policy concern. - COV-3 (no cross-predicate composition). This predicate makes no cross-predicate claim: composing it with a sibling execution predicate (for example a runtime trace of a different execution) does not yield end-to-end coverage, and a consumer MUST NOT infer a composite guarantee unless its policy binds both attestations to the same execution (a shared subject digest and run identifier).
What a verifier can and cannot independently check (honesty)
- A verifier CAN confirm the evidence is authentic, bound to its subject image, and signed by a
trusted key; recompute the
result, the coverage-integrity binding, and the on-wire vocabulary digest; evaluate GATE 1 (coverage validity) from the carried bytes and derive the GATE 2 tier against its own key policy; validate each observation record's DSSE signature and the drop-evident batch root; and read the observation environment exactly as recorded. - A verifier CANNOT, from the evidence alone, confirm that the corpus content behind
corpus.digestis genuinely adversarial (it confirms the manifest commitment and checks corpus identity against a consumer-pinned digest, not corpus quality), nor that the assessment taxonomy is complete. - A verifier CANNOT distinguish a genuine evasion from a benign no-op beyond what the substrate's sensors observed under the pinned catch policy: it trusts the substrate's sensor efficacy and the producer's taxonomy, and is honest only within the coverage the anchors pin.
- Coverage is only as trustworthy as the named substrate observation key's un-compromised lifetime.
Where one party holds both the assembly and the observation key (the single-root deployment), that
single trust root is a single point of total failure: a compromised or dishonest key-holder can
mint any coherent bundle, as with every signed attestation. The soundness rules constrain what a
validly-signed bundle can claim and still verify, not what a key-holder can sign; rejecting an
under-defended posture (
unsafe_bypass_egress) is the consumer admission policy's job.
Related standards
- in-toto attestation - this predicate is proposed in
the in-toto attestation namespace as the active-stimulus sibling to the passive
runtime-tracefamily:runtime-tracecarries unbounded, monitor-specific logs; this predicate carries a bounded adversarial campaign with per-attack rows, an on-wire observation vocabulary, and a cryptographic coverage bound. - RATS (RFC 9334) - the two-gate structure is the
RATS appraisal split: GATE 1 (coverage validity) is the Verifier's key-independent "Appraisal
Policy for Evidence"; the per-row evidence tier is the Relying Party's trust-relative "Appraisal
Policy for Attestation Results", and the tier names
attested/unattested/declaredalign to that vocabulary rather than minting bespoke terms. - in-toto training / build provenance (downstream join) - a
vmi.attributionobservation record carries a per-model ModelBOM committed by the record's decodedenrichment_digest, whose entries follow the ml-bom.md raw-file-SHA-256 identity contract: an in-toto training-provenance attestation whose subject is the weight file keyed by the same raw-file hash chains to this evidence by digest equality, with no shared naming and no live environment. - in-toto
test-result- the fused-resultprecedent: a REQUIREDresultenum recomputable from the carried lists. This predicate follows the same recomputable-summary discipline. - SVR / SLSA VSA - deliberately NOT this predicate (see Purpose). A consumer MAY emit an SVR/VSA downstream over this evidence, binding it by the evidence's DSSE payload digest; this predicate itself carries no delegated policy decision.
- DSSE - the envelope, used at TWO layers: the outer
statement envelope (see signing-envelope.md) AND each
observationRecords[]entry, which is itself a DSSE envelope over one observation payload (payloadTypeapplication/vnd.probity.aee-observation.v1+json). Reusing the standard shape keeps the inner recordscosign verify-blob-compatible and gives every rail one verify path. - SCITT - a future transparency-log inclusion
proof (a COSE Receipt) attaches at the DSSE envelope layer as a sibling to
signatures, NOT in this predicate body; the envelope is the correct staple point and the predicate schema stays unchanged.
Versioning
This is Adversarial Execution Evidence v0.7 (predicate type URI suffix /v0.7). Adding a new OPTIONAL
field - including additive attackResults optionals, doesNotAssert entries, and _ext extensions -
is additive and stays within the version; a verifier ignores fields it does not recognize, and an
unrecognized observation-record aeeKind covers nothing and is otherwise ignored (fail-closed forward
compatibility). A rename or removal of a field, a change to the DSSE observation-record shape or the
+json media-type rule, a change to the containmentObserved / basis / method / posture
vocabularies, a change to the run-binding construction (aeeBindingVersion), or a change to either
appraisal gate is breaking and requires a new version URI. A member is born exactly when a normative
reader consumes it: if a future version makes the shared-reference evidencing obligation checkable,
attribution strength acquires a normative reader at that version and becomes a required member then, not retroactively
and not through a verifier-invented heuristic in the meantime.
Changelog
- 2026-07-29 - Run binding moved to
aeeBindingVersion: 2, and the posture registry closed normatively. The pre-image gainsobservationVocabulary, the carried vocabulary digest, so narrowing the caught set after the run breaks every record's binding instead of re-deriving for free; and itsnetworkPostureinput becomes the RFC 8785 canonical digest of the carriednetworkPostureobject rather than the value of that object's owndigestmember, bringing the posture string inside the signature it had been sitting beside. Both inputs are configuration already on the wire, so the change costs no bytes and adds no comparison - each closes through the equality every record'saeeRunBindingalready runs - and both are fixed before corpus injection, which is the admission test for any binding input, since the arming record carries the digest and is signed before injection. Version 1 is retired with no alias and no dual-accept window; the absent-member default is now the implemented version rather than a fixed number, so omitting the optional declaration stays legal across a version change. Separately,networkPosture.postureis stated closed at its four registered values with an unregistered value malformed, resolving a divergence in which the upstream prose introduced the values as an example while the proto beside it, this document, the producer, both binding surfaces and the shipped admission policy all treated them as a closed, fail-closed set. New failure code:posture-vocabulary. - 2026-07-29 - v0.6 timestamp profile and descriptor typing (spec text; no wire-shape change, no
signed byte, digest, signature, or conformance vector moves).
issuedAtis typed as the in-toto framework'sTimestamprather than as a bare RFC 3339 string, and the profile that type leaves open is pinned on the field: uppercase date-time separator and zone designator, and a zone designator ofZ,+00:00, or-00:00. The zone rule was previously written only onarmedAt, so a statement whoseissuedAtcarried+05:00was conformant here while being off-guideline, and the case rule was written nowhere;armedAtnow cites the profile instead of restating half of it. The schema SSOT carried the field asformat: date-time, which is annotation-only under the default draft 2020-12 vocabulary and so asserted nothing; it now also carries apattern, which every conforming validator enforces, so the schema states the profile rather than merely describing it.observationEnvironment.substrateandobservationEnvironment.catchPolicyare stated to be ResourceDescriptors,corpusis stated NOT to be one, and the rule holding the remaining four members is written down: a member carrying the pre-image its own digest is taken over keeps that pre-image on the statement's own JSON surface, because the only descriptor member that could hold it is base64contentand material inside a base64 member sits outside every byte-level rule this document states. This profile narrows the descriptor rather than widening it - the schema SSOT'sadditionalProperties: falseexcludesuri,downloadLocation,mediaType,annotations, andcontenton both members. - 2026-07-28 - v0.6 vetting rounds 7-9 (spec text; no wire-shape change).
aeeChainScopeis a duplicate-free array of registered dimension tokens (UTF-16 code-unit sorted) with a two-sided equality gate and fail-closed rejection of a non-array, an unregistered token, or a non-canonical order, replacing the free-form producer string that left scope-narrowing ungateable; the gap, fork, and genesis rules key on the evaluated tuple while consumer policy compares the declared dimension set. An arming payload MAY carry a read-firstaeeBindingVersion, rejected fail-closed when unimplemented and never driving the derivation. The whole statement is parsed as strict I-JSON (a duplicate member anywhere is malformed) and every string literal MUST be a well-formed sequence of Unicode scalar values (valid UTF-8, no overlong form, no CESU-8, paired surrogate escapes, no raw control below U+0020, four-hex\u), checked on the raw bytes before any decoded string is read. A normative JSON nesting-depth bound of 128 is stated with its counting rule (open containers, the outermost brace is depth 1).armedAtrequires a zero UTC offset, an out-of-rangeobservationRefsindex is a fault on any row, duplicateattackIdrows are malformed, the single-subject requirement applies on a statement of any basis, and the three coverage sets are a disjoint partition. The fork-consistency ceiling gains its demand-disclosure yield (a consumer policy MAY require a contiguous, fork-free chain). No vector-forced reading is unpinned: agreement on a rule no vector exercises is untested rather than confirmed. Strict I-JSON also excludes the Unicode noncharacters (U+FDD0..U+FDEF, U+nFFFE/U+nFFFF) per RFC 7493 section 2.1, and the JSON nesting bound now charges a level per open container so an empty-container leaf is counted at its own depth. - 2026-07-23 - v0.6 review revisions (spec text; no wire-shape change beyond optional additive
members). BMP-only upgraded from producer hygiene to a verifier rejection obligation, with the
UTF-16 sort rule retained as defense in depth. External anchors rescoped from verifier inputs to
consumer-policy obligations with a single conjoined
admittedadmission result (validity AND tier policy AND anchors); the anchor comparison is not a validity gate. Run-entropy pre-image gains a SHOULD for a publicly datable, round-unpredictable component (proven signing-time floor; asserted ceiling unchanged). OptionalaeeRunSeq/aeePrevRunBinding/aeeChainScopearming-payload members for cross-run gap evidence (ordering-only; equivocation semantics; registration-receipt completion, SCITT RFC 9943 / COSE receipts RFC 9942). Shared-reference evidencing restated as a producer obligation outside every gate; selectors do no covering work. Registered-claims precedent note (RFC 7519, RFC 9711, OCI, RFC 6839) with the fail-closed and verify-then-read departures stated. - v0.6 - Two-gate substrate backing for
basis: substrate. Byte-checkable coverage validity (references resolve in range and class-match; every covering payload is canonical+jsoncarrying the reserved members withaeeRunBindingequal to the derived run binding;methodcapped by the weakest signedaeeMethod;batchRootrecomputes) is a VALIDITY requirement and a consumption precondition (GATE 1, RATS "Appraisal Policy for Evidence") - a violation makes the attestation invalid, independent of any consumer. The one trust-relative step - the covering signatures verify against a consumer-named substrate key - is a per-row evidence tierattested/unattested/declared(GATE 2, RATS "Appraisal Policy for Attestation Results"); a consumer with no pinned substrate root treats every substrate row asunattested, and anunattestedsubstrate row ranks withartifact(rank, never relabel). Caught intercepted rows are covered byinterceptionrecords, reconstructed rows byexaminationrecords, and clean intercepted rows by BOTH a run-levelarmingrecord and asealedrecord. The observation vocabulary now travels on-wire (observationVocabulary: labels, caught subset, JCS digest), so the recompute and GATE 1 are pure functions of carried bytes. RenamedinterceptRecords->observationRecords,interceptRefs->observationRefs, anddoes_not_assert->doesNotAssert(old spellings rejected, no alias). Record signatures are DSSE PAE over(payloadType, payload);payloadTypeis now a producer-defined+jsonmedia type (Probity emitsapplication/vnd.probity.aee-observation.v1+json).batchRootis pinned to RFC 6962 with domain separation, duplicate records rejected, and the leaf is now over the record's PAE bytes; it is required whenever records exist. A newrunEntropydigest folds a substrate-emitted run-start value into a versioned run binding, anti-splice rather than a freshness challenge.issuedAtis now REQUIRED. Removed the v0.3 payload chain,expectedPayloadSha256,dstIp/dstPort/wasTls, and the orphan-hmac degraded-record concept; attribution strength is non-normative producer nuance. Stated the field-partition (delta-I) and the named non-claims (run-population completeness out-of-wire; no cross-predicate composition). - v0.5 - Split the per-row
basisinto two orthogonal REQUIRED fields:basis(closed vocabularysubstrate/artifact) names the vantage, and the newmethod(closed vocabularyintercepted/reconstructed) names the directness, both by weakest-input composition. The 0.4 valuessubstrate_observed/artifact_reported/inferredare rejected, not aliased (inferredhad no successor because it conflated the two axes). MadeactualLayerREQUIRED on every row (a missing member is malformed, a different altitude than the fail-closed row members) and extended its literalnoneto caught rows (observed-but-not-enforced). Added the consumer strength orderings (basisbounds afail,methodbounds apass, fail-closed rows at the lattice bottom) and stated the row-travel design invariant and thebasis/methodproducer-claim trust boundary. - v0.4 - Added a required per-row
basisfield (closed vocabularysubstrate_observed/artifact_reported/inferred, fail-closed on unknown) so each observation carries its own vantage; pinned theactualLayerclean-run behavior to the literalnonerather than omitting the field; and renameddoes_not_asserttodoesNotAssertwith no alias. - 2026-07-14 - v0.3. Breaking change to the intercept-record shape (verify-then-read).
interceptRecords[]entries became standard DSSE envelopes{ payload, payloadType, signatures[] }carrying the EXACT signed bytes, replacing the flat readable-field object; the readable fields moved INSIDE the signed payload and are read only AFTER the per-record signature verifies overPAE(payloadType, b64decode(payload)).batchRootbecame a SINGLE predicate-level RFC-6962 field. Addeddoes_not_assert[]and documented the orphan-hmac degraded state. (All superseded by v0.6.) - 2026-07-08 - v0.2. Breaking restoration of the run-integrity anchors the v0.1 redesign dropped:
the corpus
manifest.classesbecame aclass -> attackId[]map with coverage checked at ATTACK granularity; a run-identity binding on every intercept record (closes cross-run transplant);interceptRefsas an unforgeable foreign key; removal of the redundantcontainedboolean withactualLayer <-> caughtcoherence enforced; the composite payload chain; out-of-vocabularycontainmentObservedfail-closed; CANON-EQ (signed payload must be its own JCS canonical form); andexpectedCorpusDigest+expectedSubstrateDigestas REQUIRED offline-verifier inputs. - 2026-07-06 - Initial v0.1. The greenfield evidence-first replacement for the retired
scorecard-verdictpredicate: fused recomputableresult(replacingverdict+verdict_reason);observationEnvironmentgrouping the substrate / corpus / catchPolicy / networkPosture bindings; the digest-committed corpusmanifestreplacing a producer-asserted denominator;attackResults(wascve_results);interceptRecords(wasforensic_traces); lowerCamelCase field names.