Code signing and release integrity: GPG, Sigstore, and why it matters for auth software

Authentication libraries are high-value targets for supply chain attacks. A compromised JWT library, an OAuth SDK that silently exfiltrates tokens, or a SAML parser with a backdoor can give attackers access to every application that depends on them. The npm ecosystem has seen dozens of these attacks — typosquatted packages, maintainer account takeovers, and malicious pull requests merged into legitimate packages. Code signing and release integrity mechanisms give users a way to verify that what they installed is what you published.

The supply chain threat model

A supply chain attack can occur at several points in the software distribution pipeline:

  • Source code compromise: an attacker modifies the source code before release, either by compromising a developer's machine or by merging a malicious PR.
  • Build system compromise: the CI/CD pipeline is compromised to inject malicious code into the build artifacts, even if the source code is clean.
  • Registry compromise: the package registry (npm, PyPI) is compromised and the published artifact is replaced.
  • Typosquatting: a package with a similar name is published to trick users into installing the wrong package.
  • Dependency confusion: a public package is published with the same name as a private internal package, and the package manager fetches the attacker's version.

For auth libraries specifically, the attack payloads are high-value: logging credentials, exfiltrating tokens, or silently disabling validation checks.

GPG release signing

The traditional approach to release integrity is signing release artifacts with a GPG key. The maintainer's public key is published and users can verify the signature before trusting the artifact. This approach is widely used for Linux distributions and mature open source projects.

# Creating a release signing key
gpg --full-generate-key
# Choose RSA 4096, no expiry for a long-lived release key
# Use a separate key for releases, not your personal key

# Sign a release artifact
gpg --armor --detach-sign bastionary-sdk-1.2.3.tgz
# Produces bastionary-sdk-1.2.3.tgz.asc

# Publish the signature alongside the artifact
# Users can verify with:
gpg --verify bastionary-sdk-1.2.3.tgz.asc bastionary-sdk-1.2.3.tgz

GPG signing has significant operational overhead: key management, key distribution (via keyservers or your own page), key rotation, and the fact that most developers do not actually verify signatures. It also does not address build provenance — you are signing the output, but not proving how it was built or from which source commit.

Sigstore and cosign

Sigstore is a newer approach to supply chain security that makes signing transparent and keyless. The cosign tool (part of the Sigstore project) signs artifacts using a short-lived certificate issued against the signer's OIDC identity (typically their GitHub, Google, or Microsoft account). The signature and certificate are recorded in a public, append-only transparency log called Rekor. Anyone can verify the signature without needing to distribute or trust a separate public key.

# Sign a container image with cosign (keyless mode using OIDC)
cosign sign ghcr.io/yourorg/bastionary-sdk:1.2.3
# This will open a browser for OIDC authentication
# The signature is stored in the container registry and logged to Rekor

# Verify the signature
cosign verify \
  --certificate-identity-regexp="^https://github.com/yourorg/bastionary-sdk/" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/yourorg/bastionary-sdk:1.2.3

# Sign an artifact (non-container)
cosign sign-blob \
  --output-certificate bastionary-sdk-1.2.3.tgz.pem \
  --output-signature bastionary-sdk-1.2.3.tgz.sig \
  bastionary-sdk-1.2.3.tgz

For GitHub Actions workflows, keyless signing is built in via OIDC identity tokens. The signature binds the artifact to the specific workflow, repository, and commit that produced it.

# GitHub Actions: sign artifacts in CI
name: Release
on:
  release:
    types: [published]

permissions:
  id-token: write  # Required for OIDC keyless signing
  contents: read

jobs:
  sign-and-publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install cosign
        uses: sigstore/cosign-installer@v2

      - name: Build artifact
        run: npm pack

      - name: Sign with cosign
        run: |
          cosign sign-blob \
            --output-certificate bastionary-sdk-*.tgz.pem \
            --output-signature bastionary-sdk-*.tgz.sig \
            bastionary-sdk-*.tgz

      - name: Upload to release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            bastionary-sdk-*.tgz
            bastionary-sdk-*.tgz.pem
            bastionary-sdk-*.tgz.sig

SLSA provenance

Supply-chain Levels for Software Artifacts (SLSA, pronounced "salsa") is a framework for incrementally improving supply chain security. The levels range from basic (SLSA 1: build process documented) to highly secure (SLSA 4: hermetic, reproducible builds with two-party review). SLSA provenance is a signed attestation that describes how an artifact was built — the source repository, the commit hash, the build system, and the build parameters.

# Generating SLSA provenance with the official GitHub Actions generator
jobs:
  build:
    outputs:
      hashes: ${{ steps.hash.outputs.hashes }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci && npm pack
      - id: hash
        run: |
          echo "hashes=$(sha256sum bastionary-sdk-*.tgz | base64 -w0)" >> $GITHUB_OUTPUT

  provenance:
    needs: [build]
    permissions:
      id-token: write
      contents: write
      actions: read
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0
    with:
      base64-subjects: "${{ needs.build.outputs.hashes }}"
Verify the SLSA provenance of your own dependencies. The slsa-verifier tool can check provenance attestations for packages that publish them. GitHub's npm registry and PyPI are adding provenance support. Making verification part of your dependency update workflow catches compromised dependencies before they reach production.

npm package integrity

For npm packages specifically, package lock files (package-lock.json) and the npm registry's built-in integrity hashes provide some protection. Each package entry includes a resolved URL and an integrity hash. Running npm ci (rather than npm install) verifies these hashes on install and fails if they do not match.

// package-lock.json integrity entry
"bastionary-sdk": {
  "version": "1.2.3",
  "resolved": "https://registry.npmjs.org/bastionary-sdk/-/bastionary-sdk-1.2.3.tgz",
  "integrity": "sha512-abc123...==",
  // This hash is verified by npm ci on every install
}

// Always use npm ci in production/CI, never npm install
// npm ci: uses exact versions from lockfile, verifies integrity hashes
// npm install: updates the lockfile, does not verify integrity

The combination of lockfile integrity verification (npm ci), Sigstore signing of release artifacts, and SLSA provenance for your build pipeline gives users multiple independent mechanisms to verify the integrity of what they are installing — even if the registry, the distribution channel, or an intermediate CDN is compromised.

← Back to blog Try Bastionary free →