# 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](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](https://www.rfc-editor.org/rfc/rfc2119).

## Subject

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

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

## Predicate

```json
{
  "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](https://slsa.dev/spec/v1.0/provenance): one entry per
  resolved package, each carrying a `uri` (a [Package-URL](https://github.com/package-url/purl-spec) -
  `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](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](adversarial-execution-evidence.md) 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](https://slsa.dev/spec/v1.0/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](https://github.com/in-toto/attestation) / [DSSE](https://github.com/secure-systems-lab/dsse)**
  - the envelope; see [signing-envelope.md](signing-envelope.md).
- **[adversarial-execution-evidence](adversarial-execution-evidence.md)** - 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.
