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:
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:
- Lockfile analysis (Step 1) produces a resolved component list with integrity hashes → feeds the SBOM generator.
- SBOM (Step 2) assigns purl identifiers to every component → feeds the signature verification engine.
- Signature + provenance verification (Step 3) produces a signed attestation bundle → feeds the OPA policy engine.
- OPA policy evaluation (Step 4) gates the deployment and records a policy decision log → feeds the audit trail and compliance dashboards.
- 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,
binscripts,postinstallhooks) 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.
Related
Permalink to "Related"- Verifying Sigstore Provenance for npm Packages — step-by-step Sigstore attestation verification specific to the npm ecosystem
- Automated SBOM Generation — generate CycloneDX and SPDX inventories that feed the verification pipeline
- Lockfile Mapping & Analysis — extract precise version constraints and integrity hashes before provenance checks run
- Vulnerability Tracking & Triage — complement provenance gating with CVE-based risk scoring on verified components
- CDN Trust Mapping & Routing — extend provenance policy to third-party CDN origins using SRI enforcement