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.gzbundle 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_signaturechecks the DSSE payload type, signature under the supplied key, unambiguous statement JSON, and Statement type. ItsSignatureVerifiedresult is signature-verified, artifact-unmatched. It has not compared any archive or predicate data.verify_attestation_for_bundleand its_with_limitsvariant 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 isAttestationVerified.
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¶
_typemust equalhttps://in-toto.io/Statement/v1.predicateTypemust equal the Type URI above.predicate.schema_versionmust be the integer1. The document version1.1is not a separate statement field. An unrecognized predicate major version is refused, never reported as verified.- 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.
- 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. - Before hashing the supplied archive for subject matching, the artifact verifier applies the caller's
VerifyLimitsthrough 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. - 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. -
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.
-
Optional
predicate.extentis absent or null for not stated, preserving legacy verification. A present non-null value must be an object withretained_events_by_typeand a taggedobservedobject. 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. observed.basisis eithernot_stated(no knownsource_typeorcountsmember may be present) orproducer_reported(both are required). A producer-reported source names one of the two summary types below. All its required counts must exist. Sandboxnetworkis omitted and an attested sandbox network member refuses, including null. Missing/unknown basis refuses.- Recognize exactly
assay.profile.finishedbeforeassay.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 producesobserved: {"basis":"not_stated"}.assay.coding_agent.evidence_pack.v0contributes 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. - 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'sMAX_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.