Skip to content
Open
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
52 changes: 52 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,61 @@ jobs:
zip code-assistant-${{ matrix.name }}.zip "code-assistant${{ matrix.extension }}"
fi

- name: Set up macOS code signing
if: matrix.os == 'macos-latest'
shell: bash
env:
MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
run: |
# Skip silently when no certificate is configured: the bundle script
# then falls back to ad-hoc signing and the release still builds.
if [ -z "$MACOS_CERT_P12_BASE64" ]; then
echo "No MACOS_CERT_P12_BASE64 secret set; skipping code-signing setup."
exit 0
fi

KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
CERT_PATH="$RUNNER_TEMP/certificate.p12"

# Materialize the base64-encoded .p12 certificate.
echo -n "$MACOS_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"

# Create a dedicated, ephemeral keychain and make it the default so
# codesign can find the identity non-interactively.
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Import the Developer ID Application certificate + private key.
security import "$CERT_PATH" \
-P "$MACOS_CERT_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"

# Allow codesign/productsign to use the key without a UI prompt.
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Put our keychain in the search list so codesign sees the identity.
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
security default-keychain -s "$KEYCHAIN_PATH"

echo "Available signing identities:"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"

rm -f "$CERT_PATH"

- name: Build macOS .app bundle
if: matrix.os == 'macos-latest'
shell: bash
env:
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
run: |
# Map matrix target -> bundle script arch argument
case "${{ matrix.target }}" in
Expand Down
25 changes: 25 additions & 0 deletions crates/code_assistant/assets/Entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Entitlements applied when signing with a Developer ID identity under the
hardened runtime (required for notarization).

Code Assistant embeds gpui (Zed's UI framework), which relies on JIT /
dynamically generated executable memory. Without these two allowances the
process is killed by the hardened runtime at launch.

This mirrors the hardened-runtime keys that Zed itself uses
(crates/zed/resources/zed.entitlements). We deliberately do NOT set the
weaker keys such as com.apple.security.cs.disable-library-validation,
disable-executable-page-protection or allow-dyld-environment-variables:
Zed does not need them either (it keeps library validation commented out),
and Apple's notary service scrutinizes them.
-->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
94 changes: 94 additions & 0 deletions docs/macos-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# macOS Code Signing & Notarization

The macOS `.app` bundle is produced by `scripts/bundle-macos.sh`. The script is
usable in three ways, driven entirely by environment variables — it has no
CI-specific logic:

| Scenario | What happens |
| --- | --- |
| No env vars set (default, e.g. a local build) | **Ad-hoc** signature only. No Apple account needed. Not notarized. |
| `MACOS_SIGN_IDENTITY` set | Signs with your **Developer ID Application** identity, hardened runtime + entitlements. |
| Signing identity **and** all notary vars set | Additionally **notarizes** with `xcrun notarytool` and **staples** the ticket. |

All signing uses only tools shipping with macOS / Xcode (`codesign`,
`xcrun notarytool`, `xcrun stapler`) — no third-party actions or CLIs.

## Local usage

### Plain build (no signing)

```bash
./scripts/bundle-macos.sh # host arch, ad-hoc signed
```

### Signed build with your own certificate

If you have a *Developer ID Application* certificate in your login keychain:

```bash
# Find the exact identity string:
security find-identity -v -p codesigning

export MACOS_SIGN_IDENTITY="Developer ID Application: Jane Doe (TEAMID1234)"
./scripts/bundle-macos.sh
```

### Signed + notarized build

Add the notary credentials (an app-specific password created at
<https://appleid.apple.com>):

```bash
export MACOS_SIGN_IDENTITY="Developer ID Application: Jane Doe (TEAMID1234)"
export MACOS_NOTARY_APPLE_ID="jane@example.com"
export MACOS_NOTARY_PASSWORD="abcd-efgh-ijkl-mnop" # app-specific password
export MACOS_NOTARY_TEAM_ID="TEAMID1234"
./scripts/bundle-macos.sh universal
```

## CI usage

The `Release` workflow (`.github/workflows/release.yml`) does the same thing
automatically on the macOS build jobs. It imports the certificate into an
ephemeral keychain and then calls the exact same bundle script. When the secrets
are **not** configured, the workflow falls back to ad-hoc signing and the
release still succeeds.

### Required GitHub secrets

Configure these under **Settings → Secrets and variables → Actions**:

| Secret | Purpose |
| --- | --- |
| `MACOS_CERT_P12_BASE64` | Your *Developer ID Application* certificate **and private key** exported as a `.p12`, then base64-encoded. |
| `MACOS_CERT_PASSWORD` | The password protecting that `.p12` file. |
| `MACOS_KEYCHAIN_PASSWORD` | Any throwaway password used for the temporary keychain created on the runner. |
| `MACOS_SIGN_IDENTITY` | The identity name, e.g. `Developer ID Application: Jane Doe (TEAMID1234)`. |
| `MACOS_NOTARY_APPLE_ID` | Apple ID email used for notarization. |
| `MACOS_NOTARY_PASSWORD` | App-specific password for that Apple ID. |
| `MACOS_NOTARY_TEAM_ID` | Apple Developer Team ID (10 characters). |

If only the signing secrets (first four) are set, the app is signed but not
notarized. If none are set, the build is ad-hoc signed.

### Producing the certificate secrets

1. In Keychain Access, export your *Developer ID Application* certificate
(including its private key) as a `.p12` file and set a password.
2. Base64-encode it for storage in a secret:

```bash
base64 -i DeveloperID.p12 | pbcopy # now paste into MACOS_CERT_P12_BASE64
```

3. Put the `.p12` password into `MACOS_CERT_PASSWORD`.

## Entitlements

Signing under the hardened runtime uses
`crates/code_assistant/assets/Entitlements.plist`. Because the app embeds gpui
(Zed's UI framework), the entitlements allow JIT / unsigned executable memory,
which the hardened runtime otherwise blocks at launch. The set mirrors the
hardened-runtime keys used by Zed itself and deliberately avoids the weaker keys
(library-validation / dyld-environment / executable-page-protection) that Zed
also leaves off and that Apple's notary service scrutinizes.
80 changes: 73 additions & 7 deletions scripts/bundle-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@
# Build a macOS .app bundle for Code Assistant.
#
# Uses the static Info.plist from assets/ and replaces only the version.
# Only relies on tools that ship with macOS (plutil, codesign).
# Only relies on tools that ship with macOS / Xcode (plutil, codesign, xcrun).
#
# Usage:
# ./scripts/bundle-macos.sh # build for the host arch
# ./scripts/bundle-macos.sh --no-build ARCH # reuse existing binary
#
# ARCH can be: aarch64, x86_64, universal
#
# Code signing & notarization (all optional, via environment variables):
# MACOS_SIGN_IDENTITY Developer ID Application identity to sign with,
# e.g. "Developer ID Application: Jane Doe (TEAMID)".
# If unset, the bundle is ad-hoc signed (as before).
#
# Notarization runs only when ALL of these are set (and signing happened
# with a real identity):
# MACOS_NOTARY_APPLE_ID Apple ID email used for notarization.
# MACOS_NOTARY_PASSWORD App-specific password for that Apple ID.
# MACOS_NOTARY_TEAM_ID Apple Developer Team ID.
#
# Output:
# target/macos-bundle/Code Assistant.app
# target/macos-bundle/Code-Assistant-<version>-<arch>.zip
Expand All @@ -32,7 +43,7 @@ for arg in "$@"; do
aarch64|arm64) ARCH="aarch64" ;;
x86_64|intel) ARCH="x86_64" ;;
universal) ARCH="universal" ;;
-h|--help) sed -n '2,16p' "$0"; exit 0 ;;
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
*) echo "error: unknown argument: $arg" >&2; exit 1 ;;
esac
done
Expand Down Expand Up @@ -134,19 +145,74 @@ printf 'APPL????' > "$CONTENTS/PkgInfo"
touch "$APP_DIR"

# ---------------------------------------------------------------------------
# Ad-hoc code signature
# Code signature
# ---------------------------------------------------------------------------
if command -v codesign >/dev/null 2>&1; then
echo "==> Ad-hoc signing"
# When MACOS_SIGN_IDENTITY is provided we sign with a Developer ID identity,
# enabling the hardened runtime and applying our entitlements (required for
# notarization). Otherwise we fall back to an ad-hoc signature, which is enough
# for local use but cannot be notarized or distributed without Gatekeeper
# warnings.
ENTITLEMENTS="$ASSETS_DIR/Entitlements.plist"

if ! command -v codesign >/dev/null 2>&1; then
echo "==> codesign not available; skipping signature"
elif [[ -n "${MACOS_SIGN_IDENTITY:-}" ]]; then
echo "==> Signing with Developer ID: $MACOS_SIGN_IDENTITY"
# Sign the inner executable first, then the bundle (deep), all under the
# hardened runtime and with a secure timestamp (required by notarization).
codesign --force --timestamp --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$MACOS_SIGN_IDENTITY" \
"$CONTENTS/MacOS/$EXECUTABLE_NAME"
codesign --force --deep --timestamp --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$MACOS_SIGN_IDENTITY" \
"$APP_DIR"
echo "==> Verifying signature"
codesign --verify --deep --strict --verbose=2 "$APP_DIR"
else
echo "==> No MACOS_SIGN_IDENTITY set; ad-hoc signing"
codesign --force --deep --sign - "$APP_DIR" >/dev/null 2>&1 || true
fi

# ---------------------------------------------------------------------------
# Zip for distribution
# ---------------------------------------------------------------------------
ZIP_PATH="$OUT_DIR/Code-Assistant-$VERSION-$ARCH.zip"
rm -f "$ZIP_PATH"
( cd "$OUT_DIR" && zip -qry "$ZIP_PATH" "Code Assistant.app" )
make_zip() {
rm -f "$ZIP_PATH"
# ditto preserves the bundle structure and extended attributes; notarytool
# requires a ditto/zip archive of the .app.
( cd "$OUT_DIR" && ditto -c -k --keepParent "Code Assistant.app" "$ZIP_PATH" )
}
make_zip

# ---------------------------------------------------------------------------
# Notarization + stapling
# ---------------------------------------------------------------------------
# Only attempt notarization when we signed with a real identity and all notary
# credentials are present.
if [[ -n "${MACOS_SIGN_IDENTITY:-}" &&
-n "${MACOS_NOTARY_APPLE_ID:-}" &&
-n "${MACOS_NOTARY_PASSWORD:-}" &&
-n "${MACOS_NOTARY_TEAM_ID:-}" ]]; then
echo "==> Submitting to Apple notary service (this can take a few minutes)"
xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$MACOS_NOTARY_APPLE_ID" \
--password "$MACOS_NOTARY_PASSWORD" \
--team-id "$MACOS_NOTARY_TEAM_ID" \
--wait

echo "==> Stapling notarization ticket to the .app"
xcrun stapler staple "$APP_DIR"
xcrun stapler validate "$APP_DIR"

# Re-zip so the distributed archive contains the stapled bundle.
echo "==> Re-packaging stapled bundle"
make_zip
else
echo "==> Skipping notarization (identity and/or notary credentials not set)"
fi

echo
echo "==> Done:"
Expand Down
Loading