Repository navigation
ci(release): signed GitHub Releases with Sigstore signatures and SLSA provenance - #55
Merged
Merged
Conversation
On a v* tag, release.yml now also: - builds a reproducible source archive (git archive | gzip -n) and the Next.js standalone bundle (.next/standalone + .next/static + public, runs with `node server.js`), plus a CycloneDX SBOM of the production npm tree (npm sbom) and SHA256SUMS; - keyless-signs every file and SHA256SUMS with cosign sign-blob (<file>.sigstore.json bundles, an extension Scorecard counts) and verifies them against the workflow identity; - attests SLSA v1 build provenance for the files with the SHA-pinned actions/attest-build-provenance, ships its bundle as osiris-<tag>.intoto.jsonl (what Scorecard looks for) and verifies it with `gh attestation verify`. slsa-github-generator was not used: its reusable workflow can only be tag-pinned, which Scorecard's Pinned-Dependencies check counts as unpinned; - creates the GitHub Release with generated notes and all assets, only after the image is built, signed and verified. contents: write is scoped to that release job only. The container job is unchanged apart from a shellcheck directive for its intentional word splitting. Bump package.json/package-lock.json to 0.2.0 so the first signed tag (v0.2.0) matches the package version; v0.1.0 already exists as an asset-less release. DOCKER.md gains "Verifying a release" and README a short "Signed releases" section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every
v*tag now produces a signed GitHub Release alongside the container image the workflow already signs. The goal is for OpenSSF Scorecard's Signed-Releases check to score (today it reports "no releases found") and for users to be able to verify what they download.Release contents (example
v0.2.0)osiris-v0.2.0-source.tar.gzgit archive --prefix=osiris-v0.2.0/ HEAD | gzip -n -9. Reproducible from the tag.osiris-v0.2.0-standalone.tar.gz.next/standalone+.next/static+public); runs withnode server.js. Tarred with sorted names, a fixed mtime (commit time) and numeric root owner.osiris-v0.2.0-sbom.cdx.jsonnpm sbom --omit=dev(built into npm, so there is no extra tool to pin).SHA256SUMS*.sigstore.json(×4)cosign sign-blob --bundlebundles for each file and forSHA256SUMS.osiris-v0.2.0.intoto.jsonlactions/attest-build-provenance.The image job (build, push, sign, verify, plus the BuildKit SBOM and
provenance: mode=max) is unchanged. The only edit there is a# shellcheck disable=SC2086directive, because its word splitting is intentional.Signing / provenance design
container: unchanged.artifacts: runsnpm ciand the build, packages the files, writes the SBOM andSHA256SUMS, then signs each file with cosign. It runscosign verify-blobagainsthttps://github.com/carbon-evolution/osiris/.github/workflows/release.yml@refs/tags/<tag>, attests build provenance, and runsgh attestation verifyon every file.release: needs both jobs above. It runssha256sum -cagain, thengh release create --verify-tag --generate-notes.--clobber.workflow_dispatchon a branch runs only the container job.releasesAreSignedprobe accepts.asc .minisig .sig .sign .sigstore .sigstore.json(source). We use.sigstore.json, which is what cosign v3 writes by default.releasesHaveProvenanceonly looks for a*.intoto.jsonlasset (source).pinned_dependencies_test.go, case "job uses with no steps") reportsslsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0as an unpinned dependency. Pinned-Dependencies (currently 10) would drop to about 9. The generator would also needcontents: writeon its calling job.actions/attest-build-provenance@4d10147…(v4.2.2). It is SHA-pinned and GitHub-owned, and needs onlyattestations: write+id-token: write. It produces a Sigstore-signed in-toto statement with a SLSA v1 predicate, stored in the repo's attestations.osiris-<tag>.intoto.jsonl. The Bazel Central Registry follows the same convention for attest-build-provenance bundles.gh attestation verify, either offline with--bundleor online against GitHub's attestation store. I checked the exact flag set (--bundle --signer-workflow --source-ref --deny-self-hosted-runners) against a real attestation fromcli/cli: it passes, and--source-refis enforced.contents: read.artifactshascontents: read,id-token: writeandattestations: write.releaseis the only job withcontents: write.# vX.Y.Zcomments.upload-artifactuses the SHA already in the repo;download-artifactis pinned at9000827…(v8.0.2).env:.Verification commands (also in DOCKER.md → "4. Verifying a release"; README gets a short "Signed releases" section)
Local verification done
rhysd/actionlint1.7.12 with shellcheck reports nothing onrelease.yml. The pre-existing SC2034 inzap.ymlis untouched.npm ci && npm run build, then the packaging step'srun:block, extracted verbatim from the YAML, in anode:22container (GNU tar and coreutils).sha256sum -c SHA256SUMSpasses.git archiveon macOS.PORT=3017 node server.js.GET /returned 200 (52 KB)./_next/static/chunks/*.js,*.cssand thepublic/files all returned 200. The server was stopped afterwards.cosign/gh, so nothing was signed with a real identity.osiris-v0.2.0.intoto.jsonl, andgh attestation verifyis called with the right flags for each file.gh release createreceives all 9 assets plus the notes.Version and first tag
v0.1.0already exists: a manually created release with no assets. Scorecard skips releases without assets, which is why it reports "no releases found". There are 38 commits since then. This PR bumpspackage.jsonandpackage-lock.jsonto 0.2.0 (npm version 0.2.0 --no-git-tag-version), so the next tag matches the package version.After merging, from an up-to-date
main:Scorecard expectation
Signed-Releases scores 10 per release that has provenance and 8 for signatures only, averaged over the last 5 releases that have assets.
v0.1.0has no assets, sov0.2.0should move the check from "no releases found" to 10/10. Pinned-Dependencies stays at 10 because everything is hash-pinned. Token-Permissions should stay at 10, since all write scopes are job-level.What to check after tagging
releaserun is green:container+artifacts→release.ghcr.io/carbon-evolution/osiris:0.2.0passescosign verify.🤖 Generated with Claude Code