Release signing

OliveTin signs release binaries on two platforms:

  • 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 (signed zip/MSI are uploaded after the 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 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

One-time setup

1. Create the signing certificate (OpenSSL, no Mac/Xcode)

Work in a private directory. Keep the private key offline and never commit it.

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. 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 the full chain: leaf + Developer ID G2 intermediate + Apple Root CA.

The Apple Root must be present. With only leaf + G2, quill embeds a designated requirement of the form certificate root[field.1.2.840.113635.100.6.2.6]. On macOS that resolves to Apple Root CA (which does not have that OID), so AMFI SIGKILLs the binary with does not satisfy its designated Requirement even though notarization still succeeds. A correct chain produces certificate 1[…​] instead.

curl -fsSLO https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
curl -fsSLO https://www.apple.com/appleca/AppleIncRootCertificate.cer

openssl x509 -inform DER -in developerID_application.cer -out developerID_application.pem
openssl x509 -inform DER -in DeveloperIDG2CA.cer -out DeveloperIDG2CA.pem
openssl x509 -inform DER -in AppleIncRootCertificate.cer -out AppleRootCA.pem

# Chain file: intermediate then root (leaf is passed separately via -in).
cat DeveloperIDG2CA.pem AppleRootCA.pem > chain.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 chain.pem \
  -out Certificates.p12

Confirm the .p12 has three certificates before base64-encoding:

openssl pkcs12 -in Certificates.p12 -nodes -passin pass:"$MACOS_SIGN_PASSWORD" 2>/dev/null \
  | grep -c "BEGIN CERTIFICATE"
# expect: 3

If you already have a Mac with the certificate in Keychain Access, you can export a .p12 from there instead — include the full chain when exporting.

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

Run on a machine that has the files (Linux or macOS):

base64 -w0 < ./Certificates.p12    # MACOS_SIGN_P12
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

In Settings → Secrets and variables → Actions, create:

| Secret | Value | |--------|-------| | MACOS_SIGN_P12 | Base64 contents of the .p12 file | | MACOS_SIGN_PASSWORD | Password used when exporting the .p12 | | MACOS_NOTARY_KEY | Base64 contents of the .p8 file | | 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. GoReleaser enables the step when MACOS_SIGN_P12 is set; missing companion secrets will fail that release.

Renewal

| Item | Typical lifetime | What to do | |------|------------------|------------| | Developer ID Application certificate | ~5 years | Create a new certificate in the Apple portal, export a new full-chain .p12 (leaf + G2 intermediate + Apple Root CA), update MACOS_SIGN_P12 and MACOS_SIGN_PASSWORD. | | App Store Connect API key | Does not expire, but can be revoked | Create a new key if compromised or lost; update MACOS_NOTARY_KEY, MACOS_NOTARY_KEY_ID, and optionally MACOS_NOTARY_ISSUER_ID. | | Apple Developer Program | Annual subscription | Renew membership before it lapses; existing certificates stop working if the account is inactive. |

After updating secrets, the next release on main (via semantic-release) will use the new credentials automatically.

Verifying a signed release

From any platform (no Mac required), check that quill did not emit the broken certificate root[…​] designated requirement:

go install github.com/anchore/quill/cmd/quill@latest
quill describe OliveTin-darwin-arm64/OliveTin

The requirements line must contain certificate 1[field.1.2.840.113635.100.6.2.6]. If it says certificate root[field.1.2.840.113635.100.6.2.6], the .p12 is missing Apple Root CA — rebuild it and update MACOS_SIGN_P12.

On a Mac, also run:

tar -xzf OliveTin-darwin-arm64.tar.gz
codesign --verify --strict -vvvv OliveTin-darwin-arm64/OliveTin
spctl -a -vv -t execute OliveTin-darwin-arm64/OliveTin

codesign should report both valid on disk and satisfies its Designated Requirement. spctl should report accepted with source=Notarized Developer ID.

Configuration reference

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 publishes a GitHub release without Windows zip/MSI assets. A separate sign-windows job submits the unsigned Windows files (as workflow artifacts) to SignPath, then uploads the signed zip/MSI and updates checksums.txt on the already-published release.

Signed artifacts:

  • OliveTin.exe inside OliveTin-windows-amd64.zip

  • nested OliveTin.exe and the OliveTin-windows-amd64.msi installer (deep signing)

If SignPath secrets/vars are missing or signing fails, the release still publishes; Windows assets are simply missing until a successful sign-windows run. Install URLs for the Windows zip may 404 until signing finishes.

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):

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.

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 a GitHub release without Windows zip/MSI assets (those are built locally for SignPath only).

  2. The build job uploads the unsigned 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 upload the signed files and refresh checksums.txt.

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 → PropertiesDigital Signatures, or

  • Run:

signtool verify /pa OliveTin.exe
signtool verify /pa OliveTin-windows-amd64.msi

Configuration reference