Security Engineering · Supply-Chain Security
Provenance Attestations and the Digest They Must Bind
An attestation is a signed claim about a build. Its value depends on a subject digest that names the exact artifact, a predicate that names the process, and a verifier policy.
A provenance attestation is a statement of the form “artifact X was produced by process Y on platform Z from inputs W”. Written down, it is a JSON document. Signed, it becomes evidence. The whole of the trust argument rests on one structural property of that document: the artifact must be identified by a digest, not by a name, a tag, or a version. A provenance statement that says “the container image app:latest was built from commit abc123” is not evidence of anything, because app:latest is a mutable pointer and the claim can be true at one moment and false at the next.
The envelope, the statement, and the predicate
The in-toto attestation framework defines a layered format, and the layering is what makes the format extensible without making it unverifiable.
The outermost layer is the envelope. The DSSE format wraps an arbitrary payload as base64 and pairs it with one or more signatures over a pre-authentication encoding that includes the payload type, so that a signature cannot be replayed as a signature over a different type of statement. The payload type for in-toto statements is a fixed media type string, and its presence inside the signed bytes is what prevents an attacker from reinterpreting one statement as another.
The middle layer is the statement: a _type field, a subject array, and a predicateType with a predicate. The subject is a list of objects each having a name and a digest map from algorithm name to hex value. The statement binds the claim to the artifacts; the predicate carries the claim.
The innermost layer is the predicate, whose schema is defined by its predicateType. SLSA provenance uses a predicate type defined by the specification, with fields for the build definition, the run details, and the resolved dependencies. The predicate is where the build platform, the builder identity, the invocation parameters, and the material list go.
This three-layer design has one important consequence: verification happens at the statement level and policy is evaluated at the predicate level. A verifier can establish that the envelope’s signature is valid and that some subject digest matches the artifact in hand without understanding the predicate at all. That separation means a policy engine can be replaced without touching signature verification, and a new predicate type can be introduced without changing the envelope.
Why the subject digest is the load-bearing part
Consider what an attacker wants. They want an artifact of their choosing to be consumed under the trust that a producer’s signature confers. There are two ways to get there: produce a valid signature over their artifact, or make the verifier treat a signature over a legitimate artifact as covering theirs.
The second path is the one that the subject digest closes. If the verifier takes the artifact’s digest, computes it itself, and requires an exact match against a subject digest in a signed statement, then no amount of renaming, re-tagging, or index confusion causes a mismatch to pass. The binding is to bytes. Every weaker identity — tag, version string, package name, repository path — is a mutable reference that can be repointed at different bytes.
Two implementation details decide whether the binding is real.
First, the digest algorithm must be one the verifier computes and considers strong. A statement that lists only a digest under an algorithm the verifier does not support is unverifiable and must be rejected rather than accepted on the strength of the signature alone. A statement that lists several digests is verified by recomputing each supported one and requiring a match on at least one, with the caveat that the attacker controls the statement’s digest list only if they control the statement, which the signature prevents.
Second, the subject name is advisory. The in-toto specification is explicit that the name is a hint for humans and that the digest is the identity. A verifier that matches on name and checks the digest “if present” has an obvious bypass: omit the digest.
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "registry.example.invalid/app/server",
"digest": { "sha256": "9f2c…" }
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://example.invalid/buildtypes/v1",
"externalParameters": { "source": "https://git.example.invalid/app", "ref": "refs/tags/v1.4.0" },
"resolvedDependencies": [
{ "uri": "git+https://git.example.invalid/app@refs/tags/v1.4.0",
"digest": { "gitCommit": "4b1d…" } }
]
},
"runDetails": {
"builder": { "id": "https://builder.example.invalid/workflows/build" },
"metadata": { "invocationId": "build-2158", "startedOn": "2025-10-01T00:00:00Z" }
}
}
}
The externalParameters field is where an attacker who can influence the build’s inputs would try to hide. A policy that requires a specific source repository and a specific ref pattern is checking these fields, and the check is only meaningful if the fields are complete: a builder that omits the source revision from its provenance produces statements that verify cryptographically and are useless for policy.
Levels, and what each one actually excludes
The SLSA build track names three levels above zero, and the recommended way to read them is as a list of threats each level excludes.
Build L1 requires that provenance exist and be distributed. It excludes mistakes: a release assembled from the wrong commit, a build run from a dirty working tree. It does not exclude a forged provenance, because nothing about L1 requires a signature.
Build L2 requires that provenance be signed by a hosted build platform, with the signature key not accessible to the build’s user-defined steps. It excludes tampering after the build, and it makes forging require an explicit act rather than an omission. It does not exclude an attacker who can influence the build definition, because at L2 the build inputs are whatever the platform accepted.
Build L3 requires hardened isolation between build runs and between runs and the signing secrets. It excludes tampering during the build by other tenants on the platform, and it makes the platform’s own credentials a much harder target. It does not exclude a compromised source repository or a malicious dependency, because the build faithfully builds what it was given.
The progression matters for planning: an organization that has provenance but verifies it nowhere has achieved L1 and gained only a debugging aid. The verification step, which compares actual provenance against an expected policy, is what converts provenance into a control, and the specification treats producing and verifying as separate concerns with separate requirements.
Policy: the part nobody can supply for you
The specification deliberately does not say what an acceptable provenance looks like. That decision is per-ecosystem or per-organization, and it is where most of the difficulty lives.
A minimal policy for a container image answers three questions. Which builder identities are acceptable? The builder.id in run details must be a value the organization controls or has reviewed, which prevents an attacker who can run any workflow in the same platform from signing as an approved builder. Which source location and ref are acceptable? The external parameters must name the repository and a ref matching the release pattern, so that a build from a fork or a feature branch is rejected. Which material digests are acceptable? The resolved dependencies’ digests, when present, allow rejecting a build that incorporated a dependency version outside an approved set.
A verifier enforces the policy after signature verification and digest matching, and rejects on any unmet condition. The ordering matters: a policy evaluated on an unverified predicate is a policy an attacker writes. This is why verification implementations are structured as a signature check, then a subject digest check, then a policy evaluation, with no path that skips a step.
There is a fourth question that is often omitted: how fresh must the provenance be, and how does the verifier know the statement is the current one? A valid statement about an artifact digest remains valid forever, since the bytes do not change. What can change is which statement is served for a tag, which is why the consumer should resolve a tag to a digest once and verify against the digest thereafter. Doing otherwise reopens the mutability that the digest binding closed.
The residual gap
An attestation can only be as strong as the platform that signs it. A build platform whose own code is modifiable by the build it is running can be made to sign a false statement, and the signature will verify. This is precisely the threat that the L3 hardening requirements address, and it is also the reason the chain of trust for the build platform itself needs the same treatment recursively: the platform’s own components need provenance, and its bootstrap needs a root of trust outside the platform.
The other gap is between the statement and the artifact’s behaviour. Provenance says the artifact came from known source by a known process; it does not say the source is safe. That question is answered by vulnerability data and reachability analysis, which is what VEX statements and exploitability justification addresses, and the integrity of the statement distribution path is what keyless signing and transparency logs covers.
References
- SLSA v1.0 specification: security levels — OpenSSF SLSA project
- in-toto Attestation Framework — in-toto project
- DSSE: Dead Simple Signing Envelope — Secure Systems Lab
- JSON Web Token (JWT) — IETF RFC 7519