Mac Notarization (and GHC)

I spent a month looking into notarizing GHC for Macs. Although GHC still won’t be notarized for 9.6—more on that later—I did learn a lot about it, and I’d like to share that knowledge for posterity. If you are looking to distribute (Haskell) apps on Macs, this may be useful to you! I will also explain how GHC currently deals with the notarization requirement and hint at next steps.

My main source for notarization knowledge was Apple Developer Documentation, which is fairly complete.

What is Mac notarization?

Notarization is a security mechanism on macOS. Developers submit their apps to an Apple service that checks for malware and certain other issues. If the checks pass, Apple will provide a certificate for the app. This certificate is checked by macOS when the app is run. Although it’s possible to run un-notarized apps, Apple recommends against it, and recommends that developers get their apps notarized!

Getting apps notarized

Apple provides a notarization service to developers. The service notarizes app bundles, installer packages, or zip files that contain executables. (See glossary below.) To be notarized, the executables within must be code signed with the Hardened Runtime enabled.

I have a prototype script that notarizes a GHC bindist, where you can see the actual commands used to get an app notarized.

Checking notarization

Notarization is checked in two ways. The first is the App Store. An app must be notarized before it can be uploaded for distribution.

If you distribute your apps to users directly over the internet, however—as GHC does—there is another mechanism. (Some details in this section may still be wrong, but I have the gist of it correct. Corrections welcome!) Safari and other apps automatically make a notarization check when downloading files. Downloads that fail the check will have a ‘quarantine’ attribute applied to them, which applies recursively to the contents of archive files. Quarantined files cannot be executed unless the user makes manual security changes to their system.

Notarization certificates are stored on Apple servers, and macOS will make a network call to verify an app if necessary. To avoid this, developers can directly attach the certificate to their apps.

The notarization check is not very robust to user intervention. Notably, curl does not make a notarization check: Files downloaded with curl will not be quarantined, bypassing the whole scheme. You can also manually clear the quarantine attribute after a failed notarization check, or just cat <evil.zip >saintly.zip to conjure a new file with no quarantine attribute.

How does GHC handle notarization?

Currently, GHC deals with notarization in the following ways:

  1. For users who download a bindist with Safari, the install script automatically clears the quarantine attribute on all files. Thus, although the original download fails the notarization check, the quarantine is removed and GHC works as usual.
  2. Unlike Safari, GHCUp does not trigger a notarization check.
    • GHCUp further attempts to remove the quarantine attribute. I have not checked, but I think this has no effect.
    • The underlying GHC install script still also clears the quarantine attribute, as just mentioned.
  3. I have not checked, but I assume Stack also does not opt in to a notarization check.

What does the future hold for GHC and Mac notarization?

GHCHQ wants to notarize GHC bindists. Once this is done, bindists downloaded with Safari will pass the notarization check and not be quarantined. The primary motivation is the concern that Apple may suddenly lock down execution even further in unexpected ways. By notarizing bindists, GHC is somewhat closer to the standard workflow for Mac apps. This would also allow removing the attribute-clearing steps, and even pave the way for GHCUp to opt in to triggering a notarization check.

Wasn’t this scheduled for 9.6?

Yeah, that’s my bad. I messed up in communicating how things were going.

So how did it go?

For one thing, it took me a long time to dig into the Apple development experience. I even bought my first Mac to get hands-on experience both as a developer and user. I also struggled to understand the problem being solved and how to reach the goal in a way that satisfied my principles. There are a lot of outstanding concerns:

  • The cost of automation, particularly testing the notarization for efficacy
  • The limited scope of use cases that need notarization
  • Notarization requires enabling the Hardened Runtime, which needs to be tested to see how it interacts with GHC plugins and similar use cases
  • Opportunity cost versus redesigning how GHC is packaged for Mac in the first place
  • The ease in bypassing notarization checks
  • The need for cross-project collaboration. This is not just a GHC issue (see discussion below).

By the time I finally reached my recommendation against notarizing the existing bindist, the schedule was too tight to have a full discussion. The concern about further security requirements from Apple is valid, however, and notarization will be revisited for 9.8.

Hopefully that was useful information about Mac notarization!


GLOSSARY

App:

  • In Apple documentation, this term usually means a product built for end users and distributed as an app bundle, ideally through the App Store.
  • Other methods of distribution are officially blessed, however, and the term is obviously highly overloaded.

App bundle:

  • A distribution method for Mac applications.
  • A directory with a particular file structure and a name that ends in “.app”.
  • Treated as a single file by Finder (but not the shell).
  • Double-clicking on it will execute the application within.
  • Contains metadata and resources for the app.
  • Appears to have been replaced by installer packages for a while, but are once again the primary way to distribute applications.

Bindist:

  • A term used in GHC (not related to Apple).
  • An archive file containing the compiler, a few other utilities like haddock, core libraries, and some scripts for installing the files.
  • Similar in structure and purpose to an Apple-specific “installer package” (see above).

Code signing:

  • A mechanism for validating that files have not been tampered with.
  • Uses public key cryptography and a certificate chain rooted at Apple.
  • Acquiring a signing key requires a (free) account and some personal information.
  • In certain circumstances, the signature for a binary file is checked by macOS before the file is executed.

Hardened Runtime:

“The Hardened Runtime… protects the runtime integrity of your software by
preventing certain classes of exploits, like code injection, dynamically linked
library (DLL) hijacking, and process memory space tampering.”

Installer package:

  • An archive file designed for use with the macOS Installer.
  • May contain files to be installed, metadata, other resources, and scripts that are run by the Installer automatically.
  • Also known as a “flat package”.
  • Replaced app bundles at some point, but only for a short while.
  • Further reading OSX flat packages — docosx 0.2 documentation
9 Likes

So I guess this means for distributors (like GHCup) the best thing is to just keep using xattr -r -d com.apple.quarantine <path> no matter if it’s notarized or not?

On the contrary, since GHCUp does not opt in to quarantining bindists, I would be curious to learn if it ever needed to use xattr in the first place. It’s not something I tried yet.

In the long term, I speculate we may want to opt the whole Haskell ecosystem into notarization. If we go that path, rather than circumventing the notarization check, GHCUp would enforce it.

In the short term, it’s ain’t broke so let’s not fix it.

Edit: this was supposed to be a reply to @hasufell but I clicked the wrong button.

There are multiple tickets where users needed to use xattr to make GHC bindists work:

There seems to be an issue with stripping binaries as well:

So I don’t have clarity over the issue, still.

1 Like

Those GHC issues are both closed. The first is only tangential to code signing and notarization, and the second was dealt with in the way I’ve described above: the install script in the GHC bindist removes the quarantine attribute (assuming it exists) with xattr.

The GHCUp issue is more interesting and it would be good to try to reproduce it at some point. I found it difficult to trigger any warnings unless I was downloading something with Safari.

1 Like

Which GHC versions? Is this backported to 8.0.2?

GHCup cannot easily assume bindist fixes exist. We don’t just support installing the latest GHC version, which I think is sometimes easily forgotten.

Additionally we support users installing bindists from source and feeding them into GHCup.

I’ve yet to see a clear outline when it’s needed and when not and how tools would be able to discover when that’s the case.

1 Like

As a Mac user, Thank you for digging into this @chreekat ^^

Apple’s “walled garden” strategy is distinctly hostile to open source.

3 Likes

I think nothing should be changed in GHCUp right now, precisely because I know that GHCUp supports many versions of GHC besides HEAD! :slight_smile: This would be true even if GHC did start notarizing bindists.

In fact, one of my outstanding concerns is that there wasn’t enough up-front coordination across tools. Adding notarization must be done in a backward-compatible way so that there is no forced upgrade required for GHCUp or anything else. It is not just a GHC issue.

In other words, I pushed back on doing this now because how tools should discover and react to notarization has not been designed yet. So I guess we have some of the same concerns.