bots-garden/sidekickspublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 8c38c6e · on main · k33g · yesterday
sign-and-notarize-on-macos.md · 211 lines · 11.9 KBmarkdown
Blame HistoryOpen raw

How to sign and notarize Sidekicks for macOS

This guide shows how to sign Sidekicks with a Developer ID certificate and have Apple notarize it, so that it opens on any Mac without Open Anyway. It assumes a paid Apple Developer Program membership of which you are the Account Holder (only that role can create a Developer ID certificate), an Apple Silicon Mac with the project's development tools (Develop the application), and a release built as RELEASING.md describes.

  • Part A is done once (allow 30 minutes).
  • Part B is done for every release, after make release-build, until signing is automated (issue #58). Allow 10 to 20 minutes, mostly waiting for Apple.

Keep a Terminal window open at the root of the repository.

What signing and notarizing do

Step Tool What it brings
Sign codesign with your Developer ID certificate macOS knows who made the app and that it was not changed
Hardened runtime codesign --options runtime required by Apple to notarize
Notarize xcrun notarytool Apple scans the app and issues a "ticket"
Staple xcrun stapler the ticket is attached to the app and the .dmg: Gatekeeper accepts them even offline

The app and the .dmg are signed, notarized and stapled. The .zip holds the stapled app.

Part A — once

A1. Note your Team ID

  1. Open developer.apple.com/account, Membership details.
  2. Note the Team ID (10 characters, e.g. AB12CD34EF).

A2. Create the Developer ID Application certificate

With Xcode (the simplest):

  1. Open Xcode → Settings… → Accounts. If it is not there, click +, choose Apple ID and sign in with the email address and password of your Apple Developer account (the Apple ID, not the Team ID).
  2. Your team shows under the account, with its Team ID: select it, click Manage Certificates….
  3. Click + at the bottom left, then Developer ID Application.

Without Xcode:

  1. Open Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…, give your address, tick Saved to disk. You get a .certSigningRequest file.
  2. On developer.apple.com/account/resources/certificates, choose Developer ID Application, profile G2 Sub-CA, upload the request, then download the certificate.
  3. Double-click the .cer file: it goes to the login keychain.

A3. Check that the certificate can be used

security find-identity -v -p codesigning

A line "Developer ID Application: First Last (AB12CD34EF)" must show. That is your signing identity: copy it exactly, without the quotes.

If it is missing, or shows CSSMERR_TP_NOT_TRUSTED, install the Developer ID - G2 intermediate certificate from apple.com/certificateauthority, then run the command again.

A4. Back up the certificate and its key

The private key only exists on this Mac: without a backup, a failure means revoking the certificate and making a new one.

  1. In Keychain Access, login keychain, My Certificates, unfold Developer ID Application: … (the key shows under it).
  2. Select the certificate, File → Export Items…, format .p12, choose a password.
  3. Keep the .p12 and its password in your password manager. Never put it in the repository.

A5. Create an app-specific password for notarization

  1. Open account.apple.com, Sign-In and Security → App-Specific Passwords.
  2. Create one called sidekicks-notary and copy it (it is not shown again).

A6. Store the notarization profile in the keychain

xcrun notarytool store-credentials sidekicks-notary \
  --apple-id "you@example.com" \
  --team-id "AB12CD34EF"

Paste the app-specific password when asked. Check:

xcrun notarytool history --keychain-profile sidekicks-notary

A list (empty the first time) shows, with no authentication error.

A7. Set the two variables

Add to your ~/.zshrc, then open a new Terminal:

export SIDEKICKS_SIGN_IDENTITY="Developer ID Application: First Last (AB12CD34EF)"
export SIDEKICKS_NOTARY_PROFILE="sidekicks-notary"

Part B uses them; the automation (issue #58) will too.

A8. Keep the app's identifier

CFBundleIdentifier is com.k33g.sidekicks in build/darwin/Info.plist (and Info.dev.plist). Do not change it any more: macOS ties the permissions given to the app to it.

Part B — for every release

First pause syncing if the repository is in a synced folder (kDrive, iCloud Drive, Dropbox…): it changes the files while codesign and stapler rewrite them, which breaks the signatures and gives "corrupted" .dmg files. Everything then happens in a working folder, bin/sign (ignored by git).

B1. Build the release

As usual (RELEASING.md, steps Before and 1):

make release-build                 # or SKIP=windows make release-build for a macOS-only release

dist/ holds the unsigned packages, and bin/sidekicks.app the ad-hoc signed app.

B2. Copy the app to the working folder

W="$PWD/bin/sign"
rm -rf "$W" && mkdir -p "$W"
ditto bin/sidekicks.app "$W/sidekicks.app"

Keep this Terminal open, at the root of the repository: the next steps use $W. If syncing cannot be paused, take W="$(mktemp -d /tmp/sidekicks-sign.XXXXXX)" instead.

B3. Sign the app

xattr -cr "$W/sidekicks.app"
codesign --force --options runtime --timestamp \
  --sign "$SIDEKICKS_SIGN_IDENTITY" "$W/sidekicks.app"
codesign --verify --strict --verbose=2 "$W/sidekicks.app"
codesign -dv --verbose=4 "$W/sidekicks.app" 2>&1 | grep -E "Authority=Developer ID Application|flags=.*runtime|Timestamp="

The last command must show the Developer ID Application authority, the runtime flag and a Timestamp line.

B4. Notarize the app and staple the ticket

ditto -c -k --keepParent "$W/sidekicks.app" "$W/notarize.zip"
xcrun notarytool submit "$W/notarize.zip" --keychain-profile "$SIDEKICKS_NOTARY_PROFILE" --wait
xcrun stapler staple "$W/sidekicks.app"
spctl -a -vvv -t exec "$W/sidekicks.app"

notarytool waits for Apple's answer (a few minutes) and must end with status: Accepted. spctl must say accepted and source=Notarized Developer ID.

B5. Make the .dmg from the stapled app

wails3 tool package --format dmg --name sidekicks --out "$W" \
  --background build/darwin/dmg-background.png \
  --volume-icon build/darwin/icons.icns \
  --file-icon build/darwin/dmg-file-icon.icns \
  --window-width 540 --window-height 380
hdiutil verify "$W/sidekicks.dmg"

The command runs from the root of the repository (for the images) but reads the app and writes the .dmg in $W, with the same layout as make release-build. hdiutil verify must end with checksum … is VALID.

B6. Sign, notarize and staple the .dmg

codesign --force --timestamp --sign "$SIDEKICKS_SIGN_IDENTITY" "$W/sidekicks.dmg"
xcrun notarytool submit "$W/sidekicks.dmg" --keychain-profile "$SIDEKICKS_NOTARY_PROFILE" --wait
xcrun stapler staple "$W/sidekicks.dmg"
hdiutil verify "$W/sidekicks.dmg"
spctl -a -vvv -t open --context context:primary-signature "$W/sidekicks.dmg"
hdiutil attach -nobrowse -readonly "$W/sidekicks.dmg" && hdiutil detach "/Volumes/sidekicks"

spctl must say accepted and source=Notarized Developer ID; the .dmg must mount then unmount without error. If the volume is not called sidekicks, detach the one hdiutil attach printed.

B7. Replace the macOS packages of dist/ and compute the checksums again

DMG=$(scripts/package.sh names macos arm64 | sed -n 1p)   # the .dmg and the .zip, with the version of build/config.yml
ZIP=$(scripts/package.sh names macos arm64 | sed -n 2p)
echo "$W · $DMG · $ZIP"                                    # none of the three may be empty
rm -f dist/sidekicks.dmg                                   # what a failed copy may have left
cp "$W/sidekicks.dmg" "dist/$DMG"
rm -f "dist/$ZIP" && ditto -c -k --keepParent "$W/sidekicks.app" "dist/$ZIP"
cmp "$W/sidekicks.dmg" "dist/$DMG" && echo "identical copy"
spctl -a -vvv -t open --context context:primary-signature "dist/$DMG"   # accepted · Notarized Developer ID
(cd dist && rm -f SHA256SUMS && shasum -a 256 -- * > "$W/SHA256SUMS") && cp "$W/SHA256SUMS" dist/SHA256SUMS
cat dist/SHA256SUMS

Do not go on if cmp reports a difference or spctl refuses: dist/ would still hold the unsigned .dmg of B1, and so would the release.

B8. Try it on another Mac

  1. Copy the .dmg to another Mac (or another user account), through the web or AirDrop: it must carry the quarantine attribute of a real download.
  2. Open it, drag sidekicks to Applications, launch it: it opens directly, without Open Anyway.
  3. Go through what the hardened runtime could hinder: a local agent, an agent in a container or in sbx, the Terminal tab, opening a file, drag and drop, Settings → Open….

B9. Tag and publish

Follow the rest of RELEASING.md (steps 2 to 4): the files to attach are those of dist/. Once the release is published, download its .dmg and check that shasum -a 256 gives the same sum as SHA256SUMS (a truncated upload also gives "corrupted image"). Then delete $W and resume syncing.

Variants

  • A separate macOS release, on a version already published (e.g. v1.0.0-macos, issue #57): do not change info.version (the app stays 1.0.0; Windows refuses a version that is not made of numbers, Apple too for CFBundleShortVersionString, and the packages would be called …-1.0.0-macos-macos-…). Build macOS only with SKIP=windows make release-build, do part B, then create the tag by hand: git tag -a v1.0.0-macos -m "Sidekicks 1.0.0, macOS signed and notarized" && git push origin v1.0.0-macos. The Release workflow starts and stops at its check (the tag is not v + the version of build/config.yml): this is expected. Create the release on Rickub from that tag and attach the .dmg, the .zip and SHA256SUMS. You can also simply replace the macOS files of the v1.0.0 release.
  • Notarization is refused (status: Invalid): xcrun notarytool log <id> --keychain-profile "$SIDEKICKS_NOTARY_PROFILE" gives the reason. The usual ones: a signature without --options runtime or without --timestamp, or an unsigned file in the app.
  • "The disk image is corrupted" when opening the .dmg: it was changed after it was signed (synced folder) or truncated (upload, download). hdiutil verify on the file and shasum -a 256 compared with SHA256SUMS tell which. Do B2 to B7 again with syncing paused (or with $W in /tmp).
  • codesign refuses ("resource fork, Finder information, or similar detritus"): the app comes from a synced folder, which adds metadata. xattr -cr "$W/sidekicks.app" and do B3 again.
  • The signed app does not start, or a feature no longer works: the hardened runtime blocks something. Read Console.app at launch; if one entitlement is missing, add an entitlements file (--entitlements build/darwin/entitlements.plist in B2) with that entitlement only, and note it in issue #57.
  • Check an installed app: spctl -a -vvv /Applications/sidekicks.app and xcrun stapler validate /Applications/sidekicks.app.
  • Another Mac to build on: import the .p12 (double-click), then do A6 and A7 again.
  • The .dmg to make again, the app already notarized: if $W/sidekicks.app is still there and xcrun stapler validate "$W/sidekicks.app" succeeds, go straight to B5.

See also

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# How to sign and notarize Sidekicks for macOS

This guide shows how to sign Sidekicks with a **Developer ID** certificate and have Apple **notarize** it, so that it opens on any Mac without **Open Anyway**. It assumes a paid Apple Developer Program membership of which you are the **Account Holder** (only that role can create a Developer ID certificate), an Apple Silicon Mac with the project's development tools ([Develop the application](develop-the-application.md)), and a release built as [RELEASING.md](../../../RELEASING.md) describes.

- **Part A** is done **once** (allow 30 minutes).
- **Part B** is done **for every release**, after `make release-build`, until signing is automated (issue #58). Allow 10 to 20 minutes, mostly waiting for Apple.

Keep a Terminal window open at the root of the repository.

## What signing and notarizing do

| Step | Tool | What it brings |
| --- | --- | --- |
| Sign | `codesign` with your Developer ID certificate | macOS knows who made the app and that it was not changed |
| Hardened runtime | `codesign --options runtime` | required by Apple to notarize |
| Notarize | `xcrun notarytool` | Apple scans the app and issues a "ticket" |
| Staple | `xcrun stapler` | the ticket is attached to the app and the `.dmg`: Gatekeeper accepts them even offline |

The app **and** the `.dmg` are signed, notarized and stapled. The `.zip` holds the stapled app.

## Part A — once

### A1. Note your Team ID

1. Open [developer.apple.com/account](https://developer.apple.com/account), **Membership details**.
2. Note the **Team ID** (10 characters, e.g. `AB12CD34EF`).

### A2. Create the Developer ID Application certificate

With Xcode (the simplest):

1. Open **Xcode → Settings… → Accounts**. If it is not there, click **+**, choose **Apple ID** and sign in with the email address and password of your Apple Developer account (the Apple ID, not the Team ID).
2. Your team shows under the account, with its Team ID: select it, click **Manage Certificates…**.
3. Click **+** at the bottom left, then **Developer ID Application**.

Without Xcode:

1. Open **Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…**, give your address, tick **Saved to disk**. You get a `.certSigningRequest` file.
2. On [developer.apple.com/account/resources/certificates](https://developer.apple.com/account/resources/certificates/add), choose **Developer ID Application**, profile **G2 Sub-CA**, upload the request, then download the certificate.
3. Double-click the `.cer` file: it goes to the **login** keychain.

### A3. Check that the certificate can be used

```bash
security find-identity -v -p codesigning
```

A line `"Developer ID Application: First Last (AB12CD34EF)"` must show. That is your **signing identity**: copy it exactly, without the quotes.

If it is missing, or shows `CSSMERR_TP_NOT_TRUSTED`, install the **Developer ID - G2** intermediate certificate from [apple.com/certificateauthority](https://www.apple.com/certificateauthority/), then run the command again.

### A4. Back up the certificate and its key

The private key only exists on this Mac: without a backup, a failure means revoking the certificate and making a new one.

1. In **Keychain Access**, **login** keychain, **My Certificates**, unfold **Developer ID Application: …** (the key shows under it).
2. Select the certificate, **File → Export Items…**, format **.p12**, choose a password.
3. Keep the `.p12` and its password in your password manager. Never put it in the repository.

### A5. Create an app-specific password for notarization

1. Open [account.apple.com](https://account.apple.com), **Sign-In and Security → App-Specific Passwords**.
2. Create one called `sidekicks-notary` and copy it (it is not shown again).

### A6. Store the notarization profile in the keychain

```bash
xcrun notarytool store-credentials sidekicks-notary \
  --apple-id "you@example.com" \
  --team-id "AB12CD34EF"
```

Paste the app-specific password when asked. Check:

```bash
xcrun notarytool history --keychain-profile sidekicks-notary
```

A list (empty the first time) shows, with no authentication error.

### A7. Set the two variables

Add to your `~/.zshrc`, then open a new Terminal:

```bash
export SIDEKICKS_SIGN_IDENTITY="Developer ID Application: First Last (AB12CD34EF)"
export SIDEKICKS_NOTARY_PROFILE="sidekicks-notary"
```

Part B uses them; the automation (issue #58) will too.

### A8. Keep the app's identifier

`CFBundleIdentifier` is `com.k33g.sidekicks` in `build/darwin/Info.plist` (and `Info.dev.plist`). Do not change it any more: macOS ties the permissions given to the app to it.

## Part B — for every release

**First pause syncing** if the repository is in a synced folder (kDrive, iCloud Drive, Dropbox…): it changes the files while `codesign` and `stapler` rewrite them, which breaks the signatures and gives "corrupted" `.dmg` files. Everything then happens in a working folder, `bin/sign` (ignored by git).

### B1. Build the release

As usual ([RELEASING.md](../../../RELEASING.md), steps Before and 1):

```bash
make release-build                 # or SKIP=windows make release-build for a macOS-only release
```

`dist/` holds the unsigned packages, and `bin/sidekicks.app` the ad-hoc signed app.

### B2. Copy the app to the working folder

```bash
W="$PWD/bin/sign"
rm -rf "$W" && mkdir -p "$W"
ditto bin/sidekicks.app "$W/sidekicks.app"
```

Keep this Terminal open, at the root of the repository: the next steps use `$W`. If syncing cannot be paused, take `W="$(mktemp -d /tmp/sidekicks-sign.XXXXXX)"` instead.

### B3. Sign the app

```bash
xattr -cr "$W/sidekicks.app"
codesign --force --options runtime --timestamp \
  --sign "$SIDEKICKS_SIGN_IDENTITY" "$W/sidekicks.app"
codesign --verify --strict --verbose=2 "$W/sidekicks.app"
codesign -dv --verbose=4 "$W/sidekicks.app" 2>&1 | grep -E "Authority=Developer ID Application|flags=.*runtime|Timestamp="
```

The last command must show the **Developer ID Application** authority, the **runtime** flag and a **Timestamp** line.

### B4. Notarize the app and staple the ticket

```bash
ditto -c -k --keepParent "$W/sidekicks.app" "$W/notarize.zip"
xcrun notarytool submit "$W/notarize.zip" --keychain-profile "$SIDEKICKS_NOTARY_PROFILE" --wait
xcrun stapler staple "$W/sidekicks.app"
spctl -a -vvv -t exec "$W/sidekicks.app"
```

`notarytool` waits for Apple's answer (a few minutes) and must end with `status: Accepted`. `spctl` must say `accepted` and `source=Notarized Developer ID`.

### B5. Make the `.dmg` from the stapled app

```bash
wails3 tool package --format dmg --name sidekicks --out "$W" \
  --background build/darwin/dmg-background.png \
  --volume-icon build/darwin/icons.icns \
  --file-icon build/darwin/dmg-file-icon.icns \
  --window-width 540 --window-height 380
hdiutil verify "$W/sidekicks.dmg"
```

The command runs from the root of the repository (for the images) but reads the app and writes the `.dmg` in `$W`, with the same layout as `make release-build`. `hdiutil verify` must end with `checksum … is VALID`.

### B6. Sign, notarize and staple the `.dmg`

```bash
codesign --force --timestamp --sign "$SIDEKICKS_SIGN_IDENTITY" "$W/sidekicks.dmg"
xcrun notarytool submit "$W/sidekicks.dmg" --keychain-profile "$SIDEKICKS_NOTARY_PROFILE" --wait
xcrun stapler staple "$W/sidekicks.dmg"
hdiutil verify "$W/sidekicks.dmg"
spctl -a -vvv -t open --context context:primary-signature "$W/sidekicks.dmg"
hdiutil attach -nobrowse -readonly "$W/sidekicks.dmg" && hdiutil detach "/Volumes/sidekicks"
```

`spctl` must say `accepted` and `source=Notarized Developer ID`; the `.dmg` must mount then unmount without error. If the volume is not called `sidekicks`, detach the one `hdiutil attach` printed.

### B7. Replace the macOS packages of `dist/` and compute the checksums again

```bash
DMG=$(scripts/package.sh names macos arm64 | sed -n 1p)   # the .dmg and the .zip, with the version of build/config.yml
ZIP=$(scripts/package.sh names macos arm64 | sed -n 2p)
echo "$W · $DMG · $ZIP"                                    # none of the three may be empty
rm -f dist/sidekicks.dmg                                   # what a failed copy may have left
cp "$W/sidekicks.dmg" "dist/$DMG"
rm -f "dist/$ZIP" && ditto -c -k --keepParent "$W/sidekicks.app" "dist/$ZIP"
cmp "$W/sidekicks.dmg" "dist/$DMG" && echo "identical copy"
spctl -a -vvv -t open --context context:primary-signature "dist/$DMG"   # accepted · Notarized Developer ID
(cd dist && rm -f SHA256SUMS && shasum -a 256 -- * > "$W/SHA256SUMS") && cp "$W/SHA256SUMS" dist/SHA256SUMS
cat dist/SHA256SUMS
```

Do not go on if `cmp` reports a difference or `spctl` refuses: `dist/` would still hold the unsigned `.dmg` of B1, and so would the release.

### B8. Try it on another Mac

1. Copy the `.dmg` to another Mac (or another user account), through the web or AirDrop: it must carry the quarantine attribute of a real download.
2. Open it, drag **sidekicks** to **Applications**, launch it: it opens directly, without **Open Anyway**.
3. Go through what the hardened runtime could hinder: a local agent, an agent in a container or in sbx, the Terminal tab, opening a file, drag and drop, Settings → Open….

### B9. Tag and publish

Follow the rest of [RELEASING.md](../../../RELEASING.md) (steps 2 to 4): the files to attach are those of `dist/`. Once the release is published, download its `.dmg` and check that `shasum -a 256` gives the same sum as `SHA256SUMS` (a truncated upload also gives "corrupted image"). Then delete `$W` and resume syncing.

## Variants

- **A separate macOS release, on a version already published** (e.g. `v1.0.0-macos`, issue #57): **do not change** `info.version` (the app stays `1.0.0`; Windows refuses a version that is not made of numbers, Apple too for `CFBundleShortVersionString`, and the packages would be called `…-1.0.0-macos-macos-…`). Build macOS only with `SKIP=windows make release-build`, do part B, then create the tag by hand: `git tag -a v1.0.0-macos -m "Sidekicks 1.0.0, macOS signed and notarized" && git push origin v1.0.0-macos`. The **Release** workflow starts and stops at its check (the tag is not `v` + the version of `build/config.yml`): this is expected. Create the release on Rickub from that tag and attach the `.dmg`, the `.zip` and `SHA256SUMS`. You can also simply replace the macOS files of the `v1.0.0` release.
- **Notarization is refused** (`status: Invalid`): `xcrun notarytool log <id> --keychain-profile "$SIDEKICKS_NOTARY_PROFILE"` gives the reason. The usual ones: a signature without `--options runtime` or without `--timestamp`, or an unsigned file in the app.
- **"The disk image is corrupted" when opening the `.dmg`**: it was changed after it was signed (synced folder) or truncated (upload, download). `hdiutil verify` on the file and `shasum -a 256` compared with `SHA256SUMS` tell which. Do B2 to B7 again with syncing paused (or with `$W` in `/tmp`).
- **`codesign` refuses ("resource fork, Finder information, or similar detritus")**: the app comes from a synced folder, which adds metadata. `xattr -cr "$W/sidekicks.app"` and do B3 again.
- **The signed app does not start, or a feature no longer works**: the hardened runtime blocks something. Read **Console.app** at launch; if one entitlement is missing, add an entitlements file (`--entitlements build/darwin/entitlements.plist` in B2) with that entitlement only, and note it in issue #57.
- **Check an installed app**: `spctl -a -vvv /Applications/sidekicks.app` and `xcrun stapler validate /Applications/sidekicks.app`.
- **Another Mac to build on**: import the `.p12` (double-click), then do A6 and A7 again.
- **The `.dmg` to make again, the app already notarized**: if `$W/sidekicks.app` is still there and `xcrun stapler validate "$W/sidekicks.app"` succeeds, go straight to B5.

## See also

- [How to publish a release](publish-a-release.md) · [RELEASING.md](../../../RELEASING.md)
- [How to install Sidekicks](install-sidekicks.md)
- Apple: [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)