Provenance Verification Workflows

Permalink to "Provenance Verification Workflows"

This workflow is part of Supply Chain Auditing & Dependency Verification. Without cryptographic provenance gating, your pipeline implicitly trusts every registry package it resolves — a trust that a compromised maintainer account, a typosquatted package name, or a malicious mirror can silently violate. Provenance verification replaces that implicit trust with a verifiable, policy-enforced chain of evidence from source commit to deployed artifact.

Prerequisites

Permalink to "Prerequisites"

Conceptual foundation

Permalink to "Conceptual foundation"

Provenance is the cryptographically verifiable record of an artifact’s origin: who built it, from which source revision, on which infrastructure, and when. The main standards in this space are:

  • SLSA (Supply-chain Levels for Software Artifacts) — a graduated framework (Levels 1–4) that defines what provenance must contain and how tamper-resistant the build process must be. SLSA provenance is a signed DSSE (Dead Simple Signing Envelope) document that asserts buildType, builder.id, materials (source commits), and a digest of the produced artifact.
  • Sigstore / Cosign — a keyless PKI model where short-lived signing certificates are issued against OIDC identity claims (e.g. a GitHub Actions workflow URL). Every signature is appended to the Rekor immutable transparency log, so any party can audit signing history without obtaining private keys.
  • in-toto — a framework for expressing supply chain layouts: which steps must occur, which functionaries are authorised to perform them, and what links (signed step records) must be present. SLSA provenance is expressed as an in-toto statement.

The SLSA specification is maintained at https://slsa.dev/spec. The Sigstore project specification lives at https://docs.sigstore.dev.

The diagram below shows how these pieces fit together in a typical CI/CD pipeline:

Provenance Verification Pipeline Four-stage flow: source commit triggers a hardened build, which emits a signed artifact plus SLSA provenance. The CI verification gate checks signature and provenance against policy. Passing artifacts have SRI hashes injected into HTML templates. The browser enforces SRI at fetch time. Source Commit git tag + OIDC identity token Hardened Build SLSA L2/L3 builder emits artifact + provenance.intoto.jsonl Cosign → Rekor log CI Verification Gate slsa-verifier check cosign verify-blob OPA policy eval SBOM attestation Deploy + Browser SRI hash in <script> CSP require-sri-for policy failure → quarantine + alert SLSA Level 2 minimum

Step 1 — Map the dependency tree and resolve lockfile metadata

Permalink to "Step 1 — Map the dependency tree and resolve lockfile metadata"

Before any cryptographic check can run you need an exact, reproducible inventory of every resolved package, including transitive dependencies. Use Lockfile Mapping & Analysis to extract that inventory from your committed lockfile. Skipping this step means signature verification covers only direct dependencies and leaves the transitive graph unvalidated.

# npm: export the full resolved tree as JSON
npm ls --all --json > resolved-tree.json

# pnpm: include transitive depth
pnpm list --recursive --json > resolved-tree.json

Parse the output to collect each package’s resolved URL and integrity field (a sha512- SRI hash written by the package manager). The integrity field is your first provenance signal — a mismatch between the registry checksum and the lockfile hash is an early-warning indicator of tampering.

// extract-lockfile-integrity.mjs
import { readFileSync } from 'fs';

const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
const packages = lock.packages ?? {};

for (const [name, meta] of Object.entries(packages)) {
  if (!name || !meta.integrity) continue;
  // sha512- prefix means the package manager already computed an SRI-compatible digest
  console.log(JSON.stringify({ name, version: meta.version, integrity: meta.integrity, resolved: meta.resolved }));
}

Expected output: one JSON line per package, each carrying an integrity value beginning with sha512-. Any package missing an integrity field must be flagged for manual review before proceeding.


Step 2 — Generate an SBOM and attach component identities

Permalink to "Step 2 — Generate an SBOM and attach component identities"

An SBOM produced by Automated SBOM Generation converts the flat lockfile inventory into a structured document with Package URL (purl) identifiers, license metadata, and supplier information. Feed the SBOM into your verification engine as the authoritative component list.

# CycloneDX SBOM for a Node.js project
npx @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json --output-format JSON

# Verify the SBOM is well-formed
npx @cyclonedx/cyclonedx-cli validate --input-file sbom.cdx.json --input-format JSON

The purl for each component (e.g. pkg:npm/[email protected]) is the stable identifier you carry forward into signature lookups. CycloneDX v1.5+ supports an attestations array in the metadata block — populate this once signatures are verified in Step 3 to produce a single attestation-enriched SBOM artifact that auditors and policy engines can consume.


Step 3 — Verify Sigstore signatures and SLSA provenance

Permalink to "Step 3 — Verify Sigstore signatures and SLSA provenance"

This is the cryptographic core of the workflow. For each npm package that publishes Sigstore attestations (all packages generated with npm publish --provenance since npm 9.5), verify the attestation bundle against the Rekor transparency log.

The detailed mechanics of npm-specific Sigstore verification are covered in Verifying Sigstore Provenance for npm Packages.

Verifying an npm provenance attestation with the npm CLI:

# npm 9.5+ — built-in provenance check
npm audit signatures

# Example passing output:
# audited 312 packages in 4s
# 287 packages have verified registry signatures
# 25 packages have unverified registry signatures — investigate before deploying

Verifying a SLSA provenance bundle for a built artifact:

slsa-verifier verify-artifact \
  --provenance-path provenance.intoto.jsonl \
  --source-uri github.com/your-org/your-repo \
  --source-tag v2.1.0 \
  dist/app.tar.gz

A successful run prints PASSED: Verified SLSA provenance. A failure exits non-zero and prints the specific mismatch — treat non-zero exits as hard CI failures, not warnings.

Verifying a Cosign blob signature (keyless):

cosign verify-blob \
  --certificate-identity-regexp="^https://github.com/your-org/your-repo/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  --signature=artifact.sig \
  --certificate=artifact.pem \
  dist/app.tar.gz

The --certificate-identity-regexp anchors the identity to a specific organisation path. Without this constraint any GitHub Actions workflow in any repo could produce a signature that passes the OIDC issuer check — the regexp is a security-critical argument, not a convenience option.


Step 4 — Enforce policy with OPA/Rego

Permalink to "Step 4 — Enforce policy with OPA/Rego"

Raw signature verification answers “is this signature valid?” but not “does this artifact meet our security policy?”. Express your constraints as Rego rules so they are auditable, version-controlled, and testable:

# policy/provenance.rego
package provenance

import future.keywords.if
import future.keywords.in

# Allow only artifacts signed by our GitHub Actions org
default allow := false

allow if {
  some attestation in input.attestations
  attestation.issuer == "https://token.actions.githubusercontent.com"
  startswith(attestation.subject, "https://github.com/your-org/")
  attestation.timestamp_ns > time.now_ns() - (90 * 24 * 60 * 60 * 1000000000)
}

deny contains msg if {
  not allow
  msg := sprintf("attestation rejected: issuer=%v subject=%v", [
    input.attestations[0].issuer,
    input.attestations[0].subject
  ])
}
# Evaluate at CI time
opa eval \
  --data policy/provenance.rego \
  --input attestation-bundle.json \
  --format pretty \
  "data.provenance.deny"

# CI step fails if deny set is non-empty

Policy rules should be pinned in a separate policy/ directory, reviewed in the same PR workflow as application code, and tested with opa test against fixture bundles covering both passing and failing cases.


Step 5 — Inject SRI hashes and propagate CSP headers

Permalink to "Step 5 — Inject SRI hashes and propagate CSP headers"

Provenance verification protects the build boundary. Subresource Integrity protects the browser fetch boundary. Once artifacts are verified, compute SHA-384 digests for every asset that will be loaded by the browser and inject them into your HTML templates before deployment.

// sri-inject.mjs — runs as a post-build step
import { createHash } from 'crypto';
import { readFileSync, readdirSync, statSync } from 'fs';
import { resolve, extname } from 'path';

function computeSRI(filePath) {
  const content = readFileSync(filePath);
  const digest = createHash('sha384').update(content).digest('base64');
  return `sha384-${digest}`;
}

const distDir = resolve('./dist');
const assets = readdirSync(distDir).filter(f =>
  ['.js', '.css'].includes(extname(f)) && statSync(resolve(distDir, f)).isFile()
);

const manifest = {};
for (const asset of assets) {
  manifest[asset] = computeSRI(resolve(distDir, asset));
  console.log(`${asset}: integrity="${manifest[asset]}"`);
}

// Write for use by your HTML template engine
import { writeFileSync } from 'fs';
writeFileSync('./dist/sri-manifest.json', JSON.stringify(manifest, null, 2));

This manifest feeds your server-side template engine or static site generator so that every <script> and <link> tag carries a correct integrity attribute. Pair this with Configuring Content-Security-Policy with SRI to enforce require-sri-for script style at the browser level.


Configuration reference

Permalink to "Configuration reference"
Option / Flag Valid values Security implication
cosign verify-blob --certificate-identity-regexp Anchored regex, e.g. ^https://github.com/your-org/ Must be anchored with ^; unanchored allows any org path suffix match
cosign verify-blob --certificate-oidc-issuer https://token.actions.githubusercontent.com, https://accounts.google.com, etc. Must exactly match the issuer; wildcards are not supported
slsa-verifier --source-uri Full github.com/org/repo path Prevents cross-repo provenance substitution attacks
slsa-verifier --source-tag Semantic version tag, e.g. v2.1.0 Pins to a specific release; omit to allow any tag (not recommended for production)
OPA timestamp_ns window Duration in nanoseconds; 90-day window = 7776000000000000 Prevents accepting stale attestations from compromised, now-revoked keys
SRI digest algorithm sha384 (recommended), sha256, sha512 SHA-384 is the industry default for browser SRI; SHA-256 is too short for long-lived artifacts
npm audit signatures --json for machine-readable output Exit code 1 on any unsigned package; use --omit=dev to scope to production deps only

Integration with adjacent tooling

Permalink to "Integration with adjacent tooling"

The output of each step feeds directly into the next:

  1. Lockfile analysis (Step 1) produces a resolved component list with integrity hashes → feeds the SBOM generator.
  2. SBOM (Step 2) assigns purl identifiers to every component → feeds the signature verification engine.
  3. Signature + provenance verification (Step 3) produces a signed attestation bundle → feeds the OPA policy engine.
  4. OPA policy evaluation (Step 4) gates the deployment and records a policy decision log → feeds the audit trail and compliance dashboards.
  5. SRI injection (Step 5) produces a hash manifest → fed into HTML templates and Browser Enforcement & Security Boundaries at runtime.

For third-party scripts loaded from a CDN, the output of this pipeline also informs CDN Trust Mapping & Routing, where you map each CDN origin to an SRI policy.


Troubleshooting

Permalink to "Troubleshooting"

Error: no matching signatures: none of the expected identities matched what was in the certificate The --certificate-identity-regexp pattern does not match the identity embedded in the signing certificate. Inspect the certificate with cosign verify-blob --output-file cosign-output.json ... and check the subject field. Common cause: the workflow was triggered by a fork where the path is your-org/your-repo but the cert records the fork’s path instead.

slsa-verifier: FAILED: builder identity: expected "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/..." mismatch The --builder-id implicit default expects the SLSA GitHub Generator reusable workflow. If you are using a different SLSA-conformant builder (e.g. actions/attest-build-provenance), pass --builder-id explicitly with the correct builder workflow URL.

npm audit signatures reports unverified registry signatures The package was published before npm 9.5 or the publisher did not pass --provenance. Check the registry entry at registry.npmjs.com/<package> for a dist.attestations field. If absent, pin to a specific version in the lockfile and add to a manual exception list in your policy, or replace with a provenance-publishing alternative.

opa eval returns a non-empty deny set on a valid artifact Most commonly caused by a stale timestamp_ns boundary. If the artifact was built more than 90 days ago and you are re-verifying it (e.g. from an artifact cache), adjust the policy window or re-sign with a fresh attestation. Do not extend the window permanently — rotate instead.

cosign verify-blob succeeds but the Rekor entry is missing If the --insecure-skip-verify flag was passed during signing, the entry was not logged to Rekor. This is a signing misconfiguration — re-sign without that flag. Production pipelines must never pass --insecure-skip-verify.

SRI hash mismatch in production after CDN delivery The CDN is transforming the asset (compression, minification, byte-order-mark injection). Hash computation must happen after all transformations. Run SRI injection as the final step before uploading to the CDN origin, not before. See Static Asset Hash Generation for build-time hash generation patterns.


Security boundary

Permalink to "Security boundary"

Provenance verification confirms that an artifact was built by a specific pipeline from a specific source commit — it does not:

  • Guarantee that the source code itself is free of malicious logic. A threat actor with repository write access can commit malicious code that passes all provenance checks because the provenance is technically valid.
  • Protect against a compromised CI runner that has write access to the build environment. SLSA Level 3 raises the bar here (isolated, non-forgeable builds) but does not eliminate the risk entirely.
  • Validate that npm package metadata (description, bin scripts, postinstall hooks) matches the provenance claim. Pair this workflow with Vulnerability Tracking & Triage and static analysis of install scripts.
  • Replace network-level controls. An artifact that passes provenance verification can still be exfiltrated or tampered with in transit if HTTPS is not enforced end-to-end.
  • Provide browser-level enforcement on its own. The SRI hash injected in Step 5 is what gives the browser the cryptographic signal it needs — provenance verification upstream is a prerequisite for trusting that hash is correct.

FAQ

Permalink to "FAQ"
What is the difference between SLSA provenance and an SBOM?

An SBOM lists what is in an artifact — components, versions, licenses, supplier names. SLSA provenance records how the artifact was built: which CI runner, from which source commit, with which build inputs. Both documents are needed for a complete trust picture. The SBOM answers inventory and compliance questions; provenance answers integrity and tamper-evidence questions. In a mature pipeline you produce both and link them via a common artifact digest.

Does Sigstore require managing my own signing keys?

No. Sigstore’s keyless model uses short-lived ephemeral certificates issued by Fulcio against an OIDC identity claim (e.g. the URL of the GitHub Actions workflow that triggered the build). The private key exists only for the duration of the signing operation and is never stored. The Rekor transparency log provides the long-lived audit trail, so verifiers do not need to obtain or trust a persistent public key — they verify the certificate chain back to Fulcio’s root CA and confirm the Rekor entry is present.

At what SLSA level should most teams aim?

SLSA Level 2 — meaning a hosted build service produces signed provenance — is achievable in a single sprint for most teams using GitHub Actions with the actions/attest-build-provenance action or the SLSA GitHub Generator reusable workflow. It eliminates the most common attack classes: insider threats who modify artifacts post-build, and CI runners that produce unsigned outputs. SLSA Level 3 (isolated, non-forgeable builds) is the right medium-term target for libraries that are published to public registries and consumed by many downstream projects.

What happens when an attestation expires or a signing key is rotated?

Re-sign the artifact with the new key and publish a fresh Rekor entry before the old attestation lapses. Because Rekor entries are immutable (you cannot replace them), you add a new signing record rather than modifying the old one. Update your OPA policy’s timestamp_ns window and trusted-identity allowlist as part of the rotation runbook. Any deployed instances that cache the old artifact digest will need to re-verify against the new attestation the next time the policy engine runs.

How does provenance verification relate to SRI hashes in the browser?

They operate at different enforcement boundaries. Provenance verification runs in your CI/CD pipeline and gates whether an artifact is allowed to enter your deployment process. SRI runs in the browser and gates whether a fetched resource is allowed to execute. The two are complementary: provenance proves the asset came from a trusted build; SRI proves the bytes the browser received match what that trusted build produced. Skipping provenance verification means an attacker who compromises your artifact storage could replace the artifact and regenerate the SRI hash — both defences together close that gap.


Permalink to "Related"

Articles in This Topic

Verifying Sigstore Provenance for npm Packages
Back to Supply Chain Auditing & Dependency Verification