A MacBook

How I sign and notarize my Electron app on MacOS

The process for getting an Electron app signed and notarized on Mac.

Distributing a desktop application for MacOS requires signing it and getting it notarized by Apple. MacOS won’t let the app run otherwise.

I distribute my Electron app outside of the App Store, via direct download. If you’re distributing via the App Store, the steps may be different than what I have here. Note I am based in the United States, and I did all this on a new (as of 2021) Mac Mini with M1 chip.

Here are the steps I took to get my Electron app signed and notarized:

  1. Sign up for the Apple Developer Program and get a Developer ID
  2. Create Apple Developer ID and Apple Developer Application certificates, download them, and install them in your keychain
  3. Once those certs are in your keychain, electron-builder will sign automatically while building
  4. Use a script to notarize the app after signing

Let’s go over each step.

1. Sign up for the Apple Developer Program

Go to the Apple website to enroll in the Developer Program: https://developer.apple.com/programs/

This comes with a cost. At the time of this writing, it was 99 USD per membership per year.

Enrollment gives you two options: Individual or organization. I enrolled as an organization (using my registered LLC). This is because I like to conduct business under my business name, to benefit from the legal structure that an LLC gives me. Enrolling as an organization has more requirements for verification than an individual. I needed a DUNS number, a website, and a custom domain email. Apple called me to verify my business.

2. Create and install certificates

After completing enrollment and verification, I got a developer account on developer.apple.com. I signed in on there, went to Certificates, IDs, & Profiles.

I created two certificates, one of Developer ID Application type and one of Developer ID Installer type. Once created and download, a double-click on the file should install them in your Keychain.

3. Sign while building

With those certificates in my keychain, here’s relevant config from my electron-builder.yml:

afterSign: notarize.js
mac:
category: public.app-category.productivity
#  gatekeeperAssess: true
target: [dmg, zip] # zip is required for auto-updating because of electron-userland/electron-builder#2199
entitlements: ./entitlements.mac.plist
entitlementsInherit: ./entitlements.mac.plist
hardenedRuntime: true

Apparently electron-builder automatically uses the certificates in my keychain, because I don’t have to mention them in the config.

I did have to create an entitlements.mac.plist file. Otherwise my app, when run, would request random weird permissions that it didn’t need. I looked at the docs and some open source examples (e.g. Athens) and came up with this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <!-- https://github.com/electron/electron-notarize#prerequisites -->
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
  </dict>
</plist>

This may or may not be appropriate for your app. One requirement of my app is to run third-party executables (Prisma) that I’ve bundled with the app.

4. Notarize

The final step to getting the app to run on Mac is notarization. There are some good tutorials online on how to do this with electron-builder that I referred to (see https://philo.dev/notarizing-your-electron-application/ and https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ ).

In the electron-builder config above, see the line afterSign: notarize.js. The notarize.js script I took from here:

require('dotenv').config();
const fs = require('fs')
const path = require('path')
const electron_notarize = require('electron-notarize');

module.exports = async function (params) {
if (process.platform !== 'darwin') {
return
}

    console.log('afterSign hook triggered', params)

    let appId = 'com.funtoimagine.arinote'

    let appPath = path.join(
        params.appOutDir,
        `${params.packager.appInfo.productFilename}.app`
    )
    if (!fs.existsSync(appPath)) {
        console.log('skip')
        return
    }

    console.log(
        `Notarizing ${appId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`
    )

    try {
        await electron_notarize.notarize({
            appBundleId: appId,
            appPath: appPath,
            appleId: process.env.APPLE_ID,
            appleIdPassword: process.env.APPLE_ID_PASSWORD,
        })
    } catch (error) {
        console.error(error)
    }

    console.log(`Done notarizing ${appId}`)
}

With this, electron-builder signs and notarizes my app. The app is then ready for distribution.

Here’s my package.json script to do a production build on Mac: cross-env NODE_ENV=production npm run test && npm run prod-build && electron-builder --mac

And this does a build and publish to my Github releases: dotenv -- electron-builder -p always.

The notarization step takes 5-10 minutes for my app.