A brief history of code signing on Macs

Mac OS didn’t require or even support the signing of apps or executable code for its first 23 years. Apple announced its introduction at WWDC in 2006, and it appeared in Mac OS X 10.5 Leopard the following year. This happened in conjunction with the release of the first iPhone, on which only code signed by Apple could be run, and could be the first instance of an iOS feature being implemented in Mac OS.
In Mac OS, it was an Apple engineer known as Perry the Cynic, probably Peter Kiehtreiber, who claimed to have been responsible. As he told Jeff Johnson later, “I do work for Apple, and I designed and implemented Code Signing in Leopard. If you think it’s going to usher in a black wave of OS fascism, you have every right to blame me – it was, pretty much, my idea.”
Third-party developers were rightly concerned about Apple’s plans. In 2008, developers were told that “signing your code is not elective for Leopard. You are *expected* to do this, and your code will increasingly be forced into legacy paths as the system moves towards an ‘all signed’ environment. You may choose to interpret our transitional aids as evidence that we’re not really serious. That is your decision. I do not advise it.”
Despite those ominous remarks, it wasn’t until Gatekeeper was introduced in 2012 that code signing became of general importance. With Gatekeeper came the quarantine of apps downloaded from untrusted sources, and first run checks made on all quarantined apps, including ascertaining signing identity and code integrity.
Certificates
From the outset, there were two types of code signature: self-signed using an ad hoc certificate that has no chain of trust back to a root, and those using a certificate traceable back to Apple’s root certificate. While ad hoc certificates can provide a weak form of identification, almost all the value of code signing requires traceability to a certificate authority.
Apple therefore provides registered developers (who pay an annual subscription) with certificates for signing their code, but macOS doesn’t recognise certificates provided by any other authority. Certificates are also specific to their purpose: those used to sign apps for distribution outside the App Store, for example, are known as Developer ID Application certificates, and are distinct from those used to sign installer packages.
Until 2018-19, macOS stored information about valid certificates in a local ‘Gatekeeper’ whitelist database at /private/var/db/gkopaque.bundle, updated every couple of weeks. Since the release of macOS 10.15 Catalina that became effectively disused and wasn’t updated after 26 August 2019. Gatekeeper started performing online checks, to determine whether a certificate had been revoked by Apple as the certificate authority, probably from before El Capitan in 2015, but until Catalina those were only performed on quarantined apps undergoing their first run. From around July 2019 and macOS 10.14.6 those were extended to include apps that had already cleared quarantine.
Checks with Apple to verify certificates are made using the Online Certificate Status Protocol, OCSP, which came under fire in November 2020, when Apple’s OCSP service failed, leaving many unable to launch apps. It was subsequently realised that online checks weren’t encrypted and could have been used by man-in-the-middle attacks to identify users and their apps. Although Apple made some changes, its initial promises don’t appear to have been fulfilled.
CDHashes
When code signing was introduced, most attention was paid to the certificates it required, although Apple also stressed the importance of the cryptographic hashes in the code directory that’s actually signed. This is a data structure containing hashes for pages of executable code, resources, and metadata such as entitlements and the Info.plist property list, that are protected by the signature. The hash of each code directory is known as a cdhash, although here I’ll perversely refer to it as a CDHash for readability.
CDHashes were originally computed using SHA-1, but that was replaced by SHA-256 in macOS 10.12 Sierra, when SHA-1 became deprecated. Apps signed to work with 10.11 and earlier will therefore contain SHA-1 hashes, in addition to SHA-256 hashes if they’re intended for 10.12 and later.
Because hashes are unique and sensitive to the slightest change in data, they have become increasingly used to check the integrity of signed apps and code, and to identify it. Make a tiny change in a signed app’s Info.plist and a CDHash check will report the error and refuse to open that app.
Errors detected following launch normally result in macOS crashing the app, with a code signing error.
Privileges and entitlements
From the outset, code signatures have been used by Apple to determine access to some privileges. Among those were keychain access, code injection, access to an app sandbox, and Parental Controls. Since then, they have extended to include kernel extensions and many controlled features such as the ability to use snapshot features in APFS, and even access to bridged networking in virtualisation on Apple silicon.
This was anticipated by Mike Ash in 2008, when he wrote “Perhaps initially there will be some APIs which are only available to signed applications. At some point Apple will decide that there are some areas of the system which are too dangerous to let anyone in, even when signed. Perhaps you will begin to need Apple approval for kernel extensions, or for code injection, or other such things.”
Mach-O binaries
Code signatures are suited to the app bundle structure, where they can be stored in their own folder. Single-file Mach-O executables don’t have that flexibility, but their signatures and CDHashes can be appended to the binary, or, when necessary, added in extended attributes (xattrs). Apple discourages the latter, as xattrs are prone to get stripped when transiting some file systems, so are less robust.
Notarization
From the outset of the iOS App Store, and later that for macOS, apps provided through those stores have been signed not by their third-party developers but by Apple. That gives Apple full control over their contents and their CDHashes, and enables it to revoke an app by checking those, rather than having to revoke the signing certificate. However, as Apple doesn’t have any record of apps or code signed by developers using their certificates, it has no means of verifying those distributed outside its App Stores. This changed with the introduction of compulsory notarization in macOS Mojave 10.14 in 2018.
Although the App Store process and notarization have common objectives, of ensuring that apps and code aren’t malicious, and providing Apple with CDHashes and a copy of the app, they are also fundamentally different. Apps distributed through the App Store are reviewed by Apple, must conform to its rules, and are signed by Apple; notarized apps distributed outside the App Store are only checked for malware, aren’t required to comply with rules, and are signed by their developer.
This diagram shows the evolution of code signing on Macs, from pre-2007, 2007-2018, and from 2018 onwards. In 2024, the release of macOS 15 Sequoia now effectively blocks developers from distributing apps that aren’t notarized by closing the simple Finder bypass that could be used to launch unnotarized apps.
Apple silicon
Although Apple had long maintained that users would remain able to run completely unsigned code in macOS, that too changed with the release of the first Apple silicon Macs in November 2020. All code run natively on ARM processors is required to be signed, although that could still be using ad hoc signatures, as originally allowed in 2007. Xcode, build tools and other systems for developing executable code for Macs have been modified to ensure that, when building apps and other executables that aren’t signed using a developer certificate, they are at least ad hoc signed. It’s thus well nigh impossible to build code that isn’t signed at all.
Ad hoc signatures are also used in codeless apps such as Web Apps introduced in macOS Sonoma in 2023. These provide a property list defining the app’s scope in terms of its domain URL, its Home page within that, and an icon to use. LaunchServices registers them against a UUID, applies an ad hoc signature, and keeps a record of the app bundle’s CDHashes from that signature, against which to validate its contents before trying to run it in the future.
Abuse
Before notarization became widespread, some malicious software was signed using Apple Developer ID Application certificates. Obtaining a developer ID on the ‘black market’ hasn’t been costly or difficult, but until recently relatively few have gone to the trouble. This may be the effect of certificate revocation: once signed malware has had its certificate revoked, it’s dead and can’t be run on a Mac again, while ad hoc and unsigned malware has been harder to block.
With notarization becoming more compulsory, particularly in macOS Sequoia, there have been occasional malicious apps that have slipped through Apple’s checks, and been notarized. This is more demanding, and requires techniques to obfuscate code to evade detection. Even when successful, the lifetime of notarised malware is likely to be short, and for most not worth the effort.
Conclusion
Seventeen years ago, Mike Ash expressed his concern that code signing would lead to Apple having to approve the apps we can run on our Macs. Although in certain respects Apple does control what our apps can do, we can still run many apps that I’m sure wouldn’t meet its approval, and code signing has played an important role in preventing those that are malicious. Things could have been far worse.
References
Mike Ash, Code Signing and You, 7 March 2008, an invaluable contemporary summary
Apple’s Code Signing Guide, last updated 13 September 2016
Apple’s Inside Code Signing series:
TN3125 Provisioning Profiles
TN3126 Hashes
TN3127 Requirements
TN3161 Certificates
I’d like to acknowledge the help of Jeff Johnson of Underpass App Company in providing information this brief history, although all errors are mine alone.