Catalina, App Notarization, and Sparkle

We recently started updating our macOS apps for Catalina: so far there have been very few issues with APIs and frameworks. The biggest hurdle has been the new notarization process that’s required for apps signed with a Developer ID: customers will be unable to download and launch your product easily until this step is completed.

Notarization involves an extra step in your build process: you upload an archived binary to Apple’s server with Xcode’s Organizer window and a short time later, you can export the binary. If you’ve automated your build process, you’ll need to make changes to your scripts to accommodate this new manual step. Apple’s documentation explains the process well.

Before you can notarize the app, you’ll need to enable the hardened runtime in the target’s Capabilities panel. After flipping the switch you’ll see a array of exceptions and access permissions. You’ll want to survey this list carefully: for one product we needed Apple Events, for another Location was required.

Things start to get tricky when you go to upload the binary: if you’re using Sparkle, it’s probably been codesigned without the hardened runtime, so you’ll immediately see an error.

Sparkle Without a Sandbox

How you deal with this error depends on which of the Sparkle versions you’re using. If your app isn’t sandboxed, your life will be a bit simpler because there are fewer things you’ll need to sign manually.

After the target’s Copy Files build phase where the Sparkle.framework is moved into the application package, you’ll need to create a new Run Script step: I called ours “Sign Frameworks”. The script looks like this:

LOCATION="${BUILT_PRODUCTS_DIR}"/"${FRAMEWORKS_FOLDER_PATH}"
IDENTITY=${EXPANDED_CODE_SIGN_IDENTITY_NAME}

codesign --verbose --force --deep -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app"
codesign --verbose --force -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A"

The key part in this step is the -o runtime. The codesign manual page describes this flag as:

On macOS versions >= 10.14.0, opts signed processes into a hardened runtime environment which includes runtime code signing enforcement, library validation, hard, kill, and debugging restrictions. These restrictions can be selectively relaxed via entitlements. Note: macOS versions older than 10.14.0 ignore the presence of this flag in the code signature.

The good news here is that the build changes we’re making won’t affect your app when it runs on an older version of macOS.

Sparkle in a Sandbox

If your macOS app is in a sandbox, you’ll be using the version that relies on XPC services to perform the update. Like everything else in your application package, these will need to be signed correctly before you can submit your app for notarization.

The “Sign Frameworks” build phase should look like this:

LOCATION="${BUILT_PRODUCTS_DIR}"/"${FRAMEWORKS_FOLDER_PATH}"
IDENTITY="${EXPANDED_CODE_SIGN_IDENTITY}"

codesign --verbose --force -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate"
codesign --verbose --force --deep -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A/Resources/Updater.app"
codesign --verbose --force -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A"

You’ll also add a new Run Script build phase just before the XPC Services are embedded in your application package. Since you’ll only need to do this for release builds, the script looks like this:

if [ "${CONFIGURATION}" = "Release" ]; then
    $PROJECT_DIR/Sparkle/bin/codesign_xpc "Developer ID Application" $BUILT_PRODUCTS_DIR/*.xpc
fi

But wait, we’re not done yet! You’ll also need to update the codesign_xpc Python script with the -o runtime flag. It looks like this when you’re done:

def _codesign_service(identity, path, entitlements_path=None):
    command = ["codesign", "-f", "-o", "runtime", "-s", identity, path] + ([] if entitlements_path is None else ["--entitlements", entitlements_path])
    log_message(" ".join(map(sanitize, command)))
    ...

You’re Not Done Yet

At this point, you should be able to do a build where everything in your app is using the hardened runtime. It’s more likely that you’ve had some kind of issue along the way: this Apple document helped get me over the rough patches. (Thankfully, I didn’t have to write it this time around.)

After the notarization upload completes, you’ll see “Uploaded to Apple” in the organizer, then after a few minutes you’ll get an email and Xcode notification that your app is “Ready to distribute”. In the righthand panel underneath “Distribute App”, you’ll see that the “Export Notarized App” button is enabled and can be used to place the signed package anywhere on your Mac for further processing.

In our case, we had to split up the build scripts into two parts: previously we had a single script that did the build, signed it with the Developer ID, and then created an appcast. Sparkle’s XML file is now created with a separate script that also prepares the release to be checked into our repositories.

One final note: these instructions are based on Xcode 10, which is currently the only development tool that can be used to submit an app for notarization or the Mac App Store. Before we figured that out, we found that Xcode 11 does a better job passing along the -o runtime flag during a framework’s Code Sign On Copy. It’s likely that all this work you just did will only be needed for a few months. Sigh.