Secure Software Supply Chain: Using Sigstore, TUF & In-Toto for CI/CD Integrity
In the wake of major software supply chain attacks like SolarWinds and Log4j, securing your CI/CD pipeline has become paramount. Modern development practices demand robust cryptographic verification at every stage—from code commit to production deployment. This comprehensive guide explores how to implement Sigstore for artifact signing, The Update Framework (TUF) for secure software distribution, and in-toto for supply chain integrity verification. Learn how to build a tamper-proof software supply chain that protects against sophisticated attacks while maintaining developer productivity.
🚀 The Software Supply Chain Security Crisis
The software supply chain represents the entire lifecycle of software development, from dependencies and build processes to distribution and deployment. Recent statistics show that supply chain attacks increased by 650% in 2024, with organizations spending an average of $4.5 million per incident on remediation. The three pillars of supply chain security—provenance, integrity, and authenticity—form the foundation of modern secure development practices.
- Provenance: Verifiable information about software origins and creation process
- Integrity: Assurance that software hasn't been tampered with after creation
- Authenticity: Cryptographic verification of software source and authorship
⚡ Understanding the Security Trio: Sigstore, TUF, and in-toto
These three technologies work together to create a comprehensive security framework for your software supply chain:
- Sigstore: Provides cryptographic signing and verification with keyless certificates
- TUF (The Update Framework): Secures software update systems against compromise
- in-toto: Ensures integrity across the entire software supply chain workflow
💻 Implementing Sigstore for Artifact Signing
Sigstore provides a complete ecosystem for signing, verifying, and protecting software artifacts without the complexity of key management.
💻 GitHub Actions with Sigstore Cosign
# .github/workflows/secure-build.yaml
name: Secure Build and Sign
on:
push:
branches: [ main ]
release:
types: [ published ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Required for Sigstore keyless signing
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push container image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign container image with keyless signing
run: |
# Sign the image with Fulcio certificate
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
- name: Generate SBOM and sign it
run: |
# Generate Software Bill of Materials
cosign attest --yes \
--predicate https://example.com/predicate.json \
--type custom \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
- name: Store build provenance
uses: actions/upload-artifact@v4
with:
name: build-provenance
path: |
predicate.json
build-metadata.json
retention-days: 30
verify-signatures:
runs-on: ubuntu-latest
needs: build-and-sign
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Verify container signature
run: |
cosign verify \
--certificate-identity-regexp '.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Verify SBOM attestation
run: |
cosign verify-attestation \
--type custom \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
🔗 The Update Framework (TUF) Implementation
TUF provides a secure framework for distributing software updates, protecting against various attacks on software repositories.
💻 Python TUF Repository Management
#!/usr/bin/env python3
"""
TUF Repository Management for Secure Software Distribution
"""
import json
import hashlib
from datetime import datetime, timedelta
from typing import Dict, List
from tuf.api.metadata import (
Root, Snapshot, Targets, Timestamp,
MetaFile, Role, Key, TopLevelMetadata
)
from tuf.repository import Repository
from securesystemslib.keys import generate_ed25519_key
from securesystemslib.signer import SSlibSigner
class SecureTUFRepository:
def __init__(self, repo_path: str):
self.repo_path = repo_path
self.repository = Repository.create(repo_path)
self.setup_initial_metadata()
def setup_initial_metadata(self):
"""Initialize TUF repository with root keys and roles"""
# Generate keys for different roles
root_key = generate_ed25519_key()
timestamp_key = generate_ed25519_key()
snapshot_key = generate_ed25519_key()
targets_key = generate_ed25519_key()
# Create root metadata
root = Root(version=1, spec_version="1.0")
# Add keys to root
root.add_key(root_key, "root")
root.add_key(timestamp_key, "timestamp")
root.add_key(snapshot_key, "snapshot")
root.add_key(targets_key, "targets")
# Set role thresholds
root.roles["root"] = Role(["root"], 1)
root.roles["timestamp"] = Role(["timestamp"], 1)
root.roles["snapshot"] = Role(["snapshot"], 1)
root.roles["targets"] = Role(["targets"], 1)
# Set expiration dates
root.expires = datetime.now() + timedelta(days=365)
self.repository.root = root
def add_software_target(self, file_path: str, version: str,
checksums: Dict[str, str]):
"""Add a software target to the repository"""
target_name = f"application-{version}.tar.gz"
# Create target metadata
target_info = {
"length": len(checksums),
"hashes": checksums,
"custom": {
"version": version,
"release_date": datetime.now().isoformat(),
"vulnerability_scan": "passed",
"sbom_digest": hashlib.sha256(
f"sbom-{version}".encode()
).hexdigest()
}
}
# Add target to repository
self.repository.targets.add_target(target_name, target_info)
def publish_update(self, version: str):
"""Publish a new software version with proper signing"""
# Update snapshot metadata
snapshot = Snapshot(version=1)
snapshot.expires = datetime.now() + timedelta(days=7)
# Update timestamp metadata
timestamp = Timestamp(version=1)
timestamp.expires = datetime.now() + timedelta(hours=24)
# Sign all metadata
self.repository.root.unsigned.version += 1
self.repository.snapshot = snapshot
self.repository.timestamp = timestamp
# Write metadata to repository
self.repository.writeall()
print(f"Published version {version} with TUF protection")
def verify_update_integrity(self, target_name: str) -> bool:
"""Verify the integrity of a software update"""
try:
target_info = self.repository.get_targetinfo(target_name)
if target_info:
print(f"Target {target_name} verified successfully")
return True
except Exception as e:
print(f"Verification failed: {e}")
return False
# Example usage
def create_secure_repository():
repo = SecureTUFRepository("./secure-repo")
# Add software targets with checksums
checksums = {
"sha256": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234",
"sha512": "b2c3d4e5f6789012345678901234567890123456789012345678901234567890"
}
repo.add_software_target("app-v1.0.0.tar.gz", "1.0.0", checksums)
repo.publish_update("1.0.0")
# Verify update integrity
repo.verify_update_integrity("application-1.0.0.tar.gz")
if __name__ == "__main__":
create_secure_repository()
🎯 in-toto for Supply Chain Integrity
in-toto provides a framework to secure the integrity of entire software supply chain workflows by cryptographically verifying each step.
💻 in-toto Supply Chain Layout
#!/usr/bin/env python3
"""
in-toto Supply Chain Integrity Verification
"""
import json
from datetime import datetime
from pathlib import Path
from in_toto.models.layout import Layout, Step, Inspection
from in_toto.models.metadata import Metablock
from in_toto.runlib import in_toto_run, in_toto_verify
from securesystemslib.keys import generate_ed25519_key
from securesystemslib.signer import SSlibSigner
class SupplyChainIntegrity:
def __init__(self, project_name: str):
self.project_name = project_name
self.layout = self.create_supply_chain_layout()
self.signing_keys = {}
def create_supply_chain_layout(self) -> Layout:
"""Create in-toto layout defining the supply chain steps"""
layout = Layout(
expires=datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
readme=f"Supply chain layout for {self.project_name}",
keys={}
)
# Define supply chain steps
steps = [
Step(
name="clone",
expected_materials=[["DISALLOW", "*"]],
expected_products=[["CREATE", "source/*"]],
pubkeys=[],
expected_command=["git", "clone"],
threshold=1
),
Step(
name="security-scan",
expected_materials=[["MATCH", "source/*", "WITH", "PRODUCTS", "FROM", "clone"]],
expected_products=[["CREATE", "scan-results/*"]],
pubkeys=[],
expected_command=["trivy", "scan"],
threshold=1
),
Step(
name="build",
expected_materials=[
["MATCH", "source/*", "WITH", "PRODUCTS", "FROM", "clone"],
["MATCH", "scan-results/*", "WITH", "PRODUCTS", "FROM", "security-scan"]
],
expected_products=[["CREATE", "artifacts/*"]],
pubkeys=[],
expected_command=["docker", "build"],
threshold=1
),
Step(
name="sign",
expected_materials=[["MATCH", "artifacts/*", "WITH", "PRODUCTS", "FROM", "build"]],
expected_products=[["CREATE", "signatures/*"]],
pubkeys=[],
expected_command=["cosign", "sign"],
threshold=1
),
Step(
name="deploy",
expected_materials=[
["MATCH", "artifacts/*", "WITH", "PRODUCTS", "FROM", "build"],
["MATCH", "signatures/*", "WITH", "PRODUCTS", "FROM", "sign"]
],
expected_products=[["CREATE", "deployment/*"]],
pubkeys=[],
expected_command=["kubectl", "apply"],
threshold=1
)
]
layout.steps = steps
# Define final inspection
inspection = Inspection(
name="verify-supply-chain",
expected_materials=[["MATCH", "*", "WITH", "PRODUCTS", "FROM", "deploy"]],
expected_products=[],
run=["bash", "-c", "echo 'Supply chain verification complete'"]
)
layout.inspect = [inspection]
return layout
def generate_signing_keys(self):
"""Generate signing keys for each step in the supply chain"""
steps = ["clone", "security-scan", "build", "sign", "deploy"]
for step in steps:
key = generate_ed25519_key()
self.signing_keys[step] = key
self.layout.keys[key["keyid"]] = key
# Add key to corresponding step
for layout_step in self.layout.steps:
if layout_step.name == step:
layout_step.pubkeys = [key["keyid"]]
def execute_supply_chain_step(self, step_name: str, command: list,
materials: list, products: list):
"""Execute a supply chain step with in-toto recording"""
try:
# Run the step with in-toto recording
in_toto_run(
step_name=step_name,
product_list=products,
material_list=materials,
command=command,
signing_key=self.signing_keys[step_name]
)
print(f"Step {step_name} completed and recorded")
return True
except Exception as e:
print(f"Step {step_name} failed: {e}")
return False
def verify_supply_chain(self, link_dir: str = ".in-toto") -> bool:
"""Verify the entire supply chain integrity"""
try:
# Save layout to file
layout_metadata = Metablock(signed=self.layout)
with open("root.layout", "w") as f:
layout_metadata.dump(f)
# Verify the supply chain
in_toto_verify(
layout_path="root.layout",
link_dir=link_dir
)
print("Supply chain verification successful!")
return True
except Exception as e:
print(f"Supply chain verification failed: {e}")
return False
# Example usage
def run_secure_supply_chain():
sc = SupplyChainIntegrity("my-secure-app")
sc.generate_signing_keys()
# Execute supply chain steps
steps = [
{
"name": "clone",
"command": ["git", "clone", "https://github.com/example/repo.git", "source"],
"materials": [],
"products": ["source/"]
},
{
"name": "security-scan",
"command": ["trivy", "fs", "--format", "json", "source/"],
"materials": ["source/"],
"products": ["scan-results/"]
},
{
"name": "build",
"command": ["docker", "build", "-t", "my-app:latest", "source/"],
"materials": ["source/", "scan-results/"],
"products": ["artifacts/"]
}
]
for step in steps:
success = sc.execute_supply_chain_step(
step["name"], step["command"], step["materials"], step["products"]
)
if not success:
print(f"Supply chain broken at step: {step['name']}")
return
# Verify entire supply chain
sc.verify_supply_chain()
if __name__ == "__main__":
run_secure_supply_chain()
🔧 CI/CD Integration Patterns
Integrating these technologies into your CI/CD pipeline requires careful planning and implementation:
- GitHub Actions: Native Sigstore support with OIDC tokens
- GitLab CI: Custom runners with secure key management
- Jenkins: Pipeline libraries for supply chain security
- Tekton: Cloud-native pipeline definitions with security steps
📊 Security Metrics and Compliance
Measuring and monitoring your supply chain security is crucial for continuous improvement:
- SLSA Compliance: Track progress toward Supply-chain Levels for Software Artifacts
- Signature Coverage: Percentage of artifacts with cryptographic signatures
- Verification Rates: Success rates of artifact verification in production
- Time to Detect: Average time to detect supply chain compromises
⚡ Key Takeaways
- Sigstore provides keyless signing that eliminates complex key management overhead
- TUF secures software update systems against repository compromise and rollback attacks
- in-toto ensures end-to-end integrity verification across the entire supply chain
- Combining these technologies creates a defense-in-depth security strategy
- Automated verification should be integrated into both CI and CD pipelines
❓ Frequently Asked Questions
- What's the difference between Sigstore and traditional code signing?
- Traditional code signing requires managing and securing private keys, which can be complex and error-prone. Sigstore uses OpenID Connect and certificate authorities to provide short-lived certificates for signing, eliminating key management overhead while maintaining strong cryptographic guarantees.
- How does TUF protect against supply chain attacks?
- TUF uses a multi-signature approach with role separation and explicit trust delegation. It protects against various attacks including repository compromise, freeze attacks, mix-and-match attacks, and rollback attacks by ensuring metadata consistency and requiring multiple trusted parties for critical updates.
- Can these tools work with existing CI/CD systems?
- Yes, all three technologies are designed to integrate with existing CI/CD systems. Sigstore has native GitHub Actions support, TUF can be integrated into artifact repositories, and in-toto can wrap existing build and deployment steps without major pipeline redesigns.
- What performance impact do these security measures have?
- The performance impact is minimal for most use cases. Sigstore signing adds milliseconds, TUF metadata verification is optimized for performance, and in-toto adds minimal overhead to build steps. The security benefits far outweigh the minor performance costs for most organizations.
- How do I get started with implementing supply chain security?
- Start by implementing Sigstore for your container images, then add TUF for your internal package distribution, and finally implement in-toto for critical build pipelines. Focus on high-value artifacts first and gradually expand coverage. Use the SLSA framework as a maturity model to guide your implementation.
💬 Found this article helpful? Please leave a comment below or share it with your network to help others learn! Have you implemented software supply chain security in your organization? Share your experiences and challenges!
About LK-TECH Academy — Practical tutorials & explainers on software engineering, AI, and infrastructure. Follow for concise, hands-on guides.

No comments:
Post a Comment