# Probity Security-Verdict, v1

**predicateType:** `https://getprobity.dev/predicate/v1/security-verdict`

A security-verdict is a signed, offline-verifiable statement that a single named artifact was run in
isolation, attacked with a pinned corpus of exploits, and given a PASS or FAIL verdict with per-attack
evidence. It is the product output of an adversarial scan: an admission controller, a CI gate, or an
auditor reads it to decide whether to admit the artifact, without re-running the scan and without
trusting the tool that produced it.

This document specifies the predicate body. The signing envelope, subject binding, and offline-verify
mechanics are in [signing-envelope.md](signing-envelope.md). The forensic evidence the verdict
references is specified in [catch-record.md](catch-record.md).

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).

## Subject

The subject is the **scanned artifact, by OCI digest**. This is the deployable image the verdict is
about, so an admission controller or registry can link the verdict to the exact image it is about to
admit.

```json
"subject": [
  { "name": "<scanned-artifact-name>", "digest": { "sha256": "<64-hex-oci-digest>" } }
]
```

## Predicate

```json
{
  "verdict": "fail",
  "verdict_reason": "rule cve:CVE-2025-53109 (CRITICAL) bypassed expected layer 'substrate.vfs_hook'",
  "session_id": "<session-id-from-signed-session-manifest>",
  "corpus_digest": "<sha256-of-packaged-attack-corpus>",
  "gate_engine_commit": "<auditor-pinnable-engine-identity>",
  "substrate_ref": { "name": "<substrate-name>", "digest": { "sha256": "<64-hex>" } },
  "policy": { "id": "<policy-id>", "blocking_severities": ["CRITICAL", "HIGH"] },
  "attack_results": [
    {
      "rule_id": "cve:CVE-2025-53109",
      "severity": "CRITICAL",
      "expected_layer": "substrate.vfs_hook",
      "blocked": false,
      "actual_layer": null,
      "full_payload_sha256": "<64-hex>"
    }
  ],
  "evidence_ref": {
    "batch_root": "<64-hex-rfc6962-batch-root>",
    "bundle_id": "<64-hex-cas-bundle-id>"
  },
  "execution_envelope": { "trace_id": "<opaque-trace-id>" },
  "compromised_state_ciphertext_digest": "<sha256-on-FAIL-only>"
}
```

## Field semantics

- **`verdict`** (REQUIRED) - `pass` or `fail` (lowercase). Determined by the `policy`, not by a naive
  "every attack blocked" rule.

- **`verdict_reason`** (REQUIRED) - a human-readable summary of why the verdict is what it is. On a
  fail it names the first blocking-tier bypass; on a pass it states that all blocking-tier classes
  were contained. This is a display string; automated consumers use the structured fields.

- **`session_id`** (REQUIRED) - the session id from the producer's signed session manifest, which
  binds together the session id, substrate digest, corpus digest, and engine identity at the moment
  the corpus was injected. It binds the verdict to a specific injection event: a verifier with the
  manifest can confirm the corpus was injected into the named substrate for this exact run, rather
  than stitched together afterward. The manifest proves INJECTION; `corpus_digest` alone proves only
  the corpus DEFINITION.

- **`corpus_digest`** (REQUIRED) - the SHA-256 (64-char lowercase hex) of the packaged attack corpus
  the run was produced against. It pins the INPUT: the signed verdict commits to a specific set of
  exploit payloads, so a third party need not trust that the producer threw the *named* exploits
  rather than something else. **Honesty:** `corpus_digest` is NOT a solo completeness guarantee. It
  proves *which* corpus package was used; it does not, by itself, prove the corpus was *exhaustive* or
  that its payloads are genuinely adversarial. Whether the corpus is the complete approved set is
  verified out-of-band against an auditor-supplied anchor (see Value-binding anchors). On its own,
  `corpus_digest` is a pin on the input identity, not a proof of input quality or coverage.

- **`gate_engine_commit`** (REQUIRED) - an **auditor-pinnable identity of the engine** that produced
  the verdict, in the spirit of an SLSA `builder.id`. Its purpose is identity-binding, not source
  reproduction: an auditor who knows the approved engine identity can confirm the verdict was produced
  by that engine, and the value also participates in the signed session manifest binding. **Honesty:**
  a third party cannot, from this string alone, re-derive or re-run the engine; the field lets an
  auditor *pin and compare* an identity it already knows, not independently reconstruct the producer.

- **`substrate_ref`** (REQUIRED) - a reference (name + digest) to the isolation substrate the scan ran
  on. In this standard the substrate is an opaque, separately attested input identified only by its
  digest; how the substrate's integrity is attested is a wave-2 concern (see
  [README.md](README.md)). A verifier treats `substrate_ref.digest` as the value to check against an
  auditor-supplied approved digest and against a revocation log (below).

- **`policy`** (REQUIRED) - the severity-tier ruleset. `id` names the ruleset (versioned independently
  of the predicate so the ruleset can evolve without bumping the predicate). `blocking_severities`
  lists the severity tiers whose unblocked attack forces a `fail`.

- **`attack_results`** (REQUIRED) - one entry per attack in the corpus. (This generalizes an
  earlier CVE-only shape: an attack is no longer assumed to be a CVE.) Each entry:
  - **`rule_id`** (REQUIRED) - a namespaced rule identifier. The namespace before the colon names the
    rule family; the remainder is the family-specific id. CVE is ONE namespace among others:
    `cve:CVE-2025-53109`. Other rule families use their own namespace prefixes. A bare, un-namespaced
    id is non-conforming.
  - **`severity`** (REQUIRED) - the severity tier of the rule.
  - **`expected_layer`** (REQUIRED) - the [registry](vocabulary-registry.md) `layer` value expected to
    block this attack.
  - **`blocked`** (REQUIRED) - `true` iff a catch-record was emitted whose normalized layer equals
    `expected_layer` (and whose violation type matches). Decided over the normalized
    [vocabulary registry](vocabulary-registry.md) values.
  - **`actual_layer`** (REQUIRED) - the `layer` that actually fired, or `null` if none did.
  - **`full_payload_sha256`** (OPTIONAL) - the SHA-256 of the exact payload thrown for this attack,
    using the canonical key `full_payload_sha256` (matching the catch-record key). Pins the per-attack
    input down to the byte.

- **`evidence_ref`** (REQUIRED) - a REFERENCE to the forensic evidence, not the evidence itself. The
  verdict does NOT embed catch-records inline (which would make the envelope unbounded). Instead it
  carries:
  - **`batch_root`** (REQUIRED) - the RFC 6962-style batch Merkle root over the run's ordered
    catch-records (see [catch-record.md](catch-record.md)).
  - **`bundle_id`** (REQUIRED) - the content-addressed SHA-256 of the verdict bundle, the key under
    which the catch-records are resolvable from content-addressed storage.

  A verifier resolves the catch-records from content-addressed storage by `bundle_id`, recomputes the
  batch root, and confirms it equals `batch_root` before trusting any `blocked: true`. The records are
  the evidence behind every block; the reference keeps the verdict bounded while keeping the evidence
  cryptographically tied to it.

- **`execution_envelope`** (OPTIONAL) - application-level correlation metadata for the run (for
  example an opaque trace id). It carries no security claim; it is a convenience pointer for
  correlating the verdict with other observability data.

- **`compromised_state_ciphertext_digest`** (OPTIONAL, top-level) - present ONLY on a `fail` verdict
  that captured a forensic snapshot of the compromised state. It is the SHA-256 of that snapshot,
  encrypted to the customer's public key. The producer proves the snapshot exists and is bound to the
  verdict; only the customer can decrypt the snapshot itself. **Honesty:** a verifier confirms a
  ciphertext digest is committed in the signed verdict, not that the plaintext is a faithful capture
  of the compromised state (the verifier cannot decrypt it).

## Verdict rule (severity-tiered)

1. For each attack: `blocked = true` iff there exists a catch-record (in the referenced batch) whose
   normalized `layer` equals the attack's `expected_layer` and whose `violation_type` matches.
2. `verdict = fail` iff there exists an attack with `severity` in `policy.blocking_severities` and
   `blocked == false`; otherwise `verdict = pass`.
3. `verdict_reason` names the first blocking-tier bypass on a fail, or states that all blocking-tier
   classes were contained on a pass.

Per-attack results are ALWAYS recorded regardless of verdict. A miss in a non-blocking tier is
recorded as evidence, not as a fail.

## Verification (third-party, offline)

After the envelope checks in [signing-envelope.md](signing-envelope.md) succeed (signature, `keyid`,
`predicateType`, subject non-empty):

1. Confirm the `subject` digest equals the artifact the verifier intends to admit.
2. Validate every `attack_results[].rule_id` is namespaced, and every `expected_layer` /
   `actual_layer` (when non-null) is a value in the [vocabulary registry](vocabulary-registry.md).
3. Resolve the catch-records by `evidence_ref.bundle_id`, verify them per
   [catch-record.md](catch-record.md), recompute the RFC 6962 batch root, and confirm it equals
   `evidence_ref.batch_root`. Confirm that every `blocked: true` is backed by a matching catch-record.
4. Apply the auditor-supplied value-binding anchors (below).
5. Apply the substrate revocation check (below) before trusting a `pass`.

No live execution environment is required.

### Value-binding anchors (auditor-supplied)

A signature proves the bytes were not tampered with; it does not by itself stop two residual
forgeries. An auditor (who alone knows the approved reference values) supplies two external anchors,
checked alongside the verdict:

- **Exhaustion** - the auditor supplies the COMPLETE `rule_id` set of the approved corpus. The
  verdict's `attack_results` MUST cover exactly that set. A verdict that silently omits attacks (a
  short `attack_results`) is rejected, defeating a skip-the-hard-attacks forgery. This is the
  out-of-band check that backs the honesty caveat on `corpus_digest`: exhaustion is established
  against the auditor's anchor, not asserted by the verdict alone.

- **Substrate freshness** - the auditor supplies the approved substrate digest. `substrate_ref.digest`
  MUST equal it. A valid signature over a real-but-stale substrate is rejected.

These anchors are independently verifiable, but only relative to values the auditor brings; a verifier
with no auditor anchors can still check the signature, the subject binding, and the evidence, but
cannot establish exhaustion or freshness from the verdict alone.

### Substrate revocation (OpenVEX)

A cryptographic signature never expires, but the substrate a verdict attests can be retroactively
broken: a vulnerability disclosed AFTER the verdict was signed can turn a once-valid `pass` into a
stale one. Before trusting a `pass`, a verifier MUST consult an
[OpenVEX](https://github.com/openvex/spec) revocation log for the `substrate_ref` digest. If the log
carries an `affected` or `under_investigation` statement against that digest, the verifier MUST treat
the `pass` as revoked regardless of signature validity. This closes the time-of-check / time-of-use
gap on the substrate.

In v1 the revocation log MAY be a single static published document the verifier fetches and checks
against the `substrate_ref` digest. An automated feed that opens a VEX statement when a
substrate-relevant vulnerability is disclosed is roadmap, not v1.

## Related standards

- **[SLSA Verification Summary Attestation (VSA)](https://slsa.dev/spec/v1.0/verification_summary)** -
  a security-verdict plays a role analogous to a VSA (a signed statement that a verification ran and
  what it concluded). `gate_engine_commit` is modeled on the VSA `builder.id` idea: an
  auditor-pinnable identity for the producer of the verdict. A security-verdict diverges from a VSA in
  carrying adversarial per-attack results and a forensic-evidence reference rather than a
  policy-evaluation summary. A consumer that wants a VSA-shaped summary can derive one from a
  security-verdict.
- **[OpenVEX](https://github.com/openvex/spec)** - used for substrate revocation, as above.
- **[in-toto](https://github.com/in-toto/attestation) / [DSSE](https://github.com/secure-systems-lab/dsse)**
  - the envelope; see [signing-envelope.md](signing-envelope.md).
- **[catch-record](catch-record.md)** - the referenced forensic evidence.

## Versioning

This is the security-verdict v1 predicate. Adding a new optional field is additive and stays within
v1; a verifier ignores fields it does not recognize. A change to the verdict rule, a rename or
removal of a field, or a change to the `attack_results` shape is breaking and requires a new major
version. The `policy` ruleset is versioned independently via `policy.id`.
