Code Signing, Notarization, and Gatekeeper on macOS
How macOS verifies that an application hasn't been tampered with and hasn't been flagged as malware, before it's ever allowed to launch.
Every time you open a downloaded application on macOS for the first time, three distinct, independent checks have already run before the app’s first line of code executes: is it signed, has it been notarized, and does Gatekeeper allow it to run given where it came from. Conflating these three is common, but they answer different questions and fail in different ways.
Code signing: has this binary been tampered with
Code signing attaches a cryptographic signature to an executable, tied to a Developer ID certificate issued by Apple, covering the binary and its bundled resources. The signature doesn’t say anything about whether the app is trustworthy in intent — only that its contents match exactly what the signer produced, unmodified since signing.
codesign --sign "Developer ID Application: Daniel Cosenza" MyApp.app
codesign --verify --verbose MyApp.app
codesign --display --verbose=4 MyApp.app
A signature also records an entitlements list (specific permissions the app is requesting, like camera access or a specific sandbox exception) and a designated requirement — a formal expression of what a valid signature for this app must look like, which is what lets the system detect a modified or resigned binary rather than just an unsigned one.
codesign -d --entitlements :- MyApp.app
Notarization: has Apple scanned this for known malware
Notarization is a separate, later step: after building and signing an app, a developer uploads it to Apple’s notary service, which scans it for known malware signatures and policy violations. If it passes, Apple returns a ticket that gets stapled to the app — an offline-verifiable proof that this exact binary was scanned and came back clean, checkable even without a network connection at launch time.
xcrun notarytool submit MyApp.zip --keychain-profile "AC_PASSWORD" --wait
xcrun stapler staple MyApp.app
Notarization is not a review of the app’s functionality or a guarantee of trustworthiness in any broad sense — it’s specifically a malware and policy scan, run automatically, distinct from the manual App Store review process that only applies to apps distributed through the App Store itself.
Gatekeeper: should this specific launch be allowed
Gatekeeper is the runtime policy engine that actually decides whether to let a given app launch, based on its signature, notarization status, and — critically — the quarantine attribute attached to files downloaded from the internet.
xattr -l ~/Downloads/MyApp.app
# com.apple.quarantine: 0181;...;Safari;...
When you double-click a freshly downloaded, quarantined app for the first time, Gatekeeper checks its signature and notarization ticket and shows the “are you sure you want to open this” dialog specifically because of that quarantine flag — not because the app is unsigned. Once you approve it, macOS records that decision so you’re not prompted again for the same binary.
spctl --assess --verbose MyApp.app
spctl --status
spctl --assess runs the exact same evaluation Gatekeeper performs at launch time, which makes it the right tool for diagnosing “why won’t this open” independent of actually double-clicking the app.
What removing the quarantine flag actually does (and its risk)
xattr -d com.apple.quarantine MyApp.app removes the flag that triggers Gatekeeper’s first-launch check entirely — a common workaround for developer-signed-but-not-notarized internal tools, but one that bypasses the safety check quarantine exists to trigger, and should only be used when you’re confident about the binary’s origin, since it disables exactly the mechanism designed to give you a second look before running something downloaded from the internet.
xattr -d com.apple.quarantine MyApp.app
The three checks working together
Put together, the full picture on first launch is: quarantine (was this downloaded from the internet, should Gatekeeper even bother checking) triggers Gatekeeper, which checks the code signature (is this binary intact and attributable to a specific Developer ID) and looks for a stapled notarization ticket (did Apple’s automated scan flag anything). Any one of these failing produces a different, specific error — an invalid signature says the binary was tampered with; a missing notarization ticket (for a Developer-ID-signed app distributed outside the App Store) triggers a stronger warning; a quarantine flag with no issues at all just triggers the routine “are you sure” prompt. Knowing which of the three actually failed is the difference between debugging a real problem and just disabling a safety mechanism that was working exactly as designed.
The different flavors of signature, and what each actually means
Not every code signature carries the same weight. An ad-hoc signature (created without any real certificate at all, just enough structure for the system to compute a stable identity for the binary) satisfies macOS’s basic requirement that code be signed to run on Apple Silicon at all, but carries no attributable identity whatsoever — it proves the binary hasn’t changed since it was locally built, nothing more. A Developer ID signature, tied to a real certificate Apple issues to a registered developer, is what enables distribution outside the App Store while still being individually attributable and revocable if Apple discovers the certificate is being used for malware. A Mac App Store distribution certificate is issued only for apps submitted through App Store Connect and reviewed by Apple’s App Review process — a genuinely different, additional layer of scrutiny beyond notarization’s automated malware scan, since human review (however imperfect) is specific to the App Store path and doesn’t apply to Developer-ID-distributed software at all.
Hardened Runtime: restricting what a signed process can do to itself
Beyond verifying a binary hasn’t been tampered with externally, the Hardened Runtime — a set of additional runtime protections a developer opts into as part of the signing process — restricts what a running, correctly-signed process is allowed to do to itself: preventing code injection via DYLD_INSERT_LIBRARIES and similar mechanisms, restricting debugger attachment, and blocking a process from mapping writable-and-executable memory simultaneously except in narrowly scoped, explicitly declared cases. This closes a real gap the basic signature check alone leaves open — a legitimately signed, notarized application could otherwise still be a vector for a separate malicious library injecting itself into that trusted process’s own address space at runtime, entirely after Gatekeeper’s initial launch-time checks have already passed. Notarization specifically requires Hardened Runtime to be enabled for exactly this reason — without it, Apple’s automated malware scan would be certifying a binary that remains injectable and modifiable at runtime in ways that could undermine everything the scan itself was meant to verify.
Related:
- Verifying App Signatures and Notarization with codesign and spctl
- Fixing ‘App Is Damaged and Can’t Be Opened’ on macOS
Sources: