How thoroughly does Gatekeeper check existing apps?

My previous article about checking bundle signatures and my new app Signet to make this easy has proved controversial, despite its paucity of comments. This article looks at one contentious issue, whether checking signatures has any relevance or purpose.

My critic is Jeff Johnson, a very experienced and knowledgeable macOS and iOS developer whose opinions I greatly respect. In tweets, he stated (as part of a long discussion):
“The code signature is irrelevant to whether you should remove old apps and bundles.”
“I disagree with the whole notion that there are “signature problems”. Code signatures are designed for Gatekeeper. Gatekeeper is designed for first launch. Gatekeeper has changed over the years. Old signatures on installed apps are irrelevant, not a problem.”

Earlier, I had a shorter exchange of tweets with Thomas Reed, a security researcher for whom I also have great respect. He has been trying to raise awareness of problems with code signatures to encourage developers to do their own internal code signature checks. He wrote:
“Since macOS doesn’t check code signatures after the first run, malware could infect many of the apps on your system, without root, and you’d never know. All it would take is running the wrong app once. Plus, of course, when malware gets revoked, it’ll still run on infected Macs.”

I’m going to look at his case in more detail in an article here tomorrow, but for the moment I think it’s fair to say that these opinions are both based on the observation that, once an app or another form of code bundle has passed its full signature check at first launch, no further checks are made on that code signature. Here I will show that that is no longer true in macOS 10.14.2 (at least).

One simple test you can try is to make copies of different types of app and make changes to those copies, then try opening them. Of course, unsigned apps can tolerate substantial change without anything being aware; you have to break something internal within the app, such as its code, to cause any real problem.

codesignprobs01

For normal signed apps, the situation is much the same. In this case, I actually removed the app’s original executable code and replaced it entirely. The error message doesn’t refer to any security issue, just that something inside the app is broken.

codesignprobs02

App Store apps are more sensitive to such tampering. Fiddling with their Info.plist or making substantial changes to their code can more readily trigger warnings, which in this case aren’t quite rendered correctly by macOS, which seems unaware of the Unicode double quotation characters.

But the results are quite different when you make changes to a notarized app. Changing just a single character in the copyright string in its Info.plist, or changing two characters in a URL string embedded in the executable code, resulted in the notarized app (Signet, of course) unexpectedly quitting when it tried to start up.

codesignprobs03

This behaviour isn’t typical of Gatekeeper checks, though, and the dialog doesn’t appear to have originated from there. However the error codes generated are the same as those reported by Signet:
-67030 invalid Info.plist (plist or signature have been modified) when Info.plist has been changed, and
-67061 invalid signature (code or signature have been modified) when the executable code has been changed.

So something is checking the signatures of notarized apps when they are launched, even though they don’t have the quarantine flag set. In this case, the app had never even been uploaded, but was being run on the Mac on which it had been built. It’s time to take to the log to discover what happened (times are given in clock seconds).

30.343884 SecTrustEvaluateIfNecessary
30.345255 com.apple.securityd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (lsd[355]/0#-1 LF=0)
30.345305 com.apple.securityd cert[2]: AnchorTrusted =(leaf)[force]> 0
30.346576 com.apple.securityd MacOS error: -67030
30.346629 com.apple.securityd MacOS error: -67030
30.361455 SecTrustEvaluateIfNecessary
30.362900 com.apple.securityd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (amfid[124]/0#-1 LF=0)
30.362964 com.apple.securityd cert[2]: AnchorTrusted =(leaf)[force]> 0
30.364183 com.apple.securityd MacOS error: -67030
30.378125 com.apple.securityd MacOS error: -67030
30.378189 com.apple.securityd MacOS error: -67030
30.378271 com.apple.securityd MacOS error: -67030
30.378316 com.apple.securityd MacOS error: -67030
30.378356 com.apple.MobileFileIntegrity Basic requirement validation failed, error: (null)
30.378463 /Applications/SignetTest.app/Contents/MacOS/Signet signature not valid: -67030
30.378478 AMFI: code signature validation failed.
30.380499 SecTrustEvaluateIfNecessary
30.381862 com.apple.securityd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (amfid[124]/0#-1 LF=0)
30.381904 com.apple.securityd cert[2]: AnchorTrusted =(leaf)[force]> 0
30.383124 com.apple.securityd MacOS error: -67030
30.383692 com.apple.MobileFileIntegrity <private>: Broken signature with Team ID fatal.
30.383781 mac_vnode_check_signature: /Applications/SignetTest.app/Contents/MacOS/Signet: code signature validation failed fatally: When validating /Applications/SignetTest.app/Contents/MacOS/Signet:
The code contains a Team ID, but validating its signature failed.
Please check your system log.
30.383800 proc 17245: load code signature error 4 for file "Signet"
30.403372 com.apple.launchservices RETURNING: { "ApplicationType"="Foreground", "CFBundleExecutablePath"="/Applications/SignetTest.app/Contents/MacOS/Signet", "CFBundleIdentifier"="co.eclecticlight.Signet", "DeathTime"=now-ish 2018/12/21 09:18:30, "LSBundlePath"="/Applications/SignetTest.app", "LSDisplayName"="SignetTest", "LSExitStatus"=9, "pid"=17245 }
30.648151 Saved crash report for Signet[17245] version ??? to Signet_2018-12-21-091830_Howards-iMac-Pro.crash

Similar results were seen after small changes in executable code, but returning the error code of -67061 instead.

I had wondered whether the subsystem responsible for these additional checks might have been TCC, with its considerably extended roles in Mojave. However, they occur in response to standard SecTrustEvaluateIfNecessary entries (just as with signed but not notarized code), and the only log entries of relevance are from the com.apple.securityd subsystem performing signature checks.

Perhaps none of this should be surprising. Signatures contain a lot more than certificate information. They consist of three parts:

  • The seal, which is a collection of hashes of each component covered by the signature.
  • The digital signature, consisting of the seal encrypted using the signer’s identity, to guarantee the seal’s integrity.
  • Code requirements, rules governing verification of the code signature.

The evidence here is that com.apple.securityd has compared the hashes in the signature’s seal with hashes computed for the app bundle, and detected anomalies which show that the contents of the bundle had changed since it was signed. This may be linked to the new notarization ticket which is ‘stapled’ in notarized apps, or could be obtained from the traditional signature.

So, at present in Mojave we have:

  • Most macOS system bundles – protected by SIP and signed, so cannot be changed.
  • Apple bundled apps – protected by SIP and signed, so cannot be changed.
  • App Store apps – signed and more likely to break if changed.
  • Notarized apps – signed+notarized and almost certain to break if changed.
  • Other third party apps and bundles – signed or unsigned, not checked after first run unless the developer codes in their own checks.

I believe that code signing was introduced in macOS in 2007, extended and enhanced considerably with the introduction of Gatekeeper in 2012, and notarization is new with Mojave in 2018.

The first benefit of code signing which Apple lists in its Code Signing Guide is to:
“Ensure that a piece of code has not been altered since it was signed. The system can detect even the smallest change, whether it was intentional (by a malicious attacker, for example) or accidental (as when a file gets corrupted). When a code signature is intact, the system can be sure the code is as the signer intended.”

Thanks to Jeff Johnson @lapcatsoftware and Thomas Reed @thomasareed for their absorbing discussions.