Understanding signature checks on notarized apps in Mojave 10.14.5: 1

Code signature checks seem to get more complex with each release of macOS. A few weeks ago, I tried summarising how they work in Mojave, and how Apple has told us they will work in Catalina. I’m very grateful to Jonathan Levin for letting me know that account isn’t accurate, and with his help, some more experiments and log-gazing, over the next week or two I’m going to try to build a more accurate picture.

To start with (at least), I’m going to limit myself to one type of code: notarized apps. Apple’s own apps are fully protected by SIP and open quite differently. Those delivered by the App Store run in a sandbox, which is different again, and don’t get marked by the quarantine flag. Non-notarized apps should be getting increasingly rare now, and range from the completely unsigned to those signed by a developer signature. If Apple’s vision of the future holds good, they should gradually fade into the past, apart from any built locally.

There are many reasons for the complexity of code signature checks.

Types of signature check

First, there isn’t just one type of signature check. Each check is performed in accordance with a ‘designated requirement’ enforced by the rules of the security system, and that requirement not only determines how deep and thorough the check is, but also what errors are deemed acceptable.

The best way to see this is to take any notarized app and copy it to another folder. Run it once from there first, so macOS knows that it has run from that path, then quit it. Open the app folder in the Finder, navigate down to its Resources folder, and copy into that a file such as a PDF from somewhere else. When you then try to run that app, it should open and work just fine, despite its alien resource.

Then, using xattred perhaps, set a quarantine flag on that app. This is easy to do with xattred: just open the .app folder and click on the Add quarantine xattr button. When you next try to open that app with its quarantine flag, it will face the ‘first run’ designated requirement, which doesn’t tolerate any change in the Resources folder, and Gatekeeper will fail it, report it as damaged, and offer to put it into the Trash for you.

I’m now building into each of my apps an initial signature check to ensure that nothing has affected their integrity. Although the check which these apps is requesting isn’t as rigorous as that used when an app is first run with a quarantine flag set, so should complete quicker, it is tougher than the designated requirement for an app running normally without its quarantine flag set. My routine sets the SecCSFlags used in a call to SecStaticCodeCheckValidityWithErrors() to kSecCSCheckNestedCode. When the Resources folder has been tampered with, for example, that should be detected and generate a -67054 error meaning “a sealed resource is missing or invalid”, which quits the app immediately.

Omission of signature checks

Next, many security assessments are cached. If an app with a given signature is being run from its known path, rather than run a fresh check against its designated requirement, some checks may fall back on cached results.

One way to ensure that this doesn’t happen is to run the app from a folder from which it hasn’t been opened before. This explains why it’s possible to make some changes to the contents of an app (beyond its Resources folder) without causing it to fail its next security check. But if you run that same doctored copy of the app from a different folder, it will then be crashed when trying to open. Cached security results aren’t then being used, and the signature is being checked afresh and failing the designated requirement.

Patching vulnerabilities

Another reason for signature checks being so complex is to address edge cases which have been discovered to be exploitable weaknesses. A great deal of research effort has been devoted to the discovery of vulnerabilities in both iOS and macOS security mechanisms including signature checks.

Log entries

One problem with using the unified log to study what happens is that you only see what is written to the log. Jonathan Levin points out that AppleMobileFileIntegrity, from the kernel extension of the same name, (nearly?) always checks signatures which haven’t been cached. In contrast, the amfid service (another component of the same sub-system) only becomes involved when an app has apparently been properly signed. Currently, you’ll be very lucky to catch log entries mentioning the KEXT, whereas amfid is a more frequent source of entries.

What appears in the unified log now, in 10.14.5, is very different from what is seen when running earlier versions of Mojave. A lot of the (sometimes helpful) chatter from LaunchServices has gone, and with it some of the processes involved in signature checks have fallen silent. The following summary of a normal app launch is a useful starting point. As usual, all times are given in decimal seconds.

Four identical entries mark the action of double-clicking the app’s icon in the Finder to open it:
0.287444 Finder AppKit sendAction:

That’s followed by GUI changes for that action:
0.291196 Finder IconServices PREPARE_ICON_IMAGE
0.291231 Finder IconServices GET_ICON_IMAGE_NO_IO

After some preliminary set-up steps, amfid calls for the first trust evaluation:
0.300539 libsystem_info.dylib Retrieve User by ID
0.301029 libsystem_info.dylib Resolve user group list
0.301394 libsystem_info.dylib Membership API: translate identifier
0.304867 amfid Security SecTrustEvaluateIfNecessary
0.306129 amfid Security SecTrustEvaluateIfNecessary
0.307625 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (amfid[124]/0#-1 LF=0)
0.307697 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0

There follows a series of entries from loginwindow, IconServices again, and the first entry for the app
0.337743 PermissionScanner libsystem_info.dylib Retrieve User by ID

That’s followed by a trust evaluation prior to LaunchServices checking the app in:
0.350629 launchservicesd Security SecTrustEvaluateIfNecessary
0.351909 launchservicesd Security SecTrustEvaluateIfNecessary
0.353455 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (launchservicesd[98]/0#-1 LF=0)
0.353480 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.356747 LaunchServices CHECKIN:0x0-0xbe4be4 27783 co.eclecticlight.PermissionScanner

Next comes something of a surprise. Do you remember studentd, which Apple installed on our Macs with the 10.14.4 update?
0.356862 studentd LaunchServices 27366555: RECEIVED OUT-OF-SEQUENCE NOTIFICATION: 0 vs 8262, 257, <private>

The second appearance of the launching app is to sort its appearance out, surprisingly:
0.374507 PermissionScanner AppKit Current system appearance, (HLTB: 2), (SLS: 1)
0.377481 PermissionScanner AppKit Post-registration system appearance: (HLTB: 2)
0.385827 PermissionScanner AppKit NSApp cache appearance: […]

Then comes a very important series of entries, in which the Transparency Consent and Control (TCC) system gets to grips with the opening app. This starts with another trust evaluation:
0.462763 TCC TCCCreateDesignatedRequirementIdentityFromAuditToken: preparing xpc message
0.462852 TCC TCCCreateDesignatedRequirementIdentityFromAuditToken: requesting designated requirement
0.464450 tccd Security SecTrustEvaluateIfNecessary
0.465598 tccd Security SecTrustEvaluateIfNecessary
0.467010 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (tccd[301]/0#-1 LF=0)
0.467036 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0

TCC then seems to run into trouble (and this happens repeatedly when opening notarized apps in 10.14.5) with this error when handling the notarization (via the notarization ‘ticket’ in the app bundle):
0.469112 tccd -[TCCDAccessIdentity staticCode]: static code for: identifier co.eclecticlight.PermissionScanner, type: 0: 0x7f8bbb52c8a0 at /Applications/PermissionScanner.app
0.469208 tccd Security code signing internal problem: unexpected error from xpc: […]
0.469212 tccd Security MacOS error: -67048
0.469310 tccd Security Error registering stapled ticket: […]

Error -67048 indicates an internal error in the signature-checking process.

Despite that, another trust evaluation is called:
0.469767 tccd Security SecTrustEvaluateIfNecessary
0.470825 tccd Security SecTrustEvaluateIfNecessary
0.472241 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (tccd[301]/0#-1 LF=0)
0.472289 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0

TCC then encounters another oddity. Recognising that the app has a hardened runtime, it states that it requires an Automation entitlement, although the app doesn’t itself use AppleEvents or Automation at all:
0.473551 tccd Prompting policy for hardened runtime; service: kTCCServiceAppleEvents requires entitlement com.apple.security.automation.apple-events but it is missing for ACC:{ID: co.eclecticlight.PermissionScanner, PID[27783], auid: 501, euid: 501, binary path: '/Applications/PermissionScanner.app/Contents/MacOS/PermissionScanner'}, REQ:{ID: com.apple.appleeventsd, PID[67], auid: 55, euid: 55, binary path: '/System/Library/CoreServices/appleeventsd'}
However, this confirms that 10.14.5 recognises and acts on the hardened runtime of notarized apps.

The app itself is now well on its way to opening, and now makes it own calls to verify its code integrity:
0.529085 PermissionScanner Security SecTrustEvaluateIfNecessary
and then it’s away and running at last.