# 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](security-verdict.md) 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](substrate-integrity.md) (Attestation A) or
[security-verdict](security-verdict.md) (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](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](https://www.rfc-editor.org/rfc/rfc2119).

## 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.

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

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

## Predicate body

```json
{
  "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_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](substrate-integrity.md)
  `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](substrate-integrity.md)'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.

| 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](security-verdict.md) 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](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](security-verdict.md).
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](substrate-integrity.md)
  `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](substrate-integrity.md), each with its own discriminator.
- **A verdict.** This predicate carries evidence, not an outcome. The PASS / DEGRADED / FAIL judgement
  lives in [security-verdict](security-verdict.md).
- **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](README.md) 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](signing-envelope.md)** - the DSSE / in-toto envelope, `keyid` derivation,
  trust root, and offline-verify procedure this predicate uses without deviation.
- **[substrate-integrity](substrate-integrity.md)** - 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](security-verdict.md)** - Attestation B. Its subject is what
  `value_binding.verdict_subject_digest` binds.
- **[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.
- **[Sigstore / cosign](https://docs.sigstore.dev/)** - 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](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 `exclude_profile` vocabulary is a CLOSED registry shared with
[substrate-integrity](substrate-integrity.md): `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.
