Currently, if as a library author you are shipping dependencies as code, you can use the #if DEBUG preprocessor check to execute logic based on whether app is being built for Debug or Release.
My concern is more about the approach that should be taken when distributing frameworks/xcframeworks. One approach I am thinking of using is checking the presence of {CFBundleName}.debug.dylib in the main bundle. Is this approach reliable? Do you suggest any other approach?
Earlier I wrote:
But, honestly, it sounds like a fun weekend project
And indeed it was (-:
Pasted below is some iOS code that is able to detect how your code is signed using only public APIs. To do this, it uses a sneaky combination of XPC loopback and XPC peer requirement checking.
This code comes with a bunch of caveats. Read the doc comment before you use it [1].
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] By my count the doc comments represent well over half the total number of lines (-:
import Foundation
extension CheckSelfEntitlement {
/// Checks whether the current process claims the get-task-allow
/// entitlement.
///
/// - warning: As explained below, you shouldn’t use this routine but
/// instead should use ``isGetTaskAllowTrue()``. This routine exists solely
/// to illustrate the following point.
///
/// This routine checks for the presence of the entitlement, rather than
/// checking for it being present with a particular value. In most cases
/// checking for the presence of a Boolean entitlement is a mistake. What
/// if it’s present but has the the default value? In general we advise [1]
/// against claiming an entitlement with the default value — you might as
/// well just not claim it — but it does happen. In this case specifically,
/// when you export an Ad Hoc signed build from Xcode, the build claims
/// `get-task-allow` with a value of `false`. Given that reality, it’s
/// better to check for the entitlement and its value, as illustrated by
/// ``isGetTaskAllowTrue()``.
///
/// [1] For example, this quote from [Hardened
/// Runtime](https://developer.apple.com/documentation/security/hardened-runtime):
///
/// _The default value of these Boolean entitlements is false. When Xcode
/// signs your code, it includes an entitlement only if the value is true.
/// If you’re manually signing code, follow this convention to ensure
/// maximum compatibility. Don’t include an entitlement if the value is
/// false._
///
/// - Returns: The result of the check, or `nil` if the check failed.
static func isGetTaskAllowPresent() -> Bool? {
CheckSelfEntitlement.hasEntitlement(named: getTaskAllowEntitlementName)
}
/// Checks whether the current process claims the get-task-allow entitlement
/// with a value of true.
///
/// The get-task-allow entitlement is what the system checks for when the
/// debugger tries to attach to your process, so this is effectively
/// equivalent to “Is the current process Development signed?”
///
/// - important: Use this and not ``isGetTaskAllowPresent()``.
///
/// The get-task-allow entitlement has different values on macOS
/// (`com.apple.security.get-task-allow`) and iOS (`get-task-allow`). This
/// code uses ``getTaskAllowEntitlementName``, which has a compile-time
/// conditional to return the right value.
///
/// macOS only requires the get-task-allow entitlement if the process is
/// protected in some way, typically because it has the
/// [hardened runtime](https://developer.apple.com/documentation/security/hardened-runtime)
/// enabled. If you find that this check fails unexpected on macOS, check
/// that your development environment has added the get-task-allow
/// entitlement.
///
/// - Returns: The result of the check, or `nil` if the check failed.
static func isGetTaskAllowTrue() -> Bool? {
CheckSelfEntitlement.hasEntitlement(named: getTaskAllowEntitlementName, withValue: true)
}
/// Checks whether the current process claims the APNs entitlement with a
/// value of `production`.
///
/// The APNs entitlement grants the process access to the Apple Push
/// Notification service. The specific value determines whether the process
/// uses the production service or the sandbox service.
///
/// The APNs entitlement has different values on macOS
/// (`com.apple.developer.aps-environment`) and iOS (`aps-environment`).
/// This code uses ``apnsEntitlementName``, which has a compile-time
/// conditional to return the right value.
///
/// - Returns: The result of the check, or `nil` if the check failed.
static func isAPNsProduction() -> Bool? {
CheckSelfEntitlement.hasEntitlement(named: apnsEntitlementName, withValue: "production")
}
#if os(macOS)
private static let getTaskAllowEntitlementName = "com.apple.security.get-task-allow"
private static let apnsEntitlementName = "com.apple.developer.aps-environment"
#else
private static let getTaskAllowEntitlementName = "get-task-allow"
private static let apnsEntitlementName = "aps-environment"
#endif
}
/// Groups together routines for checking the entitlements claimed by the
/// current process.
///
/// This code supports macOS and iOS.
///
/// This code does not support the iOS simulator platform. The simulator uses a
/// system of fake entitlements that’s incompatible with the techniques used by
/// this code. If you run this code on the simulator, you’ll always get a `nil`
/// result.
///
/// This code is not necessary on macOS. That platform has other, more direct
/// approaches, including:
///
/// * On macOS 14.4 and later, use the
/// [LightweightCodeRequirements](https://developer.apple.com/documentation/lightweightcoderequirements)
/// framework. Specifically, create a constraint using
/// LightweightCodeRequirements and then check that the current process meets
/// that constraint by combining
/// `SecTaskValidateForRequirement(task:requirement:)` with
/// `SecTaskCreateFromSelf(_:)`.
///
/// * On earlier versions of macOS, use
/// `SecTaskCopyValueForEntitlement(_:_:_:)`.
///
/// * Or craft a code-signing requirement and test that requirement using
/// `SecCodeCheckValidityWithErrors(_:_:_:_:)`. For more about code-signing
/// requirements, see
/// TN3127 [Inside Code Signing: Requirements](https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements).
///
/// Unfortunately none of those APIs are available on iOS (r. 165263770), which
/// is why this code exists. This code achieves its goal by sneakily combining
/// XPC peer requirement checking with XPC loopback.
///
/// This code doesn’t support tvOS, watchOS, or visionOS because the relevant
/// XPC APIs aren’t present there (r. 165264387).
///
/// This code uses the old low-level C API because the necessary bits of the new
/// Swift API, specifically `XPCPeerRequirement`, aren’t available on iOS (r.
/// 165264387).
///
/// This code isn’t optimised for performance; if you need these results often,
/// cache it yourself.
///
/// - warning: The techniques shown here are used for introspection. If you
/// adapt them for security purposes on macOS, make sure you understand the
/// concept of the entitlements-validate flag. See the *Entitlements-Validated
/// Flag* section of [App Groups: macOS vs iOS: Working Towards
/// Harmony](https://developer.apple.com/forums/thread/721701).
enum CheckSelfEntitlement {
/// Checks whether the current process claims the named entitlement.
///
/// - warning: If you’re checking for a Boolean value, use
/// ``hasEntitlement(named:withValue:)-(_,Bool)`` instead. For an
/// explanation as to why, see ``isGetTaskAllowPresent()``.
///
/// - Parameter entitlement: The entitlement to check for.
/// - Returns: The result of the check, or `nil` if the check failed.
static func hasEntitlement(named entitlement: String) -> Bool? {
checkEntitlement(configurator: { listener in
xpc_connection_set_peer_entitlement_exists_requirement(listener, entitlement)
})
}
/// Checks whether the current process claims the named entitlement with the
/// supplied Boolean value.
///
/// - Parameters:
/// - entitlement: The entitlement to check for.
/// - value: The value to check for.
/// - Returns: The result of the check, or `nil` if the check failed.
static func hasEntitlement(named entitlement: String, withValue value: Bool) -> Bool? {
checkEntitlement(configurator: { listener in
xpc_connection_set_peer_entitlement_matches_value_requirement(listener, entitlement, xpc_bool_create(value))
})
}
/// Checks whether the current process claims the named entitlement with the
/// supplied string value.
///
/// - Parameters:
/// - entitlement: The entitlement to check for.
/// - value: The value to check for.
/// - Returns: The result of the check, or `nil` if the check failed.
static func hasEntitlement(named entitlement: String, withValue value: String) -> Bool? {
checkEntitlement(configurator: { listener in
xpc_connection_set_peer_entitlement_matches_value_requirement(listener, entitlement, xpc_string_create(value))
})
}
/// Runs an entitlement check on the current process.
///
/// This is the common implementation used by all of the `hasEntitlement(…)`
/// methods. It calls the `configurator` closure to configure the XPC
/// listener to enforce the specific entitlement check.
///
/// - Parameter configurator: A callback to configure the listener to
/// implement the specific check. Typically this just calls one of the
/// `xpc_connection_set_peer_entitlement_xyz(…)` routines.
/// - Returns: The result of the check, or `nil` if the check failed.
private static func checkEntitlement(configurator: (_ listener: xpc_object_t) -> Int32) -> Bool? {
// Configure and start an anonymous listener.
let queue = DispatchQueue(label: "check-entitlement-queue")
let listener = xpc_connection_create(nil, queue)
let err = configurator(listener)
guard err == 0 else {
// If we release a suspended listener, XPC traps )-: So we have to
// activate it with a dummy event handler, then cancel it, then
// release it (well, it’s released by Swift’s ARC machinery).
xpc_connection_set_event_handler(listener, { _ in })
xpc_connection_activate(listener)
xpc_connection_cancel(listener)
return nil
}
startListener(listener)
// Create an endpoint for that listener and connect to it.
let listenerEndpoint = xpc_endpoint_create(listener)
let client = xpc_connection_create_from_endpoint(listenerEndpoint)
xpc_connection_set_event_handler(client) { event in
// Ignore any events on the client connection. We expect to only
// receive errors here. The exact pattern of
// `XPC_ERROR_CONNECTION_INTERRUPTED` and
// `XPC_ERROR_CONNECTION_INVALID` errors depends on whether the
// connection goes through or not.
assert(xpc_get_type(event) == XPC_TYPE_ERROR)
}
xpc_connection_activate(client)
// Send an empty message to that connection, and then clean up.
let message = xpc_dictionary_create(nil, nil, 0)
let reply = xpc_connection_send_message_with_reply_sync(client, message)
xpc_connection_cancel(client)
xpc_connection_cancel(listener)
// Look at the reply. If the message went through, the entitlement
// check succeeded and we are claiming the entitlement. If the message
// failed, we assume it was blocked by the entitlement check and thus we
// are not claiming the entitlement.
switch xpc_get_type(reply) {
case XPC_TYPE_DICTIONARY:
return true
case XPC_TYPE_ERROR:
return false
default:
// Should never happen…
assert(false)
// … but return `nil` in Release builds.
return nil
}
}
/// Starts the XPC listener.
///
/// This listener calls ``startServer(_:)`` with any incoming connections
/// and ignores any errors.
private static func startListener(_ listener: xpc_object_t) {
xpc_connection_set_event_handler(listener) { event in
switch xpc_get_type(event) {
case XPC_TYPE_CONNECTION:
startServer(event)
case XPC_TYPE_ERROR:
// Ignore any error coming from the listener. We expect a
// `XPC_ERROR_CONNECTION_INVALID` error to arrive when the
// client cancels the listener.
break
default:
assert(false)
}
}
xpc_connection_activate(listener)
}
/// Starts an XPC server connection.
///
/// This server replies to any incoming message with an empty reply and
/// ignores any errors.
private static func startServer(_ server: xpc_object_t) {
xpc_connection_set_event_handler(server) { event in
switch xpc_get_type(event) {
case XPC_TYPE_DICTIONARY:
let reply = xpc_dictionary_create_reply(event)!
xpc_connection_send_message(server, reply)
case XPC_TYPE_ERROR:
// Ignore any error coming from the client. We expect a
// `XPC_ERROR_CONNECTION_INVALID` error to arrive when the
// client cancels its connection.
break
default:
assert(false)
}
}
xpc_connection_activate(server)
}
}