From 1710d1e13a5f92d6276900652857d4d306b8225a Mon Sep 17 00:00:00 2001 From: gavin mcdonough Date: Fri, 10 Jul 2026 14:14:35 -0400 Subject: [PATCH] feat(release): sign checksums.txt with cosign keyless signing 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. --- .github/workflows/release.yaml | 6 ++++++ .goreleaser.yaml | 22 ++++++++++++++++++++++ install-cli.sh | 30 ++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1d793fd66..cbd93d159 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,6 +21,9 @@ on: permissions: contents: write + # id-token lets cosign obtain a GitHub OIDC token for keyless (Sigstore) + # signing of checksums.txt during the goreleaser step. + id-token: write jobs: # GoReleaser runs on macOS so the darwin binary builds natively (CoreAudio @@ -92,6 +95,9 @@ jobs: path: .cross key: cross-${{ runner.os }}-zig0.14.1-alsa1.2.12-${{ hashFiles('scripts/setup-cross.sh') }} + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Run GoReleaser uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5797ffd4f..3d86dc5a0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -161,5 +161,27 @@ gomod: checksum: name_template: 'checksums.txt' +# Keyless (Sigstore) signing: cosign signs checksums.txt with the release +# workflow's GitHub OIDC identity — no long-lived key to manage. Every archive +# is listed in checksums.txt, so one signature covers all release artifacts. +# Verify with: +# cosign verify-blob \ +# --certificate checksums.txt.pem --signature checksums.txt.sig \ +# --certificate-identity-regexp '^https://github.com/livekit/livekit-cli/\.github/workflows/release\.yaml@.*$' \ +# --certificate-oidc-issuer https://token.actions.githubusercontent.com \ +# checksums.txt +signs: + - cmd: cosign + signature: "${artifact}.sig" + certificate: "${artifact}.pem" + args: + - sign-blob + - --output-signature=${signature} + - --output-certificate=${certificate} + - --yes + - ${artifact} + artifacts: checksum + output: true + snapshot: version_template: "{{ incpatch .Version }}-next" diff --git a/install-cli.sh b/install-cli.sh index eee747923..1e968c425 100755 --- a/install-cli.sh +++ b/install-cli.sh @@ -73,10 +73,36 @@ trap 'rm -rf "$TEMP_DIR"' EXIT curl -fsSL "$ARCHIVE_URL" -o "$TEMP_DIR/$ARCHIVE_NAME" curl -fsSL "$CHECKSUMS_URL" -o "$TEMP_DIR/checksums.txt" +# When cosign is available, verify the Sigstore signature on checksums.txt. The +# certificate identity is pinned to this repo's release workflow running for this +# exact version tag, so a valid signature proves the checksums were produced by +# livekit-cli's release CI and not substituted alongside a tampered archive. +# Releases published before signing was introduced have no .sig/.pem assets, so a +# missing signature is skipped rather than fatal; a present-but-invalid one aborts. +if command -v cosign >/dev/null; then + # No -S here: a 404 is the expected outcome for releases that predate signing, + # so curl's error output would just be noise. + if curl -fsL "$CHECKSUMS_URL.sig" -o "$TEMP_DIR/checksums.txt.sig" && + curl -fsL "$CHECKSUMS_URL.pem" -o "$TEMP_DIR/checksums.txt.pem"; then + log "Verifying signature..." + cosign verify-blob \ + --certificate "$TEMP_DIR/checksums.txt.pem" \ + --signature "$TEMP_DIR/checksums.txt.sig" \ + --certificate-identity-regexp "^https://github.com/livekit/$REPO/\.github/workflows/release\.yaml@refs/tags/v${VERSION}$" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + "$TEMP_DIR/checksums.txt" \ + || abort "Signature verification failed for checksums.txt" + else + log "No signature published for this release; skipping signature verification." + fi +else + log "cosign not found; skipping signature verification." +fi + # Verify the archive against the release's checksums.txt before extracting. The checksums # file is fetched from the same release over HTTPS, so this guards against corrupted/partial -# downloads and accidental mismatches; it is not a substitute for signature verification -# against an out-of-band key. +# downloads and accidental mismatches; when cosign is absent it is not a substitute for +# signature verification against an out-of-band key. log "Verifying checksum..." expected_sum=$(awk -v f="$ARCHIVE_NAME" '$2 == f {print $1}' "$TEMP_DIR/checksums.txt") [ -n "$expected_sum" ] || abort "Could not find a checksum for $ARCHIVE_NAME in checksums.txt"