
When you start developing for iOS, you quickly bump into Apple’s code signing system. Signing certificates, provisioning profiles, entitlements, private keys — it can feel like a confusing black box.
TL;DR Summary
- You create a private key on your mac.
- Apple gives you a certificate that links your identity to your key.
- A provisioning profile defines where and how your app can run.
- iOS checks everything before allowing the app to install and execute.
Why Does iOS Require Code Signing?
Apple uses code signing to:
- Verify that an app comes from a known, trusted developer.
- Ensure that the app hasn’t been tampered with after signing.
- Control which apps can be installed on iOS devices (for security & control of the App Store ecosystem).
- Restrict certain features to authorized developers (like Push Notifications, HealthKit, etc).
No signed app → no install.
The Main Players
Here are the key components involved in signing an iOS app:
The Flow: Step-by-Step
Let’s walk through the signing process:
1️⃣ You Generate a Key Pair
- On your Mac, you create a private/public key pair.
- The private key stays on your machine (in your Keychain).
- The public key is included in a Certificate Signing Request (CSR).
2️⃣ Apple Issues a Signing Certificate
- You upload the CSR to Apple via the Developer Portal (or Xcode does it for you).
- Apple verifies your Developer Account and issues a Signing Certificate.
- The certificate contains:
- Your public key.
- Your identity (developer or team).
- Apple’s digital signature (proving it’s a valid Apple-issued cert).
3️⃣ You Build Your App
When you compile your app:
- Xcode uses your private key to sign the app bundle.
- The signature proves that the app comes from you, and that the code hasn't changed since signing.
4️⃣ You Create a Provisioning Profile
- A Provisioning Profile combines:
- The app’s Bundle ID (your App ID).
- Your Signing Certificate.
- The device UDIDs (for development profiles).
- Any enabled entitlements.
- The profile tells iOS: "This app is allowed to run on this device, and it comes from this authorized developer."
5️⃣ The Profile Is Embedded
- The provisioning profile is embedded into the app bundle.
- When the app launches, iOS verifies:
- The app is signed with the matching private key.
- The certificate is still valid.
- The profile allows execution on this device.
- The entitlements match.
Quick Overview: Types of Certificates
Quick Overview: Types of Provisioning Profiles
Private Keys: The Weak Link
- The private key is the most sensitive piece.
- Without it, you can’t sign new builds.
- If you lose it, you’ll need to revoke the certificate and create a new one.
- This is why Apple often warns: "Keep your private keys safe and backed up."
👉 If you're using multiple Macs, make sure you export and import the private key + certificate to avoid signing issues.
Simplified Visual
[ Private Key (your Mac) ] <--signs--> [ App Bundle ]
↓
[ Public Certificate (issued by Apple) ]
↓
[ Provisioning Profile (rules + entitlements) ]
↓
[ iOS Device: validates everything ]
Modern Bonus: Xcode & Automatic Signing
- Xcode tries to automate a lot of this.
- When “Automatically Manage Signing” is enabled, it:
- Generates CSRs.
- Creates provisioning profiles.
- Downloads certificates.
- Syncs profiles.
- For many developers, this hides most of the complexity.
- BUT: when things break (or on CI/CD pipelines), understanding the manual process is invaluable.
Gotchas to Avoid
- ✅ Always backup your private key.
- ✅ Revoke old certificates you no longer need.
- ✅ Watch out for expiring provisioning profiles.
- ✅ For CI/CD: securely store certificates and private keys.
Setup your perfect CI/CD
In most local development workflows, Xcode handles this for you automatically behind the scenes. But once you move into CI/CD pipelines, automation, fastlane, the impaktfull_cli or other advanced release processes, you almost always need to provide an exportOptions.plist
file explicitly to control the export behavior and avoid interactive prompts.
Example for the App Store
<?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>method</key>
<string>app-store</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.yourcompany.yourapp</key>
<string>Your_Provisioning_Profile_Name</string>
</dict>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
</dict>
</plist>
Example for Ad Hoc
<?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>method</key>
<string>ad-hoc</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.yourcompany.yourapp</key>
<string>Your_Provisioning_Profile_Name</string>
</dict>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
</dict>
</plist>
How to use the export options?
xcodebuild -exportArchive \
-archivePath path/to/YourApp.xcarchive \
-exportOptionsPlist path/to/exportOptions.plist \
-exportPath path/to/exported/ipa
App Signing at impaktfull
We want to have as much control over our signing flow as possisble. This is why we always choose manual signing for alpha, beta and production. For development it is just easier to test things out quickly so we enable automatic signing.
Sharing your private keys should be done in a secure way. This is why using 1password and the impaktfull_cli to create builds is super important for our CI/CD flow. It allows us to create a new private key that is instantly available on all CI/CD runners.
Conclusion
Apple’s signing system is strict, but for good reason: it ensures security, trust, and app integrity across millions of devices. While confusing at first, once you understand the building blocks, certificates, keys, and profiles, it starts to make sense.