rea-reverse

What's Inside a .mobileprovision, and How to Check It

Published by rea-reverseUpdated 9 min read
On this page

Short answer: a .mobileprovision file is an XML property list wrapped in a CMS (PKCS#7) signature. security cms -D -i file.mobileprovision on a Mac, or openssl cms -verify -inform DER -noverify -in file.mobileprovision anywhere, prints the plist. Inside it are the App ID, the team, the expiry date, the device UDIDs and the developer certificates the app may be signed with. When an install fails, compare those values with what the app was actually signed with (codesign -d --entitlements -) and with its Info.plist. The iOS provisioning profile viewer on this site runs the same comparison in the browser.

This guide is for iOS developers and build engineers who get “Unable to install”, “A valid provisioning profile for this executable was not found” or a push entitlement error and want to see the facts in the files instead of re-running Xcode’s signing settings until something works.

Compare the Profile with the Signed AppSilent walkthrough with English captions.

The sample, and why it is fake

A real provisioning profile contains your team ID, your developers’ names, the UDIDs of your test phones and your signing certificates. None of that belongs in a tutorial, so every file here was built from scratch on 2026-10-20:

  • a self-signed “developer” certificate with an Apple-style subject (CN=Apple Development: Sample Dev (SYNTHDEV01), OU=SYNTEAM042), and a second self-signed certificate that plays Apple’s profile-signing role;
  • a hand-written profile plist with team SYNTEAM042, App ID SYNTEAM042.com.example.demo, two filler UDIDs and an expiry of 2026-11-05, signed with openssl cms -sign -outform DER -nodetach -binary;
  • a five-line C program compiled for iOS arm64, put in Payload/Demo.app with that profile as embedded.mobileprovision, and signed with codesign from a throwaway keychain file. Two builds: ok.ipa, whose entitlements match, and bad.ipa, signed as com.example.demo.beta with aps-environment production.

The structure is identical to a real profile. What differs is the signature: Apple signs real profiles with its own certificate chain, ours is signed by a certificate nobody trusts. That matters in exactly one place below, and the article says where. No profile, certificate or device ID from the Mac this was written on was opened.

Tools used: macOS 27.0.1, Xcode 27.0, OpenSSL 4.0.3 (Homebrew), LibreSSL 3.3.6 (/usr/bin/openssl), file 5.41.

Only inspect profiles and builds that belong to you or your team. See REA’s DISCLAIMER.md.

Step 1: Recognize the file

The first thing that surprises people is that file has no idea what this is:

$ file demo.mobileprovision
demo.mobileprovision: data
$ xxd -l 16 demo.mobileprovision
00000000: 3082 0eaf 0609 2a86 4886 f70d 0107 02a0  0.....*.H.......

30 82 starts a DER SEQUENCE, and 06 09 2a 86 48 86 f7 0d 01 07 02 is the object identifier 1.2.840.113549.1.7.2, signedData. So the file is a CMS SignedData blob, and the plist sits inside it as the signed content, in plain text. You can even grep a UDID in the raw file and get a hit, because nothing is encrypted. Signing only proves who issued the profile.

Some tools produce the BER “indefinite length” form instead (30 80 at the start); openssl cms -sign -stream does, and older profiles use it. Every decoder below read both forms of our sample.

Step 2: Decode the plist

On a Mac, the standard command is:

security cms -D -i demo.mobileprovision > profile.plist

The output was byte-for-byte the plist we had signed (cmp reported no difference). With -h0 you also get a line about the signer, which is where our fake signature shows:

$ security cms -D -h0 -i demo.mobileprovision
SMIME: 	level=0.2; type=signedData; nsigners=1;
		signer0.id="Synthetic Profile Signer"; signer0.status=SigningCertNotTrusted;
	level=0.1; type=data;
<?xml version="1.0" encoding="UTF-8"?>
...

On Linux, in CI or on Windows, use OpenSSL. -noverify matters:

$ openssl cms -verify -in demo.mobileprovision -inform DER -noverify -out profile.plist
CMS Verification successful
$ openssl cms -verify -in demo.mobileprovision -inform DER -out /dev/null
CMS Verification failure
...:cms_signerinfo_verify_cert:certificate verify error:...:Verify error: self-signed certificate

-noverify skips the certificate chain check but still checks that the content matches the signature. Without it, OpenSSL tries to build a chain to a root in its own trust store and stopped at our self-signed signer. For reading a profile you only want the content, so keep -noverify. The older openssl smime -verify -inform DER -noverify form worked too, in OpenSSL 4.0.3 and in the LibreSSL 3.3.6 that macOS ships as /usr/bin/openssl.

The two decoders differ when the content has been changed. We flipped one byte in the profile’s Name (Development to Developmenx) and decoded again. OpenSSL refused, even with -noverify:

CMS Verification failure
...:CMS_SignerInfo_verify_ex:verification failure:crypto/cms/cms_sd.c:1439:
...:CMS_verify:content verify error:crypto/cms/cms_smime.c:501:

security cms -D printed the edited plist and exited with status 0. Only -h0 gave it away, with signer0.status=DigestMismatch. If you decode profiles in a script, check that status line or use OpenSSL.

Three commands that look like they should work, and don’t:

  • plutil -p demo.mobileprovision fails with Property List error: Unexpected character 0 at line 1. plutil does not unwrap CMS; decode first.
  • security cms -D -i bad.ipa fails with security: failed to add data to decoder: UNKNOWN (-8183(d). An IPA is a ZIP; the profile is one entry inside it. unzip -p bad.ipa 'Payload/*.app/embedded.mobileprovision' | security cms -D works without unpacking anything else.
  • plutil -convert json on the decoded plist fails with Invalid object in plist for JSON format, because the profile holds <date> and <data> values. Use plutil -p to read it, or plutil -extract.

Step 3: Read what matters

plutil -p profile.plist gives the whole thing (certificate bytes shortened):

{
  "AppIDName" => "Synthetic Demo"
  "ApplicationIdentifierPrefix" => [
    0 => "SYNTEAM042"
  ]
  "CreationDate" => 2026-10-01 00:00:00 +0000
  "DeveloperCertificates" => [
    0 => {length = 1022, bytes = 0x308203fa308202e2a003020102021438 ... c4fb1139bce916fe}
  ]
  "Entitlements" => {
    "application-identifier" => "SYNTEAM042.com.example.demo"
    "aps-environment" => "development"
    "com.apple.developer.team-identifier" => "SYNTEAM042"
    "get-task-allow" => true
    "keychain-access-groups" => [
      0 => "SYNTEAM042.*"
    ]
  }
  "ExpirationDate" => 2026-11-05 00:00:00 +0000
  "Name" => "Synthetic Demo Development"
  "ProvisionedDevices" => [
    0 => "00008110-000A1B2C3D4E5F60"
    1 => "00008030-0011223344556677"
  ]
  "TeamIdentifier" => [
    0 => "SYNTEAM042"
  ]
  ...
}

Each key answers one install question:

Key What to check
Entitlements → application-identifier TEAMID.bundle.id (or TEAMID.* / TEAMID.com.example.*). Must cover the app’s CFBundleIdentifier.
Entitlements (everything else) The ceiling. The app may be signed with these entitlements or a subset, never more.
get-task-allow true only in development profiles.
aps-environment development or production; the app’s value must be the same.
ProvisionedDevices Device UDIDs for development and ad hoc. Missing in App Store profiles; enterprise profiles have ProvisionsAllDevices instead.
ExpirationDate After this date the app stops launching, not just installing.
DeveloperCertificates DER certificates. The app’s signing certificate must be one of them.
TeamIdentifier, UUID, Name Which team, and which exact profile Xcode or your CI picked.

The profile type (development, ad hoc, App Store, enterprise) is not a field. It follows from the keys: devices plus get-task-allow true is development, devices without it is ad hoc, ProvisionsAllDevices is enterprise, and no devices at all is App Store.

For scripts, plutil -extract pulls one value out:

$ plutil -extract ExpirationDate raw -o - profile.plist
2026-11-05T00:00:00Z
$ plutil -extract ProvisionedDevices raw -o - profile.plist
2

The second result caught us out: raw on an array prints its length, not its contents. Use xml1 instead of raw to see the list, then grep for the UDID you care about. The other trap is keys with dots. plutil -extract Entitlements.com.apple.developer.team-identifier raw fails with No value at that key path or invalid key path, because plutil splits the key path on every dot. PlistBuddy separates with colons, so it gets the value:

$ /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.developer.team-identifier" profile.plist
SYNTEAM042

The developer certificate is base64 under DeveloperCertificates. Extract it and hand it to OpenSSL to see who it belongs to and when it expires:

$ plutil -extract DeveloperCertificates.0 raw -o - profile.plist | base64 -d > dev.der
$ openssl x509 -inform DER -in dev.der -noout -subject -enddate -fingerprint -sha1 -nameopt RFC2253
subject=C=US,O=Synthetic Org,OU=SYNTEAM042,CN=Apple Development: Sample Dev (SYNTHDEV01),UID=SYNTH00001
notAfter=Oct 20 09:53:36 2027 GMT
sha1 Fingerprint=B0:89:55:41:F0:81:37:20:94:D6:09:00:18:35:4A:9F:76:26:90:33

That SHA-1 is the same hex string Keychain Access and security find-identity show for a signing identity, which is how you tell whether your Mac holds the matching private key. The certificates of the CMS signer (Apple’s chain on a real profile) are a separate list; openssl pkcs7 -inform DER -in demo.mobileprovision -print_certs -noout prints them. We first tried openssl cms -cmsout -print_certs and OpenSSL 4.0.3 answered cms: Unknown option or cipher: print_certs; that option belongs to pkcs7 only.

Step 4: Compare with what the app was signed with

The profile only says what is allowed. What the app claims sits in its code signature:

$ codesign -d --entitlements - --xml bad/Payload/Demo.app
<?xml version="1.0" encoding="UTF-8"?>...<key>application-identifier</key><string>SYNTEAM042.com.example.demo.beta</string><key>aps-environment</key><string>production</string>...

codesign also prints an Executable= line to stderr; redirect 2>/dev/null when you pipe the plist into another tool. Many older guides use --entitlements :-. On Xcode 27 that still works but prints warning: Specifying ':' in the path is deprecated and will not work in a future release, so use - with --xml.

To see which certificate signed the app, -dv is not enough. It printed zero Authority= lines for our build; -dvvv showed them:

$ codesign -dvvv bad/Payload/Demo.app
Identifier=com.example.demo.beta
Format=app bundle with Mach-O thin (arm64)
Authority=Apple Development: Sample Dev (SYNTHDEV01)
TeamIdentifier=not set

TeamIdentifier=not set is a side effect of our synthetic certificate: codesign takes the team from Apple-specific certificate fields that a self-signed certificate does not have. On a real build that line shows your team ID. The application-identifier prefix in the entitlements carries the team too, and it still said SYNTEAM042.

codesign --verify --verbose=2 reported valid on disk and satisfies its Designated Requirement for both builds. That check has nothing to do with the profile, so it passes for a build the device will reject.

Putting it together, this is the comparison we ran on bad.ipa with nothing but system tools:

unzip -q bad.ipa
APP=$(ls -d Payload/*.app)
security cms -D -i "$APP/embedded.mobileprovision" > prof.plist
codesign -d --entitlements - --xml "$APP" > ents.plist 2>/dev/null
for k in application-identifier aps-environment get-task-allow; do
  printf '%-24s profile=%-30s binary=%s\n' "$k" \
    "$(plutil -extract Entitlements.$k raw -o - prof.plist)" \
    "$(plutil -extract $k raw -o - ents.plist)"
done
application-identifier   profile=SYNTEAM042.com.example.demo    binary=SYNTEAM042.com.example.demo.beta
aps-environment          profile=development                    binary=production
get-task-allow           profile=true                           binary=true

Two mismatches, and either one is enough to make the device refuse the app. The loop only works for keys without dots and compares exact strings, so it does not understand TEAMID.* wildcards or arrays. It is a quick check, not a validator.

Step 5: The same check in the browser

The iOS provisioning profile viewer reads a .mobileprovision or a whole .ipa in a Web Worker and does the comparison above with wildcards and arrays handled. We ran the tool’s analyzer code from this site’s repository on the same files under Node.js 22.22.3 (with “today” set to 2026-10-20):

  • demo.mobileprovision and the BER variant: type development, team SYNTEAM042, App ID SYNTEAM042.com.example.demo, both UDIDs, the developer certificate with SHA-1 b0895541…69033, signer Synthetic Profile Signer, and one warning: expires in 15 days (2026-11-05). It also read the unsigned profile.plist directly, with an empty signer list.
  • ok.ipa: expiry warning, then bundle id, entitlements, team and signing certificate all pass. The certificate check compares the SHA-1 of the certificate in the code signature with the profile’s DeveloperCertificates.
  • bad.ipa: bundle id fails (com.example.demo.beta against SYNTEAM042.com.example.demo), and the entitlements check fails on two keys, application-identifier and aps-environment (production against development). Team and certificate pass. That is the same diagnosis the shell loop gave, reached without unzipping anything.

What it does not do is verify signatures. Our untrusted signer passed without comment, and so did the profile with the edited byte: the tool showed the name as Synthetic Demo Developmenx and raised nothing beyond the expiry warning. That is fine for reading your own files and the reason not to treat it as a validator. The device is the final judge.

Matching the error to the check

We did not install either build on a device; there is no device in this setup, and a self-signed profile would be rejected anyway. The mapping below is from the meaning of each error, and each row names the static check that covers it:

Install error Look at
“A valid provisioning profile for this executable was not found” (0xe8008015) ExpirationDate; the device’s UDID in ProvisionedDevices; the signing certificate SHA-1 against DeveloperCertificates; the bundle id against application-identifier
“The executable was signed with invalid entitlements” (0xe8008016) Every key from codesign -d --entitlements - must be allowed by the profile’s Entitlements. aps-environment and get-task-allow are the usual ones
App installs, then won’t launch after a few weeks ExpirationDate of a development or ad hoc profile, or the developer certificate’s notAfter
Works from Xcode, fails from CI The UUID and Name of the profile actually embedded. CI often picks a different one

What REA does with profiles

REA, the open-source project by morluto, version 6.7.0, inventories IPA and macOS app bundles. In the published [email protected] package (we read dist/domain/apple/appleBundleAnatomy.js and appleApplication.js from npm pack and did not run anything), embedded.mobileprovision and embedded.provisionprofile appear as signing paths of a bundle, next to _CodeSignature/CodeResources. We found no code there that decodes the CMS envelope or the entitlements, so for the contents use the commands above or the browser tool. REA is useful for the step after this one, when the app installs and you want to look at its executables; see Reverse Engineer a macOS App with REA.

Limits of this walkthrough

The sample is synthetic, so the Apple-specific parts of a real profile are missing: the Apple signing chain, the DER-Encoded-Profile key that newer profiles carry, and capability keys such as iCloud containers or App Groups. Their structure follows the same rules (a value in the app must be allowed by the profile), but we did not exercise them here. And whether a device is still enabled in your developer account, or a certificate has been revoked, is not in any file; only Apple’s portal knows that.

Next steps