When does Mojave check an app’s signature? The answer isn’t entirely consistent

We all know that app signatures are only checked when an app is first run following download from the Internet in a manner that adds a quarantine flag to them. Once they’ve cleared Gatekeeper’s checks on that first run, they’re not checked again – at least not in a way which results in any action if the signature is found to be broken.

That common knowledge isn’t entirely true in Mojave (and quite probably earlier versions of macOS): macOS does check app signatures after first run, and will check apps that have never been near the Internet, and have no quarantine flag as a result. Following various tests, I reported this here last Christmas. Only I’ve now disovered that this behaviour isn’t always consistent either.

To test this out, create a new folder in your Documents folder, and copy a properly signed app which has already passed first run testing to that folder, but don’t run it from there just yet. Open the app bundle, and make a small change to its Info.plist, such as changing the text in its copyright. One byte different should be all that is required, using a good text editor such as BBEdit. Save the modified Info.plist, and then run that copy of the app.

It’s most likely that macOS will crash that app with a -67030 or similar code signing error, something which couldn’t have occurred unless macOS was checking its signature. You may, though, be unable to reproduce this crash. One way to prevent it from happening is to open the app from that peculiar folder before changing its Info.plist. Once it has been run successfully from that location, macOS doesn’t check its signature in the same way again. But sometimes, an app which has a broken signature will open fine from a strange location, as if its signature hasn’t been checked in the same way.

When you open any app from the GUI, it’s put through a series of security checks for various purposes, but in most instances any errors returned are ignored. This allows an app with a broken signature to run quite normally once it has passed that all-important first run. One of the few – perhaps the only – situations in which serious checking of certificates takes place after first run is when an app is first run from a new location in storage.

What happens then is clearly seen in these log excerpts (times given in decimal seconds):
0.603115 Finder AppKit sendAction:
marks the end of the double-click on the app icon, which triggers these processes.

After checking for plugins, LaunchServices starts preparing to launch the app:
0.655076 LaunchServices LaunchApplication: appToLaunch={ "ApplicationType"="Foreground", "CFBundleExecutablePath"="/Users/hoakley/Documents/YetAnotherStrangeFolderForApps/Pratique.app/Contents/MacOS/Pratique", […]

When this launch is running normally, the Apple Mobile File Integrity service amfid then calls the security system to check the app, with a duplicated call of
0.661751+0100 amfid Security SecTrustEvaluateIfNecessary

Normally, this should return a good result, and app launch is allowed to proceed:
trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (amfid[124]/0#-1 LF=0)
trustd cert[2]: AnchorTrusted =(leaf)[force]> 0

But when it doesn’t, the error propagates up the chain and results in the crash:
0.664416 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (amfid[124]/0#-1 LF=0)
0.664440 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.665685 com.apple.securityd security_exception amfid Security MacOS error: -67030
0.665991 Security SecTrustEvaluateIfNecessary
0.676395 syspolicyd Newer ticket (1557439760) present in db, ignoring ticket (1557439760)
0.676515 com.apple.securityd security_exception amfid Security MacOS error: -67030
0.676729 amfid Basic requirement validation failed, error: (null)
0.676796 amfid /Users/hoakley/Documents/YetAnotherStrangeFolderForApps/Pratique.app/Contents/MacOS/Pratique signature not valid: -67030
0.676810 kernel AppleMobileFileIntegrity AMFI: code signature validation failed.
0.681196 com.apple.securityd security_exception amfid Security MacOS error: -67030
0.681777 amfid <private>: Broken signature with Team ID fatal.
0.681883 kernel proc 30068: load code signature error 4 for file "Pratique"

and that’s it, boom goes your damaged app.

So what happens when an app shouldn’t pass amfid‘s check, but somehow manages to get launched? This has happened when I was testing out my new version of xattred, which you will recall runs its own signature check once it has launched. On this occasion, rather than crashing immediately with a -67030 security error, it ran through to the point where it called its own check, and quitted gracefully instead. Instead of LaunchServices passing it to amfid for security checks, it didn’t notice that it was being run from a new location, so the security check was called by lsd, the LaunchServices daemon instead:
0.462099 lsd Security SecTrustEvaluateIfNecessary
0.463895 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (lsd[334]/0#-1 LF=0)
0.463933 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.465429 com.apple.securityd security_exception lsd Security MacOS error: -67030
0.466171 com.apple.securityd security_exception lsd Security UNIX error exception: 8
0.487124 com.apple.securityd security_exception launchservicesd Security MacOS error: -67030

But LaunchServices doesn’t respond by crashing or terminating the app, and lets it run on.

The same happens three times with TCC, which also allows the app to run on:
0.492725 tccd Security SecTrustEvaluateIfNecessary
0.494091 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (tccd[283]/0#-1 LF=0)
0.494114 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.495300 com.apple.securityd security_exception tccd Security MacOS error: -67030
0.495470 tccd Valid: For kTCCServicePostEvent access to composing Service: kTCCServiceAccessibility is Auth:{Access:Unknown}
0.495471 tccd Handling access request to kTCCServicePostEvent, from co.eclecticlight.xattred, default_allow: 0, allowed: 0, prompt_count:0, update_access: 0, preflight: yes

It’s only when my code in xattred kicks in that something is finally done about the certificate problem:
0.731315 xattred Security SecTrustEvaluateIfNecessary
0.732748 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (xattred[30063]/0#-1 LF=0)
0.732771 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.736134 com.apple.securityd security_exception xattred Security MacOS error: -67030
0.766984 opendirectoryd PID: 30063, Client: 'xattred', exited with 0 session(s), 0 node(s) and 0 active request(s)
0.767053 launchservicesd LaunchServices Got exit notification for application App:"xattred" asn:0x0-dfddfd pid:30063 refs=6 @ 0x7fd757522dc0, pid=30063, from port=0x29e17:MACH_PORT_RIGHT_RECEIVE=1, fExitStatus=0.

Although undocumented, I must presume that it’s intended behaviour that LaunchServices thus amfid calls for a full check on the signatures of apps being run from a new path for the first time, and forces any failing to quit. I haven’t been able to discover how on this one occasion a launch of xattred evaded that check by amfid. If the standard behaviour is required for security purposes, then there appear to be circumstances in which LaunchServices doesn’t follow procedure, which would seem to be a vulnerability.

Finally, you may now be wondering what effect these checks beyond first run might have, given that so many apps update themselves in place using the Sparkle mechanism, which is generally assumed to break their signatures. I have checked the 15,686 code bundles in my Applications folder, and can confirm that all reasonably recent apps, with one notable exception, have fully valid signatures. So the many there which do use Sparkle to update are also correctly updating their signatures when updating in place. When macOS does run these signature checks after the first run, hardly any apps should be in danger of failing legitimately. The one exception may be Xcode, though.

So my conclusions are:

  • macOS Mojave 10.14.4 runs app signature checks after the app’s first run, notably when first running an app from a new location.
  • It’s possible to bypass these additional signature checks, probably by confusing LaunchServices into running an lsd rather than amfid check, whose errors are largely ignored.
  • Almost all modern signed apps, including those updated in place by the Sparkle system, maintain integrity with respect to their code signatures. Few if any change their application bundle without ensuring that it’s left properly signed, normally by copying the signature into the app bundle or by installing the whole app bundle including its signature(s).
  • Coding your own apps so that they perform their own basic security checks is a worthwhile method of preventing bypass of these additional checks.

Is this erratic behaviour of LaunchServices a security vulnerability? Maybe, if you can discover how to exploit it.

Amended following a helpful suggestion by Jeff Johnson @lapcatsoftware to clarify Sparkle and similar update processes.