Apple code signing is the part of iOS development that breaks releases at 5pm on Friday. Certificates expire, profiles get out of sync, CI runners forget where their keychain went, and the Apple Developer portal makes it harder than it needs to be to figure out which app is affected. This guide walks the whole signing surface end-to-end: what the parts are, how they fit together, where the common failure modes are, and how to keep a real team's signing setup healthy as it scales.
It is written for engineers who need to ship, not for documentation completeness. If you only have time to skim, jump to the table of contents on the right and pick the section that matches your fire.
What code signing actually does
iOS, iPadOS, tvOS, watchOS, and visionOS will refuse to launch a binary that does not have a valid signature. Code signing is the cryptographic step that proves a build came from a specific Apple Developer team and has not been modified since signing. On every launch, the operating system verifies the signature, checks the embedded provisioning profile, and decides whether to run the binary.
Three things have to line up for the verification to succeed:
- The signature on the binary was produced by a private key whose certificate Apple has signed and not revoked.
- The embedded provisioning profile authorizes that certificate, is still within its validity window, and matches the binary's Bundle ID and entitlements.
- The certificate chains back to the Apple Root CA via the WWDR intermediate that issued it.
Most signing failures collapse to one of those three checks. Everything else in this guide exists to keep them aligned.
The cast of characters
iOS code signing has more moving parts than it needs to. The first time you set it up, you end up generating CSRs in Keychain Access, registering App IDs in the Apple Developer portal, downloading certificates, building provisioning profiles, and dragging .p12 files around between Macs. The pieces that matter:
- Apple Developer Program
- Apple's $99/year membership. Required for everything below. See the Apple Developer Program glossary entry for individual vs organization enrollment differences.
- Apple ID and Team ID
- The Apple ID is the per-person identity that signs in to App Store Connect and the Developer portal. The Team ID is the 10-character alphanumeric identifier of your team that prefixes every certificate, profile, and signed binary.
- Code signing certificate
- An Apple-issued certificate that says 'this public key belongs to this team'. The two you will touch the most are Apple Development (for daily debugging on your own devices) and Apple Distribution (for TestFlight, App Store, and Ad Hoc builds).
- Private key
- The secret half of the key pair. The certificate has the public half. Whoever holds the private key for a certificate can sign builds as that team. Lose it and you cannot rotate; leak it and others can sign as you.
- Certificate Signing Request (CSR)
- The file Apple wants in exchange for issuing a certificate. A CSR bundles a public key plus your team identity, signed with the matching private key as proof of possession.
- App ID
- A registered identifier in your Apple Developer account that combines your Team ID with your Bundle ID (e.g. `TEAMID.com.example.app`) and declares which capabilities your app uses. See App ID and Bundle ID for the difference.
- Provisioning profile
- An Apple-signed plist that ties an App ID to one or more certificates and (for Development and Ad Hoc) a device list. Without one, iOS refuses to launch your build. There are four provisioning profile types: Development, Ad Hoc, App Store, and In-House.
- Entitlements
- Entitlements are Apple's permission system. They declare which system capabilities (push, App Groups, iCloud, HealthKit, Sign in with Apple) the build is allowed to use. They live in an .entitlements file, get baked into the signature, and have to be authorized by the matching provisioning profile.
- App Store Connect API key
- The credential Apple issues for non-interactive access to App Store Connect. The App Store Connect API key is how modern tooling (HexSign, fastlane, Bitrise) talks to your account without you sharing an Apple ID password.
The end-to-end signing flow
From source to a launchable build, signing happens in five places. If anything in this chain breaks, the build either fails to compile, fails to upload to App Store Connect, or fails to launch on a device.
- 1
Generate a CSR locally
Keychain Access > Certificate Assistant > Request a Certificate from a CA, or `openssl req -new`. The private key is created on your machine and never leaves. The .certSigningRequest file is what you upload to Apple.
- 2
Request a certificate from Apple
In the Apple Developer portal, upload the CSR and pick the certificate type. Apple signs the public half and gives you back a .cer file. Combined with the private key already on your Mac, that's your signing identity.
- 3
Register an App ID and configure capabilities
Pick a Bundle ID (e.g. com.example.app) and decide which capabilities the app needs. Adding capabilities like push or App Groups updates the App ID and invalidates every existing provisioning profile that referenced it.
- 4
Create a provisioning profile
Pick the App ID, the certificate you just issued, and (for Development or Ad Hoc) the device list. Apple gives you back a .mobileprovision file signed with Apple's own key.
- 5
Build, sign, and embed
xcodebuild compiles the binary, codesign attaches a signature using your private key, and the chosen .mobileprovision gets copied into the .app bundle as embedded.mobileprovision. The result is an .xcarchive you can export to IPA.
Xcode tries to do all of this automatically if you tick 'Automatically manage signing'. That works for solo developers on one Mac. The moment you have multiple developers, multiple devices, or a CI runner, automatic signing creates a separate provisioning profile every time it sees a new combination and your Apple account fills up with throwaway profiles named `iOS Team Provisioning Profile: com.example.app`. Switch to manual signing in CI, and use a single shared profile per Bundle ID per distribution type. The fastlane match alternatives guide walks through what to do once you outgrow automatic signing.
Distribution methods and which profile each one needs
iOS has four distribution methods, and each one needs a specific provisioning profile type:
| Distribution | Certificate | Profile type | Device list? |
|---|---|---|---|
| Daily debug on your own devices | Apple Development | Development | Yes (your registered devices) |
| Send to a specific tester outside TestFlight | Apple Distribution | Ad Hoc | Yes (up to 100 per class per year) |
| TestFlight or App Store | Apple Distribution | App Store | No |
| Internal employees only (no App Store) | Enterprise distribution certificate (Apple Developer Enterprise Program required) | In-House | No |
Most teams ship with two profiles per app: a Development profile that every developer's device is on, and an App Store profile for TestFlight and release. Ad Hoc is the third one most teams add. In-House is rare and the Enterprise Program has strict rules around it.
The three-certificate cap and rotation
Apple limits each team to a small number of active certificates per type. Apple Distribution caps out at three; Apple Development is higher but still finite. This is the cap most teams hit, and it is the reason rotation strategy matters more than you would expect.
Why teams run into it:
- Every fastlane match nuke creates a fresh certificate, leaving the old ones counted against the cap.
- Every developer who hits 'Automatically manage signing' on a new Mac can issue another Apple Development certificate against the team's cap.
- Old certificates do not automatically count against the cap once they expire, but every active certificate does, even if no one is using it.
A rotation strategy that does not break releases
Certificates expire after one year (longer for Developer ID, shorter for some specialty types). Waiting until the day before the cliff is a guaranteed bad time. The pattern that works for most teams:
- 1
Set an alert 60 days out
Most rotations need to happen by the 7-day mark. A 60-day alert gives you room to plan, a 14-day fallback gives the on-call person enough time to act if nobody picked it up.
- 2
Reuse the existing CSR if your policy allows
If you regenerate against the same CSR, the new certificate has the same public key. Every provisioning profile that referenced the old certificate keeps working without regeneration.
- 3
Issue the new certificate, regenerate dependent profiles
If you cannot reuse the CSR, you have to issue a new certificate with a fresh key. Every profile that referenced the old certificate has to be regenerated and re-pulled by CI.
- 4
Cut over CI, then revoke the old certificate
Ship a build with the new certificate first. Once you have confirmed a release path is green end-to-end, revoke the old certificate to free up the cap.
The errors that account for 90% of failures
Most iOS signing failures come from the same dozen root causes. Knowing what the message means is half the fix.
- No signing certificate found
- codesign can't see a private key in any keychain in the search list, or the keychain is locked. On CI, this is almost always a missing `security unlock-keychain` or `security set-key-partition-list` call after importing the .p12.
- errSecInternalComponent
- Specifically a keychain partition-list issue. The fix is `security set-key-partition-list -S apple-tool:,apple: -k <password> <keychain>` after import.
- Provisioning profile doesn't include signing certificate
- The profile was generated before the certificate you are signing with existed (or after it was revoked). Regenerate the profile against the current certificate.
- Unable to verify your provisioning profile
- Usually the embedded profile has expired, was revoked, or its certificates were revoked. Less commonly, the bundle was tampered with after signing.
- No matching profiles found
- Xcode's automatic signing cannot find a Development profile that covers your Bundle ID, the current device, and your team. Either let it create one, or switch to manual signing and bind a known-good profile.
- ITMS-90161, ITMS-90035, and friends
- App Store Connect upload errors that mostly trace back to entitlement mismatches between the .ipa and the App ID. Regenerate the profile after toggling capabilities in the portal, and re-archive.
- App installed but push tokens never arrive
- Push entitlement was added in the .entitlements file but the App ID's push capability was never enabled in the Apple Developer portal, or the profile was never regenerated. Both have to change.
Signing in CI
Local signing usually 'just works' because the Mac you build on has the keychain populated. CI does not. The runner needs a fresh keychain with the certificate and private key imported, the keychain has to be unlocked, codesign needs partition-list permission, and the right provisioning profile has to be installed at `~/Library/MobileDevice/Provisioning Profiles/`. The Apple code signing in CI/CD guide walks through the full pattern, including ready-to-paste GitHub Actions, Bitrise, CircleCI, GitLab, and fastlane setups.
The shortest version is three steps the runner has to do before xcodebuild:
- Create a scoped keychain (`security create-keychain`), set it as the default, and unlock it.
- Import the .p12 with the private key (`security import` plus `set-key-partition-list`).
- Drop the .mobileprovision into `~/Library/MobileDevice/Provisioning Profiles/`.
Where teams get into trouble
Knowing the failure modes ahead of time is cheaper than learning them at 5pm on a Friday. The recurring themes across teams that have shipped to the App Store for years:
- Signing material in git. Even encrypted with fastlane match, a .p12 in a public repo (or in a private repo that gets forked or migrated) is a credential leak waiting to happen.
- One person owns the only copy of the private key. They leave the company and rotation becomes a fire drill instead of a Tuesday.
- Onboarding new developers takes hours of clicking around in the Apple Developer portal because nobody documented which provisioning profile maps to which app.
- Capabilities drift between the Apple Developer portal and the project's .entitlements file. Push works in dev and silently fails in prod.
- Every CI runner downloads, imports, unlocks, and sets partition lists from scratch on every build. The slowest part of your build pipeline is now the part that has nothing to do with your code.
Where to go next
If you came here looking for a specific topic, the next stop depends on which one:
- Setting up signing in CI: Apple code signing in CI/CD.
- Moving off fastlane match: fastlane match alternatives & migration.
- Mac apps outside the App Store: macOS notarization for Electron + native.
- A specific term: the glossary has 36 plain-English definitions of every Apple signing concept this guide mentions.