Apple Codesigning In Depth: Part II
The second of a multi-part series covering Apple codesigning in depth. This post covers odd corner cases with code signing, how to interrogate a code signature, and how to debug signing issues.
This is the second in a series of posts covering Apple codesigning. In this post I’m going to dive into some additional details on code signatures, focusing in particular on weird corner cases and how you can debug issues with code signatures.
As before, the technical 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 information related to build systems is based on a combination of personal experience and knowledge of how large scale build systems work. If you’re looking for an introduction to codesigning in general, please have a look at my last post.
Weird Code Signature Cases to Consider
In my previous post we covered the architectural niceties of how code signatures for Apple systems work, but we didn’t get into all the weird corner cases that happen in actuality. Before we dig into how to debug code signatures or their impact on build systems, we first need to talk about how they are confusing.
Entitlements but Complicated
Last time we talked about how entitlements are features that you are permitted to use that end up being encoded in your code signature for apps, and they have to be a subset of your provisioning profiles declared entitlements. In practice, the way you get entitlements into your provisioning profiles can be different.
Capabilities
In the web interface for the Apple development portal you can edit your app identifier to define the capabilities. The capabilities have a relationship to your entitlements, as they get reified in the final profile to one or more entitlements. Most of these are fairly ho-hum, but some are very odd and reflect past now obsolete architectural choices Apple made. The Catalyst entitlement is something that can no longer be granted, but you may run into from past app IDs that used it - now you don’t need it even if you are using Catalyst. Some capabilities may have impacts on others, or even get turned into multiple final entitlements in the provisioning profile. We’ll talk more in the debugging section, but when running into codesigning issues you should always decode your profiles and compare them by hand - don’t trust the portal.
Additional Capabilities and Restricted Entitlements
Additional capabilities (now depending on your portal sometimes called “Request Capabilities”) is a relatively recent addition to the page for app identifiers and lets you add more capabilities, some of which may be restricted, to your app ID. These are separate for complicated reasons, but they overlap with what I’m going to talk about next, which is extended entitlements. Sometimes if you select one of these, it may conflict or confuse the extended entitlements you end up selecting, and have outcomes you don’t expect. Whenever you’re using these capabilities be cautious.
Restricted Entitlements
Many of the entitlements available in Additional Capabilities require you to apply to Apple and receive permission. These capabilities are often things that are higher privileged - e.g. provisioning eSIMs, integrating with CarPlay, etc. I won’t get into these a lot, but these entitlements are very interesting and can sometimes be fairly buggy. In the old days these were all assigned per provisioning profile in an “Extended Entitlements” selector but that only let you select one and was extremely painful, we’re fortunate they updated it.
SDK Signatures
I discussed SDK signatures in my previous post, but I didn’t talk about Xcode’s specific validation logic for such signatures. There’s an interesting case that I didn’t talk about that deserves a tiny bit of attention. Apple recommends that SDKs are signed, and encourages people to use developer program certificates for this signing.
Apple isn’t picky about the specific cert that you use in the developer program for signing, and Xcode only validates that the signature is correct and that it is part of the same Team ID as the previous signatures were, facilitating easy changing of certs as they expire or change1. Despite this easy switch, we still want our old SDKs to remain valid, so must be careful.
Apple now has a flag for codesign permitting secure timestamps for code signatures, permitting signatures to remain valid after the certificate expires unless the certificate is revoked. Secure timestamps make it much easier to justify using a lower security certificate that expires more frequently as your signing certificate instead of a longer lived but riskier cert like a Developer ID certificate, so I encourage teams to use these lower security certificates if they are not already and are able.
All this being said, I still strongly advise against using the same cert for other distributions and your SDKs - sharding them in case you need to revoke one is important, as if you revoke the one you use to sign your SDKs you’re going to invalidate every SDK you have in the wild and scare your customers/consumers.
Debugging Codesigning Issues
Most failures to install iOS applications in development or dogfooding environments relate to an issue with the underlying code signature. Automatic signing in Xcode has made this all much easier over the years, but there’s a lot of reasons you might not be able to use that, and even with that things can still go wrong. We’re going to dig into how various things can break and how to get to the bottom of them, but first we have to talk about how to get the data you’ll need to compare.
Interrogating Code Signatures and Profiles
We’re going to need to do three things to really dig into our code signatures:
- Extraction of entitlements and certificate information from a code signature
- Extraction of entitlements and valid certificates from a provisioning profile
- Reading your info.plist details
Each of these gets us part of the data we need to do a “three way match” and check that everything is valid. So let’s take it step by step.
Extracting Entitlements from Code Signatures
To extract entitlements from a given code signature, run the following command replacing the requisite path
1
codesign -d --entitlements - /path/to/your/whatever.app
This will give you output on STDOUT of the entitlements. Of note, this is only for the app or bundle you directly point to. That is to say, any sub-bundles and their entitlements are not included and will need to be checked separately. This can become a bit of a nightmare as you go deeper. You’ll get an output like below:
1
2
3
4
5
6
7
8
9
10
11
12
13
[Dict]
[Key] com.apple.application-identifier
[Value]
[String] ABCDEFG.your.app
[Key] com.apple.developer.team-identifier
[Value]
[String] ABCDEFG
[Key] com.apple.developer.usernotifications.communication
[Value]
[Bool] true
[Key] com.apple.security.device.camera
[Value]
[Bool] true
Once extracted, you’ll want to save this for later as you’ll compare it to other data we extract. Before we go on though, go ahead and also extract information about what certificate was used to issue this code signature via the following command:
1
codesign -dvvv /path/to/your/app.app
This should give you some output like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Executable=/path/to/your/app.app/app
Identifier=com.whatever
Format=app bundle with Mach-O universal (arm64)
CodeDirectory v=20500 size=763 flags=0x10000(runtime) hashes=13+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=...
CandidateCDHashFull sha256=...
Hash choices=sha256
CMSDigest=...
CMSDigestType=2
CDHash=...
Signature size=8977
Authority=Certificate name here (team ID here)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Jan 01, 2026 at 8:12:19 PM
Notarization Ticket=stapled
Info.plist entries=33
TeamIdentifier=ABCDEFG
Runtime Version=15.5.0
Sealed Resources version=2 rules=13 files=23
Internal requirements count=1 size=176
Just note this output separately and set it aside for now.
Extracting Entitlements from Provisioning Profiles
The next thing we’ll need to do is get data from the provisioning profile. For macOS apps this will be in the .app bundle at the path Contents/embedded.provisionprofile, for all other apps this will be in the .app bundle at the root in a file called embedded.mobileprovision. Either way extraction works the same way:
1
security cms -D -i /path/to/your/app.app/embedded.mobileprovision
This will decode your provisioning profile and display the full set of outputs as XML, an example of it ran on an application on my machine is 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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppIDName</key>
<string>Your App Name</string>
<key>ApplicationIdentifierPrefix</key>
<array>
<string>ABCDEFG</string>
</array>
<key>CreationDate</key>
<date>2026-01-01T22:00:38Z</date>
<key>Platform</key>
<array>
<string>OSX</string>
</array>
<key>IsXcodeManaged</key>
<false/>
<key>DeveloperCertificates</key>
<array>
<data>...</data>
</array>
<key>Entitlements</key>
<dict>
<key>keychain-access-groups</key>
<array>
<string>ABCDEFG.*</string>
</array>
<key>com.apple.developer.team-identifier</key>
<string>ABCDEFG</string>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
</dict>
<key>ExpirationDate</key>
<date>2031-01-01T22:00:38Z</date>
</dict>
</plist>
I have excluded a key called “DER encoded profile” - for now you can safely ignore this. We’ll dig into what all this means a bit later, for now just note whatever was emitted by the command.
Reading Your Info.Plist Details
Open your info.plist in Xcode or any other editor that can decode it. You’re going to be looking at the info.plist quite a bit as you go through this. In particular, you’re going to want to pull out any references to your Team ID, the keychain access groups, and anything your app is requesting access to (e.g. camera access, notifications, etc). All of these are things we’ll want to debug.
Debugging Codesigning Problems
Now that we’ve learned how to interrogate a code signature and get all the information we need out of it to figure out what’s wrong. I’m going to break this down by issues we see from common to uncommon.
Expired Signature
If you failed to use secure timestamps during the signing process, if your certificate has expired the signature is now invalid. I’d initially confirm that the certificate that is shown in the signature information is non-expired. You can also run codesign -dvv --verify /path/to/your/app.app and see the output to confirm this.
Expired Provisioning Profile
Provisioning profiles expire independently of certificates, and yours could be expired. Check the key ExpirationDate in the exported profile information and make sure it didn’t just pass. If you have the app installed and it no longer runs, this is also a frequent offender.
Certificate Not Valid with Profile
Provisioning profiles are only considered valid when paired with signatures from certain certificates2. The provisioning profile contains base64 encoded certificates, without the private keys of course, that are valid for use with it. Generally this will only be one certificate, but can be multiple in some cases. If you base64 decode the elements of the array in the key DeveloperCertificates from the profile data you can then compare them to the certificate information from the code signature - be careful though, you need to compare checksums and hashes, not just names.
Mismatched App ID or Team ID
App ID, including the team identifier prefix, need to be the same between the info.plist, the provisioning profile, and the entitlements, if they are not the signature overall is not valid. This also needs to be generally true for most places the TeamID is listed - though this is only true for some specific security entitlements in reality, if it’s different anywhere else it’s an indicator you have something misconfigured.
Mismatched Keychain Access Groups
The provisioning profile contains a very important key keychain-access-groups. This key indicates the keychain access groups that the application is entitled to access. This is one of the original, very old, ways to share data between apps by the same publisher. The provisioning profile will specify this generally as your Team ID followed by a wildcard. Note what this is.
Now, you can go into your entitlements and check the key keychain-access-groups in your info.plist, and make sure that what you specify here is a subset of the set in the provisioning profile. If this matches, you’re good, if not, then this is likely your issue.
Of note, in addition to your team ID, you may include app group related keychain access groups here, but you have to be sure they also match or you’ll run into the same issue.
Mismatched Entitlements
Outside of these “big ones”, there are all the other entitlements that one has. You’ll need to go through and confirm that each and every one matches. Look for new ones that are recent for you to add, or ones that Apple has recently changed or created. If you have multiple app IDs for various reasons you likely should have branching logic on your entitlements based on the app ID you’re compiling, so you should check this branch logic and any entitlements that should be different to see if they are.
What’s Next?
In my final article in this series I’m going to dig into the applications of all of this knowledge to large scale build systems, considerations around management of risk in enterprise codesigning deployments, and talk about supply chain security and insider risk.
Footnotes
This is interesting from a supply chain security perspective, as from Apple’s validation logic in Xcode an SDK signature is equally valid whether it is signed with a low security Apple Development certificate or a high security Developer ID certificate as long as it’s from the same developer program. More on this in another article… ↩︎
This is likely obvious, but this is required to be sure that someone can’t just re-sign a binary with their own certificate after exploiting it and rely on the provisioning profile to give them permissions they want. ↩︎