Skip to content

ci(release): sign the archives with cosign beside their attestation - #336

Merged
BryanFRD merged 1 commit into
mainfrom
ci/sign-release-archives
Sep 4, 2026
Merged

BryanFRD merged 1 commit into
mainfrom
ci/sign-release-archives

Conversation

@BryanFRD

@BryanFRD BryanFRD commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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 .sigstore bundle 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.com

docs/releases.md gains 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: write and id-token: write for 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.

@BryanFRD
BryanFRD enabled auto-merge (squash) September 4, 2026 16:41

@ferrfleet ferrfleet Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +97 to +111
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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; }

Comment thread docs/releases.md
Comment on lines +23 to +25
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

SonarQube — aucune nouvelle issue

Comparaison 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

@BryanFRD
BryanFRD merged commit fcff6b6 into main Sep 4, 2026
24 checks passed
@BryanFRD
BryanFRD deleted the ci/sign-release-archives branch September 4, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant