How to automatically sign macOS apps using GitHub Actions

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.

![](upload://7Yult1GOj8Cxywzmg0YBwT6ETBD.png)

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

![](upload://rDkQKlUgdRDqkxrePlyAXk3WKuZ.jpeg)

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.

![](upload://s3tVcaERPKz2wsu98D0AwtYU9mj.jpeg)

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.

![](upload://gA2bcehzZJ6UI5COz6KpAefNyo4.jpeg)

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.

![](upload://ugCAJRm6gohejHUsdksuDN2Mu36.jpeg)

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.

![](upload://4F6wl71OYNyDFH3oWfjQSmtiQmJ.png)

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/[email protected]
     # 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


This is a companion discussion topic for the original entry at https://localazy.com/blog/how-to-automatically-sign-macos-apps-using-github-actions

Question. As a malicious agent, could I not create a fork of a public repo, modify the workflow and then do something like:

MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
echo ${MACOS_CERTIFICATE_PWD}

Then, if that workflow is set to automatically build any PR, the password would be leaked? I guess we should control this to only run on push to master and be very paranoid about it.

EDIT:
Via a PR I could also modify the scope over which github CI is executed and force it to run.

Hey @Nikolay_Bogoychev,
Good question!
Github designed its Actions with security in mind, and what you’re suggesting is not possible.

Secrets are unavailable in public repositories during pull_request and forked workflows. Moreover, they never appear in the build logs.

Nevertheless, it’s always required to be cautious when working with secrets, esp. in public repositories.

Someone already did a nice summary on what to be careful about in this StackOverflow answer:

Thanks for your quick reply and the referenced reading!
This is all very useful indeed, i just want to make sure everything is secure before using it. You can’t actually “unleak” a private key or a certificate once you leak it xD.

1 Like

Hi again @janbilek . I attempted to use this in my workflow:

    - name: Produce DMG and Sign # This overrides the previous dmg # We follow https://localazy.com/blog/how-to-automatically-sign-macos-apps-using-github-actions
      working-directory: ${{github.workspace}}/build               # it should be secure as those can't leak unless we accept a PR with a bad workflow.
      shell: bash
      env: 
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PST: ${{ secrets.NOTARIZE_PST }}
          APPLE_DEVELOPER_ID: ${{ secrets.APPLE_DEVELOPER_ID }}
      run: |
            echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
            security create-keychain -p 1234asdf my.new.keychain security default-keychain -s my.new.keychain
            security unlock-keychain -p 1234asdf my.new.keychain
            security import certificate.p12 -k my.new.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
            security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k 1234asdf my.new.keychain
            ../dist/macdmg.sh ${{github.workspace}}/build

And I’m getting a weird error about the keychain name already existing. I tried using the previous one (the one you suggested in your blog post) but I got the same error:

Run echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
  echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
  security create-keychain -p 1234asdf my.new.keychain security default-keychain -s my.new.keychain
  security unlock-keychain -p 1234asdf my.new.keychain
  security import certificate.p12 -k my.new.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
  security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k 1234asdf my.new.keychain
  ../dist/macdmg.sh /Users/runner/work/translateLocally/translateLocally/build
  shell: /bin/bash --noprofile --norc -e -o pipefail {0}
  env:
    BUILD_TYPE: Release
    qt_version: 6.2.1
    ccache_basedir: /Users/runner/work/translateLocally/translateLocally
    ccache_dir: /Users/runner/work/translateLocally/translateLocally/.ccache
    ccache_compilercheck: content
    ccache_compress: true
    ccache_compresslevel: 9
    ccache_maxsize: 200M
    ccache_cmake: -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache
    pythonLocation: /Users/runner/hostedtoolcache/Python/3.10.0/x64
    PKG_CONFIG_PATH: /Users/runner/work/translateLocally/translateLocally/qt/Qt/6.2.1/macos/lib/pkgconfig
    Qt6_DIR: /Users/runner/work/translateLocally/translateLocally/qt/Qt/6.2.1/macos
    QT_PLUGIN_PATH: /Users/runner/work/translateLocally/translateLocally/qt/Qt/6.2.1/macos/plugins
    QML2_IMPORT_PATH: /Users/runner/work/translateLocally/translateLocally/qt/Qt/6.2.1/macos/qml
    CCACHE_COMPILER_CHECK: content
    CCACHE_BASEDIR: /Users/runner/work/translateLocally/translateLocally
    CCACHE_COMPRESS: true
    CCACHE_COMPRESSLEVEL: 9
    CCACHE_DIR: /Users/runner/work/translateLocally/translateLocally/.ccache
    CCACHE_MAXSIZE: 200M
    MACOS_CERTIFICATE: ***
  
    MACOS_CERTIFICATE_PWD: ***
    APPLE_ID: ***
    APPLE_PST: ***
    APPLE_DEVELOPER_ID: ***
security: SecKeychainCreate my.new.keychain: A keychain with the same name already exists.
Error: Process completed with exit code 48.

Any ideas?

Thanks,

Nick

Do you have access to the machine the action is running on? Or do you spin up a clean instance for each run (like default macOS runner)?

I don’t have a complete context, to be able to tell what’s happening, but to me, it seems that it runs repeatedly in the same environment i.e. the keychain was already created in some earlier run.

To resolve that, you have multiple options.

You can just remove the lines where the keychain is being created and it should work just fine:
security create-keychain ....

Or if you want the action to be less dependent on the environment, you can try to add a check whether the keychain already exists and run the commands only in that case.

@janbilek I got it to work. The issues is that you have copy/pasted two lines as one:

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

And my unfamiliarity with macos is not helping here.
In the final configuration bit. Thank you for helping me with it!

Cheers,

Nick

1 Like

Ah, I haven’t noticed it either, let’s blame the side-scroll :grin:
It’s a weird error though. I’m happy you made it work!

There is an error in the full code block at the end of this article. The following should be split into two lines.

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

like so

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

Hello and welcome @dreamingwell!
Thank you for letting us know. We’ve fixed the formatting and scheduled the article to update.