Notarizing Disk Images for Developer ID Distribution

Distributing macOS apps within .zip files nowadays is no longer a good idea. One issue is app translocation. Another issue is the mandatory notarization starting from macOS 10.15 Catalina.

App translocation goes into effect when the user downloads a .zip file containing an application, extracts it, and runs it directly without moving it anywhere using the Finder. The operating system would run your app from a temporary read-only disk image created just for the purpose of launching your app. This has an unfortunate side effect of Sparkle being unable to update your app – which prevents you from delivering updates and bug fixes seamlessly to your user.

Notarization is the method to register your software packages to Apple when you distribute outside the App Store. The problem with packaging software inside a .zip file comes from the inability to attach the notarization result on to the archive. Therefore your user’s system would need to be online to check whether your software package has been notarized. Otherwise it would prompt a “macOS can not validate…” warning to the user, possibly causing suspicion to your package. However when the notarization result is stapled to your package, macOS can validate it without making a network connection to Apple.

Thus disk images is probably the best way to distribute application bundles outside the Mac App Store. Disk images can be signed, notarized, and stapled. When you distribute apps using disk images and Sparkle, your users would find it straightforwardly effortless to install, use, and update.

But creating disk images is an involved task for the developer. There are so many tasks involved to package an application into a disk image ready for the user. This includes:

  1. Creating a blank read/write disk image for scratch-pad purposes.
  2. Copying the application bundle inside the disk image.
  3. Creating a symbolic link inside the disk image to the /Applications folder.
  4. Setting a background image for the disk image’s Finder window to instruct the user to install the application bundle.
  5. Use the Finder to carefully arrange the icons of the application bundle and the symbolic link to the /Applications folder to the background image, as well as any other additional content in the disk image.
  6. Convert the scratch-pad disk image into a compressed read-only disk image.
  7. Sign the disk image.
  8. Upload the disk image for notarization.
  9. Wait for notarization to complete.
  10. Staple the notarization ticket on to the disk image.

That’s a lot of steps to do manually on each release. Thankfully all of those steps can be automated and added to your continuous integration environment. Hence you can automatically get a finished disk image on every release. Even if you don’t have a build server, integrating this to your Xcode build would be a snap. Read on to learn how to do this.

Prerequisites

Before you begin, make sure that you have these at hand:

  • Xcode – I’ve written this guide with Xcode 11.0 in mind, but later versions should work.
  • create-dmg – This is a great tool by Andrey Tarantsov to create disk images for app distribution. Head on to the project’s site to download it or install via Homebrew.
  • App-specific password enabled on your Apple ID so that command-line tools can use it without going through two-factor authentication. Follow the instructions in HT204397 to create one if you haven’t already.
  • Your iTunes provider name (optional). This is likely relevant only if your Apple ID account is also tied to an Apple Books provider. Read more on selecting your provider name from my previous article.

Creating the Disk Image

The create-dmg command would cover steps 1–5 of the disk image creation process. It would create a disk image out of your application bundle, add a symbolic link to the /Applications folder as well as adding a background image when the disk image is shown in a Finder window.

The following is a sample command-line invocation of create-dmg. It packages YourAppName.app that is located in StagingDir into disk image AppPackage.dmg. A staging folder is required since create-dmg would copy everything inside it to the resulting disk image.

create-dmg \
--background ../window-background@2x.png \
--volname "Your App Name" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "YourAppName.app" 200 190 \
--hide-extension "YourAppName.app" \
--app-drop-link 600 185 \
~/Downloads/AppPackage.dmg \
"StagingDir/"

For more information on each parameter, you can look into create-dmg’s built-in documentation:

create-dmg --help

Note that the disk image’s background image need to be matched up with parameters to create-dmg which positions icons in the disk image. I’ve provided a generic background image that is already “aligned” with the above command.

Should this generic background is not sufficient, you can use my Affinity Designer template to create your own background. Download the template file from project DiskImageDistribution. in my Github account.

create-dmg would create an almost-ready disk image. However you would need to sign and notarize this disk image before distributing it.

Signing the Disk Image

Run the following command to sign a disk image using your Developer ID account. You shouldn’t notarize the disk image without signing it first — although the notarization process may give it a pass, Catalina’s Gatekeeper won’t trust it if the application bundle inside it was signed on 1 June 2019 or later.

xcrun codesign --force --sign "Developer ID Application: YourIdentity" AppPackage.dmg

Notarizing the Disk Image

The following command would upload the disk image to Apple for notarization. It is an asynchronous process which usually takes a minute or two. When successfully uploaded, the command would return a UUID that you can use to check the notarization process. However a successful upload does not necessarily mean successful notarization.

xcrun altool --notarize-app --primary-bundle-id com.example.YourApp -u "your.name@example.com" -p "app-specific-password"  --file AppPackage.dmg 

You could use the following command to check the notarization process, armed with the UUID returned upon upload.

xcrun altool --notarization-info "request-uuid" -u "your.name@example.com" -p "app-specific-password"

You could invoke the above command repeatedly to see whether the notarization process is completed, or wait for an e-mail from Apple telling you that it is done.

Notarization Result Notification

When the notarization process is completed, the command would provide a URL to the notarization log in the LogURL field returned. Hence if something went wrong, you could fetch that URL and see what was the issue.

Stapling the Notarization Result

Notarization would keep a copy of your package in Apple’s servers and create an identifier based on its hash. When Gatekeeper would need to check whether a package is notarized, it would derive its hash and use it to contact Apple’s servers to find out whether it was notarized. If Gatekeeper can’t connect to the Internet, it would simply refuse to open the package.

To prevent this from happening, you would need to embed the notarization result into your package. This process is called stapling – which is to staple the “certificate of notarization” that Apple provides on to your disk image. With the notarization result stapled, Gatekeeper would not need to go online to check for it – thus allowing your users to open your package as per normal.

Use the following command to staple the notarization certificate on to the disk image.

xcrun stapler staple AppPackage.dmg

The above command can also be used to poll for notarization result. When the command’s exit status value is 65, it means that the notarization process isn’t yet completed. Hence you can just call this command repeatedly in a continuous integration environment to wait for the notarization process, which usually takes less than a minute.

An example would be like so:

#!/bin/bash

...

exit_status=65
for (( ; ; ))
do
    xcrun stapler staple ...
    exit_status=$?
    if [ "${exit_status}" = "65" ]
    then
        echo "Waiting for notarization to complete"
        sleep 10
    else
        echo "Stapler status: ${exit_status}"
        break
    fi
done

...

Verifying the Resulting Disk Image

When you’ve completed everything, verify the resulting disk image using the following command:

spctl --assess --type open --context context:primary-signature --verbose "path-to-disk-image.dmg"

When it returns with a “Notarized Developer ID” then the package is ready and valid for notarization.

Putting it All Together

I’ve written a shell script that you can use as a starting point to integrate notarization into your build pipeline. You can also invoke this script as a custom build phase in Xcode.

Download the script from my Github project: DiskImageDistribution.

Next Steps

Integrate notarization into your continuous integration pipeline. You don’t need to sign and notarize every build – just the ones that you are going to distribute, including beta versions.



Avoid App Review rules by distributing outside the Mac App Store!


Get my FREE cheat sheets to help you distribute real macOS applications directly to power users.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

Avoid Delays and Rejections when Submitting Your App to The Store!


Follow my FREE cheat sheets to design, develop, or even amend your app to deserve its virtual shelf space in the App Store.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

2 thoughts on “Notarizing Disk Images for Developer ID Distribution

  1. Thanks for this article and the script from your GitHub project! It was quite helpful.

    It looks like internet-enable (for automatic mount and copy) has been removed in hdiutil on Catalina. This causes your script to break while running create-dmg. You can disable internet-enable by passing ‘–no-internet-enable’ as an option in the script’s call to create-dmg.

Leave a Reply