# 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](security-verdict.md) or an
[adversarial-execution-evidence](adversarial-execution-evidence.md) 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](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](https://www.rfc-editor.org/rfc/rfc2119).

## Why this type exists

The disclosure body was originally emitted under the
[signed-evidence](signed-evidence.md) 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.

```json
"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

```json
{
  "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

| Field | Req | Type | Semantics |
|---|---|---|---|
| `evidence_type` | REQUIRED | string, closed enum | Which kind of evidence this is, and therefore which variant section the body carries. See the registry below. |
| `tier` | REQUIRED | string, closed enum `static` \| `runtime` \| `live` | The deepest layer the evidence reaches. Mechanically determined by `coverage_map`; see the consistency rule below. |
| `coverage_map` | REQUIRED | object, closed, three REQUIRED booleans | Which layers the evidence covers: `static`, `runtime`, `hosting`. At least one MUST be `true`. |
| `honest_scope` | REQUIRED | non-empty string | The producer's prose statement of what the evidence does NOT show. Self-declared; see the observation table. |
| `is_synthetic_fixture` | REQUIRED | boolean | `true` 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. |
| `reproduce` | OPTIONAL | non-empty string | A 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_type` | Variant section | What the variant carries |
|---|---|---|
| `live-attestation` | `live_attestation` | The hardware-attestation posture of a long-running, continuously observed agent |
| `multi-server-benchmark` | `benchmark` | A 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"`.

| Field | Req | Type | Semantics |
|---|---|---|---|
| `integrity_tier` | REQUIRED | string, closed enum `roadmap` \| `attested` | `attested` 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_quote` | REQUIRED, nullable | base64 string or `null` | The hardware attestation quote. |
| `ephemeral_pubkey` | REQUIRED, nullable | base64 string or `null` | The per-session public key bound into the quote. |
| `observer_in_tcb` | REQUIRED | boolean | Whether 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"`.

| Field | Req | Type | Semantics |
|---|---|---|---|
| `name` | REQUIRED | non-empty string | What was compared against what, in one line. |
| `corpus` | REQUIRED | object, closed | The composition of the pinned input corpus, by count. |
| `metadata_layer_scanners` | REQUIRED | object, non-empty | One tally per scanner run at the metadata layer, keyed by scanner id. |
| `self_authored_vuln_server_tool_depth` | REQUIRED | object, non-empty | Per-scanner tool-depth result on the self-authored vulnerable server, keyed by scanner id. |
| `source_code_layer_semgrep` | REQUIRED | object, closed | The 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.**

| Member | Why it is self-declared |
|---|---|
| `honest_scope` | Prose. 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_fixture` | A 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_map` | The 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_attestation` | Including `observer_in_tcb` and `integrity_tier`. The consistency rule is structural. No member of this section is grounded by anything in the statement. |
| `reproduce` | A 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](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](security-verdict.md) and
  [adversarial-execution-evidence](adversarial-execution-evidence.md).
- **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](substrate-integrity.md)'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](README.md) 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](signing-envelope.md)** - the DSSE / in-toto envelope, `keyid` derivation,
  trust root, and offline-verify procedure this predicate uses without deviation.
- **[signed-evidence](signed-evidence.md)** - 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](security-verdict.md)** and
  **[adversarial-execution-evidence](adversarial-execution-evidence.md)** - the outcome-bearing
  predicates a disclosure bounds rather than replaces.
- **[in-toto attestation](https://github.com/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](https://github.com/secure-systems-lab/dsse)** - ALIGNED, PAEv1 and ed25519, no deviation.
- **[SLSA VSA](https://slsa.dev/spec/v1.0/verification_summary)** - 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](README.md), 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.
