Privacy Tools Guide

How to Verify Software Supply Chain Integrity

Supply chain attacks inject malicious code between the developer and you — in the build system, the package registry, the release artifacts, or the CI pipeline. Verification cannot stop every attack, but it confirms the software you are running matches what the author intended to ship.

Verifying Downloads with Checksums

Most projects publish SHA256 checksums alongside their releases.

# Download a release and its checksum file
wget https://releases.example.com/myapp-2.1.0-linux-amd64.tar.gz
wget https://releases.example.com/myapp-2.1.0-SHA256SUMS

# Verify
sha256sum -c myapp-2.1.0-SHA256SUMS
# myapp-2.1.0-linux-amd64.tar.gz: OK

# If you only have the expected hash (no checksum file)
echo "a3f2b1c4d5e6... myapp-2.1.0-linux-amd64.tar.gz" | sha256sum -c

Checksums prove the file was not corrupted in transit, but they do not prove the file came from the developer — the checksum file could be replaced alongside the binary. GPG or Sigstore signatures provide stronger guarantees.

Verifying GPG Signatures

# Import the developer's public key
gpg --keyserver keys.openpgp.org --recv-keys 0xABCDEF1234567890

# Or from a project's own keyserver URL
curl https://example.com/signing-key.asc | gpg --import

# Verify the signature
gpg --verify myapp-2.1.0-linux-amd64.tar.gz.asc myapp-2.1.0-linux-amd64.tar.gz
# Good signature from "Developer Name <dev@example.com>"

# Check the key fingerprint matches what the project documents
gpg --fingerprint 0xABCDEF1234567890

A good signature means:

It does not mean the key itself was not compromised (that requires a trust chain or key transparency).

Sigstore / cosign for Container Images

Sigstore is the modern replacement for GPG-signed container images. GitHub Actions can sign images automatically during CI using keyless signing (OIDC-based).

# Install cosign
curl -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 -o cosign
chmod +x cosign
sudo mv cosign /usr/local/bin/

# Verify a signed image
cosign verify \
  --certificate-identity "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp:v2.1.0

# Output: Verified OK
# Shows: signature, certificate, build workflow reference
# In your GitHub Actions workflow — sign the image during build
- name: Sign the container image
  env:
    DIGEST: ${{ steps.build.outputs.digest }}
  run: |
    cosign sign --yes ghcr.io/myorg/myapp@${DIGEST}

The keyless model ties the signature to the GitHub Actions workflow identity — you can verify not just “who signed” but “which pipeline signed, from which repo, on which branch.”

Pinning GitHub Actions to Commit SHAs

Referencing an action by tag (actions/checkout@v4) means the tag can be moved to point to different code. Pin to a SHA instead.

# Find the SHA for a tag
gh api repos/actions/checkout/git/refs/tags/v4 --jq '.object.sha'
# Returns a tag SHA, then dereference to the commit:
gh api repos/actions/checkout/git/tags/abc123... --jq '.object.sha'

# Or check the commit directly on GitHub
# github.com/actions/checkout/commits/v4
# Bad — tag can be updated
uses: actions/checkout@v4

# Good — pinned to commit SHA
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

Use pin-github-actions to automate this:

npx pin-github-actions .github/workflows/*.yml

SLSA Framework

SLSA (Supply-chain Levels for Software Artifacts) defines four levels of build integrity guarantees.

Level Requirement
SLSA 1 Provenance generated (who built it, when, from what source)
SLSA 2 Signed provenance
SLSA 3 Provenance from a hardened build platform (GitHub Actions, GCB)
SLSA 4 Hermetic, reproducible builds
# Generate SLSA provenance for a release with slsa-github-generator
# In your GitHub Actions workflow:
jobs:
  build:
    outputs:
      digests: ${{ steps.hash.outputs.digests }}
    steps:
      - name: Build
        run: |
          go build -o myapp .
          sha256sum myapp > myapp.sha256

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: myapp
          path: myapp

      - id: hash
        name: Generate hash
        run: |
          echo "digests=$(sha256sum myapp | base64 -w0)" >> "$GITHUB_OUTPUT"

  provenance:
    needs: [build]
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
    with:
      base64-subjects: "${{ needs.build.outputs.digests }}"
    permissions:
      id-token: write
      contents: write
      actions: read
# Verify SLSA provenance
slsa-verifier verify-artifact myapp \
  --provenance-path myapp.intoto.jsonl \
  --source-uri github.com/myorg/myapp \
  --source-tag v2.1.0

Software Bill of Materials (SBOM)

An SBOM lists every component in your software — useful for quickly checking whether a newly disclosed vulnerability affects you.

# Generate SBOM for a Go binary
syft myapp -o spdx-json > myapp.spdx.json

# Generate SBOM for a container image
syft ghcr.io/myorg/myapp:v2.1.0 -o cyclonedx-json > myapp.cdx.json

# Scan SBOM for known vulnerabilities
grype sbom:./myapp.cdx.json

# Example output:
# NAME            INSTALLED  FIXED-IN   TYPE  VULNERABILITY   SEVERITY
# openssl         3.1.2      3.1.3      deb   CVE-2023-XXXX   High
# Attach SBOM to a container image (stored alongside in OCI registry)
cosign attach sbom --sbom myapp.cdx.json ghcr.io/myorg/myapp:v2.1.0

# Retrieve and verify the attached SBOM
cosign download sbom ghcr.io/myorg/myapp:v2.1.0

Verifying npm Packages

# Check package integrity against npm registry
npm audit signatures

# Verify a specific package's signature
npm pack mypackage@1.2.3 --dry-run
# Then inspect: npm view mypackage dist.integrity

# Check that your installed packages match the registry
npm ci --audit=all  # in CI

# Use socket.dev for behavior analysis
npx @socketsecurity/cli npm info suspicious-package

Dependency Verification Script

#!/bin/bash
# verify-release.sh <url> <expected-sha256>
# Example: ./verify-release.sh https://releases.example.com/myapp.tar.gz abc123...

URL="$1"
EXPECTED_SHA="$2"
TMPFILE=$(mktemp)

curl -fsSL "$URL" -o "$TMPFILE"
ACTUAL_SHA=$(sha256sum "$TMPFILE" | awk '{print $1}')

if [ "$ACTUAL_SHA" = "$EXPECTED_SHA" ]; then
  echo "OK: $URL"
  echo "SHA256: $ACTUAL_SHA"
else
  echo "FAIL: SHA256 mismatch"
  echo "  Expected: $EXPECTED_SHA"
  echo "  Got:      $ACTUAL_SHA"
  rm -f "$TMPFILE"
  exit 1
fi

rm -f "$TMPFILE"