Repository navigation
ci(release): sign the archives with cosign beside their attestation - #336
Conversation
There was a problem hiding this comment.
Signing step looks right: it runs after the archives are uploaded and while they are still in the workspace, the installer is the same SHA pin as chart.yml, TAG comes through the environment rather than being interpolated into the script, and the job already carries contents: write and id-token: write, so cosign's ambient OIDC works without new permissions.
Nit: nothing verifies a bundle before a user does. The verify job already downloads lfsx-server-x86_64-unknown-linux-musl.tar.gz; pulling the matching .sigstore and running the exact cosign verify-blob command from docs/releases.md there would prove per release that the bundle format cosign wrote is the one the documented command reads, and that the identity regexp still matches the certificate. That coupling is the part most likely to drift silently: a cosign major version changes the bundle format, the release still ships, and the first person to notice is someone verifying a download. It needs a cosign install in a job that has none, so it is more than a one-line change, which is why it is not a suggestion. Two inline nits below.
| set -euo pipefail | ||
|
|
||
| target='${{ matrix.target }}' | ||
|
|
||
| # tar.gz on every platform but Windows, which gets the zip, so the | ||
| # pair this runner did not produce is skipped rather than guessed at. | ||
| for bin in lfsx-server lfsx ; do | ||
| for ext in tar.gz zip ; do | ||
| archive="${bin}-${target}.${ext}" | ||
| [ -f "$archive" ] || continue | ||
|
|
||
| cosign sign-blob --yes "$archive" --bundle "${archive}.sigstore" | ||
| gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber "${archive}.sigstore" | ||
| done | ||
| done |
There was a problem hiding this comment.
Nit: if the archive names ever move (a taiki-e/upload-rust-binary-action bump, an archive: rename), every [ -f ] misses, the loop signs nothing and the step exits 0. The release then ships unsigned with a green workflow, which is the exact state this PR exists to fix. Two archives per runner is a fixed number, so assert it:
| set -euo pipefail | |
| target='${{ matrix.target }}' | |
| # tar.gz on every platform but Windows, which gets the zip, so the | |
| # pair this runner did not produce is skipped rather than guessed at. | |
| for bin in lfsx-server lfsx ; do | |
| for ext in tar.gz zip ; do | |
| archive="${bin}-${target}.${ext}" | |
| [ -f "$archive" ] || continue | |
| cosign sign-blob --yes "$archive" --bundle "${archive}.sigstore" | |
| gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber "${archive}.sigstore" | |
| done | |
| done | |
| set -euo pipefail | |
| target='${{ matrix.target }}' | |
| signed=0 | |
| # tar.gz on every platform but Windows, which gets the zip, so the | |
| # pair this runner did not produce is skipped rather than guessed at. | |
| for bin in lfsx-server lfsx ; do | |
| for ext in tar.gz zip ; do | |
| archive="${bin}-${target}.${ext}" | |
| [ -f "$archive" ] || continue | |
| cosign sign-blob --yes "$archive" --bundle "${archive}.sigstore" | |
| gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber "${archive}.sigstore" | |
| signed=$((signed + 1)) | |
| done | |
| done | |
| # One archive per binary, whatever this runner's extension is. Zero | |
| # or one means the names moved and the loop quietly matched nothing. | |
| [ "$signed" -eq 2 ] || { echo "::error::signed $signed archives, expected 2"; exit 1; } |
| Every release ships four kinds of proof: a `.sha256` beside each archive, a build provenance | ||
| attestation on each archive, a `.sigstore` signature bundle beside each archive, and a CycloneDX | ||
| SBOM per crate (`lfsx-server.cdx.json`, `lfsx.cdx.json`). They answer different questions. |
There was a problem hiding this comment.
Nit: "every release" is not true for the bundle, as the PR body says: v1.16.x and everything before it has no .sigstore asset, so a reader on a current download follows the cosign command below and finds nothing to pass to --bundle. Correct the version if the next tag is not v1.17.0.
| Every release ships four kinds of proof: a `.sha256` beside each archive, a build provenance | |
| attestation on each archive, a `.sigstore` signature bundle beside each archive, and a CycloneDX | |
| SBOM per crate (`lfsx-server.cdx.json`, `lfsx.cdx.json`). They answer different questions. | |
| Every release ships a `.sha256` beside each archive, a build provenance attestation on each | |
| archive, and a CycloneDX SBOM per crate (`lfsx-server.cdx.json`, `lfsx.cdx.json`). Releases from | |
| v1.17.0 on also ship a `.sigstore` signature bundle beside each archive. They answer different | |
| questions. |
SonarQube — aucune nouvelle issueComparaison entre le projet bac à sable de cette PR et la branche par défaut : SonarQube Community n'analyse pas les PR, ce delta est calculé côté CI. Détail |
OpenSSF Scorecard reports "Project has not signed or included provenance with any releases" for v1.16.0, v1.16.1 and v1.16.4.
The provenance half of that is not quite true: every archive has carried a build provenance attestation since #283, verifiable with
gh attestation verify. But those attestations live in GitHub's attestation store, not beside the download, so a scanner reading the release assets cannot see them, and neither can anyone who does not want to ask GitHub. Scorecard is reading the release the way a stranger would, and by that measure it is right.So each archive now gets a
.sigstorebundle uploaded next to it, signed keyless with cosign, the same tool and the same pinned installer the chart already uses. Verification needs nothing but cosign and the public transparency log:cosign verify-blob lfsx-server-x86_64-unknown-linux-musl.tar.gz \ --bundle lfsx-server-x86_64-unknown-linux-musl.tar.gz.sigstore \ --certificate-identity-regexp '^https://github.com/FerrLabs/LFSX/' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comdocs/releases.mdgains that command and says what the two identity flags are for: without them cosign confirms that somebody signed the bytes, not that this repository's workflow did.The loop signs what the runner actually produced, tar.gz everywhere and zip on Windows, skipping the pair that does not exist rather than assuming it. The job already had
contents: writeandid-token: writefor the upload and the attestation, so nothing new is granted.Only future releases get bundles. The three releases Scorecard names keep their attestations and their checksums, and the check should clear on the next one.