How do you decode and audit a provisioning profile: which fields matter, how do you tell development, ad hoc and App Store profiles apart, and does decoding verify it?
Predict first
A sample profile is signed, decoded, then one byte of the signed copy is changed. What do decoding and the signature check report?
// === run.sh ===
openssl req -x509 -newkey rsa:2048 -nodes -keyout k.pem -out c.pem -subj "/CN=Sample Signer" -days 2 2>/dev/null
openssl cms -sign -signer c.pem -inkey k.pem -in p.plist -outform DER -nodetach -out sample.mobileprovision
security cms -D -i sample.mobileprovision > dec.plist
echo "team: $(plutil -extract TeamIdentifier.0 raw dec.plist)"
echo "app id: $(plutil -extract Entitlements.application-identifier raw dec.plist)"
echo "devices: $(plutil -extract ProvisionedDevices raw dec.plist)"
GTA=$(plutil -extract Entitlements.get-task-allow raw dec.plist)
echo "kind: development (devices listed, get-task-allow=$GTA)"
EXP=$(plutil -extract ExpirationDate raw dec.plist)
NOW=$(date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "2027-02-01T00:00:00Z" +%s)
END=$(date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$EXP" +%s)
echo "days left on 2027-02-01: $(( (END - NOW) / 86400 ))"
perl -pe 's/Demo Development/Demo Developmenx/' < sample.mobileprovision > tampered.mobileprovision
security cms -D -i tampered.mobileprovision | grep -o "Demo Developmenx" | head -1
echo "security cms -D on the altered copy: exit $?"
openssl cms -verify -inform DER -in tampered.mobileprovision -noverify -out /dev/null 2>/dev/null || echo "signature check on the altered copy: FAILED"
openssl cms -verify -inform DER -in sample.mobileprovision -noverify -out /dev/null 2>/dev/null && echo "signature check on the original: ok"
// === p.plist ===
<?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>Name</key><string>Demo Development</string>
<key>TeamIdentifier</key><array><string>ABCDE12345</string></array>
<key>ExpirationDate</key><date>2027-03-01T10:00:00Z</date>
<key>ProvisionedDevices</key><array><string>00008110-001A2B3C4D5E6F78</string><string>00008120-00AA11BB22CC33DD</string></array>
<key>Entitlements</key><dict>
<key>application-identifier</key><string>ABCDE12345.com.example.demo</string>
<key>get-task-allow</key><true/>
<key>aps-environment</key><string>development</string>
</dict></dict></plist>
- The altered copy still decodes with exit 0, but the signature check fails; the original passes
- The altered copy fails to decode
- Both copies fail the signature check
Mental model. The profile is a visa stamp in a passport. Reading what the stamp says is easy; knowing it is genuine is a different test that only the border officer can run.
A .mobileprovision is a CMS (PKCS#7) envelope around an XML property list. security cms -D -i file.mobileprovision prints the plist, and plutil -extract or PlistBuddy read single keys. The fields that explain most signing errors are: TeamIdentifier and Entitlements:application-identifier (team plus bundle ID, which may end in a wildcard), DeveloperCertificates (which certificates may sign), Entitlements (the ceiling on what the app may claim, Q479), ExpirationDate and, for the type, ProvisionedDevices. The type is not a field, you infer it: devices listed plus get-task-allow = true is a development profile, devices listed with get-task-allow = false is ad hoc, no device list is App Store (and ProvisionsAllDevices marks an enterprise profile). The check below builds a sample profile, decodes it and prints those answers, including how many days remain before expiry, which is the number to alert on in a pipeline.
Decoding is not validation. In the check, a copy with one changed byte still decodes with exit status 0 and prints the altered name, because security cms -D only unwraps the envelope. The integrity check is a separate step (the check uses openssl cms -verify -noverify, which tests the content signature of the sample but, since the sample is self-signed, deliberately skips the certificate chain; for a real profile the chain must lead to Apple, and Xcode and the device do that check). Practical uses: in CI, fail the job when a profile expires within a few weeks, when its entitlements are not a superset of the entitlements file you sign with, or when the device list lacks a new test device; locally, installed profiles live in ~/Library/Developer/Xcode/UserData/Provisioning Profiles, so diff the profile Xcode picked against the one you expected. This was run with a sample, self-made profile; a real one needs a paid developer account and was not used.
Decoding unwraps the envelope without checking it; verification is a separate step.
Common traps
- Treating a successful
security cms -Das proof the file is genuine. It only unwraps the envelope; trust comes from verifying the signature chain. - Guessing the profile type from its name. “Distribution” profiles come in ad hoc, App Store and enterprise flavours; read
ProvisionedDevicesandget-task-allow.
They might follow up with
- Why do wildcard App IDs not work with push or iCloud? Those capabilities need an exact App ID, so their entitlements cannot be granted to a wildcard profile.
- How do I know which profile Xcode embedded? Decode
embedded.mobileprovisioninside the built app (iOS) with the same command.
Related questions
- 25.1.1 How does iOS code signing work: certificates, App IDs, provisioning profiles, signing identities, export…
- 25.1.2 What is the difference between a capability, an entitlement and an Info.plist key, and what breaks when they…
- 6.1.3 What are entitlements, and what happens when they do not match the provisioning profile?
- 25.2.3 How do you design a release pipeline: hermetic builds, secrets, caching, artefact promotion and the order of…
What is inside a code signature: the code directory, hashes, the… →