From e322887c4abb4b6463b3c5e12fb5118b3cc13f71 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 18 Jul 2026 10:36:18 +0100 Subject: [PATCH 1/4] fix: Windows artifact signing --- .github/workflows/build-and-release.yml | 96 +++++++++++++++++++ .goreleaser.yml | 2 + docs/modules/dev/pages/signing.adoc | 109 ++++++++++++++++++++-- docs/modules/dev/signpath/README.md | 18 ++++ docs/modules/dev/signpath/windows-msi.xml | 8 ++ docs/modules/dev/signpath/windows-zip.xml | 7 ++ var/windows/signpath-publish-signed.sh | 81 ++++++++++++++++ 7 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 docs/modules/dev/signpath/README.md create mode 100644 docs/modules/dev/signpath/windows-msi.xml create mode 100644 docs/modules/dev/signpath/windows-zip.xml create mode 100755 var/windows/signpath-publish-signed.sh diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index bef8e4c..4058a43 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -37,6 +37,11 @@ on: jobs: build: runs-on: ubuntu-latest + outputs: + new_release_published: ${{ steps.release.outputs.new_release_published }} + new_release_git_tag: ${{ steps.release.outputs.new_release_git_tag }} + windows_zip_artifact_id: ${{ steps.upload-windows-zip.outputs.artifact-id }} + windows_msi_artifact_id: ${{ steps.upload-windows-msi.outputs.artifact-id }} steps: - name: Checkout uses: actions/checkout@v6 @@ -129,6 +134,7 @@ jobs: uses: docker/setup-buildx-action@v4 - name: release + id: release if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) uses: cycjimmy/semantic-release-action@v5 with: @@ -140,9 +146,99 @@ jobs: GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }} GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }} + - name: Upload unsigned Windows zip for SignPath + id: upload-windows-zip + if: steps.release.outputs.new_release_published == 'true' + uses: actions/upload-artifact@v7 + with: + name: unsigned-windows-zip + path: dist/OliveTin-windows-amd64.zip + if-no-files-found: error + + - name: Upload unsigned Windows MSI for SignPath + id: upload-windows-msi + if: steps.release.outputs.new_release_published == 'true' + uses: actions/upload-artifact@v7 + with: + name: unsigned-windows-msi + path: dist/OliveTin-windows-amd64.msi + if-no-files-found: error + - name: Archive binaries if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false uses: actions/upload-artifact@v7 with: name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}" path: dist/OliveTin*.* + + sign-windows: + name: Sign Windows artifacts (SignPath) + needs: build + if: needs.build.outputs.new_release_published == 'true' + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Require SignPath configuration + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }} + SIGNPATH_SIGNING_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + run: | + missing=0 + for name in SIGNPATH_API_TOKEN SIGNPATH_ORGANIZATION_ID SIGNPATH_PROJECT_SLUG SIGNPATH_SIGNING_POLICY_SLUG; do + if [[ -z "${!name}" ]]; then + echo "Missing required SignPath setting: ${name}" >&2 + missing=1 + fi + done + if [[ "${missing}" -ne 0 ]]; then + echo "Windows signing is required before a draft release can be published. Configure SignPath secrets/vars (see docs/modules/dev/pages/signing.adoc)." >&2 + exit 1 + fi + + - name: Sign Windows zip + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: windows-zip + github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }} + wait-for-completion: true + output-artifact-directory: signed-windows-zip + + - name: Sign Windows MSI + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: windows-msi + github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }} + wait-for-completion: true + output-artifact-directory: signed-windows-msi + + - name: Publish signed Windows assets and undraft release + env: + GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }} + run: | + zip_path="$(find signed-windows-zip -type f -name 'OliveTin-windows-amd64.zip' | head -n 1)" + msi_path="$(find signed-windows-msi -type f -name 'OliveTin-windows-amd64.msi' | head -n 1)" + if [[ -z "${zip_path}" || -z "${msi_path}" ]]; then + echo "Signed Windows artifacts not found after SignPath:" >&2 + find signed-windows-zip signed-windows-msi -type f >&2 || true + exit 1 + fi + ./var/windows/signpath-publish-signed.sh \ + "${{ needs.build.outputs.new_release_git_tag }}" \ + "${zip_path}" \ + "${msi_path}" diff --git a/.goreleaser.yml b/.goreleaser.yml index 9ae58c8..79902a9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -183,6 +183,8 @@ nfpms: dst: /usr/share/man/man1/OliveTin.1.gz release: + # Stay draft until the sign-windows job replaces unsigned Windows assets and undrafts. + draft: true extra_files: - glob: ./dist/OliveTin-windows-amd64.msi footer: | diff --git a/docs/modules/dev/pages/signing.adoc b/docs/modules/dev/pages/signing.adoc index a00ab7f..beb0dec 100644 --- a/docs/modules/dev/pages/signing.adoc +++ b/docs/modules/dev/pages/signing.adoc @@ -1,31 +1,38 @@ -# macOS release signing +# Release signing + +OliveTin signs release binaries on two platforms: + +* **macOS** — Developer ID + notarization via [quill](https://github.com/anchore/quill) inside GoReleaser (optional if secrets are missing). +* **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (required before a draft release is published). + +## macOS release signing Release builds can sign and notarize the `darwin` binaries using [quill](https://github.com/anchore/quill) via GoReleaser. This runs on the existing Linux CI runner; no macOS runner is required. Signing is **optional**. If the GitHub secrets below are not all set, GoReleaser skips macOS signing and publishes unsigned binaries (the previous behaviour). -## Prerequisites +### Prerequisites - An active [Apple Developer Program](https://developer.apple.com/programs/) membership. - A **Developer ID Application** certificate (not "Apple Development" or "Mac App Distribution"). - An [App Store Connect API key](https://appstoreconnect.apple.com/access/integrations/api) with at least **Developer** access. -## One-time setup +### One-time setup -### 1. Create the signing certificate +#### 1. Create the signing certificate 1. Open [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list). 2. Create a certificate of type **Developer ID Application**. 3. Download the `.cer` file and double-click it to add it to **Keychain Access** on a Mac. 4. In Keychain Access, export the certificate as a **Personal Information Exchange (`.p12`)** file. You will set an export password — remember it; this becomes `MACOS_SIGN_PASSWORD`. -### 2. Create the notarization API key +#### 2. Create the notarization API key 1. Open [App Store Connect → Users and Access → Integrations → App Store Connect API](https://appstoreconnect.apple.com/access/integrations/api). 2. Create a key with **Developer** role (or Admin). 3. Download the `.p8` file once (it cannot be downloaded again). Note the **Key ID** shown in the portal and the **Issuer ID** at the top of the API keys page. -### 3. Base64-encode the key files +#### 3. Base64-encode the key files Run on a machine that has the files (Linux or macOS): @@ -36,7 +43,7 @@ base64 -w0 < ./AuthKey_XXXXXX.p8 # MACOS_NOTARY_KEY On macOS without GNU coreutils, use `base64 -i file | tr -d '\n'`. -### 4. Add GitHub repository secrets +#### 4. Add GitHub repository secrets In **Settings → Secrets and variables → Actions**, create: @@ -50,7 +57,7 @@ In **Settings → Secrets and variables → Actions**, create: All five must be present for signing to run. Any missing secret disables signing for that release. -## Renewal +### Renewal | Item | Typical lifetime | What to do | |------|------------------|------------| @@ -60,7 +67,7 @@ All five must be present for signing to run. Any missing secret disables signing After updating secrets, the next release on `main` (via semantic-release) will use the new credentials automatically. -## Verifying a signed release +### Verifying a signed release On a Mac, download a `OliveTin-darwin-*.tar.gz` release artifact and run: @@ -71,8 +78,90 @@ spctl -a -vv -t execute OliveTin-darwin-arm64/OliveTin A signed and notarized binary should report `accepted` with `source=Notarized Developer ID`. -## Configuration reference +### Configuration reference - GoReleaser: `notarize.macos` in [`.goreleaser.yml`](.goreleaser.yml) - CI secrets: [`.github/workflows/build-and-release.yml`](.github/workflows/build-and-release.yml) (`release` step) - [GoReleaser notarization docs](https://goreleaser.com/customization/notarize/) + +## Windows release signing (SignPath) + +Windows Authenticode signing uses [SignPath Foundation](https://signpath.org/) (free for qualifying open-source projects). It does **not** use GoReleaser Pro. + +GoReleaser creates a **draft** GitHub release with unsigned Windows assets. A separate `sign-windows` job submits those assets to SignPath, replaces them on the draft (including updated `checksums.txt`), then publishes the release. + +Signed artifacts: + +* `OliveTin.exe` inside `OliveTin-windows-amd64.zip` +* nested `OliveTin.exe` and the `OliveTin-windows-amd64.msi` installer (deep signing) + +Signing is **required** to publish. If SignPath secrets/vars are missing, `sign-windows` fails and the draft stays unpublished. + +### Prerequisites + +- Approval for the [SignPath Foundation open-source program](https://signpath.io/product/open-source). +- The SignPath GitHub App installed on the OliveTin organization/repository. +- A SignPath project linked to this repository, with a release signing policy. + +### One-time setup + +#### 1. Apply for SignPath Foundation + +1. Open https://signpath.io/product/open-source and apply with the OliveTin GitHub repository URL. +2. After approval, create (or confirm) the organization and project in the SignPath portal. + +#### 2. Install the SignPath GitHub App + +1. Install the SignPath GitHub App and grant access to the OliveTin repository. +2. Link the Trusted Build System **GitHub.com** to the SignPath project (required so SignPath can verify the workflow artifact origin). + +#### 3. Create artifact configurations + +In the SignPath project, create two artifact configurations with these slugs (must match CI). Paste the XML from the reference copies in this repo (SignPath does **not** load them automatically): + +* slug `windows-zip` ← [`signpath/windows-zip.xml`](../signpath/windows-zip.xml) +* slug `windows-msi` ← [`signpath/windows-msi.xml`](../signpath/windows-msi.xml) + +Use **Custom** XML in the SignPath UI and paste the file contents. Do **not** use **Upload an artifact sample** on these `.xml` files — SignPath will treat them as XML documents to sign (`xml-file`), which is unavailable on the Foundation/Open Source plan. See [`signpath/README.md`](../signpath/README.md). + +#### 4. Add GitHub secrets and variables + +In **Settings → Secrets and variables → Actions**: + +| Kind | Name | Value | +|------|------|-------| +| Secret | `SIGNPATH_API_TOKEN` | CI submitter API token from SignPath | +| Variable | `SIGNPATH_ORGANIZATION_ID` | SignPath organization ID | +| Variable | `SIGNPATH_PROJECT_SLUG` | SignPath project slug (e.g. `olivetin`) | +| Variable | `SIGNPATH_SIGNING_POLICY_SLUG` | Signing policy slug (e.g. `release-signing`) | + +#### 5. Pipeline behaviour + +On a new semantic-release from `main`: + +1. GoReleaser publishes container images and creates a **draft** GitHub release (including unsigned Windows zip/MSI). +2. The build job uploads those Windows files as GitHub Actions artifacts. +3. The `sign-windows` job submits each artifact to SignPath, waits for completion, then runs `var/windows/signpath-publish-signed.sh` to clobber-upload signed files, refresh `checksums.txt`, and undraft the release. + +All jobs in this chain use GitHub-hosted runners (required by SignPath for OSS projects). + +### Verifying a signed release + +On Windows, download `OliveTin-windows-amd64.msi` or extract `OliveTin.exe` from the zip, then either: + +* Right-click → **Properties** → **Digital Signatures**, or +* Run: + +```bat +signtool verify /pa OliveTin.exe +signtool verify /pa OliveTin-windows-amd64.msi +``` + +### Configuration reference + +- Draft release: `release.draft: true` in [`.goreleaser.yml`](.goreleaser.yml) +- CI job: `sign-windows` in [`.github/workflows/build-and-release.yml`](.github/workflows/build-and-release.yml) +- Publish helper: [`var/windows/signpath-publish-signed.sh`](var/windows/signpath-publish-signed.sh) +- SignPath artifact configs (reference only): [`signpath/windows-zip.xml`](../signpath/windows-zip.xml), [`signpath/windows-msi.xml`](../signpath/windows-msi.xml) +- [SignPath GitHub Actions docs](https://docs.signpath.io/trusted-build-systems/github) +- [SignPath artifact configuration examples](https://docs.signpath.io/artifact-configuration/examples) diff --git a/docs/modules/dev/signpath/README.md b/docs/modules/dev/signpath/README.md new file mode 100644 index 0000000..c6e285b --- /dev/null +++ b/docs/modules/dev/signpath/README.md @@ -0,0 +1,18 @@ +# SignPath artifact configurations (reference only) + +These XML files are **paste templates** for the SignPath web UI. SignPath does not load them from this repository. + +## Do not upload these `.xml` files as artifact samples + +If you use **Upload an artifact sample** on `windows-zip.xml` / `windows-msi.xml`, SignPath treats them as XML documents to sign and generates an `` configuration. That feature is not available on SignPath Foundation / Open Source, and you get an error like: + +> feature which is not currently available (XML element name: 'xml-file') + +## Correct setup + +1. In the SignPath project, **Add** an artifact configuration. +2. Choose **Custom** (edit XML), not “upload sample” of these files. +3. Paste the full contents of `windows-zip.xml` or `windows-msi.xml`. +4. Set the slug to `windows-zip` or `windows-msi` (must match CI). + +Optional: use **Upload an artifact sample** with a real `OliveTin-windows-amd64.zip` or `.msi` from a build, then trim the generated config to match these references. diff --git a/docs/modules/dev/signpath/windows-msi.xml b/docs/modules/dev/signpath/windows-msi.xml new file mode 100644 index 0000000..7789ea7 --- /dev/null +++ b/docs/modules/dev/signpath/windows-msi.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/modules/dev/signpath/windows-zip.xml b/docs/modules/dev/signpath/windows-zip.xml new file mode 100644 index 0000000..d595c5a --- /dev/null +++ b/docs/modules/dev/signpath/windows-zip.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/var/windows/signpath-publish-signed.sh b/var/windows/signpath-publish-signed.sh new file mode 100755 index 0000000..2f7578a --- /dev/null +++ b/var/windows/signpath-publish-signed.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}" +ARCH="${ARCH:-amd64}" +ZIP_NAME="OliveTin-windows-${ARCH}.zip" +MSI_NAME="OliveTin-windows-${ARCH}.msi" +CHECKSUMS_NAME="checksums.txt" + +usage() { + echo "Usage: $(basename "$0") " >&2 + exit 1 +} + +TAG="${1:-}" +SIGNED_ZIP="${2:-}" +SIGNED_MSI="${3:-}" + +if [[ -z "${TAG}" || -z "${SIGNED_ZIP}" || -z "${SIGNED_MSI}" ]]; then + usage +fi + +if [[ ! -f "${SIGNED_ZIP}" ]]; then + echo "Signed zip not found: ${SIGNED_ZIP}" >&2 + exit 1 +fi + +if [[ ! -f "${SIGNED_MSI}" ]]; then + echo "Signed MSI not found: ${SIGNED_MSI}" >&2 + exit 1 +fi + +if ! command -v gh >/dev/null; then + echo "gh is required to update the GitHub release" >&2 + exit 1 +fi + +mkdir -p "${DIST_DIR}" +cp -f "${SIGNED_ZIP}" "${DIST_DIR}/${ZIP_NAME}" +cp -f "${SIGNED_MSI}" "${DIST_DIR}/${MSI_NAME}" + +checksums_path="${DIST_DIR}/${CHECKSUMS_NAME}" +if ! gh release download "${TAG}" --pattern "${CHECKSUMS_NAME}" --dir "${DIST_DIR}" --clobber; then + echo "Failed to download ${CHECKSUMS_NAME} from release ${TAG}" >&2 + exit 1 +fi +if [[ ! -f "${checksums_path}" ]]; then + echo "${CHECKSUMS_NAME} not found after download from release ${TAG}" >&2 + exit 1 +fi + +update_checksum() { + local file_name="${1}" + local new_checksum + new_checksum="$(cd "${DIST_DIR}" && sha256sum "${file_name}")" + + if [[ -f "${checksums_path}" ]] && grep -qF " ${file_name}" "${checksums_path}"; then + local tmp + tmp="$(mktemp)" + grep -vF " ${file_name}" "${checksums_path}" > "${tmp}" || true + printf '%s\n' "${new_checksum}" >> "${tmp}" + mv "${tmp}" "${checksums_path}" + else + printf '%s\n' "${new_checksum}" >> "${checksums_path}" + fi +} + +update_checksum "${ZIP_NAME}" +update_checksum "${MSI_NAME}" + +gh release upload "${TAG}" \ + "${DIST_DIR}/${ZIP_NAME}" \ + "${DIST_DIR}/${MSI_NAME}" \ + "${checksums_path}" \ + --clobber + +gh release edit "${TAG}" --draft=false + +echo "Published signed ${ZIP_NAME} and ${MSI_NAME} on release ${TAG}" From f4f644fbc3f15aad33b42d8621f75e9c3d72d258 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 18 Jul 2026 11:21:29 +0100 Subject: [PATCH 2/4] fix: Early support for MacOS Signing (#931) --- .github/workflows/build-and-release.yml | 5 +++ .goreleaser.yml | 17 ++++++++++ docs/modules/dev/pages/signing.adoc | 41 +++++++++++++++++++++---- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 4058a43..2761a0c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -145,6 +145,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }} GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }} + 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: Upload unsigned Windows zip for SignPath id: upload-windows-zip diff --git a/.goreleaser.yml b/.goreleaser.yml index 79902a9..47a80c2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -182,6 +182,23 @@ nfpms: - src: var/manpage/OliveTin.1.gz dst: /usr/share/man/man1/OliveTin.1.gz +# Sign and notarize darwin binaries via quill (no macOS runner required). +# Skipped when MACOS_SIGN_P12 is unset so local/snapshot builds stay unsigned. +notarize: + macos: + - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' + ids: + - default + 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: 30m + release: # Stay draft until the sign-windows job replaces unsigned Windows assets and undrafts. draft: true diff --git a/docs/modules/dev/pages/signing.adoc b/docs/modules/dev/pages/signing.adoc index beb0dec..f0baeb9 100644 --- a/docs/modules/dev/pages/signing.adoc +++ b/docs/modules/dev/pages/signing.adoc @@ -7,7 +7,7 @@ OliveTin signs release binaries on two platforms: ## macOS release signing -Release builds can sign and notarize the `darwin` binaries using [quill](https://github.com/anchore/quill) via GoReleaser. This runs on the existing Linux CI runner; no macOS runner is required. +Release builds can sign and notarize the `darwin` binaries using [quill](https://github.com/anchore/quill) via GoReleaser. This runs on the existing Linux CI runner; no macOS runner or Xcode is required. Signing is **optional**. If the GitHub secrets below are not all set, GoReleaser skips macOS signing and publishes unsigned binaries (the previous behaviour). @@ -19,12 +19,41 @@ Signing is **optional**. If the GitHub secrets below are not all set, GoReleaser ### One-time setup -#### 1. Create the signing certificate +#### 1. Create the signing certificate (OpenSSL, no Mac/Xcode) + +Work in a private directory. Keep the private key offline and never commit it. + +```sh +mkdir -p ~/apple-signing && cd ~/apple-signing +chmod 700 . + +openssl genrsa -out developer_id_app.key 2048 +openssl req -new -key developer_id_app.key -out developer_id_app.csr \ + -subj "/emailAddress=you@example.com/CN=Your Name/C=GB" +``` 1. Open [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list). -2. Create a certificate of type **Developer ID Application**. -3. Download the `.cer` file and double-click it to add it to **Keychain Access** on a Mac. -4. In Keychain Access, export the certificate as a **Personal Information Exchange (`.p12`)** file. You will set an export password — remember it; this becomes `MACOS_SIGN_PASSWORD`. +2. Create a certificate of type **Developer ID Application**. Prefer **G2 Sub-CA** if the portal asks. +3. Upload `developer_id_app.csr` and download the resulting `.cer` (often named `developerID_application.cer`). + +Build a `.p12` that includes Apple's Developer ID G2 intermediate: + +```sh +curl -fsSLO https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer + +openssl x509 -inform DER -in developerID_application.cer -out developerID_application.pem +openssl x509 -inform DER -in DeveloperIDG2CA.cer -out DeveloperIDG2CA.pem + +# Export password becomes MACOS_SIGN_PASSWORD. +# On OpenSSL 3 (e.g. Fedora), -legacy improves compatibility with some tooling: +openssl pkcs12 -export -legacy \ + -inkey developer_id_app.key \ + -in developerID_application.pem \ + -certfile DeveloperIDG2CA.pem \ + -out Certificates.p12 +``` + +If you already have a Mac with the certificate in Keychain Access, you can export a `.p12` from there instead; the OpenSSL path above is enough when you do not. #### 2. Create the notarization API key @@ -55,7 +84,7 @@ In **Settings → Secrets and variables → Actions**, create: | `MACOS_NOTARY_KEY_ID` | Key ID from App Store Connect (e.g. `ABC123DEF4`) | | `MACOS_NOTARY_ISSUER_ID` | Issuer UUID from App Store Connect | -All five must be present for signing to run. Any missing secret disables signing for that release. +All five must be present for signing to run. GoReleaser enables the step when `MACOS_SIGN_P12` is set; missing companion secrets will fail that release. ### Renewal From 43fd83da26f793f39f04b99ca45930bb106a762b Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 18 Jul 2026 11:38:18 +0100 Subject: [PATCH 3/4] chore: Various windows and signing improvements --- .github/workflows/build-and-release.yml | 2 ++ docs/modules/dev/pages/signing.adoc | 28 ++++++++++++------------- var/windows/signpath-publish-signed.sh | 13 +++++++++++- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 2761a0c..6122520 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -187,6 +187,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + persist-credentials: false - name: Require SignPath configuration env: diff --git a/docs/modules/dev/pages/signing.adoc b/docs/modules/dev/pages/signing.adoc index f0baeb9..500db94 100644 --- a/docs/modules/dev/pages/signing.adoc +++ b/docs/modules/dev/pages/signing.adoc @@ -2,14 +2,14 @@ OliveTin signs release binaries on two platforms: -* **macOS** — Developer ID + notarization via [quill](https://github.com/anchore/quill) inside GoReleaser (optional if secrets are missing). +* **macOS** — Developer ID + notarization via [quill](https://github.com/anchore/quill) inside GoReleaser (optional only when `MACOS_SIGN_P12` is unset). * **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (required before a draft release is published). ## macOS release signing Release builds can sign and notarize the `darwin` binaries using [quill](https://github.com/anchore/quill) via GoReleaser. This runs on the existing Linux CI runner; no macOS runner or Xcode is required. -Signing is **optional**. If the GitHub secrets below are not all set, GoReleaser skips macOS signing and publishes unsigned binaries (the previous behaviour). +Signing is **optional** only when `MACOS_SIGN_P12` is unset — GoReleaser then skips macOS signing and publishes unsigned binaries. When `MACOS_SIGN_P12` is set, the companion macOS secrets below are required or the release fails. ### Prerequisites @@ -109,9 +109,9 @@ A signed and notarized binary should report `accepted` with `source=Notarized De ### Configuration reference -- GoReleaser: `notarize.macos` in [`.goreleaser.yml`](.goreleaser.yml) -- CI secrets: [`.github/workflows/build-and-release.yml`](.github/workflows/build-and-release.yml) (`release` step) -- [GoReleaser notarization docs](https://goreleaser.com/customization/notarize/) +- GoReleaser: `notarize.macos` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`] +- CI secrets: link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`] (`release` step) +- link:https://goreleaser.com/customization/notarize/[GoReleaser notarization docs] ## Windows release signing (SignPath) @@ -148,10 +148,10 @@ Signing is **required** to publish. If SignPath secrets/vars are missing, `sign- In the SignPath project, create two artifact configurations with these slugs (must match CI). Paste the XML from the reference copies in this repo (SignPath does **not** load them automatically): -* slug `windows-zip` ← [`signpath/windows-zip.xml`](../signpath/windows-zip.xml) -* slug `windows-msi` ← [`signpath/windows-msi.xml`](../signpath/windows-msi.xml) +* slug `windows-zip` ← link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-zip.xml[`signpath/windows-zip.xml`] +* slug `windows-msi` ← link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-msi.xml[`signpath/windows-msi.xml`] -Use **Custom** XML in the SignPath UI and paste the file contents. Do **not** use **Upload an artifact sample** on these `.xml` files — SignPath will treat them as XML documents to sign (`xml-file`), which is unavailable on the Foundation/Open Source plan. See [`signpath/README.md`](../signpath/README.md). +Use **Custom** XML in the SignPath UI and paste the file contents. Do **not** use **Upload an artifact sample** on these `.xml` files — SignPath will treat them as XML documents to sign (`xml-file`), which is unavailable on the Foundation/Open Source plan. See link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/README.md[`signpath/README.md`]. #### 4. Add GitHub secrets and variables @@ -188,9 +188,9 @@ signtool verify /pa OliveTin-windows-amd64.msi ### Configuration reference -- Draft release: `release.draft: true` in [`.goreleaser.yml`](.goreleaser.yml) -- CI job: `sign-windows` in [`.github/workflows/build-and-release.yml`](.github/workflows/build-and-release.yml) -- Publish helper: [`var/windows/signpath-publish-signed.sh`](var/windows/signpath-publish-signed.sh) -- SignPath artifact configs (reference only): [`signpath/windows-zip.xml`](../signpath/windows-zip.xml), [`signpath/windows-msi.xml`](../signpath/windows-msi.xml) -- [SignPath GitHub Actions docs](https://docs.signpath.io/trusted-build-systems/github) -- [SignPath artifact configuration examples](https://docs.signpath.io/artifact-configuration/examples) +- Draft release: `release.draft: true` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`] +- CI job: `sign-windows` in link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`] +- Publish helper: link:https://github.com/OliveTin/OliveTin/blob/main/var/windows/signpath-publish-signed.sh[`var/windows/signpath-publish-signed.sh`] +- SignPath artifact configs (reference only): link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-zip.xml[`signpath/windows-zip.xml`], link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-msi.xml[`signpath/windows-msi.xml`] +- link:https://docs.signpath.io/trusted-build-systems/github[SignPath GitHub Actions docs] +- link:https://docs.signpath.io/artifact-configuration/examples[SignPath artifact configuration examples] diff --git a/var/windows/signpath-publish-signed.sh b/var/windows/signpath-publish-signed.sh index 2f7578a..941e592 100755 --- a/var/windows/signpath-publish-signed.sh +++ b/var/windows/signpath-publish-signed.sh @@ -42,6 +42,7 @@ cp -f "${SIGNED_ZIP}" "${DIST_DIR}/${ZIP_NAME}" cp -f "${SIGNED_MSI}" "${DIST_DIR}/${MSI_NAME}" checksums_path="${DIST_DIR}/${CHECKSUMS_NAME}" +checksums_backup="${DIST_DIR}/${CHECKSUMS_NAME}.orig" if ! gh release download "${TAG}" --pattern "${CHECKSUMS_NAME}" --dir "${DIST_DIR}" --clobber; then echo "Failed to download ${CHECKSUMS_NAME} from release ${TAG}" >&2 exit 1 @@ -50,6 +51,7 @@ if [[ ! -f "${checksums_path}" ]]; then echo "${CHECKSUMS_NAME} not found after download from release ${TAG}" >&2 exit 1 fi +cp -f "${checksums_path}" "${checksums_backup}" update_checksum() { local file_name="${1}" @@ -70,12 +72,21 @@ update_checksum() { update_checksum "${ZIP_NAME}" update_checksum "${MSI_NAME}" +# Replace binaries first so a failed checksums upload leaves the draft recoverable. gh release upload "${TAG}" \ "${DIST_DIR}/${ZIP_NAME}" \ "${DIST_DIR}/${MSI_NAME}" \ - "${checksums_path}" \ --clobber +if ! gh release upload "${TAG}" "${checksums_path}" --clobber; then + echo "Failed to upload updated ${CHECKSUMS_NAME}; restoring previous asset" >&2 + restore_dir="$(mktemp -d)" + cp -f "${checksums_backup}" "${restore_dir}/${CHECKSUMS_NAME}" + gh release upload "${TAG}" "${restore_dir}/${CHECKSUMS_NAME}" --clobber + rm -rf "${restore_dir}" + exit 1 +fi + gh release edit "${TAG}" --draft=false echo "Published signed ${ZIP_NAME} and ${MSI_NAME} on release ${TAG}" From ad22434ec86495ee2eef6591d18f51ad4b58e573 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 18 Jul 2026 12:24:18 +0100 Subject: [PATCH 4/4] chore: fix macos signing --- .goreleaser.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 47a80c2..b56a7bd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -187,8 +187,7 @@ nfpms: notarize: macos: - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' - ids: - - default + # Defaults to project_name when omitted; must match builds[].id (also project_name). sign: certificate: "{{.Env.MACOS_SIGN_P12}}" password: "{{.Env.MACOS_SIGN_PASSWORD}}"