Skip to content
New kind of attack — caught and fixed before it was publicly known·Read the timeline
Probity Open Predicate Standard
/predicate/v1/sut-provenance
View raw .md →

Probity SUT-Provenance, v1

predicateType: https://getprobity.dev/predicate/v1/sut-provenance

A sut-provenance attestation is a signed, offline-verifiable statement that a scanned System-Under-Test (SUT) image was built from a known base image and a fully integrity-pinned dependency closure. It binds the deployable SUT image (by OCI digest) to the exact, reproducible set of dependencies that went into it, so a verdict produced against that image can be tied to a verifiable build input rather than an opaque, unpinned one.

This document specifies the predicate body. The signing envelope, subject binding, and offline-verify mechanics are in signing-envelope.md. The wire shape is additionally pinned by the JSON Schema SSOT at spec/schemas/v1/sut-provenance-v1.schema.json.

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in RFC 2119.

Subject

The subject is the built SUT image, by OCI digest, scoped by the catalog server name:

"subject": [
  { "name": "probity-sut/<server-name>", "digest": { "sha256": "<64-hex-oci-digest>" } }
]

Predicate

{
  "sut_provenance_proof": {
    "proof_type": "sut-provenance-v1",
    "server_name": "<catalog-server-name>",
    "packaging": "npm",
    "base_image_digest": "<64-hex>",
    "base_image_ref": "node:20-bookworm-slim",
    "builder": {
      "tool": "catalog/build_suts.py",
      "commit": "<7-to-40-hex git sha>",
      "vendor_command": "npm ci"
    },
    "resolved_dependencies": [
      {
        "uri": "pkg:npm/left-pad@1.3.0",
        "digest": { "sha512": "<128-hex>" }
      }
    ],
    "dep_closure_sha256": "<64-hex>",
    "dep_count": 1
  }
}

Field semantics

  • proof_type (REQUIRED) - the discriminator sut-provenance-v1. A verifier MUST reject an artifact whose proof_type it does not recognize; it severs this proof family from every other.

  • server_name (REQUIRED) - the catalog server name the image was built for. It scopes the subject (probity-sut/<server_name>).

  • packaging (REQUIRED) - the build ecosystem, one of npm or pip. Fixes which digest algorithm the dependency closure uses (npm Subresource-Integrity converts to sha512; a pip --require-hashes lock pins sha256).

  • base_image_digest (REQUIRED) - the SHA-256 (64-char lowercase hex) of the base image the SUT was built FROM. Honesty: this pins WHICH base image was used; it is best-effort provenance metadata and is NOT the load-bearing guarantee (the dependency closure is). A build host that cannot resolve the base image digest omits the whole provenance emission rather than fabricate one.

  • base_image_ref (REQUIRED) - the human-readable base image reference (e.g. node:20-bookworm-slim).

  • builder (REQUIRED) - the build provenance: tool (the builder identity), commit (the builder's git commit, 7-to-40-char lowercase hex), and vendor_command (the integrity-verifying install command). Honesty: builder.commit is an auditor-pinnable identity of the build tree, not a recipe a third party can re-run to reproduce the bytes.

  • resolved_dependencies (REQUIRED) - the fully digest-pinned dependency closure, expressed as an array of SLSA / in-toto ResourceDescriptors: one entry per resolved package, each carrying a uri (a Package-URL - pkg:npm/<name>@<version> or pkg:pypi/<name>@<version>; the npm scope @ prefix is percent-encoded to %40, e.g. pkg:npm/%40scope/pkg@1.2.3) and a digest map ({<alg>: <lowercase-hex>}, with alg in {sha512, sha256} and the hex of the algorithm's width - sha512=128, sha256=64). The array MUST be non-empty and EVERY entry MUST carry a non-empty hex digest - an unpinned dependency cannot produce a conformant predicate (fail-closed). The producer harvests this from a lockfile (an npm package-lock.json or a uv --generate-hashes requirements lock) and installs with the ecosystem's integrity-verifying installer, so the closure is the exact, reproducible input set. npm Subresource-Integrity (sha512-<base64>) is converted to the ResourceDescriptor's lowercase-hex digest; a pip --require-hashes sha256 is already hex.

  • dep_closure_sha256 (REQUIRED) - the SHA-256 (64-char lowercase hex) that binds the closure. It is computed deterministically: the resolved_dependencies array is SORTED by uri (the stable per-package key) and each entry serialised with sorted keys and compact separators before hashing. This determinism is a SEMANTIC constraint a JSON Schema cannot express; the producer and verifier both enforce it. A payload whose dep_closure_sha256 does not re-derive from its carried resolved_dependencies is rejected. (The field name is retained from v1: it is the dependency-closure hash, now over resolved_dependencies.)

  • dep_count (REQUIRED) - the number of entries in resolved_dependencies. MUST equal len(resolved_dependencies).

Verification (third-party, offline)

After the envelope checks in signing-envelope.md succeed (signature, keyid, predicateType, subject non-empty):

  1. Confirm the subject digest equals the SUT image the verifier intends to reason about.
  2. Confirm proof_type == "sut-provenance-v1", packaging is in {npm, pip}, and base_image_digest is a 64-char lowercase-hex sha256.
  3. Confirm resolved_dependencies is non-empty, EVERY entry's uri is a pkg: purl, and EVERY entry carries a non-empty hex digest.
  4. Re-derive dep_closure_sha256 from the carried resolved_dependencies (sort by uri, then by the entry's full RFC 8785 (JCS) canonical bytes as a fully-discriminating tiebreak; the hash is the SHA-256 of the RFC 8785 canonical JSON of the sorted array) and confirm it equals the carried value, and that dep_count == len(resolved_dependencies).

No live execution environment is required.

When a sut-provenance attestation accompanies an adversarial-execution-evidence attestation about the same SUT image, the verifier additionally confirms the two subjects carry the same digest. (The evidence predicate's core deliberately carries no provenance embed; a producer that embeds one does so via the evidence predicate's _ext extension convention, and the embedded copy's re-derived dep_closure_sha256 MUST match the standalone attestation's.)

Honesty

A verifier confirms (a) the closure-hash binds the carried, fully-pinned closure, and (b) the subject binding ties the provenance to a specific SUT image. It does NOT confirm that the registry tarballs the integrity hashes pin are themselves benign, nor that the closure is the complete transitive set the maintainer intended - it proves the closure that WAS used is pinned and reproducible, not that pinned means safe. The base image digest is best-effort metadata; the load-bearing, mandatory guarantee is the integrity-pinned dependency closure.

Related standards

  • SLSA Provenance - a sut-provenance attestation plays a role analogous to SLSA build provenance (a signed statement of how an artifact was built), specialised to a pinned dependency closure with a deterministic binding hash. resolved_dependencies adopts the SLSA Provenance v1 resolvedDependencies shape (an array of in-toto ResourceDescriptors), so external SLSA tooling can parse the closure. builder.commit is modeled on the SLSA builder.id idea: an auditor-pinnable identity of the build tree.
  • in-toto / DSSE
  • adversarial-execution-evidence - the evidence attestation this provenance most commonly accompanies (subject-digest paired; optionally embedded via _ext).

Versioning

This is the sut-provenance v1 predicate. Adding a new optional field is additive and stays within v1; a verifier ignores fields it does not recognize and reads only the fields it cross-references. A change to the dep_closure_sha256 derivation, a rename or removal of a field, or a change to the resolved_dependencies entry shape is breaking and requires a new major version.

Get access

Run your agents through Probity.

A few details about your setup and we'll get you gating agents fast.