Mobile Computing · Mobile Platforms
Code Signing and Entitlement Enforcement at Load Time
What a signature actually covers on a mobile binary, how the kernel or loader checks it before the first instruction, and what an entitlement grants that a file mode cannot.
Every mobile platform verifies that the code it is about to run came from a known publisher and has not been modified since it was signed. The verification is not a formality performed at install time and then forgotten. It is repeated at load time, by a component that is in the trust path before any of the app’s own code executes, and the result is not a boolean but a set of permissions that the code is allowed to exercise.
What the signature covers
A signature over a binary is a hash of the binary’s contents, encrypted or otherwise authenticated with a private key, plus the certificate chain that ties the key to a publisher. Verifying it means recomputing the hash and checking the authentication against the public key in the leaf certificate.
The interesting design question is which bytes are covered. A monolithic signature over the whole file is simple but makes the file immutable: any change, including one the platform itself makes, invalidates it. Both major platforms therefore structure the signed unit so that some parts are covered and others are not.
On Android, the APK signing scheme produces a digest over the contents of the archive’s entries, not over the archive bytes, and places the signature in a block between the archive’s data and its central directory. The block is outside the coverage of the ZIP format’s own offsets, so it can be added without breaking the archive. Because the digest covers entry contents rather than compressed bytes, the same logical content can be re-compressed, or aligned differently for different architectures, without invalidating the signature. Replacement of one build by another is prevented by the package manager rather than by the signature format: it records the signing certificate associated with a package name at first install, and refuses an update signed by a different key unless that key has been endorsed by the original through a rotation statement.
On Apple’s platforms the signed unit is the bundle, and the signature covers a mixture of things: the Mach-O header and the pages of the executable, a list of the bundle’s resources with a hash per file, and the entitlements. The executable’s code pages are covered page by page, which allows the loader to verify a page as it is faulted in rather than verifying the whole binary at once, and allows the system to page in only the parts actually executed. The resource list is covered as a whole, so a modified resource is detected when the bundle is validated rather than when the file is read.
The chain to a root the platform ships
A leaf certificate is only as trustworthy as the chain above it. Both platforms ship a set of roots and accept only chains that terminate in one. The developer certificate is issued by an intermediate operated by the platform vendor, which is issued by a root stored on the device and not user-modifiable.
Apple’s model adds a second dimension: the certificate must be accompanied by a provisioning record that names the app’s identifier and the devices or distribution channel it is valid for. The record is itself signed, and it is what allows a development build to run on a specific device while the same binary is refused on another. Android’s model instead treats the developer’s own key as the root of the app’s identity: the certificate is self-signed and the platform does not require it to chain to a vendor root, because the security property being established is continuity of identity across updates rather than authorisation by a central authority. Distribution through the platform’s store adds a separate signature by the store, which is what allows the store to authorise an install without the developer’s key being present on the device.
The difference matters for what a signature can be used to prove. In the Apple model, a valid signature says the code was authorised to run on this device. In the Android model, a valid signature says this build came from whoever signed the previous build of the same package. Signature-level permissions, as noted in Permission Grants and Where They Are Enforced, rest on the second meaning: two packages signed by the same key may share data.
Verification at load, not only at install
Installing a package and running it are separate events, and the verification performed at install does not protect the run. A file on storage can be replaced after installation by anything with write access to that path, which on a rooted or compromised device includes the user.
The defence is to verify again at the moment of execution, from a component that is already trusted. On Apple’s platforms the kernel checks the signature of the main executable before it maps the first page, and the dynamic loader verifies pages as they are faulted in, using a hash table embedded in the binary. A modified page fails the check and the process is terminated rather than allowed to execute it. Because the check is per page and lazy, it costs a hash computation per page fault, which is small relative to the fault itself.
On Android the equivalent is the package manager’s verification at install plus the platform’s use of the package’s signature to compute its identity, combined with the fact that the installed files live on a partition that the app cannot write. The app’s own data directory is writable, but the installed code is not. Integrity of the installed code therefore rests on filesystem permissions and on verified boot, described below, rather than on a per-page check at load.
Verified boot closes the gap at a coarser granularity. The bootloader verifies the signature of the boot image, which verifies the signature of the system image, which contains the package manager and the installed apps. A modification anywhere in that chain either fails the boot or puts the device into a state the platform reports. The chain is verified once at boot rather than per process, and the guarantee it provides is that the code on the system partition is the code that was signed by the platform vendor. What that guarantee does not cover is the app’s own writable storage, which is where the isolation described in Per-App UID Isolation and the Data Directory Contract does the work instead.
Entitlements as a second axis
A signature proves authorship. It does not, by itself, grant anything. The grant is expressed separately, and the separation is what makes it possible for the same signed code to run with different privileges in different contexts.
An entitlement is a key-value pair embedded in the signature and therefore covered by it. The loader reads the entitlements after verifying the signature, and the kernel and system services consult them when the app requests a privileged operation. The property that makes this work is that entitlements cannot be added to a binary without invalidating the signature, so a modified app cannot claim a privilege it was not granted. On Apple’s platforms the set of entitlements a certificate may carry is constrained by the provisioning record, so a development certificate cannot be used to ship an entitlement reserved for a system component.
Android expresses the same idea through the manifest’s declared permissions and the signature check between the declaring and requesting package. A permission declared with signature protection is granted only if the requesting package is signed by the same key as the declaring package, which means the grant is derived from the signature rather than stored separately. The manifest is inside the signed APK, so it cannot be edited without breaking the signature.
The two mechanisms differ in when the decision is made. An entitlement is read at load time, from the signed blob, and it constrains what the process may do for its whole lifetime. A signature permission is evaluated at install time and recorded as a grant, and the process’s kernel state is built from that record at launch. The first is a property of the code; the second is a property of the installation.
<!-- Entitlements are part of the signed blob, so editing this changes the hash. -->
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.example.shared</string>
</array>
An application group is the clearest example of the mechanism. The entitlement names a group identifier, the kernel accepts that identifier as a shared container when the process’s sandbox profile is built, and a process without the entitlement is denied access to the container regardless of the container’s own permissions. The grant is therefore both a claim in the signed blob and a rule in the profile, and neither is useful without the other.
Where the model breaks
Self-signed identities mean that a developer who loses a key cannot update their app. Android’s answer is a key rotation mechanism that lets a new key be endorsed by the old one through a signed statement, so that the platform can follow the chain from the original identity to the new key. The mechanism only works if the old key is still available to sign the endorsement; a lost key without a pre-registered rotation plan cannot be recovered.
Development provisioning records expire. A build signed with an expired record is refused at launch even though its signature is intact, and the failure appears as a launch failure rather than as an authorisation error, because the loader checks the record’s validity along with the signature.
Jailbroken or rooted devices remove the enforcement without removing the mechanism. The signature is still valid and still checked by the loader, but the loader has been replaced or the check has been patched out. Nothing in the app’s own code can detect this reliably, because any check the app performs runs in the same compromised environment. The platform’s response is attestation: a hardware-backed key that signs a statement about the device’s state, verified by a server that knows the expected values. That moves the trust decision off the device, at the cost of requiring the server to maintain a policy for which states are acceptable.
Finally, signature verification is a hash over data the verifier must read. For a large binary the first verification touches the whole file, which is why the lazy per-page scheme exists: it converts a startup cost proportional to binary size into a cost proportional to the pages executed. The trade is that a page that is never executed is never verified, so an attacker who can cause a page to be mapped without being verified gains execution of unverified code. The page table entries for such pages are marked so that the fault handler performs the check, which is why the guarantee depends on the kernel’s mapping discipline as much as on the hash.
References
- Apple Platform Security — App code signing — Apple Inc.
- Android Developers — APK signing scheme — AOSP documentation
- RFC 5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile — IETF