SPECIFICATIONS IN-TOTO PREDICATE TYPE

Attestree catalog evidence bundle, v1

The predicate of the signed statement over a catalog evidence export: one row per exported package version, each pointing at that version’s own signed statement and SBOM files, or saying why it has none.

TYPE URI
https://attestree.com/attestation/evidence-bundle/v1
KIND
in-toto predicate type
USED AS
predicateType of the in-toto Statement v1 in attestation.intoto.jsonl, at the root of a catalog evidence bundle
SIGNED BY
The deployment’s catalog-attest key: ECDSA P-256 with SHA-256, in a DSSE envelope
EMITTED SINCE
Platform 0.240.0
REPLACES
https://wep.dev/detonation/v1 as the bundle’s predicate. Its bundle-level detonation fields described only the first row; verifiers still accept it for bundles exported before, and say so.
PAGE UPDATED
2026-10-05

The bundle

The catalog’s Export evidence action (POST /api/v1/catalog/export-evidence) writes one archive, catalog-evidence-bundle-<id>.tar.zst. At its root, attestation.intoto.jsonl is a DSSE envelope (payloadType application/vnd.in-toto+json) holding an in-toto Statement v1 with this predicate. Beside it are manifest.json with its detached signature and certificate, README.txt, and one keys.json (format attestree-keys/v1, the same document the platform serves at GET /api/v1/attestation/keys). Each exported version has a directory, evidence/<id>@<version>/, holding its package.json and provenance.json and, when they exist:

  • statement.intoto.jsonl: the version’s own detonation statement, byte for byte;
  • sbom.cdx.json: the stored CycloneDX document, a third-party SBOM;
  • sbom.summary.cdx.json: its component summary.

The subjects

One subject, with its SHA-256, for every entry in the archive except the envelope itself. Every file but the envelope is a subject, and every subject is a file. The manifest signature commits the list of exported versions; the subjects are what commit the files’ bytes.

The predicate

Every field is always present; a value that does not apply is null.

{
  "bundleId": "<uuid, as manifest.json names it>",
  "tenant": "<issuer claim the bundle was exported from>",
  "evidenceIdsSha256": "<manifest.json evidenceIdsSha256>",
  "rows": [
    {
      "package": { "manager": "winget", "id": "Contoso.App", "version": "1.0" },
      "path": "evidence/[email protected]",
      "resultId": "<uuid of the run the row exports> | null",
      "installerSha256": "<lowercase hex, that run's installer digest> | null",
      "statement": { "digest": { "sha256": "…" }, "keyid": "<lowercase SPKI SHA-256>" } | null,
      "statementAbsent": null | "<reason>",
      "sbom": { "digest": { "sha256": "…" }, "summaryDigest": { "sha256": "…" } | null } | null,
      "sbomAbsent": null | "<reason>"
    }
  ]
}
FieldMeaning
bundleIdThe export, as manifest.json names it.
tenantThe tenant the bundle was exported from.
evidenceIdsSha256The manifest’s value: the SHA-256 of the sorted, comma-joined package@version list.
rowsOne per exported version, in package@version ordinal order, which is the archive’s order.
rows[].pathThe version’s directory. The file names inside it are fixed.
rows[].resultIdThe detonation run the row exports: the version’s newest result since its ingest. null when there is no run.
rows[].installerSha256That run’s installer digest.
rows[].statementThe row statement’s payload digest (the digest the platform stores) and its keyid. The envelope file’s own digest is in the subjects.
rows[].statementAbsentnull exactly when statement is set. Otherwise: not-detonated, manual-attest, not-attested, sign-failed, attested-before-statements, statement-mismatch or statements-unavailable.
rows[].sbomThe digests of sbom.cdx.json and, when present, sbom.summary.cdx.json.
rows[].sbomAbsentnull when sbom is set, and when there is no run. Otherwise: no-install-location, no-footprint, unparseable, too-large, not-retained, not-produced, run-not-succeeded, sbom-store-unavailable or sbom-not-in-statement.

A row’s statement is picked by the row’s resultId, never as “the version’s current statement”, and the platform checks it against the row before writing a byte. A stored statement about another run or another installer is left out, and the row reads statement-mismatch. A row only carries the SBOM files its statement names.

What the signature means

The platform exported exactly these files. The bundle statement makes no detonation claim of its own: it states no network posture, CA, tier or engine at bundle level, so a bundle of versions that were never detonated signs no detonation claim. Each row’s detonation facts are signed in that row’s own statement.

The per-row check

For every row that cites a statement, a verifier reads <path>/statement.intoto.jsonl and requires all of:

  1. one single-signature DSSE envelope with payloadType application/vnd.in-toto+json;
  2. its decoded payload hashes to statement.digest.sha256;
  3. its keyid equals statement.keyid;
  4. it is an in-toto Statement v1 with predicateType https://attestree.com/attestation/detonation/v1;
  5. it names exactly one subject, whose digest.sha256 equals the row’s installerSha256;
  6. its predicate.package equals the row’s package;
  7. its predicate.run.resultId equals the row’s resultId;
  8. its predicate.tenant equals the bundle predicate’s tenant.

Its signature is checked against the same trust anchor as the bundle statement. Where the row cites an SBOM, the row statement’s SBOM and summary digests must name the same files.

How to verify

wep-bundle-verify catalog-bundle catalog-evidence-bundle-<id>.tar.zst --expected-fingerprint <sha256>

Or pass --key keys.json with a key file you took from the console. With neither, the verifier exits 15: the bundle’s own certificate and keys.json are never an anchor on their own. It checks the bundle statement, that its subjects are exactly the archive’s other files byte for byte, the manifest’s evidenceIdsSha256 and manifest.json.sig under the same key, and the per-row check above under the same key and pins. A row signed by a key the bundle carries but your anchor does not hold fails. The output counts the rows, and the rows without a statement by reason.