The upcoming release of macOS Catalina requires notarization. In turn the notarization process wants the app to use the hardened runtime for new releases. Furthermore the hardened runtime by default doesn’t allow 3rd party shared frameworks — dynamic libraries or frameworks would need to be signed by the same identity as the process which loads it, except for libraries that are part of the operating system.
This puts notarization at odds with shared libraries or frameworks — those installed in a common location such as
~/Library/Frameworks — as opposed to those located inside an
.app bundle. Typically these shared frameworks are created by one entity and then consumed by applications made by another entity – hence making the framework’s signing entities different.
Why install frameworks in a separate location than the app bundle? One reason is to save the user’s disk space. Another example is for frameworks that facilitates inter-process communications, two apps developed separately can link to the same framework which in turn performs the actual inter-process communication in which the method is internal to the library. Yet another case is for plug-ins – the plug-in architecture defined by
CFPlugIn calls for essentially having plug-ins as dynamic libraries packaged in a bundle, much like a
.framework bundle, with minor differences.
Let’s say that you’re ready to update your app for notarization. You go to Xcode, turn on Hardened Runtime, and build the app. Alas, the app failed to load a dynamic library. The path is correct. The file name is correct. But the library just doesn’t load. You disable Hardened Runtime and voilà, it runs as per normal.
Perhaps you are distributing plug-ins or shared libraries. Then you started getting complaints from your customers saying that when they enable Hardened Runtime, plug-in won’t load but everything else loads just fine.
What should you do? Wouldn’t be nice if libraries just loads flawlessly in the hardened runtime environment? Read on.
For a hardened runtime app to load libraries made by someone else other than the app’s vendor, there are two things that needs to be done:
- App side: disable library validation
- Library side: define a deployment target of macOS 10.9 or later
Hardened runtime is a property of the process – it is the running “app” that chooses whether it runs in “hardened” mode or otherwise. This is why the setting in Xcode appears only on application targets (which also includes helper application as well as application extensions) and not on framework targets.
The App Side
If your app needs to load libraries made by other companies – either link to them, load it dynamically using
dlopen or use it as a plugin via
CFPlugInCreate, you would need Disable Library Validation – this is an option under Hardened Runtime, Alternatively add.
com.apple.security.cs.disable-library-validation with a boolean value of
YES to the app’s entitlements file.
How do you know whether the entitlement has been added correctly? Simply run app and inspect the app’s running instance by using a command similar to below:
codesign -d -v --entitlements :- $(pgrep YourApp)
The command above will display the effective entitlements of the process at near the end of the command’s output. If you can’t find the disable library validation entitlement there, then there’s something wrong with your build process.
The Library Side
However if you make shared libraries for other organizations’ apps to consume (i.e. link or load) then you would need to ensure that the minimum deployment target is macOS 10.9 “Mavericks” if you want to support apps running in macOS 10.15 “Catalina”. Furthermore since Catalina is 64-bit only, your library must also have a 64-bit slice.
Similarly you would also need to make sure that the downstream dependencies of your library also has a minimum deployment target of macOS 10.9. This includes both the direct dependencies and transitive ones, all the way down to the libraries and frameworks built-in to the operating system.
You can check the minimum deployment version of a dynamic library using the following command:
otool -l /path/to/library.dylib | grep -B 1 -A 3 LC_VERSION_MIN
However for frameworks, you need to specify the dynamic library file inside the framework bundle to
otool for it to display the minimum deployment target:
otool -l /path/YourLibrary.framework/Versions/Current/YourLibrary | grep -B 1 -A 3 LC_VERSION_MIN
What if your library needs to support macOS earlier than Mavericks? For plain
dylib files, you could increase its version and similarly for
.framework bundles you can have multiple library versions inside the same bundle and only support Mavericks in a new version. Then communicate to your library consumers to use this new version for hardened runtime apps – they would need to create a new app version anyway to support hardened runtime and be notarized.
Since Catalina is just around the corner, you should begin notarizing your macOS apps soon. If your app accepts plugins or otherwise loads 3rd party frameworks that are not distributed within the app bundle itself, you should start auditing whether these frameworks have a proper minimum deployment target attribute – some libraries doesn’t — and it is at macOS 10.9 or later. If you find such a library or framework, you should contact its developers to get it corrected before Catalina hits the virtual shelves.