in-toto Statements: Bind Verified Claims to the Artifact You Actually Deploy
Build artifact policy around in-toto Statement v1: digest-bound subjects, authenticated payloads, predicate typing, authorized signers, and negative tests.
A valid signature over a release report does not prove that the report applies to the bytes being deployed. Artifact policy must connect an authenticated claim to an immutable artifact identity, interpret the claim under the correct schema, and decide whether its signer is authorized to make that claim. The in-toto Statement layer provides the binding and type information; it is not itself a complete trust policy.
This article follows the in-toto attestation framework’s Statement v1 and the maintained v1-layer specification reviewed on October 11, 2026. The DSSE discussion follows its version 1.0.2 protocol and envelope documents. Examples use an explicitly fictional review predicate to illustrate structure, not an invented industry standard or a production signing implementation.
Separate the envelope, statement, and predicate
The envelope handles serialization and authentication. Its payload contains the Statement, whose subject identifies the artifacts and whose predicateType identifies the claim’s schema. The predicate contains the type-specific metadata.
Each layer answers a different question. Envelope verification asks whether the exact payload bytes were authenticated by acceptable keys under the chosen signature protocol. Statement processing asks which artifacts those bytes describe and which predicate type they contain. Predicate policy asks whether the authenticated information satisfies an operational requirement.
Collapsing these into “attestation verified” hides important failures. A signature can be authentic while its signer lacks release authority. An authorized signer can make a claim about another artifact. A correctly bound statement can describe a build without asserting that the build passed the tests required by your release policy.
Subject identity is based on content digest
A Statement v1 has _type equal to https://in-toto.io/Statement/v1, a required subject array, and a required predicateType. Every subject entry must have a digest. Subjects are assumed immutable, and matching is based on digest regardless of content type.
ResourceDescriptor supports additional fields such as name, URI, media type, and download location. Those can add useful context, but a friendly filename is not a substitute for the content binding. The Statement specification allows producers and consumers to give names contextual meaning; names and URIs should be unique within the subject array when set.
The following is unsigned illustrative JSON. Its all-zero digest is intentionally synthetic and must not be used to authorize any real release:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "release.tar.gz",
"digest": {
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
],
"predicateType": "https://example.com/attestations/release-review/v1",
"predicate": {
"decision": "approved",
"reviewPolicy": "example-policy-v1"
}
}
The example predicate’s semantics exist only for this demonstration. A real organization must publish and version its schema and decide who may issue it. For a standardized predicate, implement that specification instead of guessing field meanings from similar-looking JSON.
Verify the exact bytes before interpreting the claim
DSSE authenticates both the serialized payload and its payload type through pre-authentication encoding. Its keyid is only an unauthenticated hint for selecting candidate keys, not an authorization credential. Trust comes from successful verification with keys accepted by your policy, not from a convincing key identifier in the envelope.
The DSSE security guidance requires the payload bytes passed to the application to be the same bytes that were verified. Do not verify one envelope representation and later reparse a separately obtained envelope to extract its payload. That can break the relationship between authentication and interpretation.
For in-toto, the envelope payload type identifies the serialization and statement family. A predicate-specific storage media type is not a reliable substitute for the authenticated Statement’s predicateType. Check the latter after envelope verification and safe parsing.
A production parser should bound input size and reject malformed required fields. Adopt an unambiguous JSON parsing policy, including duplicate-member handling, and use a maintained signature implementation. The example below does not decode an envelope, verify signatures, or make a deployment decision.
Make artifact matching explicit and testable
This Python 3 helper operates on a Statement that a separate, trusted verifier has already authenticated and parsed. It checks a single algorithm supported by this local policy and fails closed when basic required structure is missing. Contextual requirements such as the expected output name can be added separately; they do not replace digest matching.
import re
SHA256_HEX = re.compile(r"[0-9a-f]{64}\Z")
def binds_sha256(statement, expected_digest, expected_predicate):
if not isinstance(statement, dict):
return False
if statement.get("_type") != "https://in-toto.io/Statement/v1":
return False
if statement.get("predicateType") != expected_predicate:
return False
if not isinstance(expected_digest, str):
return False
if not SHA256_HEX.fullmatch(expected_digest):
return False
subjects = statement.get("subject")
if not isinstance(subjects, list) or not subjects:
return False
matched = False
for subject in subjects:
if not isinstance(subject, dict):
return False
digests = subject.get("digest")
if not isinstance(digests, dict) or not digests:
return False
value = digests.get("sha256")
if value is not None:
if not isinstance(value, str) or not SHA256_HEX.fullmatch(value):
return False
matched |= value == expected_digest
return matched
This is a deliberately narrow lowercase SHA-256 policy, not a universal validator for every ResourceDescriptor or digest algorithm. Returning true means the structural binding condition holds. It says nothing about signature validity, the predicate’s approval decision, signer identity, freshness, or whether other required attestations exist.
Compute the expected digest from the exact artifact bytes that will be consumed. For container workflows, identify whether policy targets an index, a platform manifest, or another OCI object. Those are distinct objects with distinct digests. Matching one is not automatically authorization for every object referenced by it.
Add authority and context outside the binding helper
Define accepted signer identities, supported predicate versions, required build or review context, and artifact selection rules. A build-provenance issuer and a security-review issuer may have different authorities. If your policy needs both claims, require both from their respective trusted sources.
Digest identity alone also does not establish release freshness. Identical bytes can legitimately appear in several releases. If the policy requires a particular release channel, review generation, or recent scan, that context needs authenticated evidence and explicit evaluation. Do not fabricate a timestamp field or assume a filename carries an authenticated release order.
The specification permits omitted predicate content when the predicate type completely describes the claim. Whether that is sufficient for your particular predicate is a schema and policy question. Do not assume that an empty object means either universal approval or universal failure without reading that predicate’s contract.
Preserve the framework’s monotonic safety model
The layer specification requires consumers to ignore unrecognized fields unless a predicate specifies otherwise. Its extension model is built around a monotonic principle: ignoring a field or attestation must not turn a denial into approval. Design positive authorization requirements rather than a rule that allows deployment whenever it fails to find a negative report.
For example, requiring an authorized approval under the required policy is safer than denying only when a recognized rejection field happens to be present. A parser that does not understand a newly added field should not silently manufacture the missing approval. Extra diagnostic metadata can be ignored without satisfying an unmet requirement.
Keep syntax compatibility and business authorization separate. Rejecting every unfamiliar extension can break valid compatible documents, while accepting any unknown predicate type can admit claims whose semantics you never implemented. Recognize the required type and evaluate its known mandatory evidence.
Exercise negative paths and retain an audit trail
Test a matching subject, an altered artifact digest, the same filename with a different digest, a different predicate type, an empty subject list, and malformed digest entries. Separately test rejected signatures, unauthorized but cryptographically valid signers, and missing required companion claims. An end-to-end test must not mock away the signature stage and then claim deployment verification succeeded.
Record the selected artifact digest, authenticated payload digest, signer identity, predicate type, policy version, and decision reason. Redact secrets and avoid logging full private build inputs unnecessarily. If verification fails, preserve the artifact and evidence for investigation rather than changing names or suppressing the failed check.
The useful success condition is precise: the bytes you deploy are covered by authenticated, correctly typed claims from authorized sources, and those claims meet the policy actually required for that release. “A signature exists” is only an intermediate observation.
Related:
- OCI Referrers: Discover and Promote the Artifact Graph, Not Just the Image
- TUF Update Trust: Roles, Root Rotation, Freshness, and Recovery Boundaries
Sources: