Notarising Swift Package Development Tools for Distribution

8 May 2024

With macOS's introduction of Gatekeeper, binaries downloaded from the internet must be code-signed before they can be run. This presents a challenge for development tools that we may wish to distribute in the form of pre-compiled binaries. However, as we shall see in this article, distributing our code-signed binaries need not be too much of a headache.

The Approach

In order to allow our pre-compiled binaries to run without the user having to manually override Gatekeeper, the binary must be both code-signed by a registered developer and be notarised using Apple's notarytool. In order to simplify this process, we will create a reusable script to automate the process of code-signing a built Swift Package binary and uploading it to the notarisation service.

Step 1 - Building

Before we can code-sign a binary, first we must build it. We start by creating a script that allows the name of the package and its version number to be specified. It is assumed that this script runs from the root directory of the package:

printHelp() {
    read -r -d '' HELP << EOM
Usage:
    build-and-sign.sh <tool-name> <version-number>
EOM
    >&2 echo "$HELP"
}

if [ $# -ne 2 ]; then
    printHelp
    exit 1
fi

tool_name=$1
version_number=$2

With the boilerplate out of the way, we can start building:

# Build the package
xcrun swift build -c release --arch arm64 --arch x86_64

Next, we copy the built binary to the current directory, being careful to remove any existing build artefacts first:

# Remove existing build artefact (if any)
rm $tool_name

# Copy the built binary to the current directory
ditto .build/apple/Products/Release/$tool_name .

Step 2 - Code Signing

Every Apple-platform developer loves code-signing, and knows how straight forward a process it is without any awkward pitfalls. However, when distributing a binary outside of the Mac App Store that is also not a Mac app, we need to use something called a Developer ID certificate, which is different from the development and distribution certificates that we are used to. Instructions for creating one of these certificates can be found on the Apple Developer website.

Once a Developer ID signing identity has been created, we are ready to code-sign our binary. The first step is to discover the code-signing identity to use by running the following command:

security find-identity -p codesigning -v

This will give a list of available code-signing identities in the following format:

1) DFD9F6C6A9B0499A81EDF57E8763AF85 "Apple Development: Joe Blogs (5EFGT13RF6)"
2) EED523AC2304487CB67D0E7489931C2D "Developer ID Application: Joe Blogs (GRE9WAL4C3)"

Here we can see our Developer ID signing identity, and this is what we will use in the next command in our script:

# Codesign the binary. `-o runtime` specifies the hardened runtime
codesign -o runtime -s "<developer-id-identity>" $tool_name

Replace "<developer-id-identity>" with the value after the identifier from the list of code signing identities we checked earlier. Given the earlier example, the correct value to use would be "Developer ID Application: Joe Blogs (GRE9WAL4C3)".

Note that we are passing the -o flag to specify the hardened runtime. This is needed in order to allow the binary to be executed without specifying any entitlements that detail hardened runtime exemptions.

Step 3 - Notarisation

Now that we have built and code-signed our binary, we can upload it to Apple's notarisation service using notarytool. However, in order to do this we must first create an app-specific password that will allow notarytool to access the notarisation service on our behalf. Steps to create an app-specific password can be found on the Apple support website.

Once you have created your app-specific password, add it to your keychain using the following command:

xcrun notarytool store-credentials --apple-id "<your-apple-id-email-addres>" --password "<your-app-specific-password>" --team-id "<your-team-id>"

For the team ID, using the signing identities we checked earlier, the correct value would be GRE9WAL4C3. When running this command, notarytool will ask you for a profile name to store in the keychain. For the purposes of the script, it is assumed that this profile name is NOTARY_PASSWORD.

Now that we have our app-specific password in our keychain, we are finally ready to upload our code-signed binary to the notarisation service:

# Zip the signed binary
ditto -c -k $tool_name $tool_name-$version_number.zip

# Upload the signed zip file to the notary service
xcrun notarytool submit $tool_name-$version_number.zip --keychain-profile "NOTARY_PASSWORD" --wait

The --wait parameter allows us to see the progress of the notarisation process in the terminal through to completion:

Conducting pre-submission checks for my-dev-tool-1.0.0.zip and initiating connection to the Apple notary service...
Submission ID received
  id: 02718c11-e4a3-469d-b5c7-862991153015
Upload progress: 100.00% (7.53 MB of 7.53 MB)   
Successfully uploaded file
  id: 02718c11-e4a3-469d-b5c7-862991153015
  path: /Users/joeblogs/MyDevTool/my-dev-tool-1.0.0.zip
Waiting for processing to complete.
Current status: Accepted........
Processing complete
  id: 53844CFC-7693-4B63-AA03-47CE7170B95C
  status: Accepted

Conclusion

Handling code-signing and notarisation for distributing our development tool binaries may seem a daunting task. In reality, whilst it may be a complex process, only a small number of steps are actually needed in order to achieve our goal.

The full script discussed in this article can be found on GitHub.

Sources

https://developer.apple.com/​forums/​thread/​701514#701514021

https://developer.apple.com/​forums/​thread/​701581#701581021

https://developer.apple.com/​documentation/​security/​notarizing_macos_software_​before_distribution/​customizing_the_​notarization_workflow