Skip to content

Verify Sigstore attestation cryptographically in pluggy upgrade #29

@ch99q

Description

@ch99q

Problem

pluggy upgrade calls assertAttestedByWorkflow (src/commands/upgrade.ts:239) to confirm a downloaded release binary has a Sigstore attestation in this repo's GitHub attestation store. The function checks two things:

  1. At least one attestation exists for the binary's sha256.
  2. The leaf certificate's SAN identifies the issuing OIDC identity as a workflow under https://github.com/{repo}/.github/workflows/.

It deliberately does not verify the Sigstore bundle cryptographically. From the inline comment:

We deliberately don't verify the Sigstore signature in-binary: that would mean shipping a Fulcio trust root and a full bundle verifier, a substantial amount of crypto code. The check above trusts GitHub's attestation API as a transport (same channel as the asset download), which is a meaningful improvement over no attestation at all without the implementation cost of a complete client-side Sigstore verifier.

The trade-off catches the realistic attacks (URL substitution, attestation-from-another-repo) but leaves gaps:

  • GitHub API as transport: a compromise of api.github.com (or its TLS chain) defeats both the asset download and the attestation lookup at once.
  • No Rekor SET check: we don't prove the attestation was published to the public transparency log within the cert's short validity window. A replayed attestation is in principle accepted as long as the SAN matches.
  • No Fulcio chain validation: a malformed or self-signed leaf cert that happens to include the expected SAN string in its subjectAltName would pass the current check.

Proposed solution

Replace the SAN-only check with full Sigstore bundle verification using @sigstore/verify:

  • Bundle the Fulcio public-good trust root and Rekor public key (as constants).
  • Pass the bundle returned by GET /repos/{owner}/{repo}/attestations/sha256:{hex} to the verifier.
  • Configure the verifier to require the same identity SAN we check today (https://github.com/{repo}/.github/workflows/*).
  • Keep the attestation-existence check; the API call is already the lookup mechanism.

Approximate shape:

import { Verifier, toSignedEntity } from "@sigstore/verify";

const verifier = new Verifier({
  trustedRoot: FULCIO_TRUSTED_ROOT,           // constants shipped in the binary
  ctlogThreshold: 1,
  tlogThreshold: 1,
});

verifier.verify(toSignedEntity(bundle), {
  certificateIdentities: [
    { issuer: "https://token.actions.githubusercontent.com", subjectAlternativeName: expectedIdentityPrefix }
  ],
  artifactDigest: { algorithm: "sha256", hex: sha256Hex },
});

This eliminates the GitHub-API-as-transport assumption and adds Rekor SET verification.

Alternatives considered

  • Status quo (SAN-only): cheap, catches the obvious attacks, depends on api.github.com integrity. Already in assertAttestedByWorkflow.
  • Pin the leaf cert's full DER: would catch some replay attacks but breaks every release that rotates the OIDC identity (every release). Not viable.
  • Pull from the npm sigstore CLI as a child process: removes the binary-size cost but introduces a runtime dependency on Node + npm + a separate package, defeating the single-file Bun-compiled distribution model.
  • Skip attestation entirely: regression on the security of the upgrade path.

Additional context

  • Inline comment block: src/commands/upgrade.ts:214-237.
  • actions/attest-build-provenance workflow already produces these attestations; no upstream change needed.
  • @sigstore/verify is the official Sigstore JS SDK: https://www.npmjs.com/package/@sigstore/verify. Bundle size adds ~400 KB to the compiled binary (rough estimate; worth measuring before deciding).
  • Rekor public key and Fulcio trust root rotate rarely (years). The maintenance is real but bounded.
  • Discussed in the ch99q/review-code-conventions PR (refactor: align CLI with quality conventions, add why/outdated/audit, polish UX #28) review thread, where the current trade-off was flagged as honest-but-skippable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecuritySecurity-related issues and improvements

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions