From af59b993e6c3bad983accd1d93d3c0520b229f62 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Fri, 25 Sep 2026 15:46:06 +0200 Subject: [PATCH 1/2] add macOS installers to the release Signed-off-by: Peter Jausovec --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bf287a..689db7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,6 +108,13 @@ jobs: pattern: signed-windows-installer-* merge-multiple: true + - name: Download macOS installers + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: release + pattern: macos-installer-* + merge-multiple: true + - name: Create GitHub release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: @@ -123,5 +130,6 @@ jobs: - Agentdesktop binaries for amd64 and arm64 on Linux, macOS, and Windows are attached below. - Signed Windows MSI installers for amd64 and arm64 are attached below. - Linux binaries include keyless Sigstore build provenance. Verify a downloaded binary with `gh attestation verify --repo ${{ github.repository }}`. + - macOS PKG installers for amd64 and arm64 are attached below. env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 288e71c1029217974639acbea85ee2d1ee214a06 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Wed, 14 Oct 2026 08:49:50 -0700 Subject: [PATCH 2/2] macos signing wip Signed-off-by: Peter Jausovec --- .github/scripts/import-macos-signing.sh | 35 ++++++---- .github/scripts/notarize-macos.sh | 26 +++++++ .github/workflows/release-macos.yml | 68 ++++++++++--------- .github/workflows/release.yml | 7 +- frontend/desktop/MACOS_INSTALLER.md | 57 +++++++++------- .../desktop/scripts/package-macos.test.mjs | 2 +- 6 files changed, 116 insertions(+), 79 deletions(-) create mode 100644 .github/scripts/notarize-macos.sh diff --git a/.github/scripts/import-macos-signing.sh b/.github/scripts/import-macos-signing.sh index 6d1c3a7..6b46dda 100644 --- a/.github/scripts/import-macos-signing.sh +++ b/.github/scripts/import-macos-signing.sh @@ -1,31 +1,31 @@ #!/bin/bash set -euo pipefail -: "${APPLE_APPLICATION_CERTIFICATE:?Set the base64-encoded PKCS#12 GitHub secret}" -: "${APPLE_CERTIFICATE_PASSWORD:?Set the PKCS#12 password GitHub secret}" +: "${APPLE_APPLICATION_CERTIFICATE:?Set the base64-encoded Developer ID Application PKCS#12 GitHub secret}" +: "${APPLE_CERTIFICATE_PASSWORD:?Set the Developer ID Application PKCS#12 password GitHub secret}" +: "${APPLE_INSTALLER_CERTIFICATE:?Set the base64-encoded Developer ID Installer PKCS#12 GitHub secret}" +: "${APPLE_INSTALLER_CERTIFICATE_PASSWORD:?Set the Developer ID Installer PKCS#12 password GitHub secret}" umask 077 keychain="${RUNNER_TEMP}/agentdesktop-signing.keychain-db" -certificate="${RUNNER_TEMP}/agentdesktop-signing.pem" archive="${RUNNER_TEMP}/agentdesktop-signing.p12" trap 'rm -f "$archive"' EXIT -printf '%s' "$APPLE_APPLICATION_CERTIFICATE" | base64 --decode > "$archive" -openssl pkcs12 -in "$archive" -clcerts -nokeys \ - -passin env:APPLE_CERTIFICATE_PASSWORD -out "$certificate" keychain_password="$(openssl rand -hex 32)" echo "::add-mask::${keychain_password}" security create-keychain -p "$keychain_password" "$keychain" security set-keychain-settings -lut 21600 "$keychain" security unlock-keychain -p "$keychain_password" "$keychain" -security import "$archive" -k "$keychain" -P "$APPLE_CERTIFICATE_PASSWORD" \ - -T /usr/bin/codesign + +import_identity() { + printf '%s' "$1" | base64 --decode > "$archive" + security import "$archive" -k "$keychain" -P "$2" -T "$3" +} +import_identity "$APPLE_APPLICATION_CERTIFICATE" "$APPLE_CERTIFICATE_PASSWORD" /usr/bin/codesign +import_identity "$APPLE_INSTALLER_CERTIFICATE" "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" /usr/bin/pkgbuild security set-key-partition-list -S apple-tool:,apple:,codesign: -s \ -k "$keychain_password" "$keychain" > /dev/null -# Trust our self-signed certificate on this disposable build runner only. -sudo security add-trusted-cert -d -r trustRoot -p codeSign \ - -k "$keychain" "$certificate" keychains=() while IFS= read -r existing; do existing="${existing#*\"}" @@ -36,7 +36,12 @@ while IFS= read -r existing; do done < <(security list-keychains -d user) security list-keychains -d user -s "$keychain" "${keychains[@]}" -# Select the exact certificate, independent of its display name. -identity="$(openssl x509 -in "$certificate" -noout -fingerprint -sha1 | cut -d= -f2 | tr -d ':')" -security find-identity -v -p codesigning "$keychain" | grep -F "$identity" -echo "APPLE_SIGNING_IDENTITY=$identity" >> "$GITHUB_ENV" +# Validity needs Apple's Developer ID intermediate, which GitHub-hosted runners preinstall. +identities="$(security find-identity -v "$keychain")" +echo "$identities" +application="$(awk '/"Developer ID Application: / { print $2; exit }' <<< "$identities")" +installer="$(awk -F '"' '/"Developer ID Installer: / { print $2; exit }' <<< "$identities")" +: "${application:?APPLE_APPLICATION_CERTIFICATE has no valid Developer ID Application identity}" +: "${installer:?APPLE_INSTALLER_CERTIFICATE has no valid Developer ID Installer identity}" +echo "APPLE_SIGNING_IDENTITY=$application" >> "$GITHUB_ENV" +echo "APPLE_INSTALLER_SIGNING_IDENTITY=$installer" >> "$GITHUB_ENV" diff --git a/.github/scripts/notarize-macos.sh b/.github/scripts/notarize-macos.sh new file mode 100644 index 0000000..cefdfe3 --- /dev/null +++ b/.github/scripts/notarize-macos.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +: "${APPLE_API_KEY:?Set the App Store Connect team API private key GitHub secret}" +: "${APPLE_API_KEY_ID:?Set the App Store Connect API key ID GitHub variable}" +: "${APPLE_API_ISSUER:?Set the App Store Connect API issuer ID GitHub variable}" + +umask 077 +key="${RUNNER_TEMP}/agentdesktop-notary.p8" +trap 'rm -f "$key"' EXIT +printf '%s\n' "$APPLE_API_KEY" > "$key" +credentials=(--key "$key" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER") + +result="$(xcrun notarytool submit "$1" "${credentials[@]}" \ + --wait --timeout 1h --output-format json)" || true +echo "$result" +id="$(jq -r '.id // empty' <<< "$result" 2> /dev/null)" || true +if [[ -n "$id" ]]; then + # The log also lists warnings for accepted submissions. + xcrun notarytool log "$id" "${credentials[@]}" || true +fi +outcome="$(jq -r '.status // empty' <<< "$result" 2> /dev/null)" || true +if [[ "$outcome" != Accepted ]]; then + echo "Notarization of $1 finished with status '${outcome:-unknown}'" >&2 + exit 1 +fi diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 5dcce5b..fd610a9 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -7,11 +7,6 @@ on: description: Canonical release version without a v prefix required: true type: string - secrets: - APPLE_APPLICATION_CERTIFICATE: - required: true - APPLE_CERTIFICATE_PASSWORD: - required: true env: CARGO_TERM_COLOR: always @@ -20,6 +15,7 @@ env: jobs: build-macos-binaries: runs-on: ${{ matrix.runner }} + environment: production-signing permissions: contents: read env: @@ -68,6 +64,8 @@ jobs: env: APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }} + APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} run: bash .github/scripts/import-macos-signing.sh - name: Sign macOS binary @@ -77,9 +75,22 @@ jobs: run: | binary="target/${TARGET}/release/agentdesktop" codesign --force --sign "$APPLE_SIGNING_IDENTITY" \ - --identifier dev.agentdesktop.tray --options runtime --timestamp=none "$binary" + --identifier dev.agentdesktop.tray --options runtime --timestamp "$binary" codesign --verify --strict --verbose=2 \ - -R="identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" "$binary" + -R="anchor apple generic and identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" "$binary" + + - name: Notarize macOS binary + shell: bash + env: + TARGET: ${{ matrix.target }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ vars.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ vars.APPLE_API_ISSUER }} + run: | + # Bare binaries cannot be stapled; Gatekeeper looks up their ticket online. + archive="${RUNNER_TEMP}/agentdesktop-notarize.zip" + ditto -c -k "target/${TARGET}/release/agentdesktop" "$archive" + bash .github/scripts/notarize-macos.sh "$archive" - name: Stage binary and checksum shell: bash @@ -95,11 +106,8 @@ jobs: timeout-minutes: 2 shell: bash run: | - # remove-trusted-cert can hang on headless macOS runners: - # https://github.com/actions/runner-images/issues/12116 - # Leave public trust metadata to disposable GitHub-hosted VM teardown. # Delete exported files first, even if keychain deletion later fails. - rm -f "${RUNNER_TEMP}/agentdesktop-signing.pem" "${RUNNER_TEMP}/agentdesktop-signing.p12" + rm -f "${RUNNER_TEMP}/agentdesktop-signing.p12" "${RUNNER_TEMP}/agentdesktop-notary.p8" if [ -f "${RUNNER_TEMP}/agentdesktop-signing.keychain-db" ]; then security delete-keychain "${RUNNER_TEMP}/agentdesktop-signing.keychain-db" fi @@ -113,6 +121,7 @@ jobs: build-macos-installers: runs-on: ${{ matrix.runner }} + environment: production-signing permissions: contents: read strategy: @@ -156,6 +165,8 @@ jobs: env: APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }} + APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} run: bash .github/scripts/import-macos-signing.sh - name: Build PKG @@ -171,7 +182,7 @@ jobs: app="target/${TARGET}/release/bundle/macos/agentdesktop.app" codesign --verify --deep --strict --verbose=2 "$app" codesign --verify --strict \ - -R="identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" \ + -R="anchor apple generic and identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" \ "$app/Contents/MacOS/agentdesktop" - name: Stage PKG @@ -186,22 +197,16 @@ jobs: mkdir -p release cp "${source_path}" "release/${OUTPUT_NAME}" - # Production notarization placeholder: notarize and staple the signed - # package here, before calculating its checksum or smoke-testing it. - # - name: Notarize and staple PKG - # shell: bash - # env: - # OUTPUT_NAME: agentdesktop-macos-${{ matrix.arch }}.pkg - # APPLE_ID: ${{ secrets.APPLE_ID }} - # APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - # run: | - # xcrun notarytool submit "release/${OUTPUT_NAME}" \ - # --apple-id "${APPLE_ID}" \ - # --password "${APPLE_APP_SPECIFIC_PASSWORD}" \ - # --team-id "${APPLE_TEAM_ID}" \ - # --wait - # xcrun stapler staple "release/${OUTPUT_NAME}" + - name: Notarize and staple PKG + shell: bash + env: + OUTPUT_NAME: agentdesktop-macos-${{ matrix.arch }}.pkg + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ vars.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ vars.APPLE_API_ISSUER }} + run: | + bash .github/scripts/notarize-macos.sh "release/${OUTPUT_NAME}" + xcrun stapler staple "release/${OUTPUT_NAME}" - name: Checksum PKG shell: bash @@ -269,16 +274,13 @@ jobs: timeout-minutes: 2 shell: bash run: | - # remove-trusted-cert can hang on headless macOS runners: - # https://github.com/actions/runner-images/issues/12116 - # Leave public trust metadata to disposable GitHub-hosted VM teardown. # Delete exported files first, even if keychain deletion later fails. - rm -f "${RUNNER_TEMP}/agentdesktop-signing.pem" "${RUNNER_TEMP}/agentdesktop-signing.p12" + rm -f "${RUNNER_TEMP}/agentdesktop-signing.p12" "${RUNNER_TEMP}/agentdesktop-notary.p8" if [ -f "${RUNNER_TEMP}/agentdesktop-signing.keychain-db" ]; then security delete-keychain "${RUNNER_TEMP}/agentdesktop-signing.keychain-db" fi - - name: Upload PKG (self-signed application) + - name: Upload PKG uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: macos-installer-${{ matrix.arch }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 689db7f..62063bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,9 +70,6 @@ jobs: uses: ./.github/workflows/release-macos.yml with: version: ${{ needs.setup.outputs.version }} - secrets: - APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} windows: needs: setup @@ -108,7 +105,7 @@ jobs: pattern: signed-windows-installer-* merge-multiple: true - - name: Download macOS installers + - name: Download notarized macOS installers uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: release @@ -129,7 +126,7 @@ jobs: - Helm chart: `oci://${{ env.CHART_REGISTRY }}/agentdesktop-controller:${{ needs.setup.outputs.version }}` - Agentdesktop binaries for amd64 and arm64 on Linux, macOS, and Windows are attached below. - Signed Windows MSI installers for amd64 and arm64 are attached below. + - Notarized macOS PKG installers for amd64 and arm64 are attached below. - Linux binaries include keyless Sigstore build provenance. Verify a downloaded binary with `gh attestation verify --repo ${{ github.repository }}`. - - macOS PKG installers for amd64 and arm64 are attached below. env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/frontend/desktop/MACOS_INSTALLER.md b/frontend/desktop/MACOS_INSTALLER.md index fe60f4f..76ebdad 100644 --- a/frontend/desktop/MACOS_INSTALLER.md +++ b/frontend/desktop/MACOS_INSTALLER.md @@ -44,33 +44,40 @@ for this; ad-hoc signing (`-`) does not preserve identity across changed builds. ## Release signing The release workflow signs both architectures of the standalone macOS binary -and the application inside the PKG. Configure these repository Actions secrets: - -| Secret | Value | -| --- | --- | -| `APPLE_APPLICATION_CERTIFICATE` | Base64-encoded `.p12` containing the signing certificate and private key | -| `APPLE_CERTIFICATE_PASSWORD` | Password protecting that `.p12` | - -The workflow imports the identity into a temporary keychain, trusts the -self-signed certificate on the build runner, and selects it by fingerprint. -It verifies the application signature before uploading artifacts and removes -the signing identity afterward. Missing secrets fail the macOS release jobs. - -Keep a secure backup of the certificate, private key, and password. Reuse them -for future releases; generating a replacement certificate changes the identity -that protects existing Keychain entries. The application identifier remains -`dev.agentdesktop.tray`, including for standalone binaries. - -Self-signing requires no Apple account. It preserves application identity but -does not provide Gatekeeper approval or notarization. The PKG container itself -remains unsigned. Devices enrolled under an earlier ad-hoc signature still need -credential recovery or re-enrollment once; adopting this certificate does not -unlock the old entries. - -For Apple-trusted distribution, use Developer ID Application signing, sign the -PKG with Developer ID Installer, then notarize and staple the package: +and the application inside the PKG with a Developer ID Application certificate, +signs the PKG with a Developer ID Installer certificate, and notarizes both. +The PKG is stapled; standalone binaries cannot be, so Gatekeeper checks their +notarization online. + +The macOS jobs run in the protected `production-signing` GitHub environment. +Configure these there: + +| Name | Kind | Value | +| --- | --- | --- | +| `APPLE_APPLICATION_CERTIFICATE` | Secret | Base64-encoded `.p12` with the Developer ID Application certificate and private key | +| `APPLE_CERTIFICATE_PASSWORD` | Secret | Password protecting that `.p12` | +| `APPLE_INSTALLER_CERTIFICATE` | Secret | Base64-encoded `.p12` with the Developer ID Installer certificate and private key | +| `APPLE_INSTALLER_CERTIFICATE_PASSWORD` | Secret | Password protecting that `.p12` | +| `APPLE_API_KEY` | Secret | Contents of an App Store Connect team API key (`.p8`) | +| `APPLE_API_KEY_ID` | Variable | ID of that API key | +| `APPLE_API_ISSUER` | Variable | Issuer ID of that API key | + +Only the Apple Developer Account Holder can create Developer ID certificates. +`notarytool` needs a team API key; individual keys do not work. Missing values +fail the macOS release jobs. + +Keychain entries survive upgrades and certificate renewals because the +application identifier remains `dev.agentdesktop.tray`, including for +standalone binaries, and Developer ID signatures are matched by team. Devices +installed from earlier self-signed or ad-hoc builds cannot read their stored +identity after upgrading; the daemon discards it and enrolls again. + +To notarize a locally built package, store the API key once, then submit and +staple the package: ```sh +xcrun notarytool store-credentials agentdesktop-notary \ + --key AuthKey_KEYID.p8 --key-id KEYID --issuer ISSUER_ID xcrun notarytool submit "Agent Desktop.pkg" \ --keychain-profile agentdesktop-notary --wait xcrun stapler staple "Agent Desktop.pkg" diff --git a/frontend/desktop/scripts/package-macos.test.mjs b/frontend/desktop/scripts/package-macos.test.mjs index 6b8a328..80d2850 100644 --- a/frontend/desktop/scripts/package-macos.test.mjs +++ b/frontend/desktop/scripts/package-macos.test.mjs @@ -146,8 +146,8 @@ test("both release signing cleanups always run with a short timeout", () => { const signingFiles = [ "agentdesktop-signing.keychain-db", - "agentdesktop-signing.pem", "agentdesktop-signing.p12", + "agentdesktop-notary.p8", ]; function signingCleanupFixture(t, files = signingFiles) {