feat(release): sign checksums.txt with cosign keyless signing - #903
Draft
mcdgavin wants to merge 3 commits into
Draft
feat(release): sign checksums.txt with cosign keyless signing#903mcdgavin wants to merge 3 commits into
mcdgavin wants to merge 3 commits into
Conversation
Sign releases via Sigstore keyless signing: goreleaser invokes cosign sign-blob on checksums.txt using the release workflow's GitHub OIDC identity, so there is no signing key to store or rotate. Each release gains checksums.txt.sig and checksums.txt.pem, and the signature is recorded in the Rekor transparency log. install-cli.sh verifies the signature opportunistically: when cosign is installed it pins the certificate identity to this repo's release workflow at the exact version tag, aborting on an invalid signature. Releases that predate signing (no .sig/.pem assets) and systems without cosign fall back to the existing checksum-only verification.
Address review findings on the signing PR: - Fail closed when the signature is missing for a release that is known to be signed (any version after LAST_UNSIGNED_VERSION=2.17.0), so an attacker who can substitute release assets cannot strip checksums.txt.sig to force a silent checksum-only fallback. - Distinguish a genuine 404 (release predates signing) from transport errors and other HTTP failures when fetching .sig/.pem; anything but a clean 404 now aborts instead of logging 'no signature published' and proceeding unverified. - Scope id-token: write to the release job instead of the whole workflow; the Fulcio certificate identity is per-workflow, not per-job, so the docker job's third-party actions must not be able to mint the identity installers trust.
- install-cli.sh: switch cosign verify-blob from --certificate-identity-regexp to --certificate-identity. The expected identity is fully known, and the regexp form left $VERSION's dots as wildcards (v2.4.1 also matched a cert for tag v2x4y1); exact matching removes the escaping burden entirely. - .goreleaser.yaml: the documented verify recipe pinned the identity with '@.*$', which accepts a signature replayed from any older signed release. Document the exact-identity form with the tag substituted instead, matching what the installer enforces.
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.
Follow-up to #880, which verifies archives against
checksums.txt— but the checksums file ships alongside the archive in the same release, so it can't detect substituted release assets. This PR signs releases so the checksums are bound to an identity an attacker can't forge.Approach: keyless (Sigstore) signing of
checksums.txt.goreleaser.yaml: asignssection runscosign sign-blobonchecksums.txtduring release. Keyless — cosign uses the release workflow's GitHub OIDC identity, so there is no signing key to store or rotate. Each release gainschecksums.txt.sig+checksums.txt.pem, and the signature is recorded in the Rekor transparency log. Since every archive is listed inchecksums.txt, one signature covers all release artifacts..github/workflows/release.yaml: adds asigstore/cosign-installerstep (pinned by SHA, matching repo convention) andid-token: writescoped to thereleasejob only — the Fulcio certificate identity is per-workflow, not per-job, so grantingid-tokenworkflow-wide would let the docker job's third-party actions mint the exact identity installers trust.install-cli.sh: when cosign is installed, verifies the signature with the certificate identity pinned (exact match, no regexp) to this repo's release workflow at the exact version tag. The check fails closed where it matters:.sig/.pemon any release afterv2.17.0(the last unsigned release) aborts — an attacker who substitutes assets can't strip the signature to force a checksum-only fallback.Manual verification for users (substitute the exact version you downloaded — pinning the tag rejects signatures replayed from an older release):
cosign verify-blob \ --certificate checksums.txt.pem --signature checksums.txt.sig \ --certificate-identity "https://github.com/livekit/livekit-cli/.github/workflows/release.yaml@refs/tags/v<VERSION>" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ checksums.txtTesting
goreleaser checkpasses on the modified config.v2.17.0release with missing signature → abort; transport error / mixed HTTP statuses → abort; valid sig → continue; invalid sig → abort.fetch_status(the HTTP-status-aware download helper) tested against the real v2.17.0 release assets: 200 forchecksums.txt, 404 for the nonexistent.sig, 000 on transport failure — all without trippingerrexit.v0.0.0-signing-test) before the next real release.Possible follow-ups (out of scope here)
--bundle) at sign time and verify with--bundle --offline, removing the per-install Rekor lookup and TUF fetch (also de-flakesinstall-test.yaml, which runs the installer with cosign on every PR).--certificate-identityflag) skips with a message instead of hard-aborting.dockerjob (same keyless flow on the pushed digest).actions/attest-build-provenance) as a cheap complement — users could verify withgh attestation verify.