Dec 13, 2018

Mac app notarization from the command line

Apple announced that early next year macOS will start highlighting notarization status more prominently. This is great, because currently the difference between Developer ID and notarized Developer ID apps is just a short sentence in a Gatekeeper dialog. Also notarization, which is optional today, will be required by future versions of macOS. The new documentation is very detailed and surprisingly precise about resolving common issues, but does not describe how to automate an asynchronous notarization process.

In this article we will create a Makefile for notarization, as I prefer make to Bash scripts.

Build an archive using a Xcode bot

As an example, I’m going to notarize a NewMacApp introduced in the article about distributing Mac apps. For a more production-like experience, we will use a Xcode bot.

And let’s start with the configuration. Make sure that the Do Not Export Product item is selected in the Archive popup:

An Xcode bot panel with configuration for a Mac app prepared for notarization

Xcode bot configuration for the NewMacApp

To sign the final product, we will use a custom ExportOptions.plist, but from the command line.

In the Signing Options we can disable automatic certificate and profile management by the Xcode Server:

An Xcode bot panel with signing options for a Mac app prepared for notarization

Xcode bot signing options for the NewMacApp

For public distribution, a profile is not necessary, and we will access the certificate directly from the command line.

If you run a new integration now, the bot should create an Xcode archive.

Enable a Hardened Runtime

In order to be notarized, a Mac app must have a Hardened Runtime enabled. Sandboxing is not required (thanks to Matt Massicotte for correction). Jeff Johnson perfectly describes the difference between the two, I highly recommend his article.

So let’s enable both the App Sandbox the Hardened Runtime in the Capabilities tab of our NewMacApp project:

An Xcode settings panel with a Hardened Runtime enabled

Xcode Capabilities tab with enabled App Sandbox and Hardened Runtime

Don’t forget to commit the changes, to make them available by the next integration.

Export an archive using a Developer ID

An archive built by our Xcode bot is signed for development, but we need the Mac app signature to include the Developer ID. We can sign the app using xcodebuild -exportArchive which requires a property list with export options. So create a file named ExportOptions.plist in the root project folder:

<?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>
	<key>destination</key>
	<string>export</string>
	<key>method</key>
	<string>developer-id</string>
	<key>signingCertificate</key>
	<string></string>
	<key>signingStyle</key>
	<string>manual</string>
	<key>teamID</key>
	<string></string>
</dict>
</plist>

Please note that we leave an empty certificate UUID and a team ID for the git repository. An up-to-date certificate stored in your Keychain can be accessed using a command line utility named security. Here begins our Makefile. A standard utility named plutil can be used to fill both placeholders:

.PHONY: notarize

SIGNING_CERTIFICATE := $(shell security find-certificate -Z -c "Developer ID Application:" | grep "SHA-1" | awk 'NF { print $$NF }')
TEAM_ID := $(shell security find-certificate -c "Developer ID Application:" | grep "alis" | awk 'NF { print $$NF }' | tr -d \(\)\")

notarize:	
  /usr/bin/plutil -replace signingCertificate -string $(SIGNING_CERTIFICATE) ExportOptions.plist
  /usr/bin/plutil -replace teamID -string $(TEAM_ID) ExportOptions.plist

And now, if the Xcode bot runs a command make notarize from the project folder, the ExportOptions.plist would be filled with the correct UUID and the Team ID necessary for the export:

$ xcrun xcodebuild -exportArchive -archivePath $(XCS_ARCHIVE) -exportPath $(EXPORT_PATH) -exportOptionsPlist ./ExportOptions.plist -configuration Release

Send a zipped Mac app bundle for notarization

Once we have the Mac app signed using our Developer ID certificate, the bundle can be archived as a .zip file and then sent to Apple for notarization using a new command line utility from the Xcode toolchain named altool:

$ ditto -c -k --keepParent $(BUNDLE_APP) $(BUNDLE_ZIP)
$ xcrun altool --notarize-app --primary-bundle-id "app.nativeconnect.NewMacApp.zip" -u $(DEVELOPER_USERNAME) -p $(DEVELOPER_PASSWORD) -f $(BUNDLE_ZIP) --output-format xml > $(UPLOAD_INFO_PLIST)

Some notable things from the last script:

  1. We are ‘zipping’ and submitting only the bundle NewMacApp.app, not the whole Xcode archive.
  2. Our dSYM files with debug symbols are not needed, a notarization service expects only the application binary and embedded frameworks or libraries.
  3. The primary bundle ID is like a follow-up number. It does not need to match the application bundle ID and will be included into a notification email.
  4. The altool will generate useful output, and we’re opting-in for XML to generate that output as a property list, more convenient for automation.
  5. Apple ID credentials are needed to upload the app for notarization. We don’t provide them directly however, and instead use environment variables.

Provide credentials using the local Keychain

The altool requires our username and password for authentication. First, let’s define two environment variables in our Xcode bot Arguments panel, the DEVELOPER_USERNAME and the DEVELOPER_PASSWORD:

An Xcode bot panel with arguments for a Mac app prepared for notarization

Xcode bot arguments for the NewMacApp

Note a special prefix @keychain: before the item named XCODE. So second, let’s create a special item with our real password in the Keychain:

A Keychain password item with Apple ID credentials for a Mac app notarization

Keychain password item with Apple ID and password

If we run a modified Makefile now, the Xcode bot would ask for a permission to access the local Keychain and send our Mac app for notarization.

Wait while the Mac app is notarized by Apple

A property list that we saved above can be used to retrieve an up-to-date information about the submission. In particular, we are interested in the so called Request UUID provided by Apple. The altool is able to fetch the latest status of the upload using this identifier.

We should check the current status of the upload until it changes. Let’s create a helper function in the Makefile:

define wait_for_notarization
  while true; do \
    /usr/bin/xcrun altool --notarization-info `/usr/libexec/PlistBuddy -c "Print :notarization-upload:RequestUUID" $(UPLOAD_INFO_PLIST)` -u $(DEVELOPER_USERNAME) -p $(DEVELOPER_PASSWORD) --output-format xml > $(REQUEST_INFO_PLIST) ;\
    if [ `/usr/libexec/PlistBuddy -c "Print :notarization-info:Status" $(REQUEST_INFO_PLIST)` != "in progress" ]; then \
      break ;\
    fi ;\
    sleep 60 ;\
  done
endef

In the script above, we look at the reported Status every minute, while its value is equal to in progress. Eventually the status will change, and the function will break. Before stapling the Mac app with a notarization ticket, the upload status should become success:

if [ `/usr/libexec/PlistBuddy -c "Print :notarization-info:Status" $(REQUEST_INFO_PLIST)` != "success" ]; then \
  false; \
fi

Finally, our Mac app is checked by Apple and ready for distribution. There is just one more thing — we can attach the notarization ticket to the binary itself, so that it becomes available offline:

$ xcrun stapler staple $(BUNDLE_APP)

Done. The bundle report should look something like this, if you check it using a fantastic tool named RB App Checker Lite:

Evaluating the application “NewMacApp”.

The application was signed by “Apple Root CA”, “Developer ID Application: Vadim Shpakovski (1ABCDE2F3G)”.
	Both the verified timestamp and the signing-time are: Dec 12, 2018 at 01:02:03.
	The object code format is “app bundle with Mach-O thin (x86_64)”.
	The signature contains the Team ID “1ABCDE2F3G”.
	Both bundle and signing identifiers are “app.nativeconnect.NewMacApp”.
	The signature specifies explicit requirements. 
		The requirements specify the Team ID “1ABCDE2F3G”.
			This matches the Team ID contained in the signature.
	Gatekeeper assessment: PASS (Notarized Developer ID). 
	Requirements and resources validate correctly.

The code signature has the UUID “12E5297C-DAEB-7F0E-AEA5-1FEF8127ADF0”.
	Executable code for x86_64 has the UUID “A8A4D331-4171-37A2-86F6-1C0D36D6F70E”.

Conclusion

From my experience, the whole notarization process usually takes 3-5 minutes and does not depend on the bundle size. I uploaded a full version of the Makefile to the GitHub. And you can adopt it for another workflow, if you do not use Xcode bots for continuous distribution for example.

Notarize your apps, good luck!