Signing a daemon with a restricted entitlement
Wrap a daemon in an app-like structure to use an entitlement thatʼs authorized by a provisioning profile.
Overview
Some APIs are usable from a daemon but require that the daemon claim a restricted entitlement thatʼs authorized by a provisioning profile. For example, the Endpoint Security API is usable from a daemon but requires that the daemon has the com.apple.developer.endpoint-security.client entitlement, and that it’s authorized by a provisioning profile. This is problematic because a daemon is a standalone executable, so you canʼt embed a provisioning profile in it. To get around this limitation, wrap your daemon in an app-like structure.
Create a minimal daemon project
The basic idea is to create an app target, rather than a command-line tool target, and then remove all of the app-specific content and replace it with your daemon code. To start, create a new project with an app target by choosing File > New > Project and selecting the macOS > App template. Set the Interface popup to Storyboard, the Life Cycle popup (if present) to AppKit App Delegate, and the Language popup to Swift.
In the General tab of the target editor, ensure that the Bundle Identifier field has the right value. This is important because your provisioning profile is tied to your App ID, and the bundle identifier is a key part of that App ID.
Also, clear the Deployment Info > Main Interface field and set the App Icons > Source popup to “Donʼt use asset catalogs”.
Switch to the Signing & Capabilities tab and configure it as follows:
Ensure that “Automatically manage signing” is set.
Select your team in the Team popup.
Remove the App Sandbox capability (if present). The App Sandbox is, as the name suggests, not appropriate for daemons.
Add the Hardened Runtime capability, which youʼll need to notarize your daemon.
Add the Custom Network Protocol capability, which enables the
com.apple.developer.networking.custom-protocolentitlement that must be authorized by a provisioning profile. Adding it to your target triggers Xcodeʼs automatic code-signing machinery to register your App ID, create a provisioning profile for that App ID, and embed that provisioning profile in the built product.
Switch to the Build Settings tab and remove the Enable App Sandbox (ENABLE_APP_SANDBOX) build setting, if present.
Switch to the Info tab and delete all the app-specific items (NSPrincipalClass, NSMainStoryboardFile, NSSupportsSuddenTermination, NSSupportsAutomaticTermination, and CFBundleIconFile). Some of these items may not be present, depending on the exact version of Xcode youʼre using.
In the Project navigator, remove the AppDelegate.swift, ViewController.swift, Assets.xcassets, and Main.storyboard files.
Add a main.swift file and populate it with your daemon code. For a minimal daemon, use this:
import Foundation
/// A helper for calling the Security framework from Swift.
func secCall<Result>(_ body: (_ resultPtr: UnsafeMutablePointer<Result?>) -> OSStatus ) throws -> Result {
var result: Result? = nil
let err = body(&result)
guard err == errSecSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
}
return result!
}
func main() throws {
let me = try secCall { SecCodeCopySelf([], $0) }
let meStatic = try secCall { SecCodeCopyStaticCode(me, [], $0) }
let infoCF = try secCall { SecCodeCopySigningInformation(meStatic, [], $0) }
let info = infoCF as NSDictionary
let entitlements = info[kSecCodeInfoEntitlementsDict] as? NSDictionary
NSLog("entitlements: %@", entitlements ?? [:])
}
try! main()This code logs the current processʼs entitlements, which is a good way to confirm that youʼre set up correctly.
Build and run the daemon from Xcode. The program logs its entitlements:
2021-08-04 16:24:10.979941+0100 DaemonInAppsClothing[50219:4886989] entitlements: {
"com.apple.application-identifier" = "SKMME9E2Y8.com.example.apple-samplecode.DaemonInAppsClothing";
"com.apple.developer.networking.custom-protocol" = 1;
"com.apple.developer.team-identifier" = SKMME9E2Y8;
"com.apple.security.get-task-allow" = 1;
}The final structure of your daemon should look like this:
DaemonInAppsClothing.app/
Contents/
Info.plist
MacOS/
DaemonInAppsClothing
PkgInfo
_CodeSignature/
CodeResources
embedded.provisionprofileNote the presence of the embedded provisioning profile; itʼs this profile that authorizes your daemon to use the com.apple.developer.networking.custom-protocol entitlement.
Test your daemon
To properly test your daemon, run it in a daemon context. First, copy the built daemon to a secure location:
% sudo mkdir "/Library/Application Support/DaemonInAppsClothing"
% sudo cp -R "DaemonInAppsClothing.app" "/Library/Application Support/DaemonInAppsClothing/"Now create a launchd property list file that points to the daemonʼs main executable:
% /usr/libexec/PlistBuddy -c "Add :Label string com.example.apple-samplecode.DaemonInAppsClothing" "com.example.apple-samplecode.DaemonInAppsClothing.plist"
File Doesn't Exist, Will Create: com.example.apple-samplecode.DaemonInAppsClothing.plist
% /usr/libexec/PlistBuddy -c 'Add :Program string "/Library/Application Support/DaemonInAppsClothing/DaemonInAppsClothing.app/Contents/MacOS/DaemonInAppsClothing"' "com.example.apple-samplecode.DaemonInAppsClothing.plist"
% cat com.example.apple-samplecode.DaemonInAppsClothing.plist
…
<dict>
<key>Label</key>
<string>com.example.apple-samplecode.DaemonInAppsClothing</string>
<key>Program</key>
<string>/Library/Application Support/DaemonInAppsClothing/DaemonInAppsClothing.app/Contents/MacOS/DaemonInAppsClothing</string>
</dict>
</plist>Copy that to /Library/LaunchDaemons and then load and start your daemon:
% sudo cp com.example.apple-samplecode.DaemonInAppsClothing.plist /Library/LaunchDaemons
% sudo launchctl load /Library/LaunchDaemons/com.example.apple-samplecode.DaemonInAppsClothing.plist
% sudo launchctl start com.example.apple-samplecode.DaemonInAppsClothing Run the Console app and look in the system log. At the point when you started the daemon, youʼll see a log entry like this:
entitlements: {
"com.apple.application-identifier" = "SKMME9E2Y8.com.example.apple-samplecode.DaemonInAppsClothing";
"com.apple.developer.networking.custom-protocol" = 1;
"com.apple.developer.team-identifier" = SKMME9E2Y8;
"com.apple.security.get-task-allow" = 1;
}If you miss it, run the launchctl start command to start your daemon again.
Integrate your daemon code
Now that you have a working minimal daemon, itʼs time to integrate your real daemon code into the project. Add your code to the project as you would with any other Xcode project. If necessary, replace main.swift with a C, C++, Objective-C, or Objective-C++ main entry point.
Or, if youʼre using an alternative build system, like a makefile, update it to create a structure that matches the one created by Xcode.