diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1d793fd6..cbd93d15 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 5797ffd4..3d86dc5a 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 eee74792..1e968c42 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"