macOS Security Overview

Apple provides multiple security mechanisms in macOS that rely on each other.



App Execution Security

System-Level Security

Data & Privacy Protection

The Accessibility Permission

Accessibility permission falls under the TCC framework. It opens your system to programmatic control for certain purposes.

Applications with accessibility permission are also referred to as trusted. macOS requires explicit user permission to designate an app as trusted. This takes the form of a dialog that requires admin authentication, and a Settings panel where permissions can be revoked. Spoiler alert: there is no clean way around manual authentication.

Configuration

To be able to request accessibility permissions you need to

  • Declare an entitlement if you run on a hardened runtime.
  • Declare the reason you need accessibility in your Info.plist.
  • Include a provisioning profile if you plan to distribute in the App Store.

In the entitlements file:

<key>com.apple.security.accessibility</key>
<true/>

In the Info.plist:

<key>NSAccessibilityUsageDescription</key>
<string>Accessibility permissions are used for window management.</string>

Request Accessibility

To find out if the application has permissions:

// Returns TRUE if the current process is a trusted accessibility client
let isGranted = AXIsProcessTrusted()

To prompt the user for permissions:

let options: NSDictionary = [
    kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true
]
AXIsProcessTrustedWithOptions(options)

The code above will either present a system dialog, or redirect the user to the Accessibility panel at Settings > Privacy & Security > Accessibility. Such panel can also be open programmatically with:

if let url = URL(string: 
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
) {
    NSWorkspace.shared.open(url)
}

Grant accessibility

A workflow that is reliable is to

  1. Build the application
  2. Remove and add back the application to Accessibility.
  3. Run the application from Xcode > Product > Perform Action > Run Without Building

You will be able to debug the application, but it is of course, very inconvenient to go through all this after each build.

TCC provides a utility (tccutil) that can reset permissions, but not assign them. The TCC database is a SQLite file on disk you can read, but writing requires disabling SIP, which opens the possibility to make a catastrophic mistake that damages your system.

Recommendations

(aka things to do to avoid problems)

Sign the application to make its identity clear to macOS. What I read online is that each build changes the app signature so permissions have to be requested again. But what I experienced is that signing with a Developer ID certificate is enough for the system to recognize it is the same app.

To sign your app

  1. Go to Signing & Capabilities
  2. Set a Team
  3. There are several options here. I use a Developer ID certificate and a Developer ID profile tied to it.
  4. Once signed, you can check the state of your app with codesign.

Copy the product to a stable location. What I read is that the default location of ~/Library/Developer/Xcode/DerivedData/ is hidden and changes between builds, which may confuse the system. But what I experienced is that the system recognizes the application regardless where it is generated.

FYI: changing the destination folder may cause problems if one of your dependencies declares a localization bundle of its own. If this is your case, you can still work around it and change the destination folder.

There are three ways to declare a custom location:

  • Set a build property.
  • Xcode > project name > Build Settings > Build Location > Per-configuration Build Products Path.
  • Product > Scheme > Edit Scheme… > Run > Build Configuration > Debug > Options > Build Location > Custom

Troubleshooting

The Console.app is your friend –despite its unfriendly UI. You can spot the Accesibility permission being denied if you paste the following in the search field (top, right):

type:error
subsystem:com.apple.sandbox.reporting
category:violation

The error will say something like

Sandbox: OrangeApp(16745) deny(1) mach-lookup com.apple.universalaccessAuthWarn

Another way to keep an eye on the console is to stream from the terminal:

log stream --predicate \
'process == "tccd" OR process == "OrangeApp" OR process="sandboxd"'