What is notarization?
Notarization is an automated system created by Apple that checks your macOS software for malicious content and code signing issues. It is not a manual app review such as those performed by reviewers for the Mac and iOS App Stores. The purpose of notarization is to give end users more confidence that your software has been approved by Apple. For apps distributed through the Mac App Store, this is done automatically as part of the App Store submission process. However, for developers who wish to distribute their Mac software outside the Mac App Store, they will need to have their software notarized before it can run on macOS Catalina.
While you can easily archive, export, and notarize your app manually using the Xcode Organizer, automating the process offers a more flexible and streamlined approach to software distribution.
What happens if I don't notarize?
If you don't notarize your app, then when users download it they will be greeted by a message from Gatekeeper saying that Apple could not check the software for malicious content, and there will be no immediately obvious way for the user to open your app. To open your app, they will need to either 1) right click on the application, which opens a menu with an option to open it anyways, or 2) they will need to explicitly go to their Security settings and allow the app to open. However, nothing in Gatekeeper's message indicates either of these are an option, so most users will just not be able to use your app.
Requirements
- Xcode 10 or higher is required for notarization. If you have multiple versions of Xcode installed, use the xcode-select command to ensure you are using Xcode 10.0 or higher before notarizing. You
can notarize apps that were built with < Xcode 10.0, but you must use Xcode 10 or higher to perform the actual notarization.
- Your app must have the hardened runtime enabled (Xcode Build Settings -> Enable hardened runtime) to be notarized.
- Your app must be signed with a Developer ID certificate. (
source)
Archive
To notarize your app, first you will have to create your final deliverable, such as a dmg or pkg. This is the final file that you will send to Apple for notarization. This demo will cover the entire process, from building the app to finishing notarization. To get started, first we will need to archive our app:
xcodebuild archive \
-project YourProject.xcodeproj \
-scheme "YourScheme" \
-configuration Release \
-archivePath archive/result.xcarchive
This is a simple xcodebuild command which will build and archive your app for the Release configuration, outputting the xcarchive as result.xcarchive in a folder named "archive".
Export
Next, lets export the archive as a .app executable:
xcodebuild archive \
-archivePath archive/result.xcarchive \
-exportArchive \
-exportOptionsPlist exportOptions.plist \
-exportPath yourFolder
This exports the archive as a .app executable. You pass in the path to your export options plist file to the "exportOptionsPlist" argument. At a minimum, your plist needs to include what kind of export method you will be using. Since we are distributing outside the Mac App Store, our value for the "method" key in the plist will be "developer-id". If you are not using Xcode's automatic code signing, you will also need to pass in additional keys/values identifying your signing certificates and provisioning profiles. Run "xcodebuild -help" to get a full list of all available options for "exportOptionsPlist".
Lastly, the "exportPath" is the folder to export the .app to.
Create DMG
While you can run a .app executable, most apps are distributed as a dmg or pkg. In this example we will create a dmg as our final deliverable to send to Apple for notarization. The following command will create a dmg:
hdiutil create -format UDZO -srcfolder yourFolder YourApp.dmg
"yourFolder" is the name of the folder you would like to compress into a dmg. For the purposes of this script, it should be the same as the "-exportPath" in the step above. "YourApp" is the name you would like the newly created dmg to have.
While outside the scope of this article, UDZO is a common format for disk images.
Upload to Apple
To send your final deliverable to Apple for Notarization, run the following:
requestInfo=$(xcrun altool --notarize-app \
--file "YourApp.dmg" \
--username "yourDeveloperAccountEmail@email.com" \
--password "@keychain:notarization-password" \
--asc-provider "yourAppleTeamID" \
--primary-bundle-id "com.your.app.bundle.id")
So, a few things to note:
- As expected, we pass our dmg to the "--file" argument.
- We run the command and save the output to the $requestInfo variable. This is because the command will return an identifier that we can use to query the status of our app's notarization.
- The password is an app-specific password that I have saved in my keychain as a password item called "notarization-password".
On the client-side of the notarization process, we first send the deliverable to Apple for notarization. However, Apple does not tell us when the notarization is complete. To know the status of the notarization, we have to continually query Apple and read their response.
Reading the $requestInfo response
If you have filled in all the necessary information above correctly, the $requestInfo should return text like this (except with your own value for RequestUUID):
No errors uploading 'YourApp.dmg'. RequestUUID = ghg99vjrghefe0-tjthty9e-42hv9-a76fiw-76oiauhfgibejrec
We need to retrieve the value of the RequestUUID, and we can then use it to query Apple for our app's notarization status.
Create a small ruby script called uuid.rb, and add this single line to it:
puts "#{ARGV[0].split('= ')[1]}"
This will get the first argument passed to the script, split it into an array of strings using "= " as the separating character, and return the second item in the array.
Now, we can easily retrieve the UUID with just a one line command:
uuid=$(ruby uuid.rb "$requestInfo")
Query notarization status
When notarizing manually from Xcode, Apple will send you a push notification to let you know when your notarization is finished. But for the command line, we will have to continually query until we detect that the process has finished.
Here is how we'll do it:
currentStatus = "in progress"
while [[ "$currentStatus" == "in progress" ]]; do
sleep 15
statusResponse=$(xcrun altool --notarization-info "$uuid" \
--username "yourDeveloperAccountEmail@email.com" \
--password "@keychain:notarization-password")
done
We query Apple for the notarization's status every 15 seconds, passing in our notarization's unique id for the --notarization-info parameter, our developer account email, and our app-specific password as we did above. Save the response to a variable named statusResponse.
So now we need to parse the $statusResponse and retrieve the actual status from it, so that we can determine when to break out of the loop and exit. For reference, the value of $statusResponse is a multiline string, with each line having its own piece of data, like so:
Date: 2020-05-29 18:18:35 +0000
Hash: 87bgjkhbca1767bc7liuhjyghfbc7beg7wuyebf9
LogFileURL: https://osxapps-ssl.itunes.apple.com/itunes-assets.....
RequestUUID: udhe-8enfefi-55t8dfgi-38hn-mnud7
Status: in progress
Status Code: 0
So our ruby script needs to parse this text to get the "in progress" text. Create a ruby file, status.rb, and copy the following script to it:
# the response is a multiline string, with the status being on its own line
# using the format "Status: <status here>"
# Split each line into its own object in an array
response_objects = ARGV[0].split("\n")
# get line that contains the "Status:" text
status_line = response_objects.select { |data| data.include?('Status:') }[0]
# get text describing the status (should be either "in progress" or "success")
current_status = "#{status_line.split('Status: ').last}"
# respond with value
puts current_status
Now, alter the aforementioned loop to read the following:
current_status = "in progress"
while [[ "$currentStatus" == "in progress" ]]; do
sleep 15
statusResponse=$(xcrun altool --notarization-info "$uuid" \
--username "yourDeveloperAccountEmail@email.com" \
--password "@keychain:notarization-password")
current_status=$(ruby status.rb "$statusResponse")
done
if [[ "$current_status" == "success" ]]; then
# staple notarization here
# distribute your notarized software
else
echo "Error! The status was $current_status. There were errors. Please check the LogFileURL for error descriptions"
exit 1
fi
Staple the notarization ticket
The process of attaching the notarization to our app deliverable is called "stapling". When you download an application, Gatekeeper will check Apple's servers to detect if the application has been notarized. However, not all users may have an internet connection when they open your app for the first time. If they don't, and you have not attached the notarization ticket to your app, then Gatekeeper's notarization check will fail. To staple the notarization to your app, simply run:
xcrun stapler staple "YourApp.dmg"
It is worth noting that you cannot staple a zip file directly. Instead, you will need to first unzip its contents, run the above stapler command for each item in the zip file, and then create a new zip file with all stapled items.
LogFileURL
If there is an error, be sure to checkout the LogFileURL that is in the $statusResponse. It contains useful information and can help you pinpoint exactly what went wrong during the notarization process. Also, try running the notarization commands using the --verbose flag for more detailed output.