Skip to content

Commit b03f91e

Browse files
committed
Distribution: Prepare Apple signing
- Add Apple code signing and notarization to release workflow - Untested
1 parent 60063ec commit b03f91e

3 files changed

Lines changed: 293 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,47 @@ jobs:
3434
- name: Install x86_64 target for universal binary
3535
run: rustup target add x86_64-apple-darwin
3636

37+
- name: Import Apple certificate
38+
env:
39+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
40+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
41+
run: |
42+
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
43+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
44+
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
45+
46+
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
47+
48+
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
49+
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
50+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
51+
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" \
52+
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
53+
security set-key-partition-list -S apple-tool:,apple: \
54+
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
55+
security list-keychain -d user -s $KEYCHAIN_PATH
56+
57+
- name: Set up notarization credentials
58+
env:
59+
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
60+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
61+
run: |
62+
mkdir -p ~/private_keys
63+
echo -n "$APPLE_API_KEY_BASE64" | base64 --decode \
64+
> ~/private_keys/AuthKey_${APPLE_API_KEY}.p8
65+
3766
- name: Build and release
3867
uses: tauri-apps/tauri-action@73fb865345c54760d875b94642314f8c0c894afa # v0
3968
env:
4069
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4170
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
4271
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
72+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
73+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
74+
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
75+
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
76+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
77+
APPLE_API_KEY_PATH: ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
4378
with:
4479
projectPath: ./apps/desktop
4580
tagName: ${{ github.ref_name }}
@@ -144,3 +179,7 @@ jobs:
144179
-H "Content-Type: application/json" \
145180
-H "X-Hub-Signature-256: sha256=$SIGNATURE" \
146181
-d "$PAYLOAD"
182+
183+
- name: Clean up keychain
184+
if: always()
185+
run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
],
5656
"macOS": {
5757
"bundleName": "Cmdr",
58+
"signingIdentity": "Developer ID Application: Rymdskottkarra AB (83H6YAQMNP)",
5859
"entitlements": "./Entitlements.plist"
5960
}
6061
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)