10.2 — Code Signing Deep Dive

Opening scenario

It’s 11pm. Your release branch builds locally. CI fails:

error: No profiles for 'com.acme.notes' were found:
Xcode couldn't find any iOS App Store provisioning profiles matching 'com.acme.notes'.

You regenerate profiles in the Apple Developer portal. Still fails. You delete derived data, restart Xcode, restart your Mac. The build that worked an hour ago no longer works and you don’t know why.

Code signing is the single biggest source of “works on my machine” frustration in iOS. Understanding the actual cryptographic machinery — not just clicking checkboxes — is what separates engineers who can ship from those who can’t.

Context taxonomy

ArtifactPurposeLives whereExpiry
Apple Development certificateSign development builds for your devicesKeychain (private), Apple portal (public)1 year
Apple Distribution certificateSign Ad Hoc, App Store, Enterprise buildsKeychain + portal1 year
Apple Push Notification key (p8)Auth APNs requestsAnywhere; download onceNever expires (revocable)
Development profileBundle ID × cert × device list (≤ 100)~/Library/MobileDevice/Provisioning Profiles/1 year
Ad Hoc profileOff-store distribution to listed devicesSame dir1 year
App Store profileBuild that App Store acceptsSame dir1 year
Enterprise profileIn-house distribution, any deviceSame dir1 year
EntitlementsXML capabilities map embedded in binary.entitlements in Xcode projectn/a

Concept → Why → How → Code

Concept. Code signing answers three questions:

  1. Who produced this binary? (Certificate)
  2. What is it allowed to do? (Entitlements)
  3. Where is it allowed to run? (Provisioning profile constraints)

Why. iOS refuses to launch a binary unless every byte hashes correctly against the embedded signature, the signing certificate chains to Apple’s root, and a matching valid provisioning profile is installed (or, for App Store, the profile was present at app submission time).

How — the flow at build time.

Source code
    ↓ compile
Mach-O binary + resources
    ↓ codesign --sign "Apple Distribution: Acme Inc."
Signed .app bundle (contains _CodeSignature/CodeResources hash map)
    ↓ embed PROVISIONING_PROFILE
.app ready for IPA packaging
    ↓ xcodebuild -exportArchive
.ipa (zip of Payload/MyApp.app + metadata)

Inspect a built .app.

# What's the signing identity?
codesign -dv --verbose=4 build/MyApp.app 2>&1 | head -20

# What entitlements does it claim?
codesign -d --entitlements :- build/MyApp.app

# What provisioning profile is embedded?
security cms -D -i build/MyApp.app/embedded.mobileprovision | plutil -p -

# Verify the signature is intact
codesign --verify --deep --strict --verbose=2 build/MyApp.app

# Re-sign with a different identity (useful for white-label apps)
codesign --force --sign "Apple Distribution: Other Co." \
         --entitlements ./resigned.entitlements \
         build/MyApp.app

The five most common errors and their exact fixes.

ErrorRoot causeFix
No profiles for 'X' were foundNo profile in portal matches bundle ID + cert + teamRegenerate profile; if using automatic signing, toggle “Automatically manage signing” off and on
Provisioning profile doesn't include signing certificateProfile generated before a new cert was issuedRegenerate profile in portal — selecting the new cert
code object is not signed at allBundled framework/extension was not re-signedAdd a Run Script phase calling codesign -fs "$EXPANDED_CODE_SIGN_IDENTITY" Frameworks/*.framework
Entitlement com.apple.developer.X not supportedCapability not enabled on App IDEnable it in Identifiers → App IDs → Edit
errSecInternalComponent (CI only)Keychain locked or not in search listRun security unlock-keychain and security set-key-partition-list -S apple-tool:,apple:

In the wild

  • Apple’s own apps (Pages, Numbers) are signed with Apple Distribution: Apple Inc. — confirm via codesign -dv on a downloaded .app.
  • WhatsApp uses a Group ID (group.net.whatsapp.WhatsApp.shared) entitlement to share a keychain item between the main app and the share extension — both must be signed with the same team.
  • Apps that lazy-download frameworks (early Facebook SDK) used to break code signing on iOS 9+ because the on-disk binary no longer matched the signature manifest. Apple’s On-Demand Resources API exists partly to fix that pattern.
  • Re-signing for enterprise distribution is how MDM platforms like Jamf Pro deliver internal-only versions of consumer apps.

Common misconceptions

  1. “Automatic signing always works.” It works when (a) your account is correctly configured, (b) your bundle ID matches what’s registered, and (c) you have the right role. Otherwise, it silently fails or generates the wrong profile.
  2. “The provisioning profile is the certificate.” No. The profile references one or more certificates; it’s a separate file with its own expiry.
  3. “Deleting profiles in ~/Library/MobileDevice/... is dangerous.” It’s safe and often necessary. Xcode re-downloads what it needs.
  4. “Entitlements live in Info.plist.” No — entitlements live in a separate .entitlements file. Info.plist has unrelated metadata.
  5. “Once I sign an IPA, it works on every device of that type.” Only if it’s an App Store build. Ad Hoc/Dev builds are restricted to the device UDIDs in the profile.

Seasoned engineer’s take

There are two religions in iOS code signing: automatic (Xcode manages everything) and manual (you control every step, usually via Fastlane match). Pick one per project — never mix.

TIP. For team projects, use fastlane match with a private Git repo. Every engineer runs fastlane match development once and code signing is solved forever. Onboarding goes from a 2-hour ritual to a single command.

WARNING. Never commit a .mobileprovision or .p12 to source control without encryption. If you must check certs into Git, use match (which encrypts everything with a passphrase) or git-crypt — both are battle-tested.

When debugging code signing, work outside-in: first verify the IPA itself (codesign -dv), then the embedded frameworks, then the entitlements, then the profile. 80% of the time it’s a stale profile or a framework that wasn’t re-signed by an old Xcode script.

Interview corner

Junior“What’s the difference between a development and a distribution certificate?” A development cert lets you sign builds for testing on your own registered devices. A distribution cert lets you sign builds that can go to the App Store, TestFlight, Ad Hoc lists, or enterprise distribution.

Mid“You get a ‘no provisioning profile found’ error in CI but builds work locally. Where do you look?” Three places: the App Store Connect API key permissions, the keychain on the CI runner (it must be unlocked and the certificate imported), and the profile download path — automatic signing rarely works on headless runners, so move to manual + fastlane match.

Senior“Walk me through what happens cryptographically when iOS launches an App Store-signed binary.” iOS checks the binary’s signature against the embedded _CodeSignature/CodeResources hash list, verifies the signing certificate chains to Apple’s root, validates that the embedded provisioning profile (or App Store-issued certificate) authorizes the bundle ID and entitlements, and confirms the binary hasn’t been tampered with at rest. Any mismatch and dyld refuses to load it — the user sees a generic “cannot be opened” error.

Red flag“I always commit the certificates and profiles to the repo so CI just works.” That leaks your distribution identity to anyone with repo access; if it walks, attackers can sign malware as your company until you revoke and re-issue.

Lab preview

Lab 10.1 has you set up fastlane match against a private certs repo, then build and ship to TestFlight with one command — putting every concept here into practice.


Next: 10.3 — App Store Connect