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

Probity Signed-Evidence Envelope, v1

predicateType: https://getprobity.dev/predicate/v1/signed-evidence

A signed-evidence statement carries, under one signature, the three pieces of evidence a security-verdict references but does not itself contain: the guest-memory Merkle commitment for a scan, the value-binding that ties that commitment to a signed session and to the verdict subject, and the cross-host differential-snapshot fold pins. Before this predicate existed those three travelled separately and the fold pins travelled unsigned; folding them into one predicate lets an auditor check all three in a single offline verification instead of reconciling separate artifacts.

It does not replace substrate-integrity (Attestation A) or security-verdict (Attestation B). It complements them by signing the evidence those attestations reference.

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 reference implementation is the Python verdict_bundle package: build_signed_evidence_statement and emit_signed_evidence (_statements.py, _signing.py), the _MerkleEvidence / _ValueBinding / _FoldPin contract models (_models.py), and signed_evidence_sections_from_proof (_loaders.py), which derives the merkle_evidence section from a validated byte-identity proof.

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

Subject

The subject is the artifact the evidence is about, by digest - typically the customer MCP-server OCI image whose verdict the evidence binds to.

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

The digest is 64 lowercase hexadecimal characters with no sha256: prefix.

Predicate body

{
  "merkle_evidence": {
    "guest_memory_merkle_root": "<sha256>",
    "leaf_size_bytes": 4096,
    "exclude_profile": ["virtio_used_ring"]
  },
  "value_binding": {
    "session_manifest_digest": "<sha256>",
    "verdict_subject_digest": "<sha256>"
  },
  "fold_pins": [
    {"label": "base", "sha256": "<sha256>"},
    {"label": "diff", "sha256": "<sha256>"}
  ]
}

All three sections are REQUIRED. Each section is closed: a producer MUST NOT emit a member not listed below, and the reference producer refuses to sign a section carrying one.

merkle_evidence

The guest-memory Merkle commitment for the scan.

FieldReqTypeSemantics
guest_memory_merkle_rootREQUIREDstring, 64 lowercase hexThe root of a per-page Merkle tree over the guest-memory image the workload resumed from.
leaf_size_bytesREQUIREDpositive integerThe Merkle leaf size in bytes (4096 for 4 KiB pages). Pins the tree construction so a recomputed root is comparable.
exclude_profileREQUIREDarray of strings, MAY be emptyThe device-IO region names excluded from the byte-equality claim.
  • guest_memory_merkle_root is the FOLDED root - the commitment to the memory image the workload actually resumed from, not a fourth independently captured root. It is the folded_merkle_root of the paired substrate-integrity byte_identity_proof, carried here so this statement stands alone. The base and diff roots stay in that proof and are deliberately not duplicated: duplicating them would add bytes, not trust. Every page is hashed unmodified, including pages named by exclude_profile; the profile scopes the byte-EQUALITY claim between images, never the tree construction. A verifier recomputing this root MUST NOT mask before hashing.
  • exclude_profile draws from the same CLOSED vocabulary as substrate-integrity's fold-correctness exclude_profile: virtio_used_ring is the only legal v1 entry. An entry outside it MUST be rejected (fail-closed; no warn tier). The names are public; the byte offsets behind them are not. An empty list means nothing was excluded - a pure byte-identity fold.

value_binding

Binds the Merkle evidence to the signed session and to the verdict it supports.

FieldReqTypeSemantics
session_manifest_digestREQUIREDstring, 64 lowercase hexSHA-256 of the signed session manifest ([session_id, substrate_digest, corpus_digest, gate_engine_commit]).
verdict_subject_digestREQUIREDstring, 64 lowercase hexSHA-256 of the paired security-verdict subject (the customer MCP OCI digest).

session_manifest_digest ties the evidence to the exact injection-time session, so the evidence cannot be re-pointed at a different run. verdict_subject_digest binds the evidence to the verdict it stands behind.

fold_pins

The cross-host differential-snapshot fold pins, signed here so they are no longer carried unsigned.

FieldReqTypeSemantics
labelREQUIREDnon-empty stringA free-text identifier for the pinned value (for example base, diff, folded).
sha256REQUIREDstring, 64 lowercase hexThe pinned digest.

fold_pins MUST be a non-empty array, and its label values MUST be unique across the array: a duplicate label makes the pin set ambiguous and MUST be rejected. Each entry commits one input or output of the fold, so a verifier can confirm the fold's pinned values were the ones signed.

Producer validation

The reference producer validates every section against the contract models above (each closed to exactly the members listed, non-empty fold_pins with unique labels, a 64-lowercase-hex subject digest, exclude_profile drawn from the closed vocabulary, leaf_size_bytes a positive integer) 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.

The validated sections are re-emitted as plain data and serialized under RFC 8785 (JSON Canonicalization Scheme) - the same canonicalization the Go side applies - to produce the exact payload bytes the DSSE signature commits to. Earlier revisions of this document described the serialization as "JCS-like (sorted keys, compact separators)". That description was wrong in a way that matters: RFC 8785 sorts member names by UTF-16 code unit and pins number serialization, and an independent producer implementing the looser description would emit different bytes for the same predicate and its signature would not verify. RFC 8785 is normative.

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. A verifier MUST reject a statement whose predicateType is not in the resolving key's authorized set, so a key issued for trace records cannot mint an evidence statement even though both verify under ed25519. https://getprobity.dev/predicate/v1/signed-evidence is in the authorized set of the producer's evidence-signing key.
  • There is no transparency log, and its absence is deliberate. Signing does not upload to a public log, so there is no third-party inclusion proof, no independent timestamp, and no public record of the digests a statement binds. That is the point: 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. An earlier revision of this document instructed a verifier to check "optional Rekor inclusion"; no such entry has ever existed for this predicate, and following that instruction would have led a verifier to conclude the artifact was unverifiable.

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

  1. Confirm predicateType is exactly https://getprobity.dev/predicate/v1/signed-evidence and that the resolving key is authorized for it.
  2. Check the three sections against the field tables above: closed member sets, digest forms, leaf_size_bytes positive, exclude_profile within the closed vocabulary, fold_pins non-empty with unique labels.
  3. OPTIONALLY recompute guest_memory_merkle_root against the guest-memory image in hand and compare. Exclusions scope the equality claim, not the tree - do NOT mask before hashing.
  4. Confirm value_binding.session_manifest_digest resolves to a signed session manifest the verifier holds, and that value_binding.verdict_subject_digest equals the subject of the paired security-verdict.
  5. Confirm the fold_pins match the fold inputs and outputs the verifier expects.

No live VM, no hypervisor access, and no network access are required at any step. Steps 3 through 5 are checkable only against anchors the auditor brings; see the honesty section below.

What this predicate does NOT assert (coverage)

  • That the evidence was captured from a real run. The signature establishes that the producer committed to these values, not that a workload executed. A verifier confirms integrity, subject binding, and internal consistency, never external re-execution.
  • Fold correctness. The pins commit values; the claim that the fold reconstructs exactly the bytes Firecracker serialized is the substrate-integrity fold-correctness-v1 family's, and MUST NOT be inferred from a signed-evidence statement.
  • Entropy or clone uniqueness. Those are separate proof families under substrate-integrity, each with its own discriminator.
  • A verdict. This predicate carries evidence, not an outcome. The PASS / DEGRADED / FAIL judgement lives in security-verdict.
  • Public discoverability or non-repudiation via a transparency log. See Verification.
  • 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 all three sections are well-formed under the closed contract. All of that is offline from the statement and the key alone.
  • A verifier CAN recompute the Merkle root, resolve the session-manifest binding, and match the fold pins only when it supplies the external references - the guest-memory image, the session manifest, the paired verdict. These are real checks, but relative to anchors the auditor brings, never derivable from the statement alone.
  • A verifier CANNOT, from the statement alone, establish that the memory image was captured from the workload the subject names, or that the pinned fold values came from the fold they claim to.

Status and maturity

  • Format, producer, and signing: implemented. The statement builder, the closed contract models, and the DSSE signing path exist and are exercised end to end by the chain harness in scripts/e2e_step0.py, which emits a signed-evidence bundle and offline-verifies it in the same run.
  • Verifier recognition: implemented. The predicate type is in the recognized set of both the TypeScript verifier (packages/verify) and the Python verifier (website/public/verify/probity-verify.py), and in the authorized-predicate set of the published evidence-signing key. Structural gates beyond envelope verification and predicate-type authorization are the producer's; a consumer that needs them applies the field tables above.
  • Standardization status: wave 2. The standard README lists signed-evidence 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.
  • substrate-integrity - Attestation A. Carries the three-root byte_identity_proof this predicate's folded root is drawn from, and the fold-correctness, clone-uniqueness, and VMGenID-reseed claims this predicate does not make.
  • security-verdict - Attestation B. Its subject is what value_binding.verdict_subject_digest binds.
  • 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.
  • Sigstore / cosign - NOT USED. This predicate is a DSSE envelope signed with a static ed25519 key; there is no Fulcio certificate and no Rekor transparency-log entry. A DSSE-aware tool can verify it given the payload type and the public key.

Versioning

This is the signed-evidence v1 predicate. Its three sections 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 exclude_profile vocabulary is a CLOSED registry shared with substrate-integrity: virtio_used_ring is the only legal v1 entry and a verifier MUST reject any entry it does not recognize. Adding a name is a spec revision that ships a new published registry list, never a silent additive change a deployed verifier is expected to tolerate.

A change to the Merkle construction, to the shape or meaning of any of the three sections, or to the binding semantics 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.