From eeda665b7e85cdd770e5f28a49998a553c3aae3b Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Sun, 30 Aug 2026 13:06:00 -0700 Subject: [PATCH] ci: Authenticode-sign Windows and notarize the macOS universal binary. Release tags now fail closed without signing secrets, ship a signed Windows PE via house signerd, and replace per-arch Darwin tarballs with one stapled universal archive. Co-authored-by: Cursor --- .github/scripts/require_secrets.sh | 36 ++++++++++++++++++ .github/workflows/release.yml | 42 ++++++++++++++++++++- .goreleaser.yaml | 26 +++++++++++++ init/windows/signexe.sh | 60 ++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/require_secrets.sh create mode 100755 init/windows/signexe.sh diff --git a/.github/scripts/require_secrets.sh b/.github/scripts/require_secrets.sh new file mode 100755 index 00000000..d638d702 --- /dev/null +++ b/.github/scripts/require_secrets.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Fail closed when a publishing secret is empty. GitHub maps missing secrets +# to "" so `if: secrets.FOO != ''` is not a substitute. +set -euo pipefail + +missing=0 +need() { + local name=$1 + local val=${!name-} + if [ -z "${val}" ]; then + echo "missing secret: ${name}" >&2 + missing=1 + fi +} + +need GORELEASER_KEY +need GPG_SIGNING_KEY +need DOCKER_USERNAME +need DOCKER_PASSWORD +need CODESIGN_URL +need CODESIGN_CLIENT_CERT +need CODESIGN_CLIENT_KEY +need MACOS_SIGN_P12 +need MACOS_SIGN_PASSWORD +need MACOS_NOTARY_KEY +need MACOS_NOTARY_KEY_ID +need MACOS_NOTARY_ISSUER_ID +need PACKAGECLOUD_TOKEN +need HOMEBREW_TAP_GITHUB_TOKEN + +if [ "${missing}" -ne 0 ]; then + echo "refusing to publish with empty secrets" >&2 + exit 1 +fi + +echo "required secrets are present" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85011d43..d52f8493 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,10 @@ permissions: jobs: release: runs-on: ubuntu-latest + permissions: + contents: write + packages: write + id-token: write env: DOCKER_CLI_EXPERIMENTAL: "enabled" DOCKER_REGISTRY: "ghcr.io" @@ -24,6 +28,23 @@ jobs: with: fetch-depth: 0 - run: git fetch --force --tags + - name: Fail if publishing secrets are missing + env: + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + CODESIGN_URL: ${{ secrets.CODESIGN_URL }} + CODESIGN_CLIENT_CERT: ${{ secrets.CODESIGN_CLIENT_CERT }} + CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + run: bash .github/scripts/require_secrets.sh - name: Install RPM tooling run: sudo apt-get install -y rpm - name: Set up QEMU @@ -62,12 +83,21 @@ jobs: # on your needs. - name: "make key file" run: "echo '${{ secrets.GPG_SIGNING_KEY }}' > /tmp/key.gpg" + - name: Install golift codesign + env: + GOBIN: ${{ runner.temp }}/codesign-bin + run: | + set -euo pipefail + mkdir -p "${GOBIN}" + go install golift.io/codesign/cmd/codesign@v1.0.3 + echo "${GOBIN}" >> "${GITHUB_PATH}" + echo "CODESIGN_BIN=${GOBIN}/codesign" >> "${GITHUB_ENV}" - uses: goreleaser/goreleaser-action@v7 with: # either 'goreleaser' (default) or 'goreleaser-pro': distribution: goreleaser-pro version: latest - args: release --clean + args: release --clean --timeout 60m env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' @@ -78,6 +108,16 @@ jobs: PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} GPG_SIGNING_KEY: /tmp/key.gpg GPG_SIGNING_KEY_ID: ${{ steps.import_gpg.outputs.keyid }} + CODESIGN_URL: ${{ secrets.CODESIGN_URL }} + CODESIGN_CLIENT_CERT: ${{ secrets.CODESIGN_CLIENT_CERT }} + CODESIGN_CLIENT_KEY: ${{ secrets.CODESIGN_CLIENT_KEY }} + CODESIGN_NAME: unpoller + CODESIGN_WEBSITE: https://unpoller.com + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - name: Update Docker Hub Description uses: peter-evans/dockerhub-description@v5 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1bd4add9..783b881d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -74,6 +74,32 @@ builds: - amd64 ldflags: - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X "golift.io/version.Version={{.Version}}" -X "golift.io/version.Branch={{.Branch}} ({{.Commit}})" -X "golift.io/version.BuildDate={{.Date}}" -X "golift.io/version.BuildUser=goreleaser" -X "golift.io/version.Revision=1" + hooks: + post: + - cmd: bash init/windows/signexe.sh "{{ .Path }}" + if: '{{ eq .Os "windows" }}' + +universal_binaries: + - id: unpoller-mac + ids: [unpoller-mac] + replace: true + +# Sign + notarize the macOS universal binary (quill; works on Linux CI). +# Enabled only when MACOS_SIGN_P12 is set so local snapshots still work. +notarize: + macos: + - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' + ids: + - unpoller-mac + sign: + certificate: "{{ .Env.MACOS_SIGN_P12 }}" + password: "{{ .Env.MACOS_SIGN_PASSWORD }}" + notarize: + issuer_id: "{{ .Env.MACOS_NOTARY_ISSUER_ID }}" + key_id: "{{ .Env.MACOS_NOTARY_KEY_ID }}" + key: "{{ .Env.MACOS_NOTARY_KEY }}" + wait: true + timeout: 20m archives: - id: unpoller diff --git a/init/windows/signexe.sh b/init/windows/signexe.sh new file mode 100755 index 00000000..293834a3 --- /dev/null +++ b/init/windows/signexe.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +# Authenticode-sign a Windows PE via golift/codesign (YubiKey-backed signerd). +# GoReleaser calls this from builds.hooks.post on windows binaries, after the +# CLI is installed into a temp GOBIN (never /usr/bin/codesign). +# +# Local snapshots skip when CODESIGN_URL is unset. GitHub Actions must fail +# closed — the release contract is an Authenticode-signed Windows binary. +# +# Prefer CODESIGN_BIN, then GOBIN/codesign, then GOPATH/bin, then PATH +# entries that are not Apple's /usr/bin/codesign. + +function pick_codesign() { + if [ -n "${CODESIGN_BIN:-}" ]; then + echo "${CODESIGN_BIN}" + return + fi + if [ -n "${GOBIN:-}" ] && [ -x "${GOBIN}/codesign" ]; then + echo "${GOBIN}/codesign" + return + fi + gopath="$(go env GOPATH 2>/dev/null || true)" + if [ -n "${gopath}" ] && [ -x "${gopath}/bin/codesign" ]; then + echo "${gopath}/bin/codesign" + return + fi + while IFS= read -r p; do + case "$p" in + /usr/bin/codesign|/bin/codesign) continue ;; + esac + echo "$p" + return + done < <(type -a -p codesign 2>/dev/null || true) + return 1 +} + +function sign() { + if [ -z "${CODESIGN_URL:-}" ]; then + if [ -n "${GITHUB_ACTIONS:-}" ]; then + echo "CODESIGN_URL unset; refusing to ship an unsigned Windows binary in CI" >&2 + exit 1 + fi + echo "Skipped signing ${FILE} (CODESIGN_URL unset) .." >&2 + exit 0 + fi + + bin="$(pick_codesign)" || { + echo "CODESIGN_URL is set but golift codesign CLI not found (set CODESIGN_BIN)" >&2 + exit 1 + } + + CODESIGN_NAME="${CODESIGN_NAME:-unpoller}" \ + CODESIGN_WEBSITE="${CODESIGN_WEBSITE:-https://unpoller.com}" \ + "${bin}" -- "${FILE}" + echo "Signed ${FILE} .." >&2 +} + +[ -z "$1" ] || FILE="$1" sign