Verifying Sigstore Provenance for npm Packages
Permalink to "Verifying Sigstore Provenance for npm Packages"Part of Provenance Verification Workflows — this page covers exactly how to validate Sigstore attestation bundles for npm packages and bind the result to Subresource Integrity hashes in a CI/CD pipeline.
Quick Reference
Permalink to "Quick Reference"| Item | Value |
|---|---|
| CLI command (registry signatures) | npm audit signatures |
| CLI command (bundle verification) | cosign verify-attestation --type slsaprovenance … |
| Required npm version | 9.5.0+ |
| Attestation format | Sigstore bundle (.sigstore.json) — DSSE envelope |
| Hash algorithm for SRI | SHA-384 (preferred) |
| Fulcio CA root | https://fulcio.sigstore.dev |
| Rekor log endpoint | https://rekor.sigstore.dev |
| Browser support for SRI | Chrome 45+, Firefox 43+, Safari 11.1+, Edge 17+ |
Why Sigstore Provenance Exists
Permalink to "Why Sigstore Provenance Exists"Package registries have always provided checksums — but a checksum only proves you received the exact bytes the registry stored; it says nothing about how those bytes were built or who signed them. Sigstore provenance fills this gap by binding a build-time attestation (a signed statement produced inside the CI runner) to the published artifact. The attestation records the repository URL, the commit SHA, the workflow file path, and the OIDC identity of the runner that executed the build. Crucially, the signing key is ephemeral: Fulcio issues a short-lived certificate tied to the runner’s OIDC token and records it in the Rekor transparency log, so there is no long-lived secret to steal or rotate. When you verify a Sigstore bundle you are not just checking a hash — you are confirming that a specific GitHub Actions workflow at a specific commit produced the artifact.
This is the cryptographic foundation that Supply Chain Auditing & Dependency Verification builds on for npm packages.
Verification Pipeline Architecture
Permalink to "Verification Pipeline Architecture"The diagram below shows how a registry fetch flows through Sigstore verification before SRI hash generation.
Canonical Implementation
Permalink to "Canonical Implementation"The canonical path uses npm audit signatures for the full installed tree, then cosign verify-attestation for any package whose attestation bundle needs to be inspected at depth (for example, a tier-1 dependency with a recent CVE or an internal package published to a private registry).
Step 1 — Verify all installed packages against registry signatures
Permalink to "Step 1 — Verify all installed packages against registry signatures"# Requires npm >= 9.5.0
npm audit signatures
Expected output on a clean tree:
audited 312 packages in 4s
312 packages have verified registry signatures
Any package listed under unverified or missing signature must be treated as untrusted and blocked from the build.
Step 2 — Verify a specific package’s Sigstore attestation bundle
Permalink to "Step 2 — Verify a specific package’s Sigstore attestation bundle"When a package was published with npm publish --provenance, a .sigstore.json bundle is attached to the registry entry. Download it and verify:
# Download the bundle for a specific package version
npm pack --dry-run [email protected] 2>/dev/null
# Fetch the tarball and its attestation
curl -sL "$(npm view [email protected] dist.tarball)" -o react-18.3.1.tgz
curl -sL "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]" \
| jq '.attestations[0].bundle' > react-18.3.1.sigstore.json
# Verify: confirms Fulcio cert chain + Rekor inclusion proof
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp="^https://github.com/facebook/react/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--bundle=react-18.3.1.sigstore.json \
react-18.3.1.tgz
A successful run prints the attestation payload to stdout and exits 0. A non-zero exit means verification failed — do not proceed to compilation.
Step 3 — CI/CD pipeline gate (GitHub Actions)
Permalink to "Step 3 — CI/CD pipeline gate (GitHub Actions)"Insert the verification step between npm install and any compilation task. The id-token: write permission is required so the runner can request an OIDC token from GitHub (needed if you are also publishing; for verification-only steps it is not strictly required but keeps the workflow consistent).
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for OIDC token requests
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies (frozen lockfile)
run: npm ci --ignore-scripts
- name: Verify registry signatures
run: |
npm audit signatures
echo "All package signatures verified."
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Verify provenance attestation for tier-1 deps
run: |
# Example: verify react's published attestation
BUNDLE=$(curl -sf "https://registry.npmjs.org/-/npm/v1/attestations/react@$(node -e "console.log(require('./node_modules/react/package.json').version)")" \
| jq -r '.attestations[0].bundle | @json')
echo "$BUNDLE" > /tmp/react.sigstore.json
TARBALL_URL=$(npm view "react@$(node -e "console.log(require('./node_modules/react/package.json').version)")" dist.tarball)
curl -sfL "$TARBALL_URL" -o /tmp/react.tgz
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp="^https://github.com/facebook/react/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--bundle=/tmp/react.sigstore.json \
/tmp/react.tgz
- name: Generate SRI hashes (post-verification only)
run: node scripts/generate-sri.mjs
Step 4 — Generate SRI hashes after verification
Permalink to "Step 4 — Generate SRI hashes after verification"Compute SHA-384 digests only once verification has exited 0. Binding hashes to unverified artifacts defeats the purpose of SRI enforcement.
// scripts/generate-sri.mjs
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
const DIST = new URL("../dist/", import.meta.url).pathname;
const manifest = {};
for (const file of readdirSync(DIST).filter(f => f.endsWith(".js") || f.endsWith(".css"))) {
const content = readFileSync(join(DIST, file));
const digest = createHash("sha384").update(content).digest("base64");
manifest[file] = `sha384-${digest}`;
}
writeFileSync("dist/sri-manifest.json", JSON.stringify(manifest, null, 2));
console.log("SRI manifest written:", Object.keys(manifest).length, "assets");
In the HTML template, inject the integrity attribute from the manifest:
<!-- Built output — integrity value injected from sri-manifest.json -->
<script
src="/dist/app.abc123.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
Note the crossorigin="anonymous" attribute — omitting it is the single most common real-world SRI deployment mistake. Without it the browser sends credentials with the request, the CORS preflight may fail, and the integrity check is skipped entirely.
Variant Configurations
Permalink to "Variant Configurations"Variant 1 — Verifying packages from a private registry (Verdaccio / GitHub Packages)
Permalink to "Variant 1 — Verifying packages from a private registry (Verdaccio / GitHub Packages)"Private registries can proxy npmjs.com and add their own attestation layer. Configure a separate identity constraint pointing at your registry’s OIDC issuer:
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp="^https://github.com/your-org/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--rekor-url="https://rekor.sigstore.dev" \
--bundle=package.sigstore.json \
package.tgz
For fully air-gapped environments, substitute --rekor-url with your self-hosted Rekor instance.
Variant 2 — Policy-as-code enforcement with OPA/Rego
Permalink to "Variant 2 — Policy-as-code enforcement with OPA/Rego"When the number of packages requiring attestation grows, inline shell scripts become hard to audit. Externalise the policy:
# policy/provenance.rego
package provenance
default allow = false
allow if {
input.issuer == "https://token.actions.githubusercontent.com"
startswith(input.subject, "https://github.com/your-org/")
input.timestamp_ns > (time.now_ns() - (90 * 24 * 60 * 60 * 1000000000))
}
deny[msg] if {
not allow
msg := sprintf("Attestation rejected: issuer=%v subject=%v", [input.issuer, input.subject])
}
Feed the parsed attestation payload into the OPA engine from your CI step:
cosign verify-attestation \
--type slsaprovenance \
--bundle=artifact.sigstore.json \
artifact.tgz \
| jq '.payload | @base64d | fromjson | .predicate' \
| opa eval -d policy/provenance.rego -I 'data.provenance.allow'
Variant 3 — Lockfile-level batch verification
Permalink to "Variant 3 — Lockfile-level batch verification"For projects with large dependency trees, loop over the lockfile’s resolved packages:
#!/usr/bin/env bash
# verify-all.sh — verifies every package that has a published attestation
set -euo pipefail
FAILED=0
while IFS= read -r line; do
PKG=$(echo "$line" | jq -r '.name + "@" + .version')
ATTESTATION_URL="https://registry.npmjs.org/-/npm/v1/attestations/$PKG"
if curl -sf "$ATTESTATION_URL" > /tmp/bundle.json 2>/dev/null; then
BUNDLE=$(jq -r '.attestations[0].bundle | @json' /tmp/bundle.json)
echo "$BUNDLE" > /tmp/pkg.sigstore.json
TARBALL=$(npm view "$PKG" dist.tarball 2>/dev/null)
curl -sfL "$TARBALL" -o /tmp/pkg.tgz 2>/dev/null
if ! cosign verify-attestation \
--type slsaprovenance \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--bundle=/tmp/pkg.sigstore.json \
/tmp/pkg.tgz > /dev/null 2>&1; then
echo "FAILED: $PKG"
FAILED=$((FAILED + 1))
fi
fi
done < <(npm ls --json --all 2>/dev/null | jq -c '[.. | objects | select(.version)] | unique_by(.name + .version) | .[]')
if [ "$FAILED" -gt 0 ]; then
echo "$FAILED package(s) failed attestation verification." >&2
exit 1
fi
echo "All attestation checks passed."
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
Timestamp validation is not optional. Sigstore certificates are intentionally short-lived (10-minute validity window). Skipping the timestamp check lets expired or revoked certificates pass. Always verify that the bundle’s certificate was valid at the time the log entry was recorded — cosign does this by default when
--bundleis used, but verify your cosign version (1.13.0+). -
id-token: writescope matters for publishing, not just verification. If your CI step only verifies (does not publish), the OIDC permission is not required. Adding it unnecessarily expands the attack surface of the job token. Split publish and verify into separate jobs with least-privilege permissions. -
Transitive dependencies may lack attestations.
npm audit signaturescovers all installed packages, but many older or less-maintained packages have no Sigstore bundle. Decide upfront whether missing attestations are a hard block or a warning — and document the policy explicitly. An allow-list of exempted packages with a review date is safer than silently skipping. -
SRI hash generation must follow verification. If your build cache restores
dist/from a previous run, the cached assets may predate the current verification pass. Clear or scope the cache key so that SRI hashes are always recomputed from freshly verified artifacts. -
crossorigin="anonymous"must be present on every<script>and<link>tag that carries anintegrityattribute. Without it, the browser issues a no-CORS request and drops the integrity check. For resources on a different origin this also means the CORS preflight must succeed — ensure your CDN returnsAccess-Control-Allow-Origin: *or a specific origin match. -
OPA/policy-as-code rules must enforce a rotation window. The 90-day window in the Rego example above is illustrative; align the value with your organisation’s key rotation policy and SOC 2 or PCI DSS requirements.
Verification Steps
Permalink to "Verification Steps"After configuring the pipeline, confirm it is working correctly with these checks.
1. Local — confirm npm audit signatures passes:
npm ci --ignore-scripts && npm audit signatures
# Expected: "N packages have verified registry signatures"
# Any "unverified" line is a failure.
2. Local — confirm cosign version supports bundles:
cosign version
# Must be >= 1.13.0 for --bundle flag support
3. CI — inspect the GitHub Actions summary. After the pipeline runs, expand the “Verify registry signatures” step. The final line must be All package signatures verified. with exit code 0. A non-zero exit will red the step and halt the job automatically because of set -e semantics.
4. SRI manifest — spot-check a hash:
# Recompute manually and compare against the manifest
openssl dgst -sha384 -binary dist/app.abc123.js | openssl base64 -A
# Output must match the sha384-... value in sri-manifest.json
5. Browser DevTools — confirm integrity enforcement. Load the deployed page, open the Network tab, select the script resource, and check the Response Headers. The browser will log a Content Security Policy or Subresource Integrity error in the Console if the hash does not match — confirming enforcement is live. For a clean verification, the request must complete with status 200 and no console errors. See Configuring Content-Security-Policy with SRI for the CSP directives that reinforce this check.
Does npm audit signatures check transitive dependencies?
Yes. The command walks the full installed tree under node_modules and reports signature status for every package — direct and transitive. Packages without any signature are listed separately under unverified. You can decide whether to treat missing signatures as blocking based on your policy.
What happens if Rekor is temporarily unreachable?
By default, cosign verify-attestation requires a successful Rekor lookup to confirm the inclusion proof. If Rekor is unreachable the command fails. You can pass --insecure-skip-verify to bypass the log check, but this defeats a core security property — the immutability guarantee. In practice, Rekor has strong availability SLAs; treat an outage as a signal to hold the deployment rather than skip verification.
Can I verify provenance for packages published before Sigstore support landed?
No. Attestation bundles are only available for packages published with npm publish --provenance on npm >= 9.5.0. Packages published before this feature existed have no bundle. For those packages, fall back to lockfile hash pinning via Lockfile Mapping & Analysis and treat missing attestations as a known risk in your threat model.
Should I store the attestation bundles in my repository?
Not normally. The canonical source of truth is the registry’s attestation endpoint and the Rekor log. Storing bundles in git risks serving a stale or forged copy. Fetch the bundle at verification time from https://registry.npmjs.org/-/npm/v1/attestations/<pkg>@<version> so you are always checking the live registry state.
Related
Permalink to "Related"- Provenance Verification Workflows — the parent guide covering the full attestation pipeline
- Lockfile Mapping & Analysis — maps exact resolved versions before verification
- Automated SBOM Generation — produces the inventory that attestation verification operates against