in-toto: Software Supply Chain Attestations and Verification
in-toto is the foundational framework for software supply chain security that powers much of modern tooling — including SLSA provenance and Sigstore attestations. Understanding in-toto means understanding the underlying model that these higher-level tools implement.
The Problem in-toto Solves
The SolarWinds attack succeeded because attackers compromised the build process. The built artifacts were signed and trusted — but the build itself had been tampered with. The signature only said "this artifact came from SolarWinds," not "this artifact was built from this exact source using these exact build steps."
in-toto answers a harder question: can you cryptographically prove that each step of your build process ran correctly, with the right inputs, producing the right outputs, by the right people?
The in-toto Model
in-toto defines three core concepts:
1. Layout: The policy document. It describes the supply chain: what steps exist, who is authorized to perform each step, and what artifacts each step should produce.
2. Link: The evidence document. It records what actually happened at each step: who ran it, what inputs were consumed, what outputs were produced, and a signature from the authorized functionary.
3. Verification: Checking that a set of links satisfies the layout. Given a layout and a set of link files, in-toto can verify the complete chain of custody.
How It Works in Practice
Step 1: Define the Layout
The project owner creates a layout describing the expected supply chain:
{
"_type": "layout",
"expires": "2026-12-31T00:00:00Z",
"readme": "Supply chain for myapp",
"keys": {
"alice_key_id": {
"keytype": "rsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\n..."
}
},
"bob_key_id": {
"keytype": "rsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\n..."
}
}
},
"steps": [
{
"name": "clone",
"pubkeys": ["alice_key_id"],
"expected_command": ["git", "clone", "https://github.com/myorg/myapp"],
"expected_materials": [],
"expected_products": [
["CREATE", "myapp/src/main.py"],
["CREATE", "myapp/requirements.txt"]
]
},
{
"name": "test",
"pubkeys": ["alice_key_id"],
"expected_command": ["pytest", "myapp/tests/"],
"expected_materials": [
["MATCH", "myapp/src/main.py", "WITH", "PRODUCTS", "FROM", "clone"]
],
"expected_products": [
["MATCH", "myapp/src/main.py", "WITH", "MATERIALS"]
]
},
{
"name": "build",
"pubkeys": ["bob_key_id"],
"expected_command": ["python", "setup.py", "sdist"],
"expected_materials": [
["MATCH", "myapp/src/main.py", "WITH", "PRODUCTS", "FROM", "test"]
],
"expected_products": [
["CREATE", "dist/myapp-1.0.tar.gz"]
]
}
],
"inspect": [
{
"name": "verify-package",
"expected_command": ["tar", "xf", "dist/myapp-1.0.tar.gz", "--list"],
"expected_materials": [
["MATCH", "dist/myapp-1.0.tar.gz", "WITH", "PRODUCTS", "FROM", "build"]
],
"expected_products": [
["MATCH", "myapp-1.0/src/main.py", "WITH", "PRODUCTS", "FROM", "clone"]
]
}
],
"signatures": [
{
"keyid": "project_owner_key_id",
"sig": "..."
}
]
}The layout is signed by the project owner (analogous to the root of trust). It specifies:
- Which keys are authorized for each step
- What commands each step is expected to run
- What materials (inputs) each step expects
- What products (outputs) each step should produce
- How outputs from one step should match inputs to the next
Step 2: Record Links
Each step in the supply chain records a link:
# Using in-toto-run to record a step
in-toto-run \
--name <span class="hljs-string">"test" \
--signing-key alice.pem \
--materials myapp/src/ \
--products myapp/src/ \
-- pytest myapp/tests/This generates test.alice_key_id.link:
{
"_type": "link",
"name": "test",
"materials": {
"myapp/src/main.py": {
"sha256": "abc123..."
}
},
"products": {
"myapp/src/main.py": {
"sha256": "abc123..."
}
},
"command": ["pytest", "myapp/tests/"],
"byproducts": {
"return-value": 0,
"stdout": "12 passed in 0.45s",
"stderr": ""
},
"environment": {
"variables": {}
},
"signatures": [
{
"keyid": "alice_key_id",
"sig": "..."
}
]
}Step 3: Verify the Chain
The consumer (or a verification step in CI) verifies the full chain:
# Verify a set of link files against a layout
in-toto-verify \
--layout root.layout \
--layout-keys alice.pub bob.pub \
--link-dir /path/to/linksVerification:
- Validates the layout signature (project owner's key)
- Validates each link's signature against the authorized keys in the layout
- Checks that each step's materials match the previous step's products
- Checks that expected commands match recorded commands
- Runs inspection steps
If any check fails, verification fails — proving something went wrong in the supply chain.
Installing in-toto
# Python implementation (reference implementation)
pip install in-toto
<span class="hljs-comment"># Go implementation
go install github.com/in-toto/in-toto-golang/cmd/in-toto@latest
<span class="hljs-comment"># Verify installation
in-toto-run --version
in-toto-verify --versionThe in-toto Attestation Framework
The in-toto specification has evolved beyond the original layout/link model into a general attestation framework used by SLSA and Sigstore.
The core abstraction is the Statement:
{
"_type": "https://in-toto.io/Statement/v0.1",
"subject": [
{
"name": "myapp-v1.2.3.tar.gz",
"digest": {
"sha256": "abc123..."
}
}
],
"predicateType": "https://example.com/MyPredicateType/v1",
"predicate": {
// Arbitrary predicate data
}
}The framework defines:
- Statement: binds a subject (artifact) to a predicate
- Predicate type: URI identifying the predicate schema
- Subject: the artifact being attested
- Envelope: wraps a statement with signatures (using DSSE)
Standard Predicate Types
| Predicate Type | Use Case |
|---|---|
https://slsa.dev/provenance/v0.2 |
SLSA build provenance |
https://in-toto.io/Link/v0.9 |
Legacy in-toto links |
https://cyclonedx.org/bom |
CycloneDX SBOM |
https://spdx.dev/Document |
SPDX SBOM |
https://cosign.sigstore.dev/attestation/vuln/v1 |
Vulnerability scan results |
https://example.com/TestResult/v1 |
Custom test results |
Custom predicates let you attach any metadata — test results, approval records, license scans — as verifiable attestations.
DSSE: Dead Simple Signing Envelope
in-toto uses DSSE (Dead Simple Signing Envelope) for signing attestations:
{
"payload": "<base64(statement)>",
"payloadType": "application/vnd.in-toto+json",
"signatures": [
{
"keyid": "...",
"sig": "<base64(signature)>"
}
]
}DSSE replaces the older JWT-based signing and is the format used by Sigstore/Cosign for attestations.
Practical in-toto with Cosign
Cosign's attestation commands implement in-toto under the hood:
# Create a custom attestation
<span class="hljs-built_in">cat > test-result.json << <span class="hljs-string">EOF
{
"tests_run": 42,
"tests_passed": 42,
"tests_failed": 0,
"coverage": "87%",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
<span class="hljs-comment"># Attest to a container image
cosign attest \
--predicate test-result.json \
--<span class="hljs-built_in">type https://example.com/TestResult/v1 \
myapp:v1.2.3
<span class="hljs-comment"># Verify and extract the attestation
cosign verify-attestation \
--<span class="hljs-built_in">type https://example.com/TestResult/v1 \
--certificate-identity-regexp <span class="hljs-string">".*" \
--certificate-oidc-issuer <span class="hljs-string">"https://token.actions.githubusercontent.com" \
myapp:v1.2.3 <span class="hljs-pipe">| jq <span class="hljs-string">'.payload | @base64d <span class="hljs-pipe">| fromjson <span class="hljs-pipe">| .predicate'in-toto in a CI Pipeline
A practical CI pipeline using in-toto concepts:
name: Supply Chain Verified Build
on:
push:
tags: ['v*']
permissions:
id-token: write
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Run tests
id: test
run: |
pytest tests/ --tb=short -q 2>&1 | tee test-output.txt
echo "exit_code=$?" >> $GITHUB_OUTPUT
- name: Create test attestation predicate
run: |
PASSED=$(grep -oP '\d+ passed' test-output.txt | grep -oP '\d+' || echo "0")
FAILED=$(grep -oP '\d+ failed' test-output.txt | grep -oP '\d+' || echo "0")
cat > test-predicate.json << EOF
{
"runId": "${{ github.run_id }}",
"commit": "${{ github.sha }}",
"testsPassed": $PASSED,
"testsFailed": $FAILED,
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"workflowRef": "${{ github.workflow_ref }}"
}
EOF
- name: Build and push image
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- name: Sign image
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Attest test results
run: |
cosign attest --yes \
--predicate test-predicate.json \
--type https://example.com/TestResult/v1 \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Generate and attest SBOM
run: |
syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \
-o spdx-json > sbom.spdx.json
cosign attest --yes \
--predicate sbom.spdx.json \
--type spdxjson \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ steps.build.outputs.digest }}At the end of this pipeline, the image has three attestations:
- Test results (custom predicate)
- SBOM (spdxjson predicate)
- SLSA provenance (slsaprovenance predicate)
A consumer can verify any or all of these before deploying.
Policy Enforcement with Rego
Using Open Policy Agent (OPA), you can write policies that evaluate attestations:
# policy.rego
package main
import future.keywords.if
deny[msg] if {
# Require test attestation
not has_attestation("https://example.com/TestResult/v1")
msg := "Image must have test results attestation"
}
deny[msg] if {
# Require tests to have passed
attest := get_attestation("https://example.com/TestResult/v1")
attest.predicate.testsFailed > 0
msg := sprintf("Image has %d failed tests", [attest.predicate.testsFailed])
}
deny[msg] if {
# Require SLSA level 3
not has_attestation("https://slsa.dev/provenance/v0.2")
msg := "Image must have SLSA provenance"
}The Grafeas API
Grafeas is an open-source API for storing and retrieving artifact metadata — including in-toto attestations. It provides a central store for supply chain evidence:
# Store an attestation in Grafeas
curl -X POST https://containeranalysis.googleapis.com/v1/projects/myproject/occurrences \
-H <span class="hljs-string">"Authorization: Bearer $(gcloud auth print-access-token)" \
-H <span class="hljs-string">"Content-Type: application/json" \
-d <span class="hljs-string">'{
"resourceUri": "https://gcr.io/myproject/myapp@sha256:abc123",
"noteName": "projects/myproject/notes/my-attestor",
"attestation": {
"attestation": {
"serializedPayload": "<base64(statement)>",
"signatures": [...]
}
}
}'Google Binary Authorization (GCP's admission controller) uses Grafeas under the hood to enforce attestation requirements on GKE clusters.
in-toto vs. SLSA
These aren't competing — SLSA is built on in-toto:
- in-toto defines the general framework (layouts, links, attestation format)
- SLSA defines specific requirements and levels using in-toto's attestation format
- Sigstore/Cosign implements in-toto DSSE envelopes for signing
- Rekor stores in-toto attestations in a transparency log
Understanding in-toto gives you a mental model for what SLSA provenance actually is and why the verification properties hold.
Testing Your Attestation Pipeline
Before relying on attestations in production enforcement:
# 1. Create a test image
docker build -t <span class="hljs-built_in">test:latest .
docker push <span class="hljs-built_in">test:latest
<span class="hljs-comment"># 2. Create a test attestation
<span class="hljs-built_in">echo <span class="hljs-string">'{"test": "passed"}' > test.json
cosign attest --key cosign.key \
--predicate test.json \
--<span class="hljs-built_in">type https://example.com/test/v1 \
<span class="hljs-built_in">test:latest
<span class="hljs-comment"># 3. Verify it exists
cosign verify-attestation --key cosign.pub \
--<span class="hljs-built_in">type https://example.com/test/v1 \
<span class="hljs-built_in">test:latest
<span class="hljs-comment"># 4. Try to verify an image WITHOUT the attestation
docker pull nginx:latest
cosign verify-attestation --key cosign.pub \
--<span class="hljs-built_in">type https://example.com/test/v1 \
nginx:latest
<span class="hljs-comment"># Should fail: no attestation found
<span class="hljs-comment"># 5. Verify your admission controller blocks unsigned images
kubectl run <span class="hljs-built_in">test --image=nginx:latest
<span class="hljs-comment"># Should be rejected by policy controllerin-toto provides the cryptographic foundation for answering "was this software built correctly?" It's the underlying model whether you're using SLSA provenance, Cosign attestations, or building your own supply chain verification. The higher-level tools abstract away most of the complexity — but knowing the model helps when you need to debug or extend it.