Skip to content

Apply per-bundle entitlements - #3

Open
TartanLeGrand wants to merge 3 commits into
xtool-org:masterfrom
TartanLeGrand:feat/per-bundle-entitlements
Open

TartanLeGrand wants to merge 3 commits into
xtool-org:masterfrom
TartanLeGrand:feat/per-bundle-entitlements

Conversation

@TartanLeGrand

@TartanLeGrand TartanLeGrand commented Oct 12, 2026 •

Copy link
Copy Markdown

What does this PR do?

Lets Zupersign sign each bundle of an app with its own entitlements, so app extensions stop inheriting the entitlements of the main app. This is the zsign half of xtool-org/xtool#131.

Today zsign_sign builds a single ZSignAsset from entitlements[0] and applies it to every executable. xtool works around it by passing only the root app's entitlements (the "temp hack" in Signer.sign), so an extension is signed with the app's entitlements. For development signing that is mostly harmless; for distribution it is not, because each bundle must match its own provisioning profile (for example a widget must not claim aps-environment when its profile does not grant it).

Three commits:

  1. Apply per-bundle entitlements. One ZSignAsset per entitlements_data_t, bound to its bundle by bundle_path relative to the app directory (/ for the app itself). ZBundle::SignNode picks the asset of the bundle it is signing and falls back to the app's asset for bundles without an entry. The app's asset is moved to the front of the list because SignFolder uses the front as the default. When a provisioning profile matches a nested bundle by application identifier, only assets that actually carry profile data are considered, and the bundle keeps its bound asset otherwise. With a single entitlements entry (what xtool passes today) the behaviour is unchanged.
  2. Keep file permissions when rewriting a Mach-O. When the signature does not fit, ZMachO::ReallocCodeSignSpace writes a new file and renames it over the original, which dropped the executable bit (the main binary came out 0644 while an extension kept 0755). The mode of the original file is now restored after the rename, for thin and fat binaries.
  3. Accept self-signed identities when no WWDR intermediate matches. A leaf that is its own issuer has no chain to embed, so the CMS signature is built without one instead of failing with "Unknown issuer hash". Certificates issued by Apple WWDR are unaffected. This is only useful for tests (signing fixtures with a throwaway identity); happy to drop it if you would rather not carry it.

How was it tested?

zsign has no test suite of its own, so it is exercised through xtool (Linux x86_64, Podman, Swift 6.4), with xtool depending on a branch that combines this PR with #4 (SHA-256 only CodeDirectory) and #5 (free space check):

  • swift build of the Zupersign target on this branch alone.
  • xtool's signing tests sign a fake app containing a fake app extension with two profiles, then read back each executable's entitlements from the code signature SuperBlob (slot 5), and check embedded.mobileprovision per bundle and the executable bits. The permission test fails without commit 2.
  • Real distribution signing of a production Tauri app (app plus a WidgetKit extension) with an Apple Distribution certificate and one App Store profile per bundle, using the xtool sign command proposed in signing: sign for distribution with an existing identity xtool#326. The signed entitlements of the app and of the extension are identical, key for key and value for value, to those of the Xcode export of the same app; the extension no longer carries the app's aps-environment. App Store Connect accepted the uploads: the builds reached VALID in about 2 minutes and installed through TestFlight on an iPhone with iOS 27.

Not tested: the standalone zsign builds (linux/, macos/, windows/), only the SwiftPM target.

Remaining differences from codesign output that this PR does not change: the extension uses 4K pages (codesign 16K), dylibs get an empty entitlements blob, and files2 entries keep the legacy SHA-1 hash key. None of them blocked App Store processing.

Merge note: #4 edits the same asset.Init(...) call in Zupersign.cpp; whichever lands second needs a one-line rebase. A release containing this PR is needed before xtool can drop its workaround.

Developed with AI assistance; validated on device and against App Store Connect as described above.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2995eec6-2756-4297-9f30-731122150ad8
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A leaf that is its own issuer has no chain to embed, so build the CMS signature without one instead of failing. Useful for tests.
Zupersign built a single asset from entitlements[0] and applied it to every executable in the app. Build one asset per entitlements_data_t instead, bound to the bundle at its bundle_path, and let SignNode pick the asset of each bundle (falling back to the app's).
Making room for the signature replaces the executable with a new file, which dropped the executable bit.
@TartanLeGrand
TartanLeGrand force-pushed the feat/per-bundle-entitlements branch from b20784b to 6ad8727 Compare October 26, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant