macOS Kernel & System Extensions · Lesson 4

Build & Sign from the CLI

No Xcode project. Just swiftc, a few plists, and codesign — one script turns source into a signed app with an embedded System Extension.

🎯
By the end you'll run ./build.sh, produce a real signed NetFilterDemo.app, verify its signature — and then run it and hit the security wall on purpose. That wall is the whole point of Lesson 5.

Everything here was run on this Mac while writing the course (macOS 15.7.4, Apple Silicon, your Apple Development identity). The output below is real.

1One command

▶ run me$ cd courses/macos-kernel-extensions/demo/netfilter $ ./build.sh signing identity: Apple Development: Your Name (TEAMID1234) compiling extension… compiling app… signing extension… signing app… built: build/NetFilterDemo.app Identifier=com.example.netfilterdemo Format=app bundle with Mach-O thin (arm64) ✓ done. Run it: build/NetFilterDemo.app/Contents/MacOS/NetFilterDemo

2What the script actually does

  1. Pick the signing identitysecurity find-identity -p codesigning -v → grabs your first Apple Development cert automatically. Signing is mandatory: macOS will not load an unsigned System Extension.
  2. Compile the extensionswiftc -module-name FilterExt Sources/Filter/*.swift -o …/filter — the -module-name is why the provider class is FilterExt.FilterDataProvider in the Info.plist.
  3. Compile the appswiftc -framework SystemExtensions -framework NetworkExtension Sources/App/*.swift.
  4. Assemble two bundles — write each Info.plist and place the binaries: the app at Contents/MacOS/, the extension at Contents/Library/SystemExtensions/…systemextension/ (the layout from Lesson 2).
  5. Sign inside-out — codesign the extension first (with ext.entitlements), then the app (with app.entitlements). Order matters: a containing bundle's signature covers its contents, so nested code must be signed first.
Why two entitlement files

The app needs com.apple.developer.system-extension.install (permission to ask for activation) plus the networkextension capability. The extension needs the networkextension capability plus app-sandbox. Same codesign --entitlements flag, two different plists.

3Verify the signature

▶ run me$ codesign -dvvv build/NetFilterDemo.app 2>&1 | grep -E "Identifier|Team|Authority|flags" Identifier=com.example.netfilterdemo CodeDirectory v=20500 size=997 flags=0x10000(runtime) hashes=20+7 Authority=Apple Development: Your Name (TEAMID1234) TeamIdentifier=7LS573L8C4

Confirm the entitlements actually made it into the binary:

$ codesign -d --entitlements - --xml \ build/.../com.example.netfilterdemo.filter.systemextension | plutil -p - "com.apple.developer.networking.networkextension" => [ "content-filter-provider-systemextension" ] "com.apple.security.app-sandbox" => 1
✓ Milestone

You've produced a properly structured, properly signed System Extension from the command line — the thing most people assume requires the full Xcode GUI. The bundle is real and valid.

4Now run it — and meet the wall

Launch the container app. It should submit the activation request… but watch what actually happens:

$ ./build/NetFilterDemo.app/Contents/MacOS/NetFilterDemo zsh: killed ./build/NetFilterDemo.app/Contents/MacOS/NetFilterDemo $ echo $? 137 # 128 + 9 = SIGKILL. No output. Killed before main() even ran.
Killed: 9 — this is expected

The kernel's AMFI (Apple Mobile File Integrity) killed the process at launch. com.apple.developer.networking.networkextension is a restricted entitlement: AMFI refuses to run a binary that claims it unless a provisioning profile authorizes that claim. Your Apple Development cert signed it, but nothing provisioned it — so: instant SIGKILL.

Proof it's the entitlement (A/B test)

Re-sign the same app with empty entitlements and it launches fine — then fails gracefully at a later, expected point:

$ codesign --force --sign "$ID" --entitlements /tmp/empty.plist build/NetFilterDemo.app $ ./build/NetFilterDemo.app/Contents/MacOS/NetFilterDemo → submitting activation request for com.example.netfilterdemo.filter ❌ request failed: OSSystemExtensionErrorDomain code=2 — Missing entitlement com.apple.developer.system-extension.install
BuildResultMeaning
Restricted NE entitlement, no profileKilled: 9 (137)AMFI: unauthorized restricted entitlement
Empty entitlementsruns, then code=2can't even ask to install without system-extension.install
Correct entitlements + provisioning profileactivates ✅Lesson 5
The honest takeaway

There is no SIP-on, provisioning-free way to run your own System Extension — that's by design, and it's why the App Store is full of trustworthy ones. To cross the wall you need either a provisioning profile (keeps SIP on — our path next lesson) or developer mode (which needs SIP off). Building and signing, though? Fully done, fully CLI, fully verified.

5Flashcards

Why must the extension be signed before the app?

A bundle's signature seals its nested contents. If you signed the app first and then re-signed the extension, the app's seal would be invalid. Always sign inside-out: deepest code first.

What does exit code 137 mean here?

128 + 9 = killed by SIGKILL. At launch with no output, it's AMFI rejecting a restricted entitlement that no provisioning profile authorizes.

Two entitlements the app (not the extension) needs?

com.apple.developer.system-extension.install (to request activation) and com.apple.developer.networking.networkextension (the capability). Without the first: OSSystemExtensionErrorDomain code=2.

Did we need Xcode?

No. swiftc + bundle assembly + codesign is enough to build and sign. Xcode mainly automates the provisioning step you'll do by hand next.

💬 Ask your teacher. Curious what --options runtime (hardened runtime) does, or want to see the full build.sh walked line by line? Ask. Otherwise say "Lesson 5" — we cross the wall and watch your filter see live traffic.