How to automatically sign macOS apps using GitHub Actions

In this article, I’m going to show how to automate code signing using GitHub Actions specifically, but the same principle can be used on other CI tools with minimal tweaks.

Code signing on macOS locally is usually straightforward using the codesign utility. However, it might get quite tricky in a CI environment, where you don’t have direct access to UI tools and password dialogues.

To give you a little bit more context, we’ll be signing a macOS distribution of Localazy CLI tool. If you’d like to know more how and why we’ve built it, you can check out “How we built Localazy CLI: Kotlin MPP and Github Actions” written by my colleague Václav.

At this point, I’m assuming that you have your app or binary already compiled in the environment CI and the last step missing is to sign it before release. Another prerequisite is an Apple Developer Program subscription. This allows you to obtain the necessary certificates, release to App Store and much more.

Let’s get started 🔗

We start by obtaining the certificate. After logging to your developer account and selecting Certificates IDs & Profiles, you should be able to create a new certificate. From all the listed types, select Developer ID Application as per its description.

To be able to obtain the certificate, you need to create a Certificate Signing Request (CSR) first, which you can easily get by opening Keychain Acces and going to Certificate Assistant -> Request a Certificate from a Certificate Authority

Fill in the necessary information and select your request to be Saved to disk. Note that the email address should be the same as the one you’re logging to the developer account.

You can then upload the CSR request file to the web which should successfully create a new certificate for you. Download it and add it to your Keychain Access by simply opening it. The certificate should be added to one of your default keychains and not to the system; otherwise you might later have troubles exporting it.

To be able to use the certificate for automated code signing, we need some format which would allow us to store the certificate as a string, so we can later add it to Github Secrets as an environment variable. For that purpose, we’ll make use of a little trick by encoding it to base64 first and then decoding it during the workflow. Let’s export the certificate by selecting both the certificate and its private key, invoke its context menu and select Export 2 items …. From the available formats pick Personal Information Exchange (.p12). Then it will ask you to create a password for it. Generate it and note it down, we’ll need it shortly.

Open your terminal and encode the certificate to base64, you can also copy it to the clipboard at the same time by running:

base64 Certificates.p12 | pbcopy

Go to your Github project and navigate to Settings -> Secrets where you can add new secrets. Create a new repository secret, I’ve called it MACOS_CERTIFICATE, and paste the encoded certificate. Create another secret name, for example MAOS_CERTIFICATE_PWD, where you store the certificate password you’ve created earlier.

If you’re not already using Github Actions to build your code, create a new workflow file .github/workflows/build.yml and add the following content to it.

name: Build and Sign macOS
on:
  push:
    tags:
      - '*'
jobs:
  macos:
    runs-on: macos-11.0
    steps:
      - uses: actions/checkout@v2

     # Install dependencies and build you app here #
     # - name: Build executable
     #     run: ---

      - name: Codesign executable
        env: 
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
        run: |
      
	# We will fill this part shortly

Let’s complete the script we need to run. First, we should decode the certificate back from base64 into a certificate file which we can import into the machine’s keychain.

echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12

Running codesign with a new certificate for the first time will greet you with an UI password prompt, which is a bummer here since it basically hangs the process in CI environment. To overcome that one prompt, we need to enter a series of commands which will unlock the certificate beforehand effectively skipping the password validation.

First, we need to create a new keychain. Under the -p argument, you should specify a new keychain password which will be used later. I use build.keychain as a name for it, but it can be anything which makes sense to you. The second step sets the keychain as default in the system.

security create-keychain -p <your-password> build.keychain
security default-keychain -s build.keychain

Next, we will unlock the keychain to avoid any prompts and import our decoded certificate into it. Notice the -P parameter, where we use the certificate password we earlier exported as an environment variable. The -T option enables this certificate to be accessed by the codesign utility.

security unlock-keychain -p <your-password> build.keychain
security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign

You can now verify that the certificate was added successfully by running security find-identity -v as a next command. This is especially helpful when looking for a problem in the CI logs.

This was used to enough to avoid the password prompt until macOS 10.12.5 with its new security mechanism called partition-list appeared. It is basically an access control list (ACL). When an application is not in this list, you’ll get the above prompt when it accesses a keychain item. Therefore, we need to add codesign to this list by doing so.

security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <your-password> build.keychain

Finally, this is the moment to run the codesign utility from within the CLI without any additional prompts and sign our application with the generated certificate. The identity id can be retrieved by executing security find-identity -v

/usr/bin/codesign --force -s <identity-id> ./path/to/you/app -v

Here’s the full configuration you should put in the Codesign task:

- name: Codesign executable
        env: 
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
        run: |
          echo $MACOS_CERTIFICATE | base64 —decode > certificate.p12
          security create-keychain -p <your-password> build.keychain security default-keychain -s build.keychain
          security unlock-keychain -p <your-password> build.keychain
          security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <your-password> build.keychain
          /usr/bin/codesign --force -s <identity-id> ./path/to/you/app -v

And that’s all, your app should now get successfully signed every time the Github workflow completes.

Reference 🔗

Code Signing Tasks

Scripting the macOS Keychain - Partition IDs — Most Like Lee

Localazy Software i18n – App Localization – Multilingual app

CLI friendly app localization

Make sure you do not miss this update. Whether it is iOS or TypeScript app, you can localize your app using brand new Localazy CLI.

Read more