Apple Codesigning In Depth: Part I
The first of a multi-part series covering Apple codesigning in depth. This post covers the fundamental concepts around codesigning on Apple platforms.
This series of posts will cover Apple codesigning from a lot of angles, starting with the basic concepts and common misconceptions in this article.
The information I’ll be presenting is all public and is derived from either the Apple technical documentation or via direct experimentation with codesign
, productsign
, App Store Connect, and related tooling. The main benefit I see in writing this is to try to synthesize information that Apple has provided across a number of sources and is available via experimentation in one place. Future installments will include more subjective experiences I’ve had, and the implications for things like build caching, custom build systems, and large enterprise key management.
This first post is going to be a 10,000 ft overview of the majority of the code signing ecosystem - we will be ignoring a number of caveats so we can focus on the major concepts. If you’re interested in those details, stay tuned for the next post!
What is a code signature?
Code signatures use public key signing and infrastructures to validate that code has been signed with a certificate that belongs to a specific entity. A signature being valid and from a trusted chain should indicate that the binary is unmodified as well as originates from the signing entity.
Different ecosystems use different methodologies, algorithms, public key infrastructure, and approaches for code signing and verification - everything from individual entities providing their own self-signed certificates, to immaculately managed public key infrastructures that restrict signing certificates to validated entities who pay a fee. Regardless of the particulars though, all code signing has the fundamental goal of establishing trust in the software executed.
With that out of the way, let’s dig into Apple specific code signatures.
Apple Code Signing Concepts
There are a number of concepts that will come up repeatedly as we dig into Apple code signing, so I’m going to go over them here as a primer. For brevity, I will not be going into extreme detail on any one concept, but will go into more details as needed as we use them. One important note, and something to keep in mind as we discuss, is that once a code signature has been issued, the signed artifact cannot be modified in any way without invalidating the signature.
Certificates
Apple has a robust public key infrastructure supporting their signing ecosystem. Most, though not all, certificates used for code signing on Apple platforms are provided by Apple via an Apple Developer Program membership. I’ll dig into the most common types of code signing certificates you may run into while working on Apple technology below. Please don’t rush to tell me I’ve missed a certificate - I’m not covering some of the more specialized certificates here (Developer ID with Kext vs without Kext, MAS Installer Package, etc), we’ll get to those when we need them.
Development Certificate
Development certificates are used in the course of developing and debugging software, and signatures from these certifcates allow you to debug your builds on devices. In the past, there were platform specific development certificates, but for some time Apple has provided Universal Development certificates which can be used across all of their platforms. Development certificates are generally available to most engineers at an organization working on Apple development, and are routinely used by Xcode for your builds.
Enterprise Certificate
Enterprise certificates are available only for organizations that have signed up for Apple’s enterprise developer program which is intended for permitting deployment of internal applications to many devices in a large enterprise setting. Enterprise certificates are special in that they can be used with enterprise provisioning profiles, subject to the enterprise program agreement, to enable deployment of your application onto any device that your organization owns, not requiring specific device-by-device allowlisting.
Distribution Certificate
Distribution certificates are used to sign applications for ad-hoc distribution or to submit applications to the app store. These certificates are paired with either app store or ad-hoc distribution provisioning profiles. For app store profiles the App Store validates these signatures upon binary upload to make sure that the build is authorized for deployment before re-signing with Apple’s deployment certificate. For ad-hoc distribution, similar to a development certificate only a small list of devices are authorized to install the application.
Developer ID Certificate
A Developer ID certificate is used to assert that a binary for macOS originates from a particular developer. Apps signed in this way are not submitted for Apple’s review. Developer ID certificates are only used for macOS, and are only used for macOS applications distributed outside of the Mac App Store. Developer ID certificate variants can also be used for signing installers and kexts.
Developer ID certificates have several key differences compared to other ADP certificates.
- Developer ID certificates are the only certificate intended to be used for mass deployment to customers, all other certificates are either to be used for internal deployments, testing, small customer base installs, or for validation of uploads to Apple
- Developer ID certificates do not always use entitlements or have provisioning profiles. Provisioning profiles are only used for some special capabilities.
- Revocation of Developer ID certificates requires contacting Apple and explaining why you’re revoking them.
Developer ID certificates should be considered some of the most critical code signing assets you get from Apple due to how they can be used.
Non-ADP Certificate
These are generally not permitted for signing if you’d like your signatures to pass inspection, but are permitted for SDK signing at this time. Even in cases where they are permitted, I strongly advise just using an ADP certificate.
Bundle Signing Concepts
While you can just sign binaries, you’re most often going to be signing bundles, so let’s get into some things that apply specifically to bundles.
Bundle Identifier
Every bundle has a unique1 bundle identifier that is used to distinguish it from other bundles. An example of a bundle identifier is is.kayla.test
. Bundle identifiers have various constraints, most notably that extensions and app clips should be sub-identifiers - in this case a valid identifier for an app clip would be is.kayla.test.clip
.
Provisioning Profiles
So I’ve already talked about provisioning profiles, but we haven’t really discussed what they are. Provisioning profiles are an integral component of Apple’s codesigning verification for builds that require them, and act to say what bundles a certificate is permitted to sign, and what types of things those bundles can do. There are a variety of types of provisioning profiles - and Apple adds more pretty frequently - but you need one for each executable bundle you are signing - in the case of iOS, this is one for every App, App Clip, and App Extension in your bundle.
Provisioning profiles are issued by Apple, and are signed with an Apple certificate to make sure that no one makes modifications once they are issued.
An example of a provisioning profile after this signature has been removed, and the profile has been decoded, is shown below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<dict>
<key>AppIDName</key>
<string>Kayla Test App</string>
<key>ApplicationIdentifierPrefix</key>
<array>
<string>ABC1234</string>
</array>
<key>CreationDate</key>
<date>2024-11-19T16:22:34Z</date>
<key>Platform</key>
<array>
<string>iOS</string>
<string>xrOS</string>
<string>visionOS</string>
</array>
<key>DeveloperCertificates</key>
<array>
<data>...</data>
</array>
<key>DER-Encoded-Profile</key>
<data>...</data>
<key>Entitlements</key>
<dict>
<key>com.apple.developer.associated-appclip-app-identifiers</key>
<array>
<string>ABC1234.is.kayla.Test.Clip</string>
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>application-identifier</key>
<string>ABC1234.is.kayla.Test</string>
<key>keychain-access-groups</key>
<array>
<string>ABC1234.*</string>
<string>com.apple.token</string>
</array>
<key>get-task-allow</key>
<true/>
<key>com.apple.developer.team-identifier</key>
<string>ABC1234</string>
</dict>
<key>ExpirationDate</key>
<date>2025-11-19T16:22:34Z</date>
<key>Name</key>
<string>Kayla Test Provisioning Profile</string>
<key>ProvisionedDevices</key>
<array>
<string>...</string>
<string>...</string>
<string>...</string>
</array>
<key>TeamIdentifier</key>
<array>
<string>ABC1234</string>
</array>
<key>TeamName</key>
<string>Kayla McArthur</string>
<key>TimeToLive</key>
<integer>365</integer>
<key>UUID</key>
<string>...</string>
<key>Version</key>
<integer>1</integer>
</dict>
Entitlements
Entitlements say what an application is allowed to do. This includes common things, like what parts of the device or system keychain the app can access, to esoteric things like if the application is allowed to perform certain networking operations. Every application or app extension bundle can have different entitlements and types of entitlements.
If you’re using Xcode, entitlements are specified in Xcode in the Signing panel, if building in some other way you generally specify them using an entitlements file. Sometimes entitlements are implied by your other choices - e.g. debugging on device will automatically include the get-task-allow
entitlement in Xcode. Regardless of how you declare the entitlements, they eventually will be sent to the codesign
tool to be included in the final code signature.
An example of entitlements as shown in Xcode, and the entitlements that Xcode actually used with the codesign
command when building for debug are shown below.
Xcode Capability Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dict>
<key>application-identifier</key>
<string>ABC12345.is.kayla.Test</string>
<key>com.apple.developer.associated-appclip-app-identifiers</key>
<array>
<string>ABC12345.is.kayla.Test.Clip</string>
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.developer.team-identifier</key>
<string>ABC12345</string>
<key>get-task-allow</key>
<true/>
</dict>
Relationship Between Elements
Initially it’s hard to understand how all of these components relate together, so let’s dig into that more in depth.
Entitlements and certificates are used by the codesign tool, which acts on a bundle, to sign that bundle.
A provisioning profile is used to say which certificates are allowed to sign which bundles, with which entitlements, and what devices such a signature is valid on. The provisioning profile also must be embedded prior to signing.
Code Signing Processes and Steps
Xcode or your build tools usually do this for you (unless you need to manage it manually, which will be the subject of a future post) but we’re going to dive a bit deeper into how it works.
Bundle Signing
This is the most common signing most developers use these days, and is used prior to submission of applications to the App Store. Signing for bundles has a few requirements:
- Every sub-bundle (.framework, .app, .appext, etc) must be signed
- Every sub-bundle and the parent bundle must be signed with the same certificate2 (yes, this means if your XCFrameworks came signed by their distributor they’re going to get re-signed)
- Signing must be done depth first (Bundle A contains B contains C, you sign C, then B, then A). There is a
-deep
option to thecodesign
utility, but this is usually not what you want as it won’t let you specify some options - in some cases it is appropriate though. - The entitlements utilized for signing each bundle must be a subset of the allowed entitlements in the provisioning profile for that bundle.
- Every executable sub-bundle must also meet all of these requirements.
The actual mechanics of executing the signing is pretty simple - you go to each bundle and run the correct codesign
command, depth first. Most of the complexity comes from how you optimize the signing process in more complex environments with parallel build actions, distributed build caches, and other considerations. We’ll get to these more advanced topics in a future post.
Developer ID Signing
Developer ID signing is very different. For just basic signing, you just execute the appropriate deep codesign
command once for the bundle you’re signing, and it will perform the signing on your behalf. There are some other steps you generally want to take for Developer ID signing though since apps signed this way are not reviewed by Apple.
Additionally, installers can be signed by a Developer ID Installer certificate. To do this, you use the productsign
command which has a bit different syntax than codesign
but performs a similar purpose.
Notarization
Notarization is an optional step that can be undertaken with Developer ID signed builds. With notarization you use notarytool
to submit a binary to Apple for automated testing. This process is quick, and just validates that your binary does not appear to have any malicious code. If Apple’s system doesn’t detect anything malicious, it records a hash of your binary on their servers. Prior to running applications, macOS will check if the binary is notarized by asking Apple’s servers, and if not it may display a warning or refuse to execute them in some circumstances.
Stapling
As an optional addition to notarization, you can staple your notarization to your binary or installer. Stapling a notarization onto your application involves running a tool to attach a notarization confirmation to your application or installer so that even if a machine using your software is offline, it can verify that the binary was notarized without having to contact Apple’s servers. This is generally advised as it can also reduce latency when launching your application.
SDK Signing
Very recently Apple has introduced a new type of signing - SDK signing. These signatures can use any certificate - including ADP or self-signed certificates. I recommend always using an ADP cert, as these show the clearest validation within Xcode.
To perform SDK signing, you simply perform a codesign
command specifying the certificate you’d like to sign with, and the framework or xcframework you’d like to sign.
Verification Process and Steps
On Apple platforms, signatures are validated in different ways and in a variety of places - we’ll dig into the most common ones you’re likely to run into, and go into what validation they perform.
Devices
iOS, watchOS, visionOS, tvOS, macOS via App Store
For any application that is installed via the app store on any platform, the system will perform a variety of checks prior to executing the application. If any check in the following list fails, the application will not be executed.
- The main bundle and every sub-bundle (.framework, .app, .appext, etc) is signed, and is signed with the same non-revoked, non-expired certificate. This certificate must also be part of the approved Apple public key infrastructure, and authorized for the provisioning profile in question.
- The main bundle and each executable sub-bundle has a provisioning profile with an appropriate name (on non-macOS systems this is always
embedded.mobileprovision
) - Verification that entitlements in the Info.plist are a subset of the entitlements encoded into the code signature, and that the entitlements in the code signature are a subset of the entitlements permitted via the provisioning profile, for the main bundle and each executable sub-bundle.
Additionally, more validation will occur based on the particular provisioning profile type that is used.
Development Provisioning Profile Verification
For development provisioning profiles, in addition to the normal validation, there is a list of authorized device UUIDs/UDIDs that can be used with the profile, so the system will validate that its UUID/UDID appears in the profile prior to executing the binary.
Enterprise Provisioning Profile Verification
Enterprise provisioning profiles must have the Team ID be trusted by the user prior to launching an application. If you have ever worked for a large company and tried to install an in-house application only to get a warning dialog telling you to go into settings and hit trust, you’ve run into this.
Ad-Hoc Distribution Provisioning Profile Verification
For ad-hoc provisioning profiles, in addition to the normal validation, there is a list of authorized device UUIDs/UDIDs that can be used with the profile, so the system will validate that its UUID/UDID appears in the profile prior to executing the binary. This is very similar to development builds, however additionally the ad-hoc distribution builds can contain production service access as opposed to development limiting in some cases to stages only, and ad-hoc profiles will not allow the get-task-allow
or other debugging entitlements.
App Store Distribution Provisioning Profile Verification
App Store Distribution builds are actually re-signed by Apple prior to deployment, so generally are not installable on our devices with signatures we can issue.
macOS
For general deployments to macOS (that is, those outside of the app store), code signatures are not strictly required, but the system may add additional warnings or refuse to run the application without right clicking and selecting open unless the binary is signed. Signature verification on macOS is complicated, and there are a few steps.
- The signature will be validated to be from an unexpired and non-revoked Developer ID certificate
- Additionally, the system will check if the application is notarized either via checking for a stapled notarization or if none is available asking Apple’s servers
- If the application uses a provisioning profile for advanced capabilities, that will also be validated, similarly to how profiles are validated for App Store distribution.
ASC
App Store Connect performs signature validation during the submission process. This validation logic is similar to what is performed on devices, and is performed to make sure that your app has a match between the provisioning profile, entitlements, the certificate used for signing, and validating that the certificate is a valid distribution certificate registered to your account.
Xcode
A relatively recent addition, Xcode now provides information about the signatures used to sign SDKs by their authors, and requires signatures in some cases. Xcode will notify you if the signature has changed, or if it expects one and it is not present. One important note is that if an SDK is pre-signed, the primary security assurance this provides is that the SDK you’re using is one provided by the original developer, and has not been tampered with in transmission - you still have to re-sign SDKs before you deploy apps relying on them, and Xcode does this automatically.
You can see some examples of what Xcode may display below, including if a signature changes, or one is expected and it is not present.
Common Signing Misconceptions
codesign
is versioned with Xcode
No, codesign
is versioned along with macOS - if you want new codesign
features you need to update to a new version of macOS.
I don’t need to re-sign XCFrameworks since they have signatures now
No - see above, you need to have everything in your bundle signed by the same certificate - you still need to re-sign it!
My distribution certificate is about to expire, this means my upload to the app store won’t work on customer devices
No - your app store upload signed with your distribution certificate will be re-signed by Apple prior to being pushed to the App Store, it only has to be valid at the time of upload.
I can’t re-use old builds of my application once their signatures expire
Well, sure, but you can re-sign them with your new certificate and you can run them again - as long as your devices can still run the app.
All codesignature information lives in _CodeSignature
in the bundle
A lot of it does, but remember that binaries also have segments that can contain code signature information.
Provisioning profiles are invalid as soon as I add devices, and have to be regenerated.
No, you can keep using the local ones you’ve downloaded, they just won’t contain the latest devices you’ve added.
If I run codesign --deep
it fixes all of this
Sometimes, but more often than not no - especially if you have non-default entitlements, which is most non-trivial applications and application extensions.
Running codesign --verify --deep
shows things are great, so I know it will work on a device
Generally, no you don’t. codesign --verify
is only validating that your actual code signatures are valid - that is, that the certificate used is part of Apple’s PKI, non-expired, and not on the revocation list.
For app bundle signing trusting this is especially misleading and wrong, as it is not performing any validation of the existence nor compatibility between entitlements, provisioning profiles, and certificates.
For Developer ID signing, if this returns valid it can build some confidence, but even in that case it isn’t checking your notarization or stapling.
Local builds are always unsigned by Xcode
No, they’re usually signed either with a developer certificate or ad-hoc codesigning, which we’ll cover in another post.
What’s next?
In Part II we will dig into the actual composition of a code signature, how to read code signatures using tooling, ways you can manually validate a code signature, composition of and reading provisioning profiles, weird implications and limitations of the system, corner cases, and weird problems that come up sometimes.
In Part III we will dig into even more advanced topics including simulator signing, optimizations in signing tooling outside of Xcode, how signing interacts and disrupts distributed build systems and caching, how code signing is utilized by alternative build systems, and dig into security implications for large enterprises.
Footnotes
Generally bundle identifiers are globally unique as is, and for apps submitted to the App Store, Apple will make sure you do not create a new ASC entry that has a conflict with an existing bundle ID, but there is also another safeguard: prefixes. When you create an App Identifier, which is then used as a bundle ID, on App Store Connect it actually will prefix it with your team ID. So while in Xcode you may see
is.kayla.test
your bundle ID is actually going to be encoded asABC8473.is.kayla.test
ifABC8473
was your Team ID. This avoids you being able to trick people into installing a developer signed application that has the same bundle ID as a famous application and then extracting a user’s data. ↩︎Yeah I’m not getting into ad-hoc signing in this post. For those not familiar, briefly, using
-
as your signing identity in acodesign
command will perform an ad-hoc codesigning operation that creates a sealed but unsigned bundle or binary valid only in limited circumstances. This is most frequently used for some types of local builds executed on the machine, but I also use it when a program has integrity protection so I can’t look at its memory, and I want to, so I just ad-hoc sign it without the necessary integrity entitlement to disable the protection. ↩︎