How to code sign and notarize macOS audio plugins in CI

macOS code signing is something plugin devs tend to put off or avoid. But it’s fairly straightforward, especially when compared to Windows code signing (although that has improved lately too!)

Notarization in particular became much nicer (especially in CI) when notarytool was released at WWDC 2022.

altool was deprecated and unsupported after Fall 2023.

Let’s walk through the hows and whys — and get code signing and notarization running in CI.

It’s an additive beast with 1000 oscillators
and a ton of fun sound shaping tools

Check it out

Why code signing and notarization?

Code signing is you telling the user “hey, I made this and it hasn’t been modified since I made it.”

Notarization is Apple telling the user “we verify this dev made this, and we vouch there’s no malware in it.”

Why Bother?

I consider code signing audio plugins to be mandatory on macOS, even if you are just distributing to a handful of beta testers or friends.

You might be tempted to avoid it altogether. But this offloads inconvenience onto users less technical than yourself. And at the end of the day, it will still be you handholding them through approving your software (aka doing the system preferences dance), perhaps even having them run Terminal for the first time to try a killall -9 AudioComponentRegistrar without really knowing why, etc…

Adding this kind of friction to a customer’s first experience with your product isn’t worth it — even if it does save you a few hours. With a properly signed and notarized plugin, customers can start making music immediately — without engaging support articles or growing frustrated wrestling with technical hurdles.

Side note: In general, audio devs and their users can be superstitious and avoidant when it comes to staying up to date with the current (or gasp, the upcoming) macOS release. There’s a lot of history here… But fundamentally, the fact that we produce middleware puts us between a rock and a hard place — subject to both OS platform and DAW particularities. Notarization was a recent thorn in the audio industry’s side. Apple silicon support another. In both cases, larger audio companies took years to get up to date — at the cost of user experience and compatibility.

Interestingly, other industries don’t seem to struggle as much. Most non-audio indie macOS and iOS devs I know diligently test early OS betas and put the work in to make sure their users are all set by the time the public release is out. They accept that it’s part of the work you sign up for when publishing for the platform. It’s critical to their business…

Prereqs for Code Signing

First, make sure:

  • You have an Apple developer account. It costs $99 a year (less than the Windows code signing options).
  • You have Xcode downloaded and are logged in to your developer account.
  • You have the Xcode developer tools installed. Run xcode-select --install on the command line.

Pick a Cert

Ok, here’s the first gotcha people run into.

As a macOS developer, you can export different types of certificates for different purposes. So depending on what you are signing, you might need a different cert.

If you are distributing raw plugins (no installer), you’ll only need a Developer ID Application certificate.

If you want to create a “third party” installer (something that people download from your website), you’ll need a Developer ID Installer certificate.

To create an installer to distribute through the official Mac App Store, you’d use the Mac Installer Distribution Certificate.

Picking the wrong cert is one of the 2 things that trip people up (the other is what to sign/notarize, which we’ll get to).

Export the Cert

Let’s start with the simple plugin route. We’ll export the Developer ID Application cert from Xcode.

  1. Go into Xcode > Preferences > Accounts and make sure you are logged in.
  2. Select your “Team” and click “Manage Certificates”.
  3. (optional) Hit the plus sign bottom left and create a new “Developer ID Application” certification (or use the existing entry if it’s already there).
  4. Right click the “Developer ID Application” certificate and “Export Certificate”
  5. Choose a password. You’ll want to save this — you’ll need it each and every time you code sign. If you plan to code sign in CI, add it now as the DEV_ID_APP_PASSWORD secret.

If you plan on making an .pkg installer, repeat the above process with exporting a Developer ID Installer cert as well.

Using the cert in CI

The downloaded .p12 file needs be base64 encoded to use it in CI.

You can do this in Terminal to copy the base64 encoded certificate to your clipboard:

base64 -i path/to/mycert.p12 | pbcopy

Paste this base64 encoded string as the DEV_ID_APP_CERT secret in your CI.

See the GitHub Actions guide on secrets to get started if you haven’t done that before.

Importing your cert in CI

First, run this locally to get a look at your identities:

security find-identity -v -p codesigning

You should get output like the following:

  1) 01ACFFAE427A61A108C40F29C494E6CEOBBAD86E3 "Developer ID Application: Your Name (2DO8NL92GO)"
     1 valid identity found

The entire string in quotes is the identity you’ll be using for code signing.

In this example, it is Developer ID Application: Your Name (2DO8NL92GO).

You’ll need this while signing, so add it as the DEVELOPER_ID_APPLICATION secret in CI. Be sure that you don’t add or remove any spaces, it must be exactly the same string as the output (but without the quotes).

The string in parentheses is your Team ID. You’ll need it later for notarization. In this example it is 2DO8NL92GO. Add this as the CI secret TEAM_ID.

To get going in CI, you’ll need setup a keychain and import the cert during the CI run.

This GitHub action takes care of the process for you.

Note: you might have to specify a password for the keychain if you are signing both plugins and installers (and therefore want to run the import twice, once for a Developer ID Application, once for a Developer ID Installer).

Code signing in CI

Luckily code signing itself is super easy.

You need to code sign each binary.

So you would have one line for each AU/VST3/.app or whatever it is you are distributing.

Protip: you can also just try out code signing locally on any old .app or plugin from any other dev:

codesign --force -s "Developer ID Application: Your Name (2DO8NL92GO)" -v ~/Downloads/myApp.app --deep --strict --options=runtime --timestamp

--options=runtime specifies you’d like a hardened runtime, necessary if you want to notarize (which you do).

Now verify that signing worked locally:

codesign -dv --verbose=4 ~/Downloads/myApp.app

In CI, you’ll use the DEVELOPER_ID_APPLICATION secret you added to sign your plugin, like so (again, using GitHub Actions as the example):

codesign --force -s "${{ secrets.DEVELOPER_ID_APPLICATION}}" -v ${{ env.ARTIFACT_PATH }}/myPlugin.component --deep --strict --options=runtime --timestamp

Notarizing

Here’s how it works:

  1. You put your plugin binary in a dmg, zip or pkg container
  2. You upload it to Apple
  3. Apple verifies it’s not malware.
  4. When a user runs your software, it checks with apple to make sure the software is malware free.

If everything goes right, your users will see something like this:

Still annoyingly looks like a warning, but hey, it opens!

You’ll need to notarize each and every time you update the software, just like code signing. In other words — ideal to do in a CI pipeline.

Notary Tool

notarytool is a new tool as of WWDC 2022 that makes notarization easy. It’s also a one liner that looks like this:

xcrun notarytool submit myplugin.zip --apple-id you@email.com --password myPassword --team-id ABC123 --wait

If you didn’t grab your TEAM_ID yet, run security find-identity -v -p codesigning. It can also be found in Apple’s member center under “Membership Details”.

You might not feel comfortable using your main Apple ID username and password. Great! There’s a solution for that. Create an app-specific password by logging into appleid.apple.com. You’ll still use your Apple ID and your app-specific password will invalidate when your main password is changed.

Rule #1 of notarization: Only notarize the outermost container — the zip, the pkg, the dmg.

Rule #2 of notarization: It needs to put it in a container to upload with notarytool. A raw plugin or app can’t be submitted.

If you are distributing a pkg or dmg, just notarize that!

If you are insistent on distributing a raw .component or .app (vs. a dmg, pkg or zip) you would need to:

  1. zip it
  2. upload it to notarytool
  3. unzip it and staple the notarization to the .component or .app.

Staple

In addition to notarizing, you want to “staple” the notarization to the binary. This stamps the verified “proof” that you are malware free to the container you are distributing. When a binary is notarized, it’ll instantly work offline for your users. (Otherwise, apple will need to verify the notarization online when the binary is first run).

xcrun stapler staple yourApp.component # or yourApp.pkg/dmg

You can’t staple a .zip file. So if you want to distribute a loose .app or .component or two in a zip file, you’ll have to unzip, staple, and zip for distribution again.

A compromise to a full blown installer is a .dmg file, which you can create with one line:

hdiutil create -volname MyPlugin -srcfolder myArtifactsFolder MyPlugin.dmg

Verifying Notarization

You can run the following to verify the app/component will run smoothly on people’s machines:

spctl -vvv --assess --type exec ~/Downloads/myApp.app 

Full CI example: GitHub Actions

A full working example of audio plugin signing in CI can be seen in Pamplejuce, my JUCE plugin template for GitHub Actions.

Note that at least 6 secrets are needed in total, as described throughout this post:

If you are creating a third party installer (vs. raw plugins), you’ll need to another cert and an additional 3 secrets: DEVELOPER_ID_INSTALLER, DEV_ID_INSTALL_CERT, DEV_ID_INSTALL_PASSWORD.

Pamplejuce currently produces a “fancy” .dmg. This is a nice option for users — lightweight and explicit compared with an installer — in theory.

In practice, Apple broke .dmg‘s usefulness for audio plugins when Gatekeeper and notarization was introduced. Users can no longer drag and drop to ~/Library or /Library symlinks in a dmg. I confirmed this with Apple themselves. (Update: it’s fixed again in Ventura 14.0 but got reports of it broken in 14.1) So, Pamplejuce will move to using installers soon.

Congrats!

You made it. That wasn’t too hard, was it? Well, perhaps there are a fair number of fiddly details. Hopefully most were covered here. If not, feel free to comment.

Troubleshooting Code Signing and notarytool Issues

Code sign: “error: The specified item could not be found in the keychain.”

This means you

  1. passed the wrong identity string to codesign or
  2. it doesn’t match the identity of the cert.
  1. Double check the secret you are passing to codesign.
  2. Repeat the certificate export step, making sure to pick the same identity you are using, re-exporting the cert with a password.

notarytool: “status: Invalid”

Thanks for the detail, Apple!

The error looks like this:

Current status: Invalid.............Processing complete
  id: 437befe-2e52-401f-b17a-91b2de977cb5
  status: Invalid

To check for detail, query notarytool with the id:

xcrun notarytool log 437befe-2e52-401f-b17a-91b2de977cb5 --keychain-profile you@email.com log.json

Open up your log.json gift from Apple, to discover how you messed up:

"issues": [
    {
      "severity": "error",
      "code": null,
      "path": "Pamplejuce.dmg/libPamplejuce_SharedCode.a",
      "message": "The binary is not signed.",
      "docUrl": null,
      "architecture": null
    },
    {
      "severity": "error",
      "code": null,
      "path": "Pamplejuce.dmg/libPamplejuce_SharedCode.a",
      "message": "The signature does not include a secure timestamp.",
      "docUrl": null,
      "architecture": null
    }
  ]

In the above example I was accidentally including libPamplejuce_SharedCode.a in the dmg (and it wasn’t code signed).

“You can’t open the application because it may be damaged or incomplete”

Helpful!

This can happen if you

  1. Accidentally code signed the binary inside the .app bundle (for example MyApp.app/Contents/MacOS/myApp) and not the .app bundle itself
  2. The Info.plist is missing inside the bundle, or some some modification was made to the bundle’s contents after signing.

See a bit more detail with:

> codesign --verify myApp.app 
myApp.app: invalid Info.plist (plist or signature have been modified)

Notarization just stops working, or fails with Error 1048

One of most common and frustrating notarization problems, isn’t listed on Apple’s “Resolving common notarization problems

If you are seeing Error 1048, you may have to login to apple’s developer portal and sign developer agreements. This may happen periodically over the years, so be prepared!!!


Responses

  1. Dirk Schiller Avatar
    Dirk Schiller

    Great article! Well Done Sudara. I am also very impressed by all your JUCE work you’ve done on GitHub!

  2. Alex Voina Avatar

    I’m receiving a “replacing existing signature” log message when signing with codesign command.

    In this blog post I found he’s taking quite a few steps to “disable” signing in xcode:

    https://medium.com/flutter-community/build-sign-and-deliver-flutter-macos-desktop-applications-on-github-actions-5d9b69b0469c

    Everything worked well for me up to “stapling”:
    1. codesigned my .app (got the “replacing existing signature” log)
    2. created a dmg
    3. notarised the dmg (successfully)
    4. stapling fails

    Any thoughts? – ofc big thanks for this post, it’s a lifesaver

  3. […] Check out my detailed posts on code signing with third party certs here, or code singing and notarization on macOS here. […]

  4. Mark Avatar
    Mark

    Just to clarify :
    if I am wanting to distribute a .dmg, all I need is the “Developer ID Application” certificate and password – correct ?

    Looking at Pamplejuce that seems to be what you are doing.

    1. sudara Avatar

      Yes, exactly! And if you move to making .pkg installers (which Pamplejuce doesn’t support yet) you’ll also need Developer ID Installer.

  5. dan Avatar

    i had to add the -i flag before the path for the base64 command to work.

    base64 -i path/to/mycert.p12 | pbcopy

    thanks a bunch
    your blog is priceless

    1. sudara Avatar

      Thanks for the kind words and the typo. Fixed!

  6. Schwibbo Avatar
    Schwibbo

    Dude seriously: THANK YOU! Saved my mental health 😀

    1. sudara Avatar

      Always happy to be a sanity-provider 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *