The opinions expressed on this blog are purely mine

Building and testing an Endpoint Security macOS system extension on Bitrise

2021-03-20

Disclaimer: while writing this post, I am employed at Bitrise to work on the build infrastructure

I have decided to play a bit with the "new" macOS Endpoint Security framework, and oh boy, am I disappointed. To be fair, Endpoint Security is a pretty powerful tool, which is easy to use once set up correctly. One needs to invest an incredible amount of time & patience to get an ES app up & running.:

1
The documentation is lacking and poor
2
Half of the framework is already deprecated (tough it was announced around 2019)
3
Tooling for system extensions are not there yet (altough in theory, it has already replaced KEXTs)
4
System extension observibility is almost non-existent
5
It takes a long time to get an ES entitlement from Apple (even a Developer one), and until then, you either disable SIP on your machine or you struggle with setting up a VM

One can utilize the Endpoint Security framework in different macOS application setups:

1
System extension style application. For some reason, Apple has decided that you need an accompanying application bundle for your system extension code. This setup makes application distribution easier as it follows their current App Store app lifecycle. It may be confusing, but this is the "official" way of packaging a system extension.
2
Command-line style application running as a Daemon. You may run code using restricted entitlements (ES, DriverKit, etc.) as a Daemon if you package (and sign) it first into a classic application bundle. One drawback here is that you cannot distribute your code via the App Store, for better or worse.

I have tried both ways and decided to go with the "official" way. System extension makes it harder for an average user to fiddle with the application, and signing/distribution is way more straightforward. Plus, I need my application to be MDM friendly.

While not receiving my requested entitlement, I've started to look for a way to be able to run & test my application without disabling SIP on my machine. First, I tried working with a VM on my local machine, but the whole development flow was chunky and didn't feel right. (It also took a considerable amount of performance toll on my MacBook). Trial two was setting up a CI/CD workflow at some provider. I am already familiar with Bitrise, so it was an obvious choice. Just as I started to work on the CI/CD flow, I got my entitlements, so the workflow does utilize the provisioning profiles.


1. Get an Endpoint Security entitlement

You will need to fill out this form describing your use case and the nature of your application. This way, Apple may refuse to grant your request, but in return, they might "borrow" your application idea :) I got lucky as my request took a little less than a month.

2. Add a new device to your developer account

Here comes the trick: I happen to know that on Bitrise Gen2 architecture, the VMs of a given stack share the same UDID, so essentially I can register them as a single device. At the moment, the UDID for the Xcode 13.2.1 (Monterey) is 9C256403-D331-4775-943E-7B060C2148F0. Please note that this UDID changes every few months or so. In that case, you need to re-generate the Provisioning Profiles & re-add them, as I will describe later.

This trick will only work when using Gen2 OR xcode 13.1 and above.

3. Generate a Signing certificate

Use this form to generate a signing certificate. If you already have one, skip the step.

3. Generate Provisioning profiles

I have generated two provisioning profiles: One for the extension and the other for the "host" application bundle.

The application bundle profile needs to have the System Extension entitlement. The App ID should be something like com.org.application

The extension profile needs to have the Endpoint Security entitlement. The App ID should be something like com.org.application.extension

Hypothetically, as an alternative you may use a wildcard App ID and add the two entitlements to the single profile. In each case, don't forget to add the Bitrise VM device you have registered in step 2.

4. Setup Provisioning Profiles in your local xcode project

You may use this example project to mimic the directory structure and configurations. Setup signing for your targets (application & extension) as you normally do for other xcode projects. Please choose "Manual Signing" instead of Automatic. If you want Unit Tests, you may add a Test scheme as a third target. As for the testing scheme, a plain "Manual" development signing certificate will suffice without a Provisioning Profile.

5. Setup the application on Bitrise

1
Push your project to a Github (or any other git provider) repository, which is a requirement of CI/CD
2
Setup a Bitrise account & add a new application. I will not cover these steps. For more information, please follow the documentation.
3
You may see my workflow setup in the image below: a Certificate and profile Installer step and two Script steps.
4
Let's upload the Provisioning Profiles and the Signing cert on the Code Signing tab. You may download the Profiles from the Apple Developer website. Signing certificate in ".p12" format can be exported using the Keychain on your local machine.

Following these steps, you should be all set to execute builds and tests for the system extension. I use the following scripts respectively:

                  
# for tests
xcodebuild -project "project.xcodeproj" test -scheme app -allowProvisioningUpdates | xcbeautify
                  
                
                  
# for build
xcodebuild -project "project.xcodeproj" build -alltargets | xcbeautify
                  
                

Nothing fancy here, as I just want the app to compile.


Things I don't understand

Apple may convince me that Endpoint Security and other entitled API usage need to be restricted & requires permission in production, but why are they gatekeeping the development usage so much? If they insist that the developers have to go through an approval process, then at least evaluation shouldn't take weeks or months. And why not limit only the distribution? Why is it accepted that I need to disable SIP on my Mac (rather than paying $99 and waiting for approval) to be able to experiment?


Bonus: Unit testing the extension codebase

After adding a Unit Test scheme to the project, only the Host Application is available for selection as a test target. It means that by default, you cannot write Unit Tests for the extension codebase because the compiler won't know about the source files (based on my shallow understanding of Swift tooling). To work around this, click on the test target > Build Phases > Compile Sources > Add the extension's swift files that you want to include in the Unit Tests.


Bonus: Getting the shared UDID of VMs for a given stack on Bitrise

1
Choose the desired stack/machine combination on the Stack & Machines Tab for your given workflow
2
Add a script step to the workflow with the content:
                      
ioreg -d2 -c IOPlatformExpertDevice | awk -F\" '/IOPlatformUUID/{print $(NF-1)}'
                      
                    
1
Check to build logs for the UDID