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 discriminatorsut-provenance-v1. A verifier MUST reject an artifact whoseproof_typeit 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 ofnpmorpip. Fixes which digest algorithm the dependency closure uses (npm Subresource-Integrity converts tosha512; a pip--require-hasheslock pinssha256). -
base_image_digest(REQUIRED) - the SHA-256 (64-char lowercase hex) of the base image the SUT was builtFROM. 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), andvendor_command(the integrity-verifying install command). Honesty:builder.commitis 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 auri(a Package-URL -pkg:npm/<name>@<version>orpkg:pypi/<name>@<version>; the npm scope@prefix is percent-encoded to%40, e.g.pkg:npm/%40scope/pkg@1.2.3) and adigestmap ({<alg>: <lowercase-hex>}, withalgin{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 npmpackage-lock.jsonor a uv--generate-hashesrequirements 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-hashessha256 is already hex. -
dep_closure_sha256(REQUIRED) - the SHA-256 (64-char lowercase hex) that binds the closure. It is computed deterministically: theresolved_dependenciesarray is SORTED byuri(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 whosedep_closure_sha256does not re-derive from its carriedresolved_dependenciesis rejected. (The field name is retained from v1: it is the dependency-closure hash, now overresolved_dependencies.) -
dep_count(REQUIRED) - the number of entries inresolved_dependencies. MUST equallen(resolved_dependencies).
Verification (third-party, offline)
After the envelope checks in signing-envelope.md succeed (signature, keyid,
predicateType, subject non-empty):
- Confirm the
subjectdigest equals the SUT image the verifier intends to reason about. - Confirm
proof_type == "sut-provenance-v1",packagingis in{npm, pip}, andbase_image_digestis a 64-char lowercase-hex sha256. - Confirm
resolved_dependenciesis non-empty, EVERY entry'suriis apkg:purl, and EVERY entry carries a non-empty hexdigest. - Re-derive
dep_closure_sha256from the carriedresolved_dependencies(sort byuri, 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 thatdep_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_dependenciesadopts the SLSA Provenance v1resolvedDependenciesshape (an array of in-toto ResourceDescriptors), so external SLSA tooling can parse the closure.builder.commitis modeled on the SLSAbuilder.ididea: an auditor-pinnable identity of the build tree. - in-toto / DSSE
- the envelope; see signing-envelope.md.
- 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.