diff --git a/.github/pinact.yaml b/.github/pinact.yaml new file mode 100644 index 0000000..4a445d8 --- /dev/null +++ b/.github/pinact.yaml @@ -0,0 +1,8 @@ +# pinact config: https://github.com/suzuki-shunsuke/pinact +# +# The SLSA generic generator reusable workflow must stay pinned by a vX.Y.Z tag — +# SHA-pinning it breaks slsa-verifier's builder-identity resolution. Exclude it from +# `pinact run` so the tag pin survives automated re-pinning passes. +ignore_actions: + - name: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml + ref: "v\\d+\\.\\d+\\.\\d+" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b14a64..eb0b860 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,9 @@ jobs: permissions: contents: write packages: write + id-token: write # required for cosign keyless signing + outputs: + hashes: ${{ steps.hash.outputs.hashes }} steps: - name: Check out repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -86,6 +89,9 @@ jobs: repositories: bomly-cli permission-contents: write + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Run GoReleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: @@ -97,3 +103,22 @@ jobs: TAP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }} SCOOP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }} WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }} + + - name: Generate artifact hashes for SLSA provenance + id: hash + run: echo "hashes=$(base64 -w0 < dist/SHA256SUMS)" >> "$GITHUB_OUTPUT" + + provenance: + needs: release + permissions: + actions: read # detect the workflow that triggered this run + id-token: write # sign the provenance + contents: write # upload the provenance to the release + # The SLSA generator MUST be pinned by a full vX.Y.Z tag, not a commit SHA or + # shorter tag — slsa-verifier resolves the builder identity from this exact ref, + # and SHA-pinning breaks that resolution. See .github/pinact.yaml, which excludes + # this line from automated re-pinning. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: ${{ needs.release.outputs.hashes }} + upload-assets: ${{ startsWith(github.ref, 'refs/tags/') }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 621865f..17f9e48 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -86,6 +86,17 @@ archives: checksum: name_template: "SHA256SUMS" +signs: + - cmd: cosign + signature: "${artifact}.sigstore.json" + args: + - sign-blob + - "--bundle=${signature}" + - "${artifact}" + - "--yes" + artifacts: checksum + output: true + snapshot: version_template: "{{ incpatch .Version }}-next" @@ -106,9 +117,10 @@ release: - Alternate `bomly-lite` archives for users who prefer external Syft and Grype binaries. - Linux packages for Debian, RPM, Alpine, and Arch-compatible package managers. - Homebrew, Scoop, and WinGet package-manager manifests or publishing pull requests. - - `SHA256SUMS` for release artifact verification. + - `SHA256SUMS` for release artifact verification, signed keylessly with [cosign](https://docs.sigstore.dev/cosign/signing/overview/) (`SHA256SUMS.sigstore.json`). + - SLSA Build Level 3 provenance (`multiple.intoto.jsonl`) generated by [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator). - Each archive includes `LICENSE`, `NOTICE`, and a `licenses/` directory with third-party license texts. GitHub-native artifact attestations are planned for a future release. + Each archive includes `LICENSE`, `NOTICE`, and a `licenses/` directory with third-party license texts. See [Verify release checksums](https://github.com/bomly-dev/bomly-cli/blob/main/docs/INSTALLATION.md#verify-release-checksums) for signature and provenance verification commands. nfpms: - id: bomly-linux-packages diff --git a/dev-docs/CI.md b/dev-docs/CI.md index ad88298..3ac858e 100644 --- a/dev-docs/CI.md +++ b/dev-docs/CI.md @@ -14,7 +14,7 @@ Bomly uses GitHub Actions for validation, security analysis, smoke coverage, and | `Smoke` | Merge queue, nightly schedule, manual dispatch | Slow end-to-end coverage against real repositories, SBOMs, and containers before merge, plus scheduled drift detection | | `Update Smoke Goldens` | Manual dispatch | Regenerate golden files on a chosen ref and open a PR when the changes are intentional | | `Auto Version` | Manual dispatch | Bump `cmd/bomly/main.go`, create a semver tag, and start the release workflow | -| `Release` | Semver tags like `v1.2.3`, manual dispatch | GoReleaser packaging, checksums, Linux packages, package-manager manifests, and GitHub release publication | +| `Release` | Semver tags like `v1.2.3`, manual dispatch | GoReleaser packaging, checksums, Linux packages, package-manager manifests, GitHub release publication, cosign keyless signing, and SLSA provenance | ## Required Checks @@ -35,6 +35,7 @@ Bomly dogfoods its own domain by tracking the project's [OpenSSF Scorecard](http - **Token-Permissions** — every workflow declares a top-level `permissions:` block scoped to `contents: read`. Any write scope (release publishing, the Guard PR comment, the smoke-goldens PR) is granted at the **job** level only, never at the top level. - **Pinned-Dependencies** — all GitHub Actions are pinned by full commit SHA with a trailing `# vX.Y.Z` comment (for example `actions/checkout@ # v7.0.0`). Dependabot's `github-actions` updater understands this form and bumps both the SHA and the comment, so pinning does not freeze us on stale actions. When adding a new `uses:`, pin it the same way — `pinact run` (suzuki-shunsuke/pinact) rewrites the whole tree, or resolve a single tag with `gh api repos///commits/ --jq .sha`. The `Smoke` and `Update Smoke Goldens` workflows install `pip`/`pipenv`/`poetry` from `.github/requirements-ci-tools.txt`, a hash-locked, fully-resolved requirements file (`pip install --require-hashes`) instead of an unpinned inline `pip install`. Regenerate it after bumping `.github/requirements-ci-tools.in` with `pip-compile --allow-unsafe --generate-hashes --output-file=requirements-ci-tools.txt requirements-ci-tools.in` run from `.github/` under the same Python version the workflows use (3.12), so the resolved hash set covers the right wheel tags. - **SAST** — CodeQL runs on every push, PR, and weekly. +- **Signed-Releases** — the `release` job signs `SHA256SUMS` keylessly with [cosign](https://docs.sigstore.dev/cosign/signing/overview/) (`SHA256SUMS.sigstore.json`, GitHub OIDC identity, no managed keys), and a separate `provenance` job calls the [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) generic builder to produce a single `multiple.intoto.jsonl` SLSA Build Level 3 provenance file over every release artifact's hash, uploaded to the same GitHub release. Verification commands for end users are in [docs/INSTALLATION.md](../docs/INSTALLATION.md#verify-release-checksums). The generator's `uses:` line is pinned to the `v2.1.0` tag, not a commit SHA — SHA-pinning it breaks `slsa-verifier`'s builder-identity check — and `.github/pinact.yaml` excludes that line from automated re-pinning so it doesn't regress. A few Scorecard checks require maintainer action **outside** the repository and are not code changes: diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index dbc4e77..5687481 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -207,6 +207,34 @@ Get-FileHash .\bomly_v0.14.2_windows_amd64.zip -Algorithm SHA256 # Compare the printed hash against the matching line in SHA256SUMS. ``` +### Verify the signature + +`SHA256SUMS` is itself signed keylessly with [cosign](https://docs.sigstore.dev/cosign/signing/overview/), tying the release to the exact GitHub Actions workflow run that built it: + +```bash +curl -L -O https://github.com/bomly-dev/bomly-cli/releases/download/v0.14.2/SHA256SUMS.sigstore.json +cosign verify-blob \ + --bundle SHA256SUMS.sigstore.json \ + --certificate-identity-regexp "^https://github.com/bomly-dev/bomly-cli/.github/workflows/release.yml@.*$" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + SHA256SUMS +``` + +### Verify SLSA provenance + +Each release also publishes a single `multiple.intoto.jsonl` SLSA Build Level 3 provenance file covering every release artifact, attesting which source commit and workflow produced them: + +```bash +curl -L -O https://github.com/bomly-dev/bomly-cli/releases/download/v0.14.2/bomly_v0.14.2_linux_amd64.tar.gz +curl -L -O https://github.com/bomly-dev/bomly-cli/releases/download/v0.14.2/multiple.intoto.jsonl +slsa-verifier verify-artifact bomly_v0.14.2_linux_amd64.tar.gz \ + --provenance-path multiple.intoto.jsonl \ + --source-uri github.com/bomly-dev/bomly-cli \ + --source-tag v0.14.2 +``` + +`slsa-verifier` is available from the [slsa-framework/slsa-verifier releases](https://github.com/slsa-framework/slsa-verifier/releases). + ## CI installation For pinned CI recipes, see [CI integration](CI_INTEGRATION.md). Prefer a package-manager install when your CI environment supports it. If you download archives directly, pin a specific tag rather than `latest`.