|
| 1 | +# Apple code signing and notarization |
| 2 | + |
| 3 | +Signs Cmdr with a Developer ID Application certificate and notarizes it with Apple, so users don't see |
| 4 | +the "unidentified developer" Gatekeeper warning. Direct distribution only (not App Store). |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +- Cmdr currently builds an unsigned universal macOS binary in CI (`release.yml`) |
| 9 | +- The Tauri updater signing (`TAURI_SIGNING_PRIVATE_KEY`) is already set up — that's a separate thing from Apple signing |
| 10 | +- `Entitlements.plist` already has the right entitlements for hardened runtime |
| 11 | +- Apple requires both code signing AND notarization for Gatekeeper to pass without warnings |
| 12 | + |
| 13 | +## Phase 1: Create the Developer ID Application certificate |
| 14 | + |
| 15 | +You need two things: a Certificate Signing Request (CSR) from your Mac, and the certificate itself from Apple. |
| 16 | + |
| 17 | +### 1.1. Generate the CSR |
| 18 | + |
| 19 | +- [x] Open **Keychain Access** (Spotlight → "Keychain Access") |
| 20 | +- [x] In the left sidebar, select the **login** keychain (this is where the private key will be created) |
| 21 | +- [x] Menu bar → **Keychain Access** → **Certificate Assistant** → **Request a Certificate From a Certificate Authority...** |
| 22 | +- [x] Fill in: |
| 23 | + - **User Email Address**: your Apple ID email |
| 24 | + - **Common Name**: your full name |
| 25 | + - **CA Email Address**: leave blank |
| 26 | + - **Request is**: select **Saved to disk** |
| 27 | + - **Let me specify key pair information**: leave unchecked (the defaults — 2048-bit RSA — are what Apple expects) |
| 28 | +- [x] Click **Continue**, save the `.certSigningRequest` file somewhere (for example, Desktop) |
| 29 | + |
| 30 | +### 1.2. Create the certificate on Apple Developer portal |
| 31 | + |
| 32 | +- [x] Go to https://developer.apple.com/account/resources/certificates/list |
| 33 | +- [x] Click the **+** button (top left, next to "Certificates") |
| 34 | +- [x] Under **Software**, select **Developer ID Application** → click **Continue** |
| 35 | +- [x] For **Profile Type**, select **G2 Sub-CA** → click **Continue** |
| 36 | +- [x] Click **Choose File**, select the `.certSigningRequest` from the previous step → click **Continue** |
| 37 | +- [x] Click **Download** — saves a `developerID_application.cer` file |
| 38 | +- [x] Double-click the `.cer` file to install it into your keychain |
| 39 | +- [x] Install Apple's intermediate certificate — download and double-click: |
| 40 | + https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer |
| 41 | + (Without this, `security find-identity` won't find the certificate.) |
| 42 | + |
| 43 | +### 1.3. Import the certificate into the login keychain |
| 44 | + |
| 45 | +The `.cer` double-click installs the certificate into the **System** keychain, but the private key from |
| 46 | +the CSR step lives in the **login** keychain. They need to be in the same keychain to pair up for .p12 |
| 47 | +export. Keychain Access doesn't support drag-and-drop between keychains, so use Terminal instead: |
| 48 | + |
| 49 | +- [x] Import the `.cer` into the login keychain (adjust the path to where you saved it): |
| 50 | + ```sh |
| 51 | + security import ~/Downloads/developerID_application.cer -k ~/Library/Keychains/login.keychain-db |
| 52 | + ``` |
| 53 | +- [x] Verify: in Keychain Access, go to **login** → **My Certificates** — you should see |
| 54 | + **Developer ID Application: Rymdskottkarra AB (83H6YAQMNP)** with a disclosure triangle that expands |
| 55 | + to show the private key |
| 56 | + |
| 57 | +### 1.4. Verify it's installed |
| 58 | + |
| 59 | +- [x] Run in Terminal: |
| 60 | + ```sh |
| 61 | + security find-identity -v -p codesigning |
| 62 | + ``` |
| 63 | + You should see a line like: |
| 64 | + ``` |
| 65 | + 1) ABC123DEF456... "Developer ID Application: Rymdskottkarra AB (83H6YAQMNP)" |
| 66 | + ``` |
| 67 | + Copy that full string in quotes — you'll need it later as the **signing identity**. |
| 68 | + |
| 69 | +### 1.5. Export as .p12 for CI |
| 70 | + |
| 71 | +- [x] Open **Keychain Access** |
| 72 | +- [x] In the left sidebar, select **login** keychain, then **My Certificates** category |
| 73 | +- [x] Find **Developer ID Application: Rymdskottkarra AB (83H6YAQMNP)** — expand it to verify it has a private key |
| 74 | +- [x] Click the certificate row to select it, then menu bar → **File** → **Export Items...** (or `⇧⌘E`) |
| 75 | +- [x] Format: **Personal Information Exchange (.p12)**, save as `developer-id-application.p12` |
| 76 | +- [x] Set a strong password when prompted — you'll need this as `APPLE_CERTIFICATE_PASSWORD` |
| 77 | +- [x] Base64-encode it: |
| 78 | + ```sh |
| 79 | + base64 -i developer-id-application.p12 | pbcopy |
| 80 | + ``` |
| 81 | + This is now in your clipboard — you'll paste it as `APPLE_CERTIFICATE` in GitHub Secrets. |
| 82 | +- [x] Delete the `.p12` and `.certSigningRequest` files from disk after you've added the secrets (Phase 3) |
| 83 | + |
| 84 | +## Phase 2: Create the App Store Connect API key (for notarization) |
| 85 | + |
| 86 | +The API key approach is better than Apple ID: no MFA issues, no app-specific passwords to rotate, |
| 87 | +works reliably in CI. |
| 88 | + |
| 89 | +### 2.1. Generate the key |
| 90 | + |
| 91 | +- [x] Go to https://appstoreconnect.apple.com/access/integrations/api |
| 92 | +- [x] If prompted, accept the new terms |
| 93 | +- [x] Under **Team Keys**, click **Generate API Key** |
| 94 | +- [x] **Name**: `Cmdr CI Notarization` (or whatever you want), **Access**: **Developer** → click **Generate** |
| 95 | + |
| 96 | +### 2.2. Save the credentials |
| 97 | + |
| 98 | +- [x] Note the **Issuer ID** shown at the top of the page (for example, `abcd1234-abcd-1234-abcd-abcd1234abcd`) — this is `APPLE_API_ISSUER` |
| 99 | +- [x] Note the **Key ID** in the table row (for example, `A1B2C3D4E5`) — this is `APPLE_API_KEY` |
| 100 | +- [x] Click **Download API Key** — saves `AuthKey_A1B2C3D4E5.p8`. **You can only download this once.** |
| 101 | +- [x] Base64-encode it: |
| 102 | + ```sh |
| 103 | + base64 -i ~/Downloads/AuthKey_A1B2C3D4E5.p8 | pbcopy |
| 104 | + ``` |
| 105 | + This is now in your clipboard — paste it as `APPLE_API_KEY_BASE64` in GitHub Secrets. |
| 106 | +- [x] Store the original `.p8` file somewhere safe (for example, 1Password). Delete from Downloads after. |
| 107 | + |
| 108 | +## Phase 3: Add GitHub secrets |
| 109 | + |
| 110 | +### 3.1. Add the six secrets |
| 111 | + |
| 112 | +Go to https://github.com/vdavid/cmdr/settings/secrets/actions and add these: |
| 113 | + |
| 114 | +- [x] `APPLE_CERTIFICATE` — base64-encoded `.p12` (from 1.5) |
| 115 | +- [x] `APPLE_CERTIFICATE_PASSWORD` — the password you set when exporting the `.p12` |
| 116 | +- [x] `APPLE_SIGNING_IDENTITY` — the full string from `security find-identity`, i.e. `Developer ID Application: Rymdskottkarra AB (83H6YAQMNP)` |
| 117 | +- [x] `APPLE_API_ISSUER` — Issuer ID from App Store Connect (2.2) |
| 118 | +- [x] `APPLE_API_KEY` — Key ID from App Store Connect (2.2) |
| 119 | +- [x] `APPLE_API_KEY_BASE64` — base64-encoded `.p8` file (2.2) |
| 120 | + |
| 121 | +## Phase 4: Update `tauri.conf.json` |
| 122 | + |
| 123 | +### Add signing identity |
| 124 | + |
| 125 | +- [x] Add `signingIdentity` to the existing `bundle.macOS` section so local builds auto-sign |
| 126 | + when the cert is in your keychain: |
| 127 | + ```jsonc |
| 128 | + "macOS": { |
| 129 | + "signingIdentity": "Developer ID Application: Rymdskottkarra AB (83H6YAQMNP)", |
| 130 | + // ... existing keys |
| 131 | + } |
| 132 | + ``` |
| 133 | + |
| 134 | +## Phase 5: Update `release.yml` |
| 135 | + |
| 136 | +Three changes: import the certificate, set up notarization credentials, and clean up. |
| 137 | + |
| 138 | +### 5.1. Add certificate import step |
| 139 | + |
| 140 | +Add this **before** the existing "Build and release" step: |
| 141 | + |
| 142 | +- [x] Add **certificate import** step: |
| 143 | + ```yaml |
| 144 | + - name: Import Apple certificate |
| 145 | + env: |
| 146 | + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} |
| 147 | + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |
| 148 | + run: | |
| 149 | + CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 |
| 150 | + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db |
| 151 | + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) |
| 152 | +
|
| 153 | + echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH |
| 154 | +
|
| 155 | + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH |
| 156 | + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH |
| 157 | + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH |
| 158 | + security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" \ |
| 159 | + -A -t cert -f pkcs12 -k $KEYCHAIN_PATH |
| 160 | + security set-key-partition-list -S apple-tool:,apple: \ |
| 161 | + -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH |
| 162 | + security list-keychain -d user -s $KEYCHAIN_PATH |
| 163 | + ``` |
| 164 | +
|
| 165 | +### 5.2. Add notarization credentials step |
| 166 | +
|
| 167 | +Add this right after the certificate import step: |
| 168 | +
|
| 169 | +- [x] Add **notarization credentials** step: |
| 170 | + ```yaml |
| 171 | + - name: Set up notarization credentials |
| 172 | + env: |
| 173 | + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} |
| 174 | + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} |
| 175 | + run: | |
| 176 | + mkdir -p ~/private_keys |
| 177 | + echo -n "$APPLE_API_KEY_BASE64" | base64 --decode \ |
| 178 | + > ~/private_keys/AuthKey_${APPLE_API_KEY}.p8 |
| 179 | + ``` |
| 180 | +
|
| 181 | +### 5.3. Update the "Build and release" step |
| 182 | +
|
| 183 | +- [x] Add the Apple env vars to the existing step: |
| 184 | + ```yaml |
| 185 | + - name: Build and release |
| 186 | + uses: tauri-apps/tauri-action@73fb865345c54760d875b94642314f8c0c894afa # v0 |
| 187 | + env: |
| 188 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 189 | + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} |
| 190 | + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} |
| 191 | + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} |
| 192 | + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |
| 193 | + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} |
| 194 | + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} |
| 195 | + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} |
| 196 | + APPLE_API_KEY_PATH: ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8 |
| 197 | + with: |
| 198 | + projectPath: ./apps/desktop |
| 199 | + tagName: ${{ github.ref_name }} |
| 200 | + releaseName: 'Cmdr ${{ github.ref_name }}' |
| 201 | + releaseBody: 'See CHANGELOG.md for details.' |
| 202 | + releaseDraft: false |
| 203 | + prerelease: false |
| 204 | + args: --target universal-apple-darwin |
| 205 | + ``` |
| 206 | +
|
| 207 | +### 5.4. Add keychain cleanup step |
| 208 | +
|
| 209 | +- [x] Add this at the very end (after "Trigger website deploy"): |
| 210 | + ```yaml |
| 211 | + - name: Clean up keychain |
| 212 | + if: always() |
| 213 | + run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db |
| 214 | + ``` |
| 215 | +
|
| 216 | +## Phase 6: Test locally |
| 217 | +
|
| 218 | +### 6.1. Build and verify |
| 219 | +
|
| 220 | +- [ ] Build locally: |
| 221 | + ```sh |
| 222 | + cd apps/desktop |
| 223 | + pnpm build -- --target universal-apple-darwin |
| 224 | + ``` |
| 225 | +- [ ] Verify signing: |
| 226 | + ```sh |
| 227 | + codesign -dvv apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle/macos/Cmdr.app |
| 228 | + ``` |
| 229 | + You should see your signing identity and `Authority=Developer ID Application: ...` in the output. |
| 230 | + If it says `ad-hoc` or has no identity, the certificate isn't in your keychain or the `signingIdentity` |
| 231 | + in `tauri.conf.json` doesn't match. |
| 232 | + |
| 233 | +## Phase 7: Test in CI |
| 234 | + |
| 235 | +### 7.1. Trigger a test release |
| 236 | + |
| 237 | +- [ ] Push the `tauri.conf.json` and `release.yml` changes to a branch |
| 238 | +- [ ] Create a test tag: `git tag v0.5.0-signing-test && git push origin v0.5.0-signing-test` |
| 239 | +- [ ] Watch the release workflow — the "Build and release" step should now include signing and notarization |
| 240 | + output (notarization typically takes 2-5 minutes, sometimes up to 15-20) |
| 241 | + |
| 242 | +### 7.2. Verify the build |
| 243 | + |
| 244 | +- [ ] Download the built `.dmg` from the GitHub release, open it on a Mac, verify no Gatekeeper warning |
| 245 | +- [ ] Also verify with: `spctl --assess --type execute -v Cmdr.app` — should say `accepted` |
| 246 | + |
| 247 | +### 7.3. Clean up |
| 248 | + |
| 249 | +- [ ] Delete the test tag and release: |
| 250 | + ```sh |
| 251 | + git tag -d v0.5.0-signing-test && git push origin :v0.5.0-signing-test |
| 252 | + ``` |
| 253 | + Then delete the GitHub release from the UI |
0 commit comments