in-toto: Software Supply Chain Attestations and Verification

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

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/links

Verification:

  1. Validates the layout signature (project owner's key)
  2. Validates each link's signature against the authorized keys in the layout
  3. Checks that each step's materials match the previous step's products
  4. Checks that expected commands match recorded commands
  5. 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 --version

The 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:

  1. Test results (custom predicate)
  2. SBOM (spdxjson predicate)
  3. 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 controller

in-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.

Read more