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. The forensic evidence the verdict references is specified in catch-record.md.
The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in RFC 2119.
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.
"subject": [
{ "name": "<scanned-artifact-name>", "digest": { "sha256": "<64-hex-oci-digest>" } }
]
Predicate
{
"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) -passorfail(lowercase). Determined by thepolicy, 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_digestalone 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_digestis 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_digestis 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 SLSAbuilder.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). A verifier treatssubstrate_ref.digestas the value to check against an auditor-supplied approved digest and against a revocation log (below). -
policy(REQUIRED) - the severity-tier ruleset.idnames the ruleset (versioned independently of the predicate so the ruleset can evolve without bumping the predicate).blocking_severitieslists the severity tiers whose unblocked attack forces afail. -
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 registrylayervalue expected to block this attack.blocked(REQUIRED) -trueiff a catch-record was emitted whose normalized layer equalsexpected_layer(and whose violation type matches). Decided over the normalized vocabulary registry values.actual_layer(REQUIRED) - thelayerthat actually fired, ornullif none did.full_payload_sha256(OPTIONAL) - the SHA-256 of the exact payload thrown for this attack, using the canonical keyfull_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).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 equalsbatch_rootbefore trusting anyblocked: 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 afailverdict 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)
- For each attack:
blocked = trueiff there exists a catch-record (in the referenced batch) whose normalizedlayerequals the attack'sexpected_layerand whoseviolation_typematches. verdict = failiff there exists an attack withseverityinpolicy.blocking_severitiesandblocked == false; otherwiseverdict = pass.verdict_reasonnames 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 succeed (signature, keyid,
predicateType, subject non-empty):
- Confirm the
subjectdigest equals the artifact the verifier intends to admit. - Validate every
attack_results[].rule_idis namespaced, and everyexpected_layer/actual_layer(when non-null) is a value in the vocabulary registry. - Resolve the catch-records by
evidence_ref.bundle_id, verify them per catch-record.md, recompute the RFC 6962 batch root, and confirm it equalsevidence_ref.batch_root. Confirm that everyblocked: trueis backed by a matching catch-record. - Apply the auditor-supplied value-binding anchors (below).
- 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_idset of the approved corpus. The verdict'sattack_resultsMUST cover exactly that set. A verdict that silently omits attacks (a shortattack_results) is rejected, defeating a skip-the-hard-attacks forgery. This is the out-of-band check that backs the honesty caveat oncorpus_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.digestMUST 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 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) -
a security-verdict plays a role analogous to a VSA (a signed statement that a verification ran and
what it concluded).
gate_engine_commitis modeled on the VSAbuilder.ididea: 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 - used for substrate revocation, as above.
- in-toto / DSSE
- the envelope; see signing-envelope.md.
- catch-record - 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.