Skip to content
New kind of attack — caught and fixed before it was publicly known·Read the timeline
Probity Open Predicate Standard
/predicate/v1/evidence-scope-disclosure
View raw .md →

Probity Evidence-Scope Disclosure, v1

predicateType: https://getprobity.dev/predicate/v1/evidence-scope-disclosure

An evidence-scope disclosure is a signed statement about a published piece of evidence: which layers of the detection stack it covers, which tier it sits at, whether it is a real capture or a representative fixture standing in for one, and - in prose the producer writes against its own interest - what it deliberately does not show. Every other predicate in this standard says something about a subject. This one says something about the evidence, and its whole job is to travel the limits alongside the claim so a reader cannot mistake a demonstration for a measurement.

It is not a verdict. It carries no PASS / DEGRADED / FAIL, no per-attack result, and no containment chain. A security-verdict or an adversarial-execution-evidence statement carries an outcome; a disclosure carries the boundary around one. The two compose: a verdict says what happened, a disclosure says how far what happened generalizes.

This predicate rides the DSSE / in-toto envelope specified in signing-envelope.md without deviation. Read that document first; everything below concerns the predicate body and the checks a verifier performs after the envelope verifies.

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY, and OPTIONAL are to be interpreted as in RFC 2119.

Why this type exists

The disclosure body was originally emitted under the signed-evidence predicate type, whose specification defines a completely different structure (merkle_evidence, value_binding, fold_pins). Two structurally unrelated bodies shared one type URI, and both verified only because every shipped verifier gates on the predicateType string and on envelope integrity, never on the body. A reader who dereferenced the type URI - the entire reason the type is a URL - got a document describing fields the statement did not carry. This type exists so the URI resolves to the shape the bytes actually have.

There is no alias. https://getprobity.dev/predicate/v1/signed-evidence is not, and never was, a valid type for a disclosure body, and a producer MUST NOT emit one under it.

Subject

The subject is the artifact the evidence is about, by digest.

"subject": [{"name": "<artifact_name>", "digest": {"sha256": "<64-hex>"}}]

The digest is 64 lowercase hexadecimal characters with no sha256: prefix. The subject of a disclosure is whatever the evidence was gathered from - a scanned agent image, a pinned corpus manifest - and a producer MUST bind a digest a third party can recompute from bytes it can obtain, never a name alone.

Predicate body

{
  "evidence_type": "multi-server-benchmark",
  "tier": "static",
  "coverage_map": {"static": true, "runtime": false, "hosting": false},
  "honest_scope": "<prose the producer writes against its own interest>",
  "is_synthetic_fixture": false,
  "reproduce": "research/68-bakeoff-corpus/run_all.sh",
  "benchmark": { "...": "the variant section selected by evidence_type" }
}

The body has a REQUIRED core, an OPTIONAL reproduce pointer, and exactly one REQUIRED variant section named by evidence_type. Both the core and each variant section are closed: a producer MUST NOT emit a member not listed below, and the reference producer refuses to sign a section carrying one.

Core

FieldReqTypeSemantics
evidence_typeREQUIREDstring, closed enumWhich kind of evidence this is, and therefore which variant section the body carries. See the registry below.
tierREQUIREDstring, closed enum static | runtime | liveThe deepest layer the evidence reaches. Mechanically determined by coverage_map; see the consistency rule below.
coverage_mapREQUIREDobject, closed, three REQUIRED booleansWhich layers the evidence covers: static, runtime, hosting. At least one MUST be true.
honest_scopeREQUIREDnon-empty stringThe producer's prose statement of what the evidence does NOT show. Self-declared; see the observation table.
is_synthetic_fixtureREQUIREDbooleantrue when the evidence is a really-signed REPRESENTATIVE of a shape rather than a capture of a run that happened. Self-declared; see the observation table.
reproduceOPTIONALnon-empty stringA repository-relative path to the recipe that regenerates the evidence. A pointer, not a guarantee: see What this predicate does NOT assert.

The coverage_map / tier consistency rule. tier MUST equal the deepest layer set true in coverage_map, under the ordering static < runtime < live, where hosting: true maps to live and runtime: true maps to runtime. A verifier MUST reject a statement whose tier disagrees with its coverage_map. This is the one field pair in the body a verifier can check for free, and it exists so a disclosure cannot claim a deeper tier than the coverage it admits to.

coverage_map is closed at exactly three members. All three are REQUIRED and all three are booleans; a false is a positive statement that the layer is not covered, never an omission. A producer MUST NOT signal "not covered" by leaving a member out, and a verifier MUST reject a coverage_map that is missing one.

The evidence_type registry

evidence_type is a CLOSED vocabulary. It selects the variant section, and every legal value has exactly one:

evidence_typeVariant sectionWhat the variant carries
live-attestationlive_attestationThe hardware-attestation posture of a long-running, continuously observed agent
multi-server-benchmarkbenchmarkA head-to-head scanner comparison over a pinned multi-server corpus

A verifier MUST reject an evidence_type outside this registry, and MUST reject a statement whose body carries a variant section other than the one its evidence_type names (fail-closed; no warn tier, and no statement carrying two). Adding a value is a spec revision that publishes a new registry row together with the variant section's field table, never a silent additive change a deployed verifier is expected to tolerate.

live_attestation

REQUIRED when and only when evidence_type == "live-attestation".

FieldReqTypeSemantics
integrity_tierREQUIREDstring, closed enum roadmap | attestedattested means a hardware-rooted quote is carried in this statement. roadmap means the substrate primitives are built and the quote is not yet emitted.
tee_quoteREQUIRED, nullablebase64 string or nullThe hardware attestation quote.
ephemeral_pubkeyREQUIRED, nullablebase64 string or nullThe per-session public key bound into the quote.
observer_in_tcbREQUIREDbooleanWhether the observing host is inside the attested trusted computing base.

The integrity-tier consistency rule. integrity_tier == "roadmap" if and only if BOTH tee_quote and ephemeral_pubkey are null. A verifier MUST reject any other combination. Without this rule a statement could declare attested and carry nothing, which is precisely the confusion the field exists to prevent.

A verifier MUST NOT read integrity_tier == "attested" as a verified quote. The rule above is a structural check on the statement, not a validation of the quote's chain; validating a quote requires the vendor's endorsement material, which a disclosure does not carry.

benchmark

REQUIRED when and only when evidence_type == "multi-server-benchmark".

FieldReqTypeSemantics
nameREQUIREDnon-empty stringWhat was compared against what, in one line.
corpusREQUIREDobject, closedThe composition of the pinned input corpus, by count.
metadata_layer_scannersREQUIREDobject, non-emptyOne tally per scanner run at the metadata layer, keyed by scanner id.
self_authored_vuln_server_tool_depthREQUIREDobject, non-emptyPer-scanner tool-depth result on the self-authored vulnerable server, keyed by scanner id.
source_code_layer_semgrepREQUIREDobject, closedThe source-code-layer scoreboard, kept separate from the metadata layer.

corpus carries exactly servers, third_party, self_authored, malicious and benign_controls, each a non-negative integer. third_party + self_authored and malicious + benign_controls MUST each equal servers: a corpus that does not partition twice has lost a row somewhere between the manifest and the statement.

Each value of metadata_layer_scanners carries exactly malicious_servers, malicious_servers_caught, benign_controls, benign_tools_total, benign_tool_false_positives and malicious_tool_attack_findings, each a non-negative integer, with malicious_servers_caught <= malicious_servers and benign_tool_false_positives <= benign_tools_total.

Each value of self_authored_vuln_server_tool_depth is either null (that scanner produced no row for the server) or an object carrying exactly caught_tools and total_tools, non-negative integers with caught_tools <= total_tools. The key set of self_authored_vuln_server_tool_depth MUST equal the key set of metadata_layer_scanners: a scanner present in one and absent from the other is a comparison with a hole in it.

source_code_layer_semgrep carries exactly malicious_caught and benign_false_positives, each a "<caught>/<total>" string of two non-negative integers, and note, a non-empty string. The source layer is a separate scoreboard and MUST NOT be summed with the metadata layer; the note says so in the statement so the separation travels with the numbers.

Observed from outside versus self-declared (load-bearing)

This is the section a reader of a disclosure must not skip. The members of this predicate fall into three classes, and confusing the third for the first is the exact failure this type is shaped to prevent.

Class 1 - checkable from the statement and the trust anchor alone. The DSSE signature, the keyid binding, the subject digest binding, the closed member sets and value domains above, the coverage_map / tier consistency rule, the integrity_tier consistency rule, the corpus partition identities, and the scanner key-set equality. A verifier needs nothing but the bytes and a configured public key.

Class 2 - producer measurements, recomputable only against inputs the auditor supplies. Every integer under benchmark. These were produced by running the named scanners over the pinned corpus. An auditor holding the same corpus and the same scanner versions can re-derive them - that is what reproduce points at - but the statement does not let anyone re-derive them from itself. A re-derivation that disagrees is a real finding; the absence of one is not a confirmation.

Class 3 - self-declared, and not a measurement of anything.

MemberWhy it is self-declared
honest_scopeProse. Nothing observes it, nothing constrains it, and no verifier can confirm it is complete. It is the producer stating limits against its own interest, which is why it is worth carrying and exactly why it MUST NOT be read as a finding.
is_synthetic_fixtureA producer asserting whether its own artifact stands for a run or captures one. A false here is a claim, not evidence; nothing in the statement distinguishes a real capture from a well-formed fixture that says it is real.
evidence_type, tier, coverage_mapThe producer's own account of what it covered. The consistency rule ties them to each other, never to the world: a producer that ran nothing can emit an internally consistent map.
every member of live_attestationIncluding observer_in_tcb and integrity_tier. The consistency rule is structural. No member of this section is grounded by anything in the statement.
reproduceA path. The statement does not commit to the recipe's contents by digest, so it is a pointer to where a reader should go, never a promise of what is there.

A consumer building an automated decision on a disclosure MUST treat class 3 as untrusted input from the party being described. In particular, a policy that admits an artifact because is_synthetic_fixture == false has delegated its decision to the producer.

Producer validation

The reference producer validates the core and the selected variant section against the tables above - closed member sets, the enum domains, non-empty strings where REQUIRED, non-negative integers, the coverage_map / tier rule, the integrity_tier rule, the corpus partitions, the scanner key-set equality, and a 64-lowercase-hex subject digest - and raises rather than signing a malformed predicate. This is the standard's producer-strict rule: a producer that cannot vouch for a member refuses to sign it.

Every number a producer emits under benchmark MUST be derived from the run artifacts, never typed by hand. The two reference generators do this: the corpus disclosure derives its counts from matrix.json and its subject digest from the bytes of corpus/manifest.jsonl, and the live disclosure derives its body from the previously signed statement after verifying that statement's signature.

The validated body is serialized under RFC 8785 (JSON Canonicalization Scheme) to produce the exact payload bytes the DSSE signature commits to, the same canonicalization every other predicate in this standard uses.

Verification (third-party, offline)

The envelope-level procedure is signing-envelope.md §Verification procedure and is not restated here. In summary: confirm payloadType == application/vnd.in-toto+json, decode payload from base64 and keep the bytes verbatim (never re-serialize), resolve each signature's REQUIRED keyid against the configured trust anchor, and verify the raw ed25519 signature over PAE("application/vnd.in-toto+json", <payload bytes>).

Two properties of the trust model are load-bearing here and a verifier MUST enforce both.

  • A signing key is authorized per predicate type. A published verification key carries the set of predicate types it may sign, and a verifier MUST reject a statement whose predicateType is not in the resolving key's authorized set. https://getprobity.dev/predicate/v1/evidence-scope-disclosure is in the authorized set of the producer's evidence-signing key (keyid 427a1092..., published at /.well-known/verification-keys.json).
  • Transparency-log upload is deliberately disabled, and a verifier MUST NOT require one. Signing publishes nothing to any log, on purpose: the evidence stays exactly as private as the artifact it describes. A verifier MUST NOT treat the absence of a log entry as a defect and MUST NOT be built to require one.

Once the envelope verifies, a verifier performs the predicate-specific checks:

  1. Confirm predicateType is exactly https://getprobity.dev/predicate/v1/evidence-scope-disclosure and that the resolving key is authorized for it.
  2. Check the core against its field table: closed member set, evidence_type in the registry, tier in its enum, coverage_map closed with three booleans and at least one true, non-empty honest_scope, boolean is_synthetic_fixture.
  3. Confirm tier equals the deepest true layer of coverage_map.
  4. Confirm the body carries exactly the variant section its evidence_type names, and check that section against its field table - including the integrity_tier rule for live_attestation, and the partition identities, bounds and key-set equality for benchmark.
  5. Treat every class-3 member above as a producer assertion. There is no step that verifies one.
  6. OPTIONALLY re-run the recipe at reproduce against the subject the statement binds, and compare the class-2 integers.

No live VM, no hypervisor access, and no network access are required at any step. Step 6 is checkable only against inputs the auditor brings; see the honesty section below.

What this predicate does NOT assert (coverage)

  • A verdict. No PASS / DEGRADED / FAIL, and none may be inferred. Outcomes live in security-verdict and adversarial-execution-evidence.
  • That the evidence was captured from a real run. The signature establishes that the producer committed to these values, not that anything executed. is_synthetic_fixture == false is the producer saying so, and is exactly as trustworthy as the producer.
  • That honest_scope is complete. It states limits the producer chose to state. Nothing enumerates the ones it did not.
  • That the recipe at reproduce exists, runs, or produces these numbers. The path is not digest-committed. A consumer that needs the recipe pinned obtains its digest out of band.
  • Anything about the substrate. The byte-identity, fold-correctness and clone-uniqueness families are substrate-integrity's, and a disclosure's coverage_map says nothing about them.
  • A validated hardware quote. integrity_tier == "attested" is a structural declaration; the quote's endorsement chain is out of scope and is not carried.
  • Revocation. Nothing in the statement expires. A consumer that needs revocation obtains it out of band.

What a verifier can and cannot independently check (honesty)

  • A verifier CAN confirm the statement is authentic under a key it configured and authorized for this predicate type, that it binds exactly the subject digest it intended, and that the core and the selected variant section are well-formed and internally consistent under the closed contract. All of that is offline from the statement and the key alone.
  • A verifier CAN re-derive the benchmark integers only when it supplies the external inputs - the pinned corpus, the scanner builds, the recipe. That is a real check, but relative to anchors the auditor brings, never derivable from the statement.
  • A verifier CANNOT, from the statement alone or with any anchor, establish that honest_scope is complete, that is_synthetic_fixture is truthful, that the coverage_map reflects work that happened, or that any member of live_attestation describes a real machine. These are disclosures, and the type is named for what they are.

Status and maturity

  • Format, producer, and signing: implemented. Two disclosures ship signed and verify offline: the multi-server corpus bake-off (website/public/verify-bakeoff/, generated by research/68-bakeoff-corpus/sign_corpus_evidence.py) and the always-on live-attestation shape (website/public/verify-tier3/, generated by website/public/verify-tier3/sign_live_attestation_disclosure.py). Both are regenerable; neither is hand-edited.
  • Verifier recognition: implemented. The predicate type is in the recognized set of the packaged TypeScript verifier (packages/verify), its website twin, the standalone Python verifier (website/public/verify/probity-verify.py) and the admission rego, and in the authorized-predicate set of the published evidence-signing key.
  • Standardization status: wave 2. The standard README lists this type as a wave-2 family. This document is normative for the type URI and is published so the URI resolves; wave-2 membership concerns the standardization track, not whether the format is stable.

Related standards

  • signing-envelope.md - the DSSE / in-toto envelope, keyid derivation, trust root, and offline-verify procedure this predicate uses without deviation.
  • signed-evidence - a DIFFERENT type with a different body (merkle_evidence, value_binding, fold_pins). A disclosure is not a signed-evidence statement and MUST NOT be emitted under that type.
  • security-verdict and adversarial-execution-evidence - the outcome-bearing predicates a disclosure bounds rather than replaces.
  • in-toto attestation - ALIGNED. The payload is an in-toto Statement v1; subject binding and predicateType routing are used as in-toto defines them.
  • DSSE - ALIGNED, PAEv1 and ed25519, no deviation.
  • SLSA VSA - RELATED, not overlapping. A VSA summarizes a policy evaluation's outcome; a disclosure carries no outcome and instead bounds one.

Versioning

This is the evidence-scope-disclosure v1 predicate. The core and every variant section are closed at the producer: a producer MUST NOT emit a member not defined above. Per the standard's forward-compatibility rule, a verifier MUST ignore a member it does not recognize rather than rejecting the statement, and an additive optional member is a minor change that stays within v1.

The evidence_type, tier and integrity_tier vocabularies are CLOSED registries and are fail-closed at this version: a verifier MUST reject a value it does not recognize. Adding a value - and, for evidence_type, the variant section it selects - is a spec revision that publishes a new registry row, never a silent additive change.

A change to the core's meaning, to the coverage_map layer set, to the evidence_type-to-variant mapping, or to the consistency rules is breaking and ships as a new major-version URI.

Get access

Run your agents through Probity.

A few details about your setup and we'll get you gating agents fast.