Project node-addon-slsa

Expand description

GitHub repo npm version API docs CI status Test coverage

node-addon-slsa

Verifies that an npm package and its prebuilt native addon binary were produced by the same GitHub Actions workflow run. Uses sigstore for npm provenance and the GitHub Attestations API for binary verification. Aborts npm install with a SECURITY error if any check fails.

This tool trusts two infrastructure providers: GitHub Actions (build environment and attestation authority) and the sigstore public-good instance (Fulcio CA, Rekor transparency log). If either is compromised, verification may pass for malicious artifacts.

Threat Mitigation
Tampered npm package sigstore provenance verification
Tampered GitHub release GitHub Attestations API + sigstore
Mismatched artifacts Same workflow run check via URI
Man-in-the-middle on download SHA-256 hash verified against signed attestation
Path traversal via addon.path Resolved path must stay within package directory
  • Compromised CI workflow — if the workflow itself is malicious, all attestations will be valid for malicious code. This tool verifies provenance, not intent.
  • Compromised maintainer account — an attacker with write access to the repository can modify the workflow and produce legitimately attested builds.
  • Dependency confusion — the tool verifies a single package, not its transitive dependency tree.
  • Version 0.0.0 — all verification is skipped, by design, for local development and CI testing. Never publish version 0.0.0 to npm.
{
"name": "my-native-addon",
"version": "1.0.0",
"repository": {
"url": "git+https://github.com/owner/repo.git"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"addon": {
"path": "./dist/my_addon.node",
"url": "https://github.com/owner/repo/releases/download/v{version}/my_addon-v{version}-{platform}-{arch}.node.gz"
},
"scripts": {
"postinstall": "slsa wget",
"pack-addon": "slsa pack"
},
"dependencies": {
"node-addon-slsa": "0.6.2"
}
}
  • addon.path — where the native addon is installed, relative to the package root
  • addon.url — download URL template; supports {version}, {platform}, {arch} placeholders
  • postinstall — runs slsa wget on npm install: downloads the binary, verifies provenance, installs it
  • pack-addon — runs slsa pack in CI: gzip-compresses the binary before uploading to a release
  • exports["./package.json"] — required for loading the addon at runtime (see Loading the addon)
jobs:
build-addon:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.os }}
permissions:
contents: write # release upload
id-token: write # OIDC token for sigstore
attestations: write # build provenance
steps:
- uses: actions/checkout@v6
# ... set up toolchain, build native addon ...
- name: Compress binary for release
run: npx slsa pack
- name: Attest binary provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: dist/my_addon-v*.node.gz
- name: Upload binary to release
uses: softprops/action-gh-release@v2
with:
files: dist/my_addon-v*.node.gz

publish:
needs: build-addon
runs-on: ubuntu-latest
permissions:
contents: read # to fetch code
id-token: write # npm provenance via OIDC
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish --provenance --access public

Each matrix runner produces a platform-specific binary (e.g. my_addon-v1.0.0-linux-x64.node.gz). The {platform} and {arch} placeholders in addon.url resolve to process.platform and process.arch at install time, so each user downloads the correct binary for their OS.

slsa wget runs on npm install:

  1. Verifies npm package provenance via sigstore
  2. Extracts the Run Invocation URI from the Fulcio certificate
  3. Downloads the compressed binary from the GitHub release, computing a SHA-256 hash of the compressed bytes and decompressing into a temp file
  4. Verifies the binary's GitHub attestation matches the same workflow run
  5. Moves the verified binary to its final location

On failure, the temp file is removed and installation aborts.

Command / Option Purpose
slsa pack Gzip-compress the native addon for release
slsa wget Download, verify, and install the native addon
--help, -h Show usage information
Variable Purpose
GITHUB_TOKEN GitHub API auth (required for private repos, increases rate limits)
SLSA_DEBUG=1 Debug logging to stderr
Type Constructor Purpose
GitHubRepo githubRepo(value) GitHub owner/repo slug
SemVerString semVerString(value) Strict semver (no v prefix)
Sha256Hex sha256Hex(value) Lowercase hex-encoded SHA-256 (64 chars)
RunInvocationURI runInvocationURI(value) GitHub Actions run invocation URL

Constructors validate at runtime and throw TypeError on invalid input.

import {
verifyPackageProvenance,
verifyAddonProvenance,
isProvenanceError,
sha256Hex,
semVerString,
githubRepo,
} from "node-addon-slsa";
import type {
PackageProvenance,
RunInvocationURI,
VerifyOptions,
} from "node-addon-slsa";

// Verify npm package provenance via sigstore.
// Returns { runInvocationURI, verifyAddon() }.
const provenance: PackageProvenance = await verifyPackageProvenance({
packageName: "my-native-addon",
version: semVerString("1.0.0"),
repo: githubRepo("owner/repo"),
});

// With custom timeouts (e.g. behind a slow proxy):
const provenance2 = await verifyPackageProvenance({
packageName: "my-native-addon",
version: semVerString("1.0.0"),
repo: githubRepo("owner/repo"),
timeoutMs: 60_000,
retryCount: 5,
});

// Verify the addon binary was produced by the same workflow run.
await provenance.verifyAddon({ sha256: sha256Hex(hexHash) });

// Standalone binary verification when you already have a URI.
await verifyAddonProvenance({
sha256: sha256Hex(hexHash),
runInvocationURI,
repo: githubRepo("owner/repo"),
});
  • ProvenanceError — verification failed (tampered artifact, mismatched provenance). Do not retry.
  • Error — transient issue (network timeout, GitHub API rate limit). Safe to retry.

Use isProvenanceError(err) in catch blocks to distinguish the two.

Non-security errors include a Set SLSA_DEBUG=1 for detailed diagnostics hint. When reporting issues, include the debug output.

The repository field (or repository.url) determines the expected GitHub repository for attestation verification. Both CLI commands read it from package.json in the working directory. Only github.com URLs are supported (HTTPS, SSH, with or without .git suffix).

import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import packageJson from "../package.json" with { type: "json" };

const require = createRequire(import.meta.url);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const addon = require(join(root, packageJson.addon.path));

The "./package.json" export in your exports map is required for this JSON import to resolve under strict ESM exports.

Public repositories work without authentication. Private repositories require GITHUB_TOKEN. Unauthenticated requests are limited to 60/hour by GitHub. Set GITHUB_TOKEN to increase:

export GITHUB_TOKEN="$(gh auth token)"

Type Aliases§

Source§

type BundleVerifier = Awaited<ReturnType<typeof createVerifier>>

Sigstore bundle verifier created by createVerifier() from the sigstore package.

Source§

type GitHubRepo = `${string}/${string}`

GitHub owner/repo slug.

Source§

type RunInvocationURI = string & { "[___runInvocationURIBrand]": true }

Source§

type SemVerString = `${number}.${number}.${number}${string}`

Strict semver string (no v prefix): major.minor.patch[-pre][+build]. The template literal type is intentionally wider than the runtime check in semVerString because TypeScript cannot express the full regex.

Source§

type Sha256Hex = string & { "[___sha256HexBrand]": true }

Functions§

Source§

githubRepo(value: string): `${string}/${string}`

Validate and brand a string as a GitHubRepo.

throws

if the input is not in owner/repo format.

Source§

isProvenanceError(err: unknown): err is ProvenanceError

Type guard for ProvenanceError. Use in catch blocks to distinguish security failures from transient errors.

Source§

runInvocationURI(value: string): RunInvocationURI

Validate and brand a string as a RunInvocationURI.

throws

if the input is not a valid GitHub Actions run invocation URL.

Source§

semVerString(value: string): `${number}.${number}.${number}${string}`

Validate and brand a string as a SemVerString.

throws

if the input does not match major.minor.patch[-pre][+build].

Source§

sha256Hex(value: string): Sha256Hex

Validate and brand a string as a Sha256Hex.

throws

if the input is not exactly 64 lowercase hex characters.

Source§

verifyAddonProvenance(
    options: {
        repo: `${string}/${string}`;
        runInvocationURI: RunInvocationURI;
        sha256: Sha256Hex;
    } & VerifyOptions,
): Promise<void>

Verify addon binary provenance via the GitHub Attestations API. Confirms the artifact was attested in the expected workflow run and source repository.

Typically called via verifyAddon. Use directly when you already have a RunInvocationURI.

throws

ProvenanceError if no attestation matches the expected workflow run, or all attestations fail cryptographic verification.

throws

Error on transient failures (network timeout, API rate limit) — safe to retry.

example
import {
verifyAddonProvenance,
sha256Hex,
githubRepo,
runInvocationURI,
} from "node-addon-slsa";

await verifyAddonProvenance({
sha256: sha256Hex("a".repeat(64)),
runInvocationURI: runInvocationURI(
"https://github.com/owner/repo/actions/runs/123/attempts/1",
),
repo: githubRepo("owner/repo"),
});
Source§

verifyPackageProvenance(
    options: {
        packageName: string;
        repo: `${string}/${string}`;
        version: `${number}.${number}.${number}${string}`;
    } & VerifyOptions,
): Promise<PackageProvenance>

Verify npm package provenance via sigstore attestations. Checks the certificate chain, issuer identity, and source repository. Returns a PackageProvenance handle for addon verification.

throws

ProvenanceError if the package has no SLSA provenance attestation, the certificate is invalid, or the source repo does not match.

throws

Error on transient failures (network timeout, API rate limit) — safe to retry.

example
import {
verifyPackageProvenance,
semVerString,
githubRepo,
sha256Hex,
} from "node-addon-slsa";

const provenance = await verifyPackageProvenance({
packageName: "my-native-addon",
version: semVerString("1.0.0"),
repo: githubRepo("owner/repo"),
});

// Verify the addon binary was produced by the same workflow run.
const addonHash = sha256Hex("a".repeat(64)); // SHA-256 of the binary
await provenance.verifyAddon({ sha256: addonHash });

Classes§

ProvenanceError

Thrown when provenance verification detects a security issue. The message is prefixed with SECURITY: and includes remediation advice.

Interfaces§

FetchOptions

Options controlling HTTP fetch behavior (timeouts, retries, cancellation).

PackageProvenance

Returned by verifyPackageProvenance after npm provenance checks pass.

VerifyOptions

Verification options: extends FetchOptions with attestation-specific limits.