Skip to content

Predicate type: Assay evidence bundle

  • Type URI: https://docs.getassay.dev/attestation/evidence-bundle/v1
  • Version: 1.1
  • Predicate name: Assay evidence bundle

Purpose

This predicate binds a signed statement to the exact bytes of one completed Assay evidence bundle and describes the verified run inside it. It keeps two identifiers separate: the archive SHA-256 identifies the artifact, while the run root identifies an ordered sequence of event content-hash inputs under Assay's semantic-equivalence algorithm.

The contract follows ADR-044. A signature does not establish that the recorded observations are complete or that a provider-side outcome occurred. It does not upgrade the support carried by the evidence.

Use Cases

  • A recipient holding a finished .tar.gz bundle and its signed statement can check that the statement names those exact archive bytes and that the stated run metadata agrees with the verified bundle.
  • A recipient comparing re-exports can inspect semantic equivalence separately from artifact identity. Repackaging can change the archive digest without changing the ordered content-hash inputs. Matching semantic roots alone does not match the archives.

This predicate describes an Assay evidence bundle; it does not describe a software build or replace an artifact digest with a run identifier.

Prerequisites

Readers need the in-toto Statement v1 specification, the DSSE protocol, and the Assay evidence contract. Bundle verification and resource handling retain the boundaries in ADR-043.

An artifact-matching consumer needs the complete compressed archive bytes, the DSSE envelope, an explicitly supplied Ed25519 verification key, and applicable resource limits. Key selection and trust policy are external inputs. This predicate provides neither a trust root nor a transparency log or trusted timestamp.

Model

The producer finishes and verifies a bundle, derives the predicate from that verification, and computes the SHA-256 of the entire finished .tar.gz, including the gzip trailer. It constructs an in-toto Statement and signs its JCS-serialized bytes using Ed25519 over the DSSE pre-authentication encoding. The DSSE payloadType is application/vnd.in-toto+json; its payload is base64-encoded statement JSON.

Assay's library distinguishes two checks:

  • verify_envelope_signature checks the DSSE payload type, signature under the supplied key, unambiguous statement JSON, and Statement type. Its SignatureVerified result is signature-verified, artifact-unmatched. It has not compared any archive or predicate data.
  • verify_attestation_for_bundle and its _with_limits variant additionally verify the supplied bundle, match the artifact digest and bundle identifier, and compare every defined predicate field with values derived from that verified bundle. Their successful result is AttestationVerified.

The implementation checks the first DSSE signature using the caller-supplied key; the envelope's keyid is not a trust lookup or an authenticated identity policy. A verified signature does not by itself authenticate the producer metadata asserted in a run as an external identity.

Schema

The statement has _type, subject, predicateType, and predicate. The predicate has three required members: schema_version, semantic_equivalence, and run. All nested fields listed below are required too, except the explicitly optional extent projection and its source-dependent fields.

Parsing Rules

  1. _type must equal https://in-toto.io/Statement/v1. predicateType must equal the Type URI above. predicate.schema_version must be the integer 1. The document version 1.1 is not a separate statement field. An unrecognized predicate major version is refused, never reported as verified.
  2. Unknown fields within the known predicate major version are ignored. This does not mean their contents were verified. Missing or null required fields, wrong field types, and ambiguous JSON such as duplicate object members are refused. The Assay signed-payload reader also applies its strict JSON nesting, object-member and string limits.
  3. There must be exactly one subject and exactly one digest entry in that subject. The digest key must be sha256. No semantic-equivalence digest belongs in the subject's digest set.
  4. Before hashing the supplied archive for subject matching, the artifact verifier applies the caller's VerifyLimits through bundle verification. These cover compressed and decoded bytes, manifest and event bytes, event count, line bytes, path length, and JSON depth. The byte-slice library API receives already-materialized input; callers still need bounded source reads.
  5. The subject digest must match the SHA-256 of the full compressed archive, and the subject name must equal the verified manifest's bundle_id. An archive-entry hash or a run root is not an alternative artifact digest.
  6. Every defined predicate field must compare exactly with its value derived from the verified bundle. Verification recomputes the run root and actual event count, reads run and producer metadata from the verified manifest, and derives the time window from the events. A field mismatch is a refusal, not a warning. A zero-event bundle is refused, so the time window has no null case.

  7. Optional predicate.extent is absent or null for not stated, preserving legacy verification. A present non-null value must be an object with retained_events_by_type and a tagged observed object. New extent producers emit an object and no null within it. Missing or malformed required structures refuse. Unknown structured members remain ignored; every histogram key is data and participates in exact comparison, including surplus or zero-valued entries.

  8. observed.basis is either not_stated (no known source_type or counts member may be present) or producer_reported (both are required). A producer-reported source names one of the two summary types below. All its required counts must exist. Sandbox network is omitted and an attested sandbox network member refuses, including null. Missing/unknown basis refuses.
  9. Recognize exactly assay.profile.finished before assay.sandbox.summary, independent of event order. Validate both when present, including the unselected lower-ranked source. Refuse repeated instances of either recognized type, even if identical. No recognized summary produces observed: {"basis":"not_stated"}. assay.coding_agent.evidence_pack.v0 contributes only to the retained histogram. Malformed/repeated summary refusals apply only when deriving extent or checking present non-null extent; ordinary and absent/null-extent verification retain their acceptance rules.
  10. Every extent count is an integer in 0..9007199254740991 (0..2^53-1). This representation ceiling prevents loss through JCS number normalization; it is not host-resource policy. Histogram cardinality uses the strict parser's MAX_KEYS_PER_OBJECT (currently 10,000), and aggregate decoded UTF-8 key bytes are limited to 1 MiB. Checked arithmetic and both key bounds apply before new-key allocation/insertion. The producer checks its actual canonical Statement bytes against the consumer's strict parser; legacy metadata strings still contribute to size.

The extent claim boundary is specification prose, not a signed support_ceiling field. Retained counts are recomputed from artifact events. Observed counts are producer assertions read from verified payloads; matching them does not independently establish their truth, activity of one run, observation completeness, or an export option. Profile counts are aggregated entry cardinalities and degradation-vector length, not total runs, hit totals, or independent observations. An older reader can accept a signed extent while ignoring it: that is verification of its known fields only. Admission requiring checked extent needs an extent-checking reader and a present non-null checked extent. Absent, null or ignored extent is not an affirmative claim.

These rules describe verification of the artifact and its declared metadata. They do not prove observation completeness, policy correctness, safe execution, or an external action's outcome. The verified event count counts retained events; it does not establish how many observations were omitted. A count asserted by a producer in event data is not made independently true by signing or matching the archive. These limits are specification interpretation, not additional signed predicate fields.

Fields

All rows are required. Object rows name their required nested members in the following rows.

Field JSON type Contents and matching rule
_type string Exactly https://in-toto.io/Statement/v1.
subject array Exactly one artifact subject.
subject[0].name string Exactly the verified manifest's bundle_id. This identifier is not the archive digest.
subject[0].digest object Exactly one member, sha256.
subject[0].digest.sha256 string SHA-256 of the complete compressed archive, as 64 lowercase hexadecimal characters without a prefix.
predicateType string Exactly https://docs.getassay.dev/attestation/evidence-bundle/v1.
predicate object The three members below.
predicate.schema_version integer Exactly 1.
predicate.semantic_equivalence object algorithm and value.
predicate.semantic_equivalence.algorithm string Exactly assay-run-root-v1.
predicate.semantic_equivalence.value string Recomputed run root, sha256: followed by 64 lowercase hexadecimal characters.
predicate.run object run_id, event_count, producer, and time_window.
predicate.run.run_id string Exactly the verified manifest's run_id.
predicate.run.event_count integer Actual verified event count, greater than zero and within the applicable bundle-verification limit.
predicate.run.producer object name, version, and git.
predicate.run.producer.name string Exactly the verified manifest's producer name.
predicate.run.producer.version string Exactly the verified manifest's producer version.
predicate.run.producer.git string Exactly the manifest's producer git value, or the empty string if that optional manifest value is absent. The predicate field itself remains required.
predicate.run.time_window object start and end.
predicate.run.time_window.start string Earliest event time, rendered as RFC 3339 UTC with Z.
predicate.run.time_window.end string Latest event time, rendered as RFC 3339 UTC with Z.

Time-window bounds are chronological minima and maxima, not necessarily the first and last rows. Assay renders them with Chrono's SecondsFormat::AutoSi: no fractional part for whole seconds, otherwise milliseconds, microseconds, or nanoseconds as needed. The verifier compares those rendered strings exactly; an equivalent instant written with a different offset or fractional formatting is not interchangeable in this predicate.

The field list fixes the shape, not a constant serialized byte size: identifiers and producer metadata remain strings. The semantic root covers the ordered content-hash inputs of the events the bundle retained, as defined by the evidence contract. Changing those retained inputs can change the root. Merely choosing a different export detail option does not guarantee a different root when the retained inputs are unchanged. The root does not identify every possible export of a run, and equal roots do not assert that all event metadata or archive bytes are equal.

Optional extent fields (revision 1.1)

Field Type Meaning
predicate.extent.retained_events_by_type sorted object of counts Exact types and counts of all retained events; only encountered types are emitted.
predicate.extent.observed.basis string producer_reported or not_stated.
predicate.extent.observed.source_type string, required for producer_reported Selected recognized summary type; absent for not_stated.
predicate.extent.observed.counts object, required for producer_reported Source-dependent entry counts; absent for not_stated.
Summary type files network processes sandbox_degradations
assay.profile.finished files_count network_count processes_count sandbox_degradation_count
assay.sandbox.summary fs_count omitted: not stated exec_count degradation_count

All mapped source fields are required exact-domain integers. Selection rank is deterministic, not a ranking of trust. Program-entry counts remain entry counts, not sums of per-program hits. Derivation reads generic verified event payloads and does not substitute the older typed profile summary decoder. A mismatch names a static field path without echoing supplied values.

Example

This complete in-toto Statement is technically illustrative JSON, not real evidence. Its identifiers and digest values are invented; no archive or signature is supplied, and it is not a verification fixture. A real producer must derive these values from the same verified archive. This is the statement carried inside a DSSE envelope, not the envelope itself. It illustrates the still-supported v1.0 producer shape, with extent absent.

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
      "digest": {
        "sha256": "2222222222222222222222222222222222222222222222222222222222222222"
      }
    }
  ],
  "predicateType": "https://docs.getassay.dev/attestation/evidence-bundle/v1",
  "predicate": {
    "schema_version": 1,
    "semantic_equivalence": {
      "algorithm": "assay-run-root-v1",
      "value": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
    },
    "run": {
      "run_id": "illustrative-run",
      "event_count": 2,
      "producer": {
        "name": "illustrative-producer",
        "version": "1.0.0",
        "git": ""
      },
      "time_window": {
        "start": "2026-09-07T08:00:00Z",
        "end": "2026-09-07T08:00:01.250Z"
      }
    }
  }
}

Changelog and Migrations

Version 1.0 documents the artifact-subject contract introduced by ADR-044. It uses the v1 Type URI and schema_version: 1.

Legacy statements under https://assay.dev/attestation/evidence-bundle/v0 put a semantic run root in the subject digest. The artifact verifier refuses that predicate explicitly. Relabeling a v0 statement cannot recover the archive bytes it did not identify: a v1 statement must be derived from the actual verified archive and signed again.

The Rust library retains deprecated compatibility names: EVIDENCE_BUNDLE_PREDICATE_TYPE still names v0, and statement_from_manifest still constructs v0 statements. The v1 producer entrypoint is statement_for_bundle (or its _with_limits variant). These API names do not alter the wire version rules above.

Version 1.1 implements the optional extension decided in ADR-049 without changing the Type URI, schema major, legacy fields, or existing public Rust struct layouts. statement_for_bundle_with_extent and statement_for_bundle_with_extent_and_limits explicitly opt producers in; legacy producers continue emitting v1.0. verify_attestation_for_bundle_with_extent and its _with_extent_and_limits variant expose optional checked extent alongside the original result. Existing artifact-verification APIs check present extent too, discarding only the additive result projection. All use the same canonical verification pass. Signature-only verification remains artifact-unmatched. This library change does not opt the CLI producer in or change CLI presentation.