What's Inside a .mobileprovision, and How to Check It
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.
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 IDSYNTEAM042.com.example.demo, two filler UDIDs and an expiry of 2026-11-05, signed withopenssl cms -sign -outform DER -nodetach -binary; - a five-line C program compiled for iOS arm64, put in
Payload/Demo.appwith that profile asembedded.mobileprovision, and signed withcodesignfrom a throwaway keychain file. Two builds:ok.ipa, whose entitlements match, andbad.ipa, signed ascom.example.demo.betawithaps-environmentproduction.
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.mobileprovisionfails withProperty List error: Unexpected character 0 at line 1.plutildoes not unwrap CMS; decode first.security cms -D -i bad.ipafails withsecurity: 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 -Dworks without unpacking anything else.plutil -convert jsonon the decoded plist fails withInvalid object in plist for JSON format, because the profile holds<date>and<data>values. Useplutil -pto read it, orplutil -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.mobileprovisionand the BER variant: typedevelopment, teamSYNTEAM042, App IDSYNTEAM042.com.example.demo, both UDIDs, the developer certificate with SHA-1b0895541…69033, signerSynthetic Profile Signer, and one warning: expires in 15 days (2026-11-05). It also read the unsignedprofile.plistdirectly, 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’sDeveloperCertificates.bad.ipa: bundle id fails (com.example.demo.betaagainstSYNTEAM042.com.example.demo), and the entitlements check fails on two keys,application-identifierandaps-environment(productionagainstdevelopment). 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
- Open your own profile or IPA in the iOS provisioning profile viewer.
- Decode the developer certificate in full with the X.509 certificate decoder.
- Check the architectures and minimum OS of the app’s executable: Is This Mac App Universal? covers
lipoand Mach-O slices. - Got a file and not sure it is a profile at all? When the file Command Is Wrong shows what
fileand this site’s identifier say about it.