Site menu Signing an Electron app for Mac App Store
e-mail icon
Site menu

Signing an Electron app for Mac App Store

e-mail icon

After too many hours spent on preparing a biorhythm sample app for publishing in Mac App Store, I think I have a useful recipe that this article intends to share.

If you are TL;DRing and just want a signing sequence that is known to work, check this script. The enclosing folder is the sample app; it includes the necessary entitlement files, but does not include the provisioning profiles that you will have to obtain using your Apple developer account.

This article is not intended to be a complete documentation of Electron packaging tools (electron-packager, electron-osx-sign, electron-osx-flat). They have official documentation for that. I just want to clarify some pitfalls related to MacOS packaging.

Darwin and MAS packaging

MacOS applications can be packaged in two flavors: Darwin and MAS ("Mac App Store"). The MAS format is newer, and it is (obviously) intended for distribution via App Store. MAS apps are also sandboxed (more about this later).

Apps from the Store are the most trusted. Only MAS apps run straight out-of-the-box without complaints, but you can configure security to trust properly signed Darwin apps as well.

Figure 1: System Preferences, configuration of security regarding signed and MAS apps

Unsigned apps won't run right away in recent versions of MacOS. The user needs to authorize every individual app in the System Preferences panel shown above. (A useful trick is to Option-click the unsigned app: a dialog gives the option of authorizing it, just like it used to be in older versions of OS X.)

So, as a developer, you have three ways to distribute your app: MAS, signed Darwin, and unsigned Darwin. I don't recommend wide distribution of unsigned apps due to security risks. But, if you simply don't want to deal with app signing, you use the electron-packager tool and nothing more. You may want to read the next three sections of this article, and you are good to go.

In order to sign a MAS or Darwin package, you need a developer certificate signed by Apple, and to get it you need to enroll the Apple Developer Program, which costs US$ 99/year. This is why there still are so many unsigned apps out there. At least the US$ 99 fee buys you access to both iOS and MacOS; in the past, you'd have to play $99 for each platform!

Command-line development tools

In order to generate a MacOS package, you need Xcode installed, as well as Xcode command-line tools, since Electron builds are carried out outside of the IDE:

xcode-select --install

MacOS icon format

The application icon is one of the dustiest corners of every platform, I guess. Windows still uses the ICO format, and Mac demands icons in a certain ICNS format, that most graphical tools cannot generate.

ICNS is actually a bundle of images, typically the same icon scaled down to varying sizes. There are online tools, free and paid, that generate the ICNS icon to be supplied to electron-packager in order to replace the default Electron icon.

But no external tool is actually necessary. Check this script in my sample app.. It takes the original icon (that should be of 1024x1024 size) and creates the ICNS icon. The script only uses CLI development tools.

Certificates

Wnen you enroll the Developer Program, you can request the certificates manually (creating a certificate request CSR using Keychain, uploading the CSR to developer.apple.com, etc.) but it is much, much easier to let Xcode do it for you.

Figure 2: Xcode tool showing the developer certificates and provisioning profiles

There are five certificates, or identities, related to Mac app signing:

Development certificate: allows to run a signed (and, if MAS, sandboxed) app on your own machine, and also on other development/testing machines registered at developer.apple.com. A maximum of 100 machines per team can be registered, so it is not good for distribution. In order to test a MAS app, you must sign it with this certificate, otherwise the app will not run in sandboxed mode.

Developer ID application: to sign an app (Darwin or MAS) that you intend to distribute outside of App Store.

Developer ID installer: to sign the package of an app that you intend to distribute outside of App Store. (Yes, there is one certificate for the app itself and another for the package.)

Mac App Distribution (or 3rd Party Mac Developer Application): to sign a MAS app that will be submitted to App Store. The app signed with this certificate won't run anywhere, not even in the developer's machine, this is not a bug! It will only run when signed off by Apple, and the signed off version is available only at App Store.

Mac Installer Distribution (or 3rd Party Mac Developer Installer): to sign MAS packages to be submitted to App Store.

By and large, the Electron tool electron-osx-sign will pick the right certificate for each situation:

You only need to pass the identity parameter in case you belong to more than one development team (i.e. you have more than one Apple Developer account). But it is important to clarify the role of each certificate, because the nomenclature is so confusing.

The 'Developer ID' identity must also be passed when you sign a MAS package for direct distribution, outside of App Store. But most apps outside App Store will choose Darwin over MAS format, because of sandboxing limitations (explained in the next section).

Finally, it is worth to mention that the private keys of these certificates must be protected (i.e. the developer's machine must be physically secure, or use disk encryption). The private keys are not uploaded to developer.apple.com, so if you need to use the same certificates in more than one machine, you must copy the private keys somehow. The easiest way is to use Xcode's Export Developer Accounts.

Provisioning profiles and app IDs

Then we have "provisioning profiles", another big source of confusion. They are only necessary when signing MAS apps. If you are signing Darwin apps, you can ignore this section. This may be another reason why so many developers loathe the MAS format and the Mac App Store.

A provisioning profile is a secure bind of one developer certificate, one App ID, a list of machines (in case the app should only run on registered computers), and a list of Apple services that the app wants to use (e.g. Game Center, iCloud, in-app purchases).

Since each provisioning profile is tied to a particular certificate, you need at least two per MAS app: a development provisioning profile to run in developer machines, and a production profile that is only used to sign the package that is sent to App Store.

For apps developed entirely within the Xcode IDE, the provisioning profiles are automatically created. The App ID must be created in developer.apple.com, but even that is optional for apps that don't use any Apple services. I prefer to always create the App ID, just in case you want to use Apple services in a future version. Since Electron apps cannot use the Xcode IDE, we need to create and download the provisioning profiles manually, and having one profile per App ID is easier to handle than having to cope with the "wildcard" ID.

Figure 3: developer.apple.com, App ID registration page
Figure 4: developer.apple.com, development provisioning profile page
Figure 5: developer.apple.com, production provisioning profile page

Note that I created two production provisioning profiles: one to sign for App Store, another to sign for distribution outside the store. I did that because I was playing with all possible signing modes.

IMPORTANT: it is always necessary to supply a provisioning profile when signing MAS packages with electron-osx-sign. The Electron documentation does not tell you that! You must pass the development profile when the --type=development parameter is used. The app won't run without it.

And don't forget to supply the production profile when signing for App Store, otherwise the package will be rejected by Application Loader.

Sandboxing and entitlements

MAS apps run in sandboxed mode. This means that the app does not have access to the filesystem, except for its own configuration folder (a folder like $HOME/Library/Containers/app_id/Data/). Every feature used by the app (network access, network services, broader filesystem access, etc. etc.) must be explicitly allowed by a static list of entitlements.

The App Store review process does check whether the requested entitlements are congruent to the app's actual requirements. Adding a laundry list of entitlements "just to be safe" will probably cost a rejection.

In theory, sandboxing is a great idea: it gives Mac apps a security level more like iOS. In practice, it brings a number of difficulties and limitations. Many developers don't like it, many powerful apps can't operate in sandboxed mode, and that's why so many nice Mac apps are not being distributed via App Store.

In the specific case of Electron apps, the most typical problem is using some Node.js or Electron package that is not prepared to run in sandboxed mode. The problems may be subtle and difficult to debug. For example, the Electron API app.makeSingleInstance() causes the app to fail because it calls bind(), which is forbidden in sandboxed mode. Your program will fail if it uses this API. (On the other hand, a MAS app is automatically prevented from running more than one instance, so you don't lose anything by not using this particular API.)

Assuming that you simply must publish your app in Mac App Store, you need to test your app in sandboxed mode early and often. Note that packaging it as MAS is not enough to put the app in sandboxed mode. You need to sign it as well, using the development provisioning profile and supplying a list of entitlements. Only then you will have a true notion how the app is behaving. If you forget to sign the MAS package, it will run with unlimited permissions just like a Darwin-packaged app, and you are not testing it at all!

If there are problems, a good place to start looking for answers is the MacOS's Console app (found in Utilities folder). In the figure below, I put the name of my app in the filter, so I only see the relevant console messages:

Figure 6: Console tool showing system messages for sandboxed Biorhythmics app

Since you can't write files in arbitrary folders of the filesystem, and in principle you don't know the absolute path of the sandbox folder, you should query it by calling app.getPath("userData") and writing files in this folder only. There are specific entitlements that "punch a temporary hole" in the sandbox strictly for loading and saving files anywhere in the filesystem, so e.g. a text editor or graphical tool can still work in sandboxed mode. Consult the Apple documentation for more details.

In theory, electron-osx-sign tool supplies the entitlements automatically. But I found that I needed to create entitlement files manually to end up with a valid signed package. This is another thing that the tool's documentation does not tell you.

Wrapping it up

Finally, let's show the final recipe: the 'pkg' script. This script is used to sign the MAS package of the Biorhythmics sample app. Feel free to copy and adapt it to your own project.

The pkg script signs the MAS package with development profile by default. Passing 'release' as the first parameter signs the package for upload to Mac App Store. Note that, in either case, it expects a suitable provisioning profile at $HOME; it is up to you to create and download them, as explained before. (Provisioning profiles are developer-, app- and machine-specific, so it would not make sense to supply my own profiles, not even as samples.)

The parent and child entitlement files are included, since their contents are not secret and can be retrieved by any user that installs Biorhythmics from App Store. There are two of them because child processes need to inherit the parent entitlements, and Electron creates one child (renderer) process per window. The child entitlement file must never be changed, because child processes can only inherit if the child entitlements are exactly {app-sandbox, inherit} — no more, no less.

If you copy my parent entitlement file to your project, don't forget to replace the application_groups property by your own TeamID-AppID (or by some other guaranteedly unique name).

The version supplied through the --build-version=a.bb.cc parameter has a strict format: a.bb is the user-visible software version, while cc is the build number. If you submit a new build for the same version (e.g. in case the first build was rejected or you detected a bug before publishing) you need to bump the build number.

Finally, the script for Darwin package signing is also provided. Once you sign an app this way, you can distribute it to anyone, and the package is secure against impersonation or malicious changes.

The pkg_darwin script is much simpler than the MAS script, because there are no entitlements nor provisioning profiles.

e-mail icon