Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions .github/scripts/import-macos-signing.sh
Original file line number Diff line number Diff line change
@@ -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#*\"}"
Expand All @@ -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"
26 changes: 26 additions & 0 deletions .github/scripts/notarize-macos.sh
Original file line number Diff line number Diff line change
@@ -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
68 changes: 35 additions & 33 deletions .github/workflows/release-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +15,7 @@ env:
jobs:
build-macos-binaries:
runs-on: ${{ matrix.runner }}
environment: production-signing
permissions:
contents: read
env:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -113,6 +121,7 @@ jobs:

build-macos-installers:
runs-on: ${{ matrix.runner }}
environment: production-signing
permissions:
contents: read
strategy:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -108,6 +105,13 @@ jobs:
pattern: signed-windows-installer-*
merge-multiple: true

- name: Download notarized 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:
Expand All @@ -122,6 +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 <FILE> --repo ${{ github.repository }}`.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57 changes: 32 additions & 25 deletions frontend/desktop/MACOS_INSTALLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion frontend/desktop/scripts/package-macos.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down