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>"
}
]
}
| Field | Meaning |
|---|---|
bundleId | The export, as manifest.json names it. |
tenant | The tenant the bundle was exported from. |
evidenceIdsSha256 | The manifest’s value: the SHA-256 of the sorted, comma-joined package@version list. |
rows | One per exported version, in package@version ordinal order, which is the archive’s order. |
rows[].path | The version’s directory. The file names inside it are fixed. |
rows[].resultId | The detonation run the row exports: the version’s newest result since its ingest. null when there is no run. |
rows[].installerSha256 | That run’s installer digest. |
rows[].statement | The row statement’s payload digest (the digest the platform stores) and its keyid. The envelope file’s own digest is in the subjects. |
rows[].statementAbsent | null exactly when statement is set. Otherwise: not-detonated, manual-attest, not-attested, sign-failed, attested-before-statements, statement-mismatch or statements-unavailable. |
rows[].sbom | The digests of sbom.cdx.json and, when present, sbom.summary.cdx.json. |
rows[].sbomAbsent | null 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:
- one single-signature DSSE envelope with
payloadTypeapplication/vnd.in-toto+json; - its decoded payload hashes to
statement.digest.sha256; - its
keyidequalsstatement.keyid; - it is an in-toto Statement v1 with
predicateTypehttps://attestree.com/attestation/detonation/v1; - it names exactly one subject, whose
digest.sha256equals the row’sinstallerSha256; - its
predicate.packageequals the row’spackage; - its
predicate.run.resultIdequals the row’sresultId; - its
predicate.tenantequals the bundle predicate’stenant.
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.