Deep dive · Deploy fundamentals

How App Store code signing actually works

A certificate, a provisioning profile, and a signed bundle. Four pieces, one trust chain — here's what each one actually proves.

The problem "code signing" solves

When your Mac downloads an app, how does it know the code came from who it claims to? Apple's answer is a chain of cryptographic proofs — each link proving one narrow claim, and every launch of the app on a user's device re-verifying the whole chain. If any link is broken, modified, or expired, the OS refuses to run the code.

From the outside, "code signing" looks like one thing. Inside, it's four things stacked.

The four layers, from bottom to top

Apple Root CA │ │ (signs) ▼ Apple Worldwide Developer Relations CA │ │ (signs) ▼ Your Apple Distribution certificate ← you hold the private key │ │ (signs) ▼ Your provisioning profile ← which cert + which entitlements │ │ (binds to) ▼ Your signed .app / .ipa ← the actual binary │ │ (verified by) ▼ macOS / iOS on the user's device

Apple Root CA. A self-signed root certificate Apple owns. Baked into every Mac and iPhone at the factory. The anchor of trust.

Intermediate CA (WWDR). A certificate the root signed, used to sign developer certs. This indirection means Apple can rotate intermediate certs without replacing the root on millions of devices.

Your Apple Distribution certificate. A certificate Apple's WWDR signed for your team. It contains your public key; you hold the matching private key in your login keychain. This is what "signing with your cert" really means — you're signing a hash of your binary with the private key, and the cert is the public half that lets Apple (and users' devices) verify you did.

Your provisioning profile. A bundle that declares three things: which certificate(s) are allowed to sign builds for a given bundle ID, which entitlements the app is allowed to claim (iCloud, push, app groups, etc.), and (for development builds only) which devices are whitelisted. For App Store distribution, Xcode creates and refreshes these automatically; you rarely touch them directly.

Your signed .xcarchive / .app / .ipa. The build, with an embedded signature that chains all the way back up to the Apple Root CA.

What each layer proves

You can see why mismatched pieces fail so loudly. A cert for Team A can't sign for a profile scoped to Team B. A profile declaring App Sandbox can't ship a binary missing that entitlement. A binary modified after signing (even by a single byte) fails every verification.

Why Apple stacks it this way

Why not a single signing step, like Android uses? Two reasons:

How xcodebuild ties it together

When LingCode runs xcodebuild archive, it:

  1. Compiles your Swift / Obj-C into a binary.
  2. Finds an Apple Distribution certificate in the login keychain matching the team ID declared in your project.
  3. Asks Apple's developer portal (via Xcode's automatic signing) for a matching provisioning profile — creating or refreshing one as needed.
  4. Computes SHA-256 hashes of every file in the .app bundle and signs them with the cert's private key.
  5. Embeds the provisioning profile inside the bundle so the device can verify it at launch time.
  6. Wraps everything in an .xcarchive along with dSYMs and metadata.

Then xcodebuild -exportArchive re-signs for distribution (different method, different destination — App Store vs. Developer ID vs. Enterprise) and produces the uploadable artifact.

The takeaways