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.
| Field | Req | Type | Semantics |
|---|---|---|---|
guest_memory_merkle_root | REQUIRED | string, 64 lowercase hex | The root of a per-page Merkle tree over the guest-memory image the workload resumed from. |
leaf_size_bytes | REQUIRED | positive integer | The Merkle leaf size in bytes (4096 for 4 KiB pages). Pins the tree construction so a recomputed root is comparable. |
exclude_profile | REQUIRED | array of strings, MAY be empty | The device-IO region names excluded from the byte-equality claim. |
guest_memory_merkle_rootis the FOLDED root - the commitment to the memory image the workload actually resumed from, not a fourth independently captured root. It is thefolded_merkle_rootof the paired substrate-integritybyte_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 byexclude_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_profiledraws from the same CLOSED vocabulary as substrate-integrity's fold-correctnessexclude_profile:virtio_used_ringis 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.
| Field | Req | Type | Semantics |
|---|---|---|---|
session_manifest_digest | REQUIRED | string, 64 lowercase hex | SHA-256 of the signed session manifest ([session_id, substrate_digest, corpus_digest, gate_engine_commit]). |
verdict_subject_digest | REQUIRED | string, 64 lowercase hex | SHA-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.
| Field | Req | Type | Semantics |
|---|---|---|---|
label | REQUIRED | non-empty string | A free-text identifier for the pinned value (for example base, diff, folded). |
sha256 | REQUIRED | string, 64 lowercase hex | The 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
predicateTypeis 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-evidenceis 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:
- Confirm
predicateTypeis exactlyhttps://getprobity.dev/predicate/v1/signed-evidenceand that the resolving key is authorized for it. - Check the three sections against the field tables above: closed member sets, digest forms,
leaf_size_bytespositive,exclude_profilewithin the closed vocabulary,fold_pinsnon-empty with unique labels. - OPTIONALLY recompute
guest_memory_merkle_rootagainst the guest-memory image in hand and compare. Exclusions scope the equality claim, not the tree - do NOT mask before hashing. - Confirm
value_binding.session_manifest_digestresolves to a signed session manifest the verifier holds, and thatvalue_binding.verdict_subject_digestequals the subject of the paired security-verdict. - Confirm the
fold_pinsmatch 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-v1family'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,
keyidderivation, trust root, and offline-verify procedure this predicate uses without deviation. - substrate-integrity - Attestation A. Carries the three-root
byte_identity_proofthis 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_digestbinds. - in-toto attestation - ALIGNED. The payload is an
in-toto Statement v1; subject binding and
predicateTyperouting 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.