diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cee209cec..151d9ff6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,24 +80,45 @@ jobs: if-no-files-found: error retention-days: 7 + # Sigstore bundle per file plus one SLSA provenance for both, keyless via this + # workflow's OIDC identity: docs/releasing.md, "Verifying a release". attach: - name: Attach to the GitHub Release + name: Sign and attach to the GitHub Release needs: build if: github.event_name == 'release' runs-on: ubuntu-latest permissions: - contents: write + contents: write # gh release upload + id-token: write # Sigstore keyless signing (OIDC) + attestations: write # build-provenance attestation steps: - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: dist path: dist - - name: Upload sdist and wheel + - name: Build provenance + id: provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/* + - name: Sign with Sigstore + uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0 + with: + inputs: dist/* + upload-signing-artifacts: false + release-signing-artifacts: false + - name: Upload sdist, wheel, signatures and provenance working-directory: . env: GH_TOKEN: ${{ github.token }} TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber + BUNDLE: ${{ steps.provenance.outputs.bundle-path }} + run: | + # A Sigstore bundle around the in-toto SLSA statement, named .intoto.jsonl + # because that is what provenance checkers (OpenSSF Scorecard) look for. + cp "$BUNDLE" "dist/dokimi-${TAG#v}.intoto.jsonl" + ls -l dist + gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber # Off until a maintainer configures the PyPI Trusted Publisher and sets the # repository variable PYPI_PUBLISH to true (docs/releasing.md, "Enabling PyPI"). diff --git a/dokimi/changelog.d/20261105_120000_napetrov_signed-releases.md b/dokimi/changelog.d/20261105_120000_napetrov_signed-releases.md new file mode 100644 index 000000000..63997923e --- /dev/null +++ b/dokimi/changelog.d/20261105_120000_napetrov_signed-releases.md @@ -0,0 +1,7 @@ +### Added + +- **Signed releases with build provenance** — the release workflow signs the + sdist and wheel with Sigstore (keyless, the workflow's identity) and attaches + `.sigstore.json` bundles plus a SLSA build-provenance attestation + (`dokimi-X.Y.Z.intoto.jsonl`, also in `gh attestation verify`) to the GitHub + Release. `docs/releasing.md` shows how to verify a download. diff --git a/dokimi/docs/releasing.md b/dokimi/docs/releasing.md index 8d43a59fe..152eb08f2 100644 --- a/dokimi/docs/releasing.md +++ b/dokimi/docs/releasing.md @@ -72,8 +72,10 @@ the `Fragment for package changes` step in `ci.yml`'s `static` job asks for it. Publishing the release runs `.github/workflows/release.yml`. It checks the tag against the version, the changelog section and leftover fragments, builds the sdist and wheel (the wheel from the sdist), smoke-tests the installed wheel - from outside the checkout (`scripts/smoke_installed.py`), attaches both files - to the release and, once enabled, uploads them to PyPI. If it fails, fix on + from outside the checkout (`scripts/smoke_installed.py`), signs both files, + attaches them to the release with their signatures and build provenance + (see [Verifying a release](#verifying-a-release)) and, once enabled, uploads + them to PyPI. If it fails, fix on `main` and release the next patch version: a published version is never rebuilt. @@ -82,6 +84,32 @@ anything. Given a tag, it checks out that tag (and fails if it does not exist) and also runs the readiness check on it; without one it builds the ref it was dispatched from. +## Verifying a release + +Each release carries, next to the sdist and wheel: + +- `.sigstore.json`: a [Sigstore](https://www.sigstore.dev/) bundle per + file, signed keyless with the identity of `release.yml` on the release tag. +- `dokimi-X.Y.Z.intoto.jsonl`: a SLSA build-provenance attestation for both + files (which workflow run, commit and runner built them), also stored in the + repository's attestations. + +```bash +gh release download vX.Y.Z -R intel/dokimi +# signature: signed by release.yml on the tag +python -m pip install sigstore +python -m sigstore verify github dokimi-X.Y.Z-py3-none-any.whl \ + --bundle dokimi-X.Y.Z-py3-none-any.whl.sigstore.json \ + --repository intel/dokimi --ref refs/tags/vX.Y.Z --name release +# provenance, from the attached file (offline) or the repository's attestations +gh attestation verify dokimi-X.Y.Z-py3-none-any.whl -R intel/dokimi \ + --signer-workflow intel/dokimi/.github/workflows/release.yml \ + --bundle dokimi-X.Y.Z.intoto.jsonl +``` + +The same commands check the sdist (`dokimi-X.Y.Z.tar.gz`). +Releases before this (v0.1.0) are unsigned. + ## Enabling PyPI The `pypi` job of the release workflow is off until a maintainer, once: