Blog Post

So You Want to Code-Sign macOS Binaries?

How to get a certificate, the process of code-signing & notarization of macOS binaries for distribution outside of the Apple App Store.

So You Want to Code-Sign macOS Binaries? - How to get a certificate, the process of code-signing & notarization of macOS binaries for distribution outside of the Apple App Store.

Intro

As anyone who tried to code-sign Apple binaries can attest, the process is not for the faint-hearted. To make matters worse, doing so for distribution outside of their App Store is even worse. As anything with Apple that they are no longer interested in, the process is practically abandoned, it is barely documented, and asking for help at the Apple forum is close to futile. I spent some time trying to coax Apple code-signing echo-system to life and want to share my work in this blog post in case you need to do it as well.

I will concentrate my efforts on code-signing macOS binaries, as that is the only Apple approved way (as of today) of code-signing for distribution outside of the App Store. You can't use it with the iOS or any other apps that you may develop for the Apple devices, as those can only be downloaded from their App Store.

Jailbreaking Apple devices is outside of the scope of this blog post.

Contents

This post needs a table of contents:

Prerequisites

First and foremost the following are the requirements for you to code-sign your macOS binaries:

  1. Xcode - it may be possible to do it without Xcode. I didn't try it. This tutorial assumes that you will be using Xcode for building, code-signing and notarizing your binaries. I personally was using Xcode v.14.1.
  2. A Fairly Recent Mac Computer - since I'm using Xcode, and that IDE refuses to run on anything other than a Mac, I ended up using my Apple MacBook Air (with the Apple Silicon - M2 chip.)
  3. Apple Developer Account - it is needed to code-sign and notarize your binaries. It is a paid service, so expect to pony up the following to Apple on a yearly basis:
    • $99 (yearly) for an individual developer account.
    • $299 (yearly) for a business developer account, or for an account that can be used by multiple developers that are employed by an organization.
    You may ask, "Why do I need to pay for the Apple developer account when I am not posting my apps to their App Store?"

    This is a legitimate question. And I agree with that logic. Unfortunately Apple echo-system is a monopoly and we have no choice. They require you to pay and maintain your dev account to be able to code-sign and notarize your binaries that can be installed on their OS.

  4. Compiled Binary - obviously you need something to code-sign. This is usually a production (Release) binary that you are intending for distribution to the public.
Note that I have come across some companies online (usually CA's) that advertise their code-signing certificates for Apple platforms. I don't have specifics of what those certificates are for. But I can tell you with 100% certainty that you cannot use any third-party code-signing certificates to notarize your binaries for distribution outside of the Apple App Store. So don't fall for the CA upselling!

Why Bother?

The question that most people would ask, is "Why do I need to code-sign anything if my apps will never touch Apple App Store?"

It all boils down to a new strengthened security model, called System Integrity Protection (or SIP.) In regards to a binary file, if it was downloaded from the internet, and a user attempts to run it, the Gatekeeper in macOS will display this message:

Gatekeeper
Gatekeeper warning when attempting to run an unsigned CrashMe app.
"CrashMe" cannot be opened because it is from an unidentified developer.

macOS cannot verify that this app is free from malware.

Firefox downloaded this file today at 2:20 PM.
For code-signing examples in this post I will be using the CrashMe app that I showcased in my previous blog post.

Note that you will not see this warning, if you built your binary app using Xcode. This happens because the app that you built yourself does not have special extended attributes that are added to binary files that are obtained from an external source, such as the Internet.

To test your own Xcode-built binary with the Gatekeeper in macOS, upload it to an online file sharing service first, such as Google Drive, or Microsoft One Drive. After that download it using the web browser and try to run it.

Such warning is similar to what you would see on Windows, with one exception. Microsoft allows you to bypass it and run the binary in question by clicking a link in the message. While macOS flatly refuses to run it.

How to Run an Unsigned Binary on macOS?

To run an unsigned binary on a Mac you need to follow these laborious steps: go to macOS Settings -> Privacy & Security then scroll the list on the right until you see the Security section and click Open Away where you see the message: "CrashMe was blocked from use because it is not from an identified developer":

Gatekeeper
Gatekeeper options in macOS Settings.

Then type administrative account name and password, and OK another warning. And only after that the unsigned app will run.

As you can imagine most users will give up on the first step of this tedious process.

My guess is that Apple made it this laborious by design as users should not be running unsigned apps downloaded from the Internet.

You, as a developer of an app, obviously do not want to subject your users to doing all this. So the only way to bypass this rigmarole on a Mac is to code-sign and notarize your binary files.

Apple Dev Certificate

The first step of the process of code-signing your macOS binaries is to sign up and pay for the Apple Developer certificate. You can use your apple account to sign up, but first you need to choose your dev account.

There are basically two Apple dev account types: an individual and a business account. The main difference between them is how many developers can use the same certificate, and of course, the yearly charge.

A business (or an organization account, as Apple calls it) is more difficult to obtain and requires additional business verifications, as described in the Apple enrollment page.

After you enroll in the Apple developer program, you will need to wait before someone at Apple processes your submission.

It may take from a single day to several days, or even a week, to verify your account. So be patient. Apple will send you an email with the results.

As far as I know, it takes them longer to verify business accounts.

I am also not aware of possible reasons why Apple may deny the enrollment, aside from the obvious lack of provided documentation. So if anyone knows, please leave a comment below.

The money you pay for the initial enrollment to Apple is nonrefundable.

You may also reactive your previously expired Apple dev account. I did just that with mine. It took them about two days to re-activate it.

How to Download & Install Needed Apple Dev Certificates

Once you received your Apple dev certificate, you need to download it to the Mac where you will be using it to code-sign your binaries. This is usually the Mac that you use to build those binaries with the Xcode.

Follow these steps:

  1. Before you can do anything you need to create a certificate request.
  2. On a Mac that you will be using for code-signing start the Keychain app.
    You can find it in Applications -> Utilities -> Keychain Access.
  3. In the Keychain app, go to Keychain Access -> Certificate Assistant -> Request a Certificate From a Certificate Authority. Then specify your email address and set Request is to Saved to disk:
    Keychain Access
    Generating Certificate Request in the Keychain Access app.
  4. After you click Continue and provide the location where to save the certificate request file, click OK to create it.
  5. Steps above will generate a .certSigningRequest file that you will use later.
  6. Log in to your Apple developer account using the web browser.
    Apple are notorious at changing the UI and URLs of their web resources. Thus I will not be posting any screenshots or links here, as they are most certainly going to change in the future.
  7. Once logged in to the dev portal, click on Certificates, that should present you with a list of all your previously obtained code-signing certificates.
  8. Click the Plus Button to add a new certificate, and select Developer ID Application to request a new code-signing certificate for binary apps, and click Continue.
  9. Then specify a newer G2 Sub-CA certificate and click an upload link to upload your previously created .certSigningRequest file with the certificate request.
  10. Click Continue to generate a new code-signing certificate.
    Big warning here. Due to some incomprehensible reasons the Apple's dev portal does not allow to remove (or revoke) a previously issued certificate. My guess is that you need to call them to revoke it. So keep this in mind and don't just create test certificates willy-nilly as you won't be able to remove them.
  11. When done, the Apple dev portal should allow you to download the newly generated code-signing certificate, as a file with the .cer extension, usually named developerID_application.cer.
    Rename this file into something more memorable, and keep it in a safe place. This is one of the code-signing certificates that you will need to code-sign your binaries for macOS.
  12. Then import downloaded .cer file into the Keychain. For that open the Keychain app, unless it's already open, and highlight the login line in the left pane:
    Keychain Access
    Where to import the code-signing certificate into the Keychain Access app.
    If you don't do this, and try to import your code-signing certificate, you may get the following error:
    An error occurred. Unable to import "Developer ID Application: Developer Name (Certificate-ID)".

    Error: -25294

    Very helpful error message, right. 🤦‍♂️

  13. Switch to the Certificates tab in the Keychain app, and drag-and-drop your downloaded .cer file with the certificate into the main list in the Keychain app. It should show a new entry in the list.
    In my case the Keychain app was kinda buggy, so I had to quit it first, and then restart it to show my imported certificate in the list.
  14. Locate imported certificate in the Keychain app and ensure that its status is shown as "This certificate is valid":
    Keychain Access
    Ensure that your imported certificate is valid in the Keychain Access app.
    In case the Keychain app shows the following error message for your imported certificate:
    Keychain Access - certificate is not trusted
    "Developer ID Application: Developer Name (Certificate-ID)" certificate is not trusted.

    You may try this to resolve it as such:

    • Download the following certificates from the Apple PKI store: Note that there's no one-fits-all rule to fix this bug. What the error message is telling you is that some of the certificates in the chain of certificates for the one in question is not installed on your Mac.

      Alternatively, you may want to check your Mac's date and time to be correct, as a system date that is way off may result in one or more root certificates to appear expired.

      Finally, let me finish by saying that the links that I gave above may stop working in the future. In that case get the current ones from the Apple PKI store, or search for it on Google.

    • Import one, or more of the certificates above into the System or System Root keychain of the Keychain app and see if the error goes away. It may be that importing just one of the root certificates that I listed above will solve the issue.

      But, if importing all of them did not resolve the issue, try to import specific certificates that you see in the trust-chain for your certificate.

    • Restart the Keychain app.

How to Get the Installer Certificate

In case your app is distributed via an installer package (or a file with the .pkg extension) you need to obtain an additional code-signing certificate for an installer.

Don't ask me why. 🙄

The steps are very similar to the ones that I described above, so I'll go over them quickly:

  1. From the Apple developer portal, click Certificates.
  2. Click the Plus Button to add a new certificate, and select Developer ID Installer, and click Continue.
  3. Then specify a newer G2 Sub-CA certificate and click an upload link to your previously created .certSigningRequest file with the certificate request. You may reuse the old request file created earlier.
  4. Click Continue to generate a new code-signing certificate.
    Same warning goes here as well. Once you click continue and Apple Dev portal will create a certificate. There will be no option in the UI to delete, or revoke it. So create a certificate only if you need it!
  5. Download a newly created installer certificate to your Mac. You should get another .cer file. It my case it was named: developerID_installer.cer
  6. Then import the .cet file into the Keychain app, as I showed above and ensure that the Keychain shows your imported certificate as valid.

At this point you may delete all .cer files that you downloaded from the Apple dev portal from your local computer. By importing them into the Keychain you have encrypted them for your local use.

Deleting .cer files from your local computer is a good security practice, as they contain private keys that can be used to code-sign binaries on your behalf. So do not let those files lay around unattended!

How to Obtain Developer Certificate ID

And finally, one last step before we can code-sign anything, we need to obtain the developer/user ID from your freshly downloaded certificate. Follow these steps:

  • Open the Keychain app and switch to the Certificates tab for the login keychain on the left.
  • Right-click on your code-signing certificate in the list and select Get Info. It can be either Developer ID Application or Developer ID Installer certificate that bears your name, or the name of your organization.
  • In the new window, copy the User ID field from the Details section. It will be your developer ID that you will need later:
    Keychain Access - Developer ID
    Developer ID shown in the Keychain Access app.
    Your developer ID will be a random string of letters and numbers. Something similar to: Z7C8MMR1NR

What is Code-Signing & Notarization

I've been talking about code-signing and notarization all along. But what are those things? And why do we need them?

Code-signing seems to be more straightforward and it is used not only by Apple. In a nutshell, when we code-sign a file we ensure that any modifications to that file can be caught using cryptographic algorithms that make it computationally infeasible to forge such assurance.

On macOS if at least one bit in a binary file is changed since it was code-signed, the operating system will refuse to run such a file. This is a very good way to make sure that the binary file that you're running on your system is not changed in any way since the moment that its developer code-signed it.

Notarization on the other hand is a purely Apple's invention. (You can get the details here.) In a nutshell, it is the Apple's way to make developers upload the code-signed binaries to their servers for Apple to fingerprint them. After notarization completes, you will get back a small file that is called a ticket, that has to be later attached (or stapled) to the original binary that you were notarizing.

If you want me to translate it into English, then by making you notarize your binaries, you let Apple collect hashes for all programs that may potentially run on their macOS. And, if one of those binaries happens to be a malware or, if Apple decides to kill that binary for whatever reason, they can single it out using unique file hash and their Gatekeeper, and stop it from running on their macOS.

If you think about it, this is a clever way to stop the spread of unwanted software with the least amount of work. Plus, this is a guaranteed way to pinpoint a developer that could've created such software.

How to Code-Sign macOS Binaries

Now that we installed the code-signing certificates in the local Keychain, and obtained your developer certificate ID, we can start code-signing binary files.

Xcode provides its own automated code-signing of the binaries during archiving. I won't be rehashing it, since Apple already documented it. I will concentrate on code-signing via a command-line, that can be integrated into your automated build script.

The command line call to code-sign a binary file could be one of the following, using the codesign tool:

  • To sign without the use of the entitlements:
    Bash[Copy]
    codesign -s "<Developer_ID>" -f --timestamp -o runtime -i "<Bundle_ID>" "<path/to/binary>"

    Where:

    • <Developer_ID> - developer (or user) identifier that you obtained earlier. This value will not change (unless you renew your certificate) so you can hard-code it in your script.
    • <Bundle_ID> - unique bundle identifier for the app. It is usually presented in a reverse-DNS format. Example: "com.dennisbabkin.crashme"
    • <path/to/binary> - is the path to the binary to code-sign. Upon successful result this file will be overwritten with the one containing a code-signature.
  • To sign using the entitlements:
    Bash[Copy]
    	codesign -s "<Developer_ID>" -f --timestamp -o runtime -i "<Bundle_ID>" --entitlements "<path/to/file.entitlements>" "<path/to/binary>"

    The command line parameters are similar to what I described above, except for:

    • <path/to/file.entitlements> - is the path to an entitlement file. Such file is used to contain request for special permissions to run the app.

      Here's an example of one such file:

      .entitlements file[Copy]
      <?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>
      		<key>com.apple.security.files.user-selected.read-write</key>
      		<true/>
      		<key>com.apple.security.files.bookmarks.app-scope</key>
      		<true/>
      	</dict>
      </plist>

      The example above requests two entitlements to be enabled for the app that is being code-signed: com.apple.security.files.user-selected.read-write and com.apple.security.files.bookmarks.app-scope.

Both command lines above require an active internet connection to succeed.

The codesign tool will return 0 upon success, or any other error code in case of a failure.

Note that both command line examples above use the --timestamp -o switch, that will also time-stamp the code-signed binary. This is a very important step that will ensure that the code-signature remains valid after expiration of the certificate that was used for signing.

How to Code-Sign macOS .pkg Installer

Like you've probably noticed above, the .pkg installers are treated differently by Apple. To code-sign a .pkg file you will need a separate code-signing certificate and a different command line call:

Bash[Copy]
productsign --sign "Developer_ID" "path/to/installer_to_sign.pkg" "path/to/signed_installer.pkg"

Where:

  • <Developer_ID> - same developer ID that we used to code-sign other binaries. We obtained it earlier.
  • <path/to/installer_to_sign.pkg> - path to the .pkg installer file to sign.
  • <path/to/signed_installer.pkg> - path to the .pkg file to create after signing. Note that it must be a different file than the original.
For details on using the productsign tool check its manual.

How to Notarize macOS Binaries

Now that we've code-signed our binaries, let's notarize them. (I explained above why it is needed.) Without notarization your code-signed binaries will not run on the latest versions of macOS.

All work for notarization is performed by xcrun notarytool. Use the following command line:

Bash[Copy]
xcrun notarytool submit "<path/to/notarize>" --keychain-profile "<AppPwdKeychainID>" --wait

Where:

  • <path/to/notarize> - path to the file to notarize. This could be a single file, a .pkg file, or a .zip archive.
    I would advise to pack all your binaries that may be needed for notarization into a .zip archive. Alternatively, you may use the original .pkg installer. Apple notarization servers know how to unpack those.
  • <AppPwdKeychainID> - Keychain ID for the app-specific password to access your Apple ID. Apple provides this tutorial on how to get it.

    Or, follow these steps to obtain it:

    1. Log in to your apple account using a web browser. Use the same Apple ID as you used for the Apple dev account.
    2. Click on "App-Specific Passwords" and then click the plus-button to add a new entry.
    3. Give your new app-specific password a name, for instance, "AppNotarization" and click "Create". The server may ask you to confirm your Apple ID password.
    4. When completed, the server will briefly display the auto-generated password. Make sure to copy and paste it somewhere on your computer, as it won't be shown again!
      App-Specific Password
      Auto-generated app-specific password.
      If you lose your generated app-specific password you will need to revoke it and then create a new one.
    5. Next save your auto-generated app-specific password in the Keychain app on the Mac that you will be using for notarization.

      For that we will use the xcrun notarytool store-credentials command line call. (Apple described the process here.)

      It boils down to basically calling this:

      Bash[Copy]
      xcrun notarytool store-credentials "<AppPwdKeychainID>" --apple-id "<AppleID>" --team-id "<WWDR_TeamID>" --password "<AppSpecificPwd>"

      Where:

      • <AppPwdKeychainID> - some unique ID that you may want to use to refer to this app-specific password on that local computer. It may be known publicly. Example: "AppPwdNotarizID"
      • <AppleID> - Apple ID for the dev account. It is usually an email address.
      • <WWDR_TeamID> - to get this value run the following in the terminal on the same local computer:
        Bash[Copy]
        xcrun altool --list-providers -u <AppleID> -p "<AppSpecificPwd>"

        Where:

        • <AppleID> - same as above, Apple ID for the dev account.
        • <AppSpecificPwd> - plaintext app-specific password that you auto-generated above.

        After you run the altool command, it should return the WWDR_TeamID value for you. Copy-and-paste it into the store-credentials command call above.

      • <AppSpecificPwd> - plaintext app-specific password that you auto-generated above.

      As usual, the xcrun command will return 0 upon success, and any other code in case of a failure.

      I created a short Bash script that can do all this for you. (You can download it from my GitHub.) Before running it though, make sure to change the app-specific password and the WWDR_TeamID value in it first.
    6. At this point you can safely remove the plaintext auto-generated app-specific password from wherever you saved it. It is now safely stored in your local Keychain.
  • --wait - this switch will ensure that the notarytool waits for a response from the Apple server with the result of the notarization.
    Keep in mind that this command may block the execution indefinitely. At times it may take a long time to complete. Expect anything from a minute to up to 30 minutes and more to get a response.

    If you want to receive the output from the notarytool while it waits, if you specified the --wait switch, include the --verbose switch as well.

Notarization Status

If you specified the --wait switch the notarytool will return a text output after it receives a response from the Apple server. This blog post is definitely not a comprehensive tutorial on the notarytool output. (Refer to the Apple documentation for more details.)

From my personal experience, that tool can return two most common results upon expiration of the wait:

  • Failure, if you did not provide some parameters. Here's an example:
    Notarization Failure[Copy]
    Conducting pre-submission checks for NotizationFile.zip and initiating connection to the Apple notary service...
    Submission ID received
      id: 741ce82f-de27-48d0-813d-fbe2343c8e1c
    Upload progress: 100.00% (1.26 MB of 1.26 MB)
    Successfully uploaded file
      id: 741ce82f-de27-48d0-813d-fbe2343c8e1c
      path: /Users/user/Documents/NotizationFile.zip
    Waiting for processing to complete.
    Current status: Invalid.......
    Processing complete
      id: 741ce82f-de27-48d0-813d-fbe2343c8e1c
      status: Invalid
  • Success, which means that the file was notarized and now we can retrieve the ticket for it. Here's an example:
    Notarization Success[Copy]
    Conducting pre-submission checks for NotizationFile.zip and initiating connection to the Apple notary service...
    Submission ID received
      id: 741ce82f-de27-48d0-813d-fbe2343c8e1c
    Upload progress: 100.00% (720 KB of 720 KB)
    Successfully uploaded file
      id: 741ce82f-de27-48d0-813d-fbe2343c8e1c
      path: /Users/user/Documents/NotizationFile.zip
    Waiting for processing to complete.
    Current status: Accepted.......
    Processing complete
      id: 741ce82f-de27-48d0-813d-fbe2343c8e1c
      status: Accepted

How to Retrieve Notarization Log From the Apple Server

After notarization succeeds, or fails, the Apple server creates a log file with details of the process.

To retrieve the notarization log use the following command:

Bash[Copy]
xcrun notarytool log "<NotatizationGUID>" --keychain-profile "<AppPwdKeychainID>" "<path/to/save/log_file>"

Where:

  • <NotatizationGUID> - notarization GUID that was received from the notarytool submit call under the id parameter. (In the example above, it will be 741ce82f-de27-48d0-813d-fbe2343c8e1c.)
  • <AppPwdKeychainID> - Keychain ID for the app-specific password that we set up earlier.
  • <path/to/save/log_file> - is the path on the local computer where you want the tool to save the log. The resulting log will be a text file with the results of notarization. Here's an example:
    Notarization Log[Copy]
    {
    "logFormatVersion": 1,
    "jobId": "741ce82f-de27-48d0-813d-fbe2343c8e1c",
    "status": "Accepted",
    "statusSummary": "Ready for distribution",
    "statusCode": 0,
    "archiveFilename": "NotizationFile.zip",
    "uploadDate": "2023-07-02T14:47:32.385Z",
    "sha256": "75fda9029671347bd77d891e1a7fa6ede6b88553ba01fadf73686fc903a0f8d4",
    "ticketContents": [
    	{
    	"path": "NotizationFile.zip/demo.pkg",
    	"digestAlgorithm": "SHA-1",
    	"cdhash": "6fe153a26e0468fb2bd490bc71b8dbe4afb11ad3"
    	},
    	{
    	"path": "NotizationFile.zip/CrashMe.app",
    	"digestAlgorithm": "SHA-256",
    	"cdhash": "0e5c1ae2b09c3291353700b1c70658e5a66c4f22",
    	"arch": "x86_64"
    	},
    	{
    	"path": "NotizationFile.zip/CrashMe.app",
    	"digestAlgorithm": "SHA-256",
    	"cdhash": "52ced4ab54ab54174ea6127db179a89cbdfdba40",
    	"arch": "arm64"
    	},
    	{
    	"path": "NotizationFile.zip/CrashMe.app/Contents/MacOS/CrashMe",
    	"digestAlgorithm": "SHA-256",
    	"cdhash": "0e4c1ae2b09c3291553700b1c70658e5a66c4f32",
    	"arch": "x86_64"
    	},
    	{
    	"path": "NotizationFile.zip/CrashMe.app/Contents/MacOS/CrashMe",
    	"digestAlgorithm": "SHA-256",
    	"cdhash": "15ced4ab54ab54a74eab367db176a89cbdfdb3b0",
    	"arch": "arm64"
    	}
    ],
    "issues": null
    }

    A notarization log is helpful to diagnose any potential issues with the notarization.

My advice would be to save the notarization log along with the backup of your source code files and of the release build of your project.

How to Staple Binaries After Notarization

Stapling is another Apple invention. This is basically attaching a small identification hash (that Apple calls a ticket) to the binary file that was successfully notarized. This is what allows Gatekeeper to let your binary through when a user runs it. So stapling is an important last step you need to do.

Unfortunately though, not every binary can be stapled. Let me explain.

When stapling happens, the stapling tool needs to insert a stapling ticket into the binary. Some formats used by macOS are easy to do it with. For instance, the App Bundle that most Apple apps come packaged as, at a low-level is just a directory with the .app extension. So adding a small file into it is not a problem.

The issue happens with the Mach-O file, which is just a single binary file. It wouldn't be a problem for the stapling tool to add some binary data to it, if it was not code-signed. By altering even a single bit in it the stapling tool will break the code signature.

Because of that limitation, the stapling of Mach-O binaries is not supported. But there's a workaround that I will show later.

Use the following command to staple your app:

Bash[Copy]
xcrun stapler staple -q "<path/to/app_to_staple>"

Where:

  • -q - this switch makes the stapler tool run in a quiet mode. If you experience errors during stapling, remove this switch.
  • <path/to/app_to_staple> - path on the local computer to the app to staple. It will be updated upon success. (As I described above, stapling of the Mach-O binaries is not supported.)

To the best of my knowledge the stapling tool supports stapling app bundles, as well as the .pkg installers.

Finally, after your application has been stapled, it becomes suitable for the production distribution to your customers that may now download it from the internet. When they try to run it on their macOS they will see the following warning:

Gatekeeper
macOS Gatekeeper warning when attempting to run a code-signed and notarized CrashMe app.

If the user clicks Open, the Gatekeeper will allow running the app and the warning above will never be shown again for that specific binary.

The information about whether or not to show the Gatekeeper warning is stored in the undocumented com.apple.quarantine extended attribute for the binary file.

A Workaround to Staple Mach-O Binaries

As I described above stapling the Mach-O binaries is not possible because of their prior code-signature. On the other hand, stapling is the only way to let Gatekeeper run your binary on macOS. So is it a catch-22?

Well, no. There's a workaround. I personally use the following steps to pack my Mach-O into a simple app bundle, which allows stapling:

  1. You can create your own app bundle app with Xcode, or use my template.

    To create your own app bundle in Xcode, go to File -> New -> Project -> select macOS and create a project from an App template. This will give you a basic structure of the app bundle after your build the app in Xcode.

  2. Right-click on the app bundle file in Finder (this will be a directory with the .app extension), and select Show Package Contents. This will open it as if it was a regular directory.
  3. Go to Contents and delete CodeResource, if it's there.
  4. Also remove everything from the _CodeSignature directory.
  5. Then remove all files from the Resources directory, except for the AppIcon.icns file, which is your app bundle's icon. (You can remove it if you don't want it to have your custom icon.)
  6. MacOS directory should contain your binary file. For the sake of this example, I'll use my CrashMe binary. (Make sure that you chmod +x CrashMe to make it executable.)
  7. Finally, we need to adjust the Info.plist file for our contents. Open it in Xcode to edit it out:
    • Executable file - must bear the name of your binary executable in the macOS directory. In my case it should be CrashMe.
    • Bundle identifier - is the bundle ID for the app in reverse DNS format. For instance, com.dennisbabkin.CrashMe.
    • Bundle name - is some arbitrary name for the app bundle. In my case, CrashMe app.

    You can leave the rest of the fields to what they were originally, or make additional adjustments if you need to modify the app bundle plist more. (Refer to the Apple documentation for more info.)

Now your Macho-O binary can be run by double-clicking on the app bundle that we created above. And, best of all, you can now upload that app bundle to the Apple servers for notarization and later use it for stapling.

Note that a Command Line Tool app that is started from an app bundle will not have a terminal window shown for it by default.

This is an example with my CrashMe app, that was built from a command line tool template. Since my CrashMe app expects an input from the user and blocks until it receives it, the lack of the terminal window will result in it waiting forever. So keep this mind.

In case of my CrashMe app that is started from an app bundle, its icon will continue jumping in the macOS dock. To close, you will need to force-quit it. In that sense, my CrashMe app wasn't the best example to show a technique of running a Mach-O binary from an app bundle. I used it just to illustrate the idea.

In case you are dealing with the launch daemon or a launch agent and stapling, you can place those into an app bundle as well. In my experience though, macOS does not seem to care about their stapling, for as long as the .pkg installer for them is code-signed and stapled.

Automated Script to Code-Sign, Notarize, Pack and Backup

That was a lot, wasn't it? To make the process of preparing macOS binaries for distribution outside of the App Store more streamlined & robust I wrote a small Bash script that should automate the process for you. You can find it on my GitHub.

The script does the following:

  • It builds the CrashMe project using a Release (production) build scheme with the xcodebuild tool.
  • It creates an app bundle, as I showed above (to allow stapling.)
  • It code-signs the resulting binaries.
  • It uploads the signed app bundle for notarization to the Apple servers and waits for the result.
  • When notarization finishes, it downloads the notarization log and saves it in the CrashMe project.
  • It staples the notarized app bundle.
  • It then creates a .DMG image with the code-signed app bundle, that can be used for distribution.
  • Finally, it creates a .DMG image with a full backup of the source and of the binary files produced during this build.
An Xcode is required for the script that I described above to work.

Note that before you can use this automated script, make sure to open it in Xcode, or in some other text editer that doesn't add formatting, and change the following parameters on top of the file:

  • cs_ident - is your developer certificate ID. I showed how to get it here.
  • kch_app_pwd_id_ntrz - Keychain ID for the app-specific password in your local Keychain. I showed how to get it here.

When you run the build-release-sign-backup.sh script, keep in mind that it may take a while to finish since it will have to upload your built binaries to the Apple's notarization servers and wait for the response.

Bash script
Result of running the build-release-sign-backup.sh Bash script.

Upon a successful result look for the confirmation that the work is "All Done". The resulting binaries and the backup of the source code should be highlighted by the build script.

Conclusion

In despite of a relative complexity of the process that I showed above I can attest that from a user's perspective macOS is a much more secure operating system than Windows. And coupled with the extra security restrictions added to code running on the Apple Silicon, with its more stringent code-signing requirements, and with a virtual inability for third-party developers to code-sign and run kernel extensions - all this makes modern versions of the Apple operating systems quite formidable security fortresses.

What I showed above, and in my three previous blog posts [1], [2], [3] about macOS, all this is just a tip of the iceberg of that complex and closed-off operating system.

For now though I will stop my foray into macOS and in the next blog post I'll switch back to Windows. But if you feel like you want to know more about the Apple operating systems and their internals, don't hesitate to leave a comment below.

Related Articles