let's build a macos app without xcode 3: codesigning

Oh boy, now we're getting to the fun stuff: codesigning!

maybe you don't even need to codesign

If you're building a macOS app, you can maybe run your app for development without codesigning. You only really need to codesign if you're using a restricted entitlement, or you're distributing your app to others.

You can also create iOS apps and run them on the simulator without codesigning, but that's for another time.

on with the show

I've done this a few times now but I always forget what I need. Here's the usual journey I go on: I start writing a Mac app. I find an interesting API, and it says that I need an entitlement to use it.

Okay, an entitlement is just a key-value in an Entitlements.plist file, seems easy enough. From last time, we know plist files are specially formatted XML files, and I like to generate them from json using plutil.

# create an example Entitlements.json
cat > Entitlements.json << 'EOF'
  {"com.apple.security.app-sandbox": true}
EOF

# convert it with plutil
plutil -convert xml1 -o Entitlements.plist Entitlements.json

Note that this file does not go in the app bundle, you can put it anywhere else in your source repo or even a tempfile.

So now that we have it, what do we do with Entitlements.plist?

codesign is just a CLI tool

It turns out there's literally a codesign command that comes with the xcode command line tools, and I need to run that on my app bundle with Entitlements.plist as input.

Skipping right to the end, here's the final command I use. I'll explain it below.

codesign --force \
	--options runtime --timestamp \
	--entitlements "path/to/Entitlements.plist" \
	--sign "Apple Development: Ping Wong (XXXXXXXXXX)" \
	path/to/MyApp.app

Really what we're running is codesign <flags> path/to/MyApp.app, where path/to/MyApp.app is the path to the app bundle I want to sign. There are just a handful of flags here.

--force is for convenience - it removes any existing code signature on the app to avoid weird bugs.

--options runtime here enables the Hardened Runtime, from what I can tell it adds restrictions like no JIT and no attaching debuggers. I don't fully understand but I know it's required for distributing apps outside the App Store, so I always leave it on. Within the App Store it's optional but still recommended.

--timestamp ensures that codesign uses a cryptographically secure timestamp to create the code signature. Otherwise you could codesign using an expired certificate by spoofing the time on your computer. This is also required for distributing apps.

--entitlements path/to/Entitlements.plist points codesign to the entitlements I want to use with my app.

And finally, --sign "Apple Development: Ping Wong (XXXXXXXXXX)". This is the interesting and/or annoying part. In order to codesign, we're going to need a certificate.

certificates

Certificates are wrappers around public key cryptography, the same as for SSH keys and GPG keys.

For local development, you have three options

  • ad hoc signing without a certificate (--sign "-")
  • a self-signed certificate
  • an Apple Development certificate.

I use a regular Apple Development certificate, as the other two options don't work in all cases. The Apple Development certificate is sort of free - you can get it if you sign into Xcode with a standard, free Apple ID, which creates a Personal Team signing certificate.

One specific thing that's inconvenient about ad-hoc signing is that if your app requests permissions (such as the microphone or accessibility features), without a real certificate you'll have to delete and re-grant the permissions anew everytime you rebuild your app. This stumped me for a good minute because there's no obvious error message.

Anyway, if you have an Apple Developer Program membership, here's where you need to go to create a certificate: https://developer.apple.com/account/resources/certificates/list. Click the + and then make sure to select exactly Apple Development. There's a lot of different kinds of certificates so make sure you pick the right one. After you've created the certificate, download it and double click it to install it on your system.

If you want a self-signed certificate, check out Apple's codesigning guide, which has good instructions for both obtaining a self-signed certificate and obtaining Apple Development certificates.

Regardless of how you obtain a certificate, once it's installed on your machine you can run security find-identity -p codesigning and see that it's installed correctly, and grab the name of the cert (you'll see it in quotes).

debugging tips

If you're running into problems starting your app after codesigning, especially with --sign "-" (ad-hoc) or a self signed certificate, this snippet might help you to figure out what's happening. Assuming your app crashes, this will immediately checks the system log for code signature related problems (AMFI stands for AppleMobileFileIntegrity):

open MyApp.app; echo; log show --last 1s --info --style json \
    --predicate 'eventMessage contains "AMFI"' \
  | jq -r ".[] | .eventMessage"

When I run it against an app with a bad signature, I get these two chunks of output:

First, from open MyApp.app I get this cryptic error message:

The application cannot be opened for an unexpected reason, error=Error Domain=RBSRequestErrorDomain Code=5 "Launch failed." UserInfo={NSLocalizedFailureReason=Launch failed., NSUnderlyingError=0x600001831080 {Error Domain=NSPOSIXErrorDomain Code=153 "Unknown error: 153" UserInfo={NSLocalizedDescription=Launchd job spawn failed}}}

And then from log show I get the real error: entitlements which failed with my ad-hoc signature.

AMFI: code signature validation failed.
AMFI: bailing out because of restricted entitlements.
AMFI: When validating $MyApp.app:
  Code has restricted entitlements, but the validation of its code signature failed.
Unsatisfied Entitlements: com.apple.developer.team-identifierkeychain-access-groups
AMFI: hook..execve() killing xpcproxy (pid 51793): Attempt to execute completely unsigned code (must be at least ad-hoc signed).

references and further reading