How to Notarize Your Plug-ins

Tutorial
by

15 October 2019

With the dawn of macOS Catalina in October 2019 came a new security feature to our computers: Apple requires all software downloaded from the internet (including by e-mail) to be what they call notarized. Developers upload their macOS software to Apple servers where they are scanned for malicious code. Apple keeps a database of safe software that macOS’s Gatekeeper mechanism queries when a software is downloaded from the internet and used for the first time. For offline use, developers can staple the ticket (a confirmation of successful notarization) to their software so that Gatekeeper can trust it even without internet access.

Sadly, all this applies to plug-ins for Glyphs.app as long as they are downloaded via a browser or come in by e-mail. Plug-ins that are installed via Glyphs.app’s built-in Plugin Manager on the other hand are exempt (for the time being), probably because Glyphs.app itself is notarized and therefore counts as a trustworthy gateway.

If you plan to distribute a Glyphs.app plug-in yourself, for instance because your plug-in is commercial or other reasons prevent you from including it in the Plugin Manager (which can only distribute plug-ins hosted in public Github repositories), you must have your plug-in notarized by Apple.

In the natorization process, the software is uploaded to Apple servers using a command line tool that comes with Xcode 10 or later called altool. After a while (couple of minutes for my plug-in, results may vary), you receive a confirmation of the notarization from Apple by e-mail. Then you can staple the ticket to your plug-in and ship it to your users.

This tutorial is largely based on Apple’s own Customizing the Notarization Workflow article, but adapted to Glyphs.app plug-in development.

Let’s get to it, shall we?

Xcode 10 or later

If you haven’t already, install Apple’s developer suite Xcode through the App Store (link).

Apple Developer Program membership

We need a paid membership in the Apple Developer Program. Last I checked, the membership costs 99€ per year for European citizens. Apple requires the paid membership to issue you with a code signing certificate needed as a prerequsite for the notarization. Code signing is required by Gatekeeper for macOS apps already since a few OS versions ago, but if you’ve only made Glyphs.app plug-ins so far and not standalone macOS apps, chances are you never needed one yet. Now you do.

Code Signing Certificate

Once you’ve set up your paid membership, you navigate to the Certificates, Identifiers & Profiles (requires login) section of your Apple Developer account and create yourself a certificate of the Developer ID Application type. This is the type of certificate needed to sign macOS apps and plug-ins for distribution outside of the Mac App Store.

Download the certificate file (called developerID_application.cer) and install it in your macOS keychain app by double-clicking the file. In the code signing process you will later reference this certificate by your real-life name, the developer’s identity engraved in the certificate.

App-specific Password

For notarization, you need a so called app-specific password needed to connect to the Apple Developer Program in an automated build script. As of late, access to online Apple services requires two-factor authentication (you entering a number that pops up on your screen when you access the Developer Program), and because that’s not possible in an automated software build process, the app-specific passwords provide a gateway into the Apple Developer Program without two-factor authentication.

This Apple support document describes the process:

  1. Sign in to your Apple ID account page.
  2. In the Security section, click Generate Password below App-Specific Passwords.
  3. Give the password a name such as “Software Notarization”. Take note of the password shown to you upon confirmation.
  4. In macOS’ Keychain.app, navigate to the Login Keychain and manually add a new password (⌘N). Give it a name such as “Software Notarization” (need not be identical to the previous step’s name). Enter your Apple ID (e-mail) for account name as well as the actual password generated on the Apple ID account page. You will later reference this keychain item in your build script instead of hard-coding the password into your build files, in case you are hosting your files in a public repository.

Step-by-step Terminal Commands

Now we have everything ready. I will now walk you through all the steps necessary to notarize your plug-in. Please note that you need to change some commands to fit your personal setup.

Below this section I will show you how to combine everything into handy build scripts, including automatic verification of certain steps. But for your understanding let’s go step by step for now.

For clarity, you will encounter two different zip files called MyPlugin.notarize.zip and MyPlugin.ship.zip. The first is the zip file containing your code-signed plug-in that you send to Apple for notarization. The second is the zip file that, once you’ve received confirmation of successful notarization from Apple by e-mail, you staple the ticket to and ship to your users. You could use one and the same zip file name, but in this tutorial we don’t.

Open the Terminal.app and navigate to the folder that contains your plug-in.

Removing resource forks

In my build process, I’ve found my plug-in to contain data in file system resource forks, which the code signing will reject. This command will remove them:

xattr -cr MyPlugin.glyphsReporter

Code-signing

Sign the plug-in using your Developer ID Application certificate which you stored in your keychain. Reference it by your real name:

codesign --deep -s "John Doe" -f MyPlugin.glyphsReporter

Zip it up for notarization

Compress your plug-in into a zip file using Apple’s own ditto tool. Don’t try to use the standard zip tool as it strips the plug-in of the code signature we’ve just created.

The below command is identical to compressing a file in the Finder, but we’re trying to automate things here, so we’re using the command line tool.

ditto -c -k --keepParent --rsrc MyPlugin.glyphsReporter MyPlugin.glyphsReporter.notarize.zip

Notarization

Send the zip file to Apple for notarization. This process is asynchroneous, meaning that the command will exit as soon as it has submitted the file, and not wait until the notarization has actually been conducted. You will be notified by e-mail when the notarization is done.

In the command below, you will need to enter a Bundle ID which is a name freely chosen by you to identify the piece of software you have submitted. It will be mentioned in the e-mail you will receive. Also, the app-specific password previously stored in the keychain will be referenced by your Apple ID and the keychain item’s name.

Running this command might take a while to finish.

xcrun altool --notarize-app --primary-bundle-id "MyPlugin" --username "john.doe@apple.com" --password "@keychain:Software Notarization" --file MyPlugin.glyphsReporter.notarize.zip

Sit back and wait for the notarization notification by e-mail. In my case this repeatedly took under five minutes.

Ticket Stapling

Once your notarization is confirmed, you should (but don’t have to) staple the notarization ticket to the software, so that users can open it even without internet access:

xcrun stapler staple MyPlugin.glyphsReporter

Zip it up for shipping

Last we’re zipping up the notarized plug-in (with the attached ticket) one more time. Ship this zip file to your users.

ditto -c -k --keepParent --rsrc MyPlugin.glyphsReporter MyPlugin.glyphsReporter.ship.zip.

Build Scripts

As mentioned, you may combine all commands into a build script and then just execute the build script once instead of repeating all the above steps. Put the code below into two separate files called notarize.py and staple.py and place them in your plug-in’s folder. Don’t forget to adjust to your personal setup.

As things get blurry as lots of output text scrolls by your Terminal window, we must make sure that each of the essential commands get executed to our satisfaction. We do that by reading out each command’s exit code. The exit code is one of the most essential features of Unix-based operating systems. It is a number that a command returns to its caller when it’s done with its job. If a command finishes successfully, it will always announce that by returning the exit code 0 (zero). Any error that occurs in a command, be it a programming error or processing problems caused by erroneous data, will lead to an exit code higher than 0. Often, error exit codes are documented for each command and can be queried for automated handling of the exception. But for us it’s only important that all essential commands exit with 0.

Some commands in the below scripts aren’t essential, however, such as the removal of the old zip files. We’re deleting them to ensure that we’re only using the latest successfully created files. However, if some step in the build process fails, new zip files might not be created again yet. Then, trying to delete them the next time will itself cause the build script to fail, because trying to delete a non-existent file will exit non-zero. This condition is marked in the build script with True for essential commands and False for unimportant ones.

In the below build scripts, the output of each individual command is hidden from you for peace of mind, but printed in complete when a command fails. Because we’re checking the exit codes, you can rest assured that a successful build script completion means that everything is in order, or otherwise you’ll notice.

Some additional commands have been added to the build scripts below to automatically verify the code signing, notarization, and stapling.

The scripts were written for Python 3. If you’re still working with Python 2 they might not work (the subprocess part) and then I have to tell you that I don’t care and that there’s no help for you and that it’s 2019 and that you need to urgently go into a corner of shame and think about your life.

However, the scripts should work in Python 2 as well if you prefix them with these two lines:
# -*- coding: utf-8 -*- > from __future__ import print_function, subprocess

But no guarantee.

notarize.py

Because the notarization process is asynchroneously separated into a notarization stage and a stapling stage, we need to create two separate build scripts. The first will submit the plug-in for notarization:

import os, sys
from subprocess import Popen,PIPE,STDOUT

# List of commands as tuples of:
# - Description
# - Actual command
# - True if this command is essential to the build process (must exit with 0), otherwise False

commands = (
    ('Remove resource forks', 'xattr -cr MyPlugin.glyphsReporter', True),
    ('Remove zip file 1', 'rm MyPlugin.glyphsReporter.notarize.zip', False),
    ('Remove zip file 2', 'rm MyPlugin.glyphsReporter.ship.zip', False),
    ('Sign plug-in', 'codesign --deep -s "John Doe" -f MyPlugin.glyphsReporter', True),
    ('Verify signature, part 1', 'codesign -dv --verbose=4 MyPlugin.glyphsReporter', True),
    ('Verify signature, part 2', 'codesign --verify --deep --strict --verbose=2 MyPlugin.glyphsReporter', True),
    ('Zip it', 'ditto -c -k --keepParent --rsrc MyPlugin.glyphsReporter MyPlugin.glyphsReporter.notarize.zip', True),
    ('Upload for Notarization', 'xcrun altool --notarize-app --primary-bundle-id "MyPlugin" --username "john.doe@apple.com" --password "@keychain:Software Notarization" --file MyPlugin.glyphsReporter.notarize.zip', True),
)

for description, command, mustSucceed in commands:

    # Print which step we’re currently in
    print(description, '...')

    # Execute the command, fetch both its output as well as its exit code
    out = Popen(command, stderr=STDOUT,stdout=PIPE, shell=True)
    output, exitcode = out.communicate()[0].decode(), out.returncode

    # If the exit code is not zero and this step is marked as necessary to succeed, print the output and quit the script.
    if exitcode != 0 and mustSucceed:
        print(output)
        print()
        print(command)
        print()
        print('Step "%s" failed! See above.' % description)
        print('Command used: %s' % command)
        print()
        sys.exit(666)

print('Finished successfully.')
print()

Run the command: python notarize.py

staple.py

Same build script, but different commands to execute. This script staples the ticket to the plug-in once you’ve received the e-mail and zips it up again in a different zip file to ship:

import os, sys
from subprocess import Popen,PIPE,STDOUT

# List of commands as tuples of:
# - Description
# - Actual command
# - True if this command is essential to the build process (must exit with 0), otherwise False

commands = (
    ('Validate notarization', 'spctl -a -vvv -t install MyPlugin.glyphsReporter', True),
    ('Staple notarization ticket to plug-in', 'xcrun stapler staple MyPlugin.glyphsReporter', True),
    ('Validate stapling', 'stapler validate MyPlugin.glyphsReporter', True),
    ('Zip it', 'ditto -c -k --keepParent --rsrc MyPlugin.glyphsReporter MyPlugin.glyphsReporter.ship.zip', True),
)

for description, command, mustSucceed in commands:

    # Print which step we’re currently in
    print(description, '...')

    # Execute the command, fetch both its output as well as its exit code
    out = Popen(command, stderr=STDOUT,stdout=PIPE, shell=True)
    output, exitcode = out.communicate()[0].decode(), out.returncode

    # If the exit code is not zero and this step is marked as necessary to succeed, print the output and quit the script.
    if exitcode != 0 and mustSucceed:
        print(output)
        print()
        print(command)
        print()
        print('Step "%s" failed! See above.' % description)
        print('Command used: %s' % command)
        print()
        sys.exit(666)

print('Finished successfully.')
print()

Repeat

Repeat after each code change in your plug-in.

Troubleshooting

altool not working

When running the altool command, you may get an unable to find utility "altool" error in return. Quickest fix is to reset the path of the active developer directory with this command:

sudo xcode-select -s /Applications/Xcode.app

After this, just try again, or continue with the other steps manually as well.

Failed stapling

Interestingly, for some users, the Stapling script choked on the second step, the one called ‘Staple notarization ticket to plug-in’. If you execute the step yourself in Terminal:

xcrun stapler staple MyPlugin.glyphsReporter

… it worked, though.

Feedback?

If you find more problems, please make yourself heard in the forum. There is a helpful ‘Catalina and Plug-ins’ thread where you can exchange your findings with other developers. Thank you in advance for taking part in the community!


Written by Jan Gerner (Yanone).

Update 2019-10-15: Added note about Python 2.x adaptation. --RES (I do care a little.)
Update 2020-01-31: Added Troubleshooting section (thx @Mark).