Mobile Computing · Mobile Release

Certificate, Profile, and Entitlement Binding in iOS Code Signing

How a provisioning profile licenses one signing identity to assert a specific set of entitlements, and why the kernel rejects an app whose entitlements exceed its profile.

On iOS, a signature is not a statement that a binary came from someone. It is a statement, made by Apple, that a particular signing identity is permitted to run a binary carrying a particular set of entitlements. The binary’s own signature only binds the identity to the code; a separate artifact supplies the permission. Three objects have to line up before the kernel maps the first page of executable text.

Three artifacts and what each one asserts

A certificate is an X.509 identity certificate issued by Apple’s developer CA, containing the developer’s public key, a team identifier, and an extension marking whether it is a development or distribution certificate. It says who signed. It says nothing about what the signed code may do.

A provisioning profile is a signed property list, wrapped in CMS, containing: the certificate or certificates it authorises (by leaf certificate, so a specific certificate, not merely a team), the application identifier prefix and bundle identifier pattern it covers, the entitlement dictionary the app is allowed to claim, an expiration date, and for development profiles the list of permitted device identifiers. It is signed by Apple, so it cannot be edited to widen its own scope.

The entitlements embedded in the code signature are the app’s own claims: sandbox exceptions, keychain access groups, application groups, associated domains, and the restricted entitlements that gate platform features. They are part of the signed code and cannot be changed without invalidating the signature.

The rule that ties them together is subset containment. Every entitlement in the binary must appear in the profile with an equal or weaker value. The profile is the ceiling; the binary may claim less.

Why the permission lives outside the binary

The alternative design is obvious: put the allowed capabilities in the binary and let the certificate vouch for them. That fails for one reason — the certificate cannot be scoped. A distribution certificate is a long-lived key that signs any app the developer submits; if the certificate alone authorised entitlements, a leaked key would allow signing a binary with any capability the platform offers, and revocation would be the only remedy.

Splitting the permission into a separate, short-lived, per-app artifact changes the blast radius. A profile names one bundle identifier pattern and one entitlement set, and expires. A leaked distribution certificate is still dangerous — it can sign code for any app whose profile the attacker can also obtain — but it does not by itself grant capabilities, and profiles are issued per app and per distribution channel.

There is a second reason, structural rather than security-driven: entitlements that reference external state need a place to record that state. A development profile’s device list, the push environment (development versus production), and the application identifier prefix all live in the profile because they are properties of the deployment, not of the code.

The launch-time check

At process launch the kernel asks the code signing subsystem to validate the main executable. The sequence is roughly:

  1. Parse the Mach-O code signature blob and locate the CMS signature and the code directory.
  2. Verify the code directory’s page hashes against the mapped pages, so that the signed identity covers the actual bytes being executed rather than a header.
  3. Verify the CMS signature over the code directory and the embedded entitlements.
  4. Check the leaf certificate against Apple’s pinned anchor and against revocation state.
  5. Locate the provisioning profile, either embedded in the bundle or installed on the device for a development build, and verify Apple’s signature over it.
  6. Check that the profile’s expiry has not passed and that the leaf certificate is one the profile names.
  7. Compare the binary’s entitlements to the profile’s, requiring containment.
  8. Check the application identifier formed from the profile’s prefix and the bundle identifier matches the entitlement claiming it.

Step 2 is what distinguishes this from signing a tarball. A Mach-O is mapped page by page, and the code directory holds a hash per page — 4 KiB pages hashed with SHA-256 in the modern format, with the hash size recorded in the code directory header. Pages that are never executed are never hashed, which keeps launch cost proportional to what is actually touched, but it also means a modified page fails at fault time rather than at launch. A tampered branch that never executes does not trip the check until it does.

Step 7 is the one most often misunderstood. The comparison is not equality. A profile granting com.apple.developer.associated-domains permits a binary to claim it or not; a binary claiming it with a value the profile does not list fails. Wildcard profiles have an empty entitlement set for this reason: a wildcard covers many bundle identifiers, so it cannot make per-app claims, and an app that needs restricted entitlements cannot use one.

Designated requirements and what they are for

A code signature can carry a designated requirement (DR), an expression describing which future versions of this code should be treated as the same code. On macOS this governs whether a stored permission, such as an accessibility grant or a keychain item’s access control, still applies after an update. The expression is written in a small declarative language over certificate fields and code directory attributes, and it is evaluated against a candidate binary’s signature rather than against its bytes.

The practical consequence is that the DR decides what counts as an update. A DR pinned to a specific leaf certificate means a re-signed binary with a new certificate is treated as different code and loses inherited permissions; a DR anchored to an intermediate plus a team identifier survives certificate renewal. Choosing the anchor is a trade: pinning tighter limits substitution attacks, pinning looser avoids losing state on renewal. On iOS the equivalent question is largely settled by the profile, which names certificates explicitly, so renewal means re-issuing profiles rather than re-evaluating requirements.

Where the chain breaks in practice

The most common failure is an entitlement present in the binary but absent from the profile, which produces a launch failure rather than an install failure for development builds and a rejection at upload for distribution builds. The second most common is an application identifier prefix mismatch: a profile issued to one team cannot authorise a binary whose application-identifier entitlement begins with another team’s prefix, even when the bundle identifier matches.

Expiry is a boundary condition with an asymmetric cost. A development profile expires and the app stops launching on every device at once, with no warning inside the app, because the check happens before main. Distribution profiles for App Store distribution are effectively permanent, since the store re-signs the app with its own identity at delivery, but ad hoc and enterprise profiles carry their own expiry that the device enforces locally.

Clock handling matters at that boundary. Revocation and expiry are evaluated against device time, so a device whose clock is set backwards can accept an expired profile. Apple’s OCSP-based revocation check has an offline path that fails closed for some certificate classes, which means a device with no network can refuse to launch a validly signed app. That behaviour is deliberate and is the subject of the trade described in staged rollout and halt semantics: a distribution decision that cannot be reversed quickly is worse than one that can.

Why re-signing is not transparent

Because the profile is bound to a leaf certificate and the entitlements are inside the signature, changing any of the three inputs requires re-signing the whole bundle. A build server that re-signs an app for enterprise distribution with a different certificate must also supply a profile that names that certificate, and if the app claims entitlements the new profile does not carry, the re-signed app fails at launch even though the signature verifies.

This is why the profile is best treated as build input rather than build output. The entitlement set is determined by the code; the profile is a function of team, channel, and device list. Generating profiles per channel, and asserting at build time that the binary’s entitlements are a subset of the profile’s, converts a launch-time failure into a compile-time one. The same containment logic appears in the Android side, where the platform verifies a whole-file digest and a certificate chain but leaves capability grants to a runtime permission model rather than a signed profile — a difference that makes Android signatures cheaper to verify and iOS signatures stronger as authorisations.

References

  1. Apple Platform Security — Apple
  2. TN3125: Inside Code Signing: Provisioning Profiles — Apple Developer Technical Support
  3. TN3161: Inside Code Signing: Certificates — Apple Developer Technical Support
  4. RFC 5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile — IETF