We distribute our software for macOS as apps in Apple disk images, or said more simply, we distribute our software as a .app inside of a .dmg. For the past year and a half, we've handled our Apple notarization by submitting the .dmg for notarization, stapling the ticket to the disk image after success, and going on our merry way.
In the last couple of weeks, I've gotten a few reports from customers on Catalina that they're getting messages like "Cannot connect to App Store", or the more usual "can't be opened because Apple cannot check it for malicious software" when trying to use or copy or notarized apps.
My assumption here is that due to internet connectivity or Apple server outages/lag, some of our customers are seeing rejections of our notarized apps. But with the stapled notarization ticket, there shouldn't be a need to contact Apple to check the notarization. The ticket should provide the information needed to do the check offline. Of course, that means I needed to reevaluate how we're notarizing and stapling.
If you submit a DMG for notarization, the inner application is notarized also, and you can attach the notarization ticket to the DMG. However, there is no ticket attached to the inner .app, since it's nested in the DMG. What I didn't realize with this process is that there is still a check on the inner .app file, and the outer DMG ticket doesn't help with this check.
To hammer the point home, let's evaluate one of my old DMGs before I changed my build/notarization process.
If I check for a stapled ticket on the notarized and stapled DMG I see the check succeeds.
xcrun stapler validate ~/Downloads/DecipherTextMessage.dmg Processing: /Users/kwilkerson/Downloads/DecipherTextMessage.dmg The validate action worked!
But, of course, the app itself, that was packaged into the disk image before notarizing, doesn't have the ticket attached to it.
xcrun stapler validate /Volumes/Decipher\ TextMessage/Decipher\ TextMessage.app Processing: /Volumes/Decipher TextMessage/Decipher TextMessage.app Decipher TextMessage.app does not have a ticket stapled to it.
But the app is notarized if I check it.
spctl --ignore-cache --assess -vvvv /Volumes/Decipher\ TextMessage/Decipher\ TextMessage.app /Volumes/Decipher TextMessage/Decipher TextMessage.app: accepted source=Notarized Developer ID origin=Developer ID Application: Decipher Media, LLC (3Z498VWZ9Z)
For most customers, there's no problem, because when they try to open the app inside of the DMG, Gatekeeper simply asks Apple's server if the app is ok and has been notarized. But, if Apple's servers are down, or the customer's internet connection is flaky, the check for the notarized app will fail.
I believe I've solved the issue by adjusting our build process to notarize twice:
- Build the app.
- Zip the app and upload the app to Apple for notarization. Zip using
/usr/bin/ditto -c -k --keepParent your.app your.zip
- Wait for notarization success and staple the app.
- Build the dmg using the stapled app.
- Upload the dmg to Apple for notarization.
- Wait for notarization success and staple the dmg.
In retrospect this all seems more obvious. In fact, I just reread the WWDC 2019 notarization presentation slides and it mentions two-step notarization for custom installers. 1) "Submit the content as it will appear on disk" and 2) "Submit your custom installer". But it hardly seemed obvious (or easy to test) when I was first learning notarization.