.mobileprovision 里装了什么?描述文件的解码与核对
本页目录
结论先行:.mobileprovision 是一个 XML plist,外面套了一层 CMS(PKCS#7)签名。Mac 上 security cms -D -i file.mobileprovision,其他系统上 openssl cms -verify -inform DER -noverify -in file.mobileprovision,都能把里面的 plist 打印出来。plist 里有 App ID、团队、过期时间、设备 UDID 列表,以及允许给这个 App 签名的开发者证书。安装失败时,把这些值和 App 实际的签名内容(codesign -d --entitlements -)、Info.plist 对一遍,问题基本就能定位。本站的 iOS 描述文件查看器在浏览器里做的是同一套对比。
这篇写给遇到“无法安装”、“A valid provisioning profile for this executable was not found”或者推送 entitlement 报错的 iOS 开发和打包同学:与其在 Xcode 的签名设置里反复试,不如直接看文件里写了什么。
样本是自己造的,原因很简单
真实的描述文件里有团队 ID、开发者姓名、测试机 UDID 和签名证书,这些都不该出现在教程里。所以本文用到的文件全部是 2026-10-20 当天从零生成的:
- 一张自签名的“开发者”证书,主题模仿 Apple 的格式(
CN=Apple Development: Sample Dev (SYNTHDEV01)、OU=SYNTEAM042),再加一张自签名证书充当 Apple 给描述文件签名的角色; - 手写的描述文件 plist:团队
SYNTEAM042,App IDSYNTEAM042.com.example.demo,两个凑数的 UDID,2026-11-05 过期,用openssl cms -sign -outform DER -nodetach -binary签名; - 一个 5 行的 C 程序,编译成 iOS arm64,放进
Payload/Demo.app,把上面的描述文件作为embedded.mobileprovision放进去,再用一个临时 keychain 文件里的证书codesign。一共两个包:entitlements 对得上的ok.ipa,以及签成com.example.demo.beta、aps-environment为production的bad.ipa。
结构和真实描述文件完全一样,区别在签名:真实文件由 Apple 的证书链签发,我们的签名者谁都不信任。这一点只在下文一个地方有影响,到时候会说明。写作用的这台 Mac 上的描述文件、证书和设备 ID 一个都没碰。
环境:macOS 27.0.1,Xcode 27.0,OpenSSL 4.0.3(Homebrew),LibreSSL 3.3.6(系统自带的 /usr/bin/openssl),file 5.41。
只检查属于你自己或你团队的描述文件和安装包。参见 REA 的 DISCLAIMER.md。
第 1 步:先认出它是什么
第一个意外:file 完全不认识它。
$ file demo.mobileprovision
demo.mobileprovision: data
$ xxd -l 16 demo.mobileprovision
00000000: 3082 0eaf 0609 2a86 4886 f70d 0107 02a0 0.....*.H.......
30 82 是 DER 编码的 SEQUENCE 开头,06 09 2a 86 48 86 f7 0d 01 07 02 是 OID 1.2.840.113549.1.7.2,也就是 signedData。所以这是一个 CMS SignedData 结构,plist 作为被签名的内容原样放在里面,是明文。直接对原始文件 grep 某个 UDID 也能搜到,因为没有任何加密,签名只用来证明是谁签发的。
有些工具生成的是 BER 的“不定长”形式(开头是 30 80),openssl cms -sign -stream 就是这样,一些老的描述文件也是。下面所有解码方法对我们样本的两种形式都能正常处理。
第 2 步:解出 plist
Mac 上最常用的是:
security cms -D -i demo.mobileprovision > profile.plist
输出和我们签名前的 plist 逐字节一致(cmp 没有报任何差异)。加上 -h0 还会多打印一行签名者信息,我们的假签名就在这里露了馅:
$ 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"?>
...
在 Linux、CI 或者 Windows 上用 OpenSSL,关键是 -noverify:
$ 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 跳过的是证书链校验,内容和签名是否匹配照样会检查。不加它,OpenSSL 会试图在自己的信任库里找到根证书,于是卡在了我们的自签名证书上。读描述文件只需要内容,所以保留 -noverify。老写法 openssl smime -verify -inform DER -noverify 也能用,OpenSSL 4.0.3 和 macOS 自带的 LibreSSL 3.3.6 都试过。
内容被改过的时候,两种解码器的表现不一样。我们把描述文件 Name 里的一个字节改掉(Development 改成 Developmenx)再解码。OpenSSL 拒绝了,即使带着 -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 则照常输出了改过的 plist,退出码是 0。只有加 -h0 才看得到 signer0.status=DigestMismatch。如果在脚本里批量解码描述文件,要么检查这一行状态,要么改用 OpenSSL。
还有三条看起来应该能用、实际不行的命令:
plutil -p demo.mobileprovision报Property List error: Unexpected character 0 at line 1。plutil不会拆 CMS,要先解码。security cms -D -i bad.ipa报security: failed to add data to decoder: UNKNOWN (-8183(d)。IPA 是 ZIP,描述文件只是其中一个条目。unzip -p bad.ipa 'Payload/*.app/embedded.mobileprovision' | security cms -D不用整包解压就能读。- 对解出来的 plist 跑
plutil -convert json,报Invalid object in plist for JSON format,因为描述文件里有<date>和<data>类型。看内容用plutil -p,取单个值用plutil -extract。
第 3 步:读关键字段
plutil -p profile.plist 的输出(证书字节被截短):
{
"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"
]
...
}
每个字段对应一个安装问题:
| 字段 | 看什么 |
|---|---|
Entitlements → application-identifier |
TEAMID.bundle.id(或 TEAMID.*、TEAMID.com.example.*),必须覆盖 App 的 CFBundleIdentifier |
Entitlements(其余键) |
上限。App 签名时的 entitlements 只能是它的子集,不能多 |
get-task-allow |
只有开发版描述文件是 true |
aps-environment |
development 或 production,App 里的值必须一致 |
ProvisionedDevices |
开发版和 Ad Hoc 的设备 UDID。App Store 描述文件没有这一项,企业版是 ProvisionsAllDevices |
ExpirationDate |
过了这个日期,App 不只是装不上,已经装好的也打不开 |
DeveloperCertificates |
DER 证书列表,App 的签名证书必须在里面 |
TeamIdentifier、UUID、Name |
哪个团队、Xcode 或 CI 到底选了哪一个描述文件 |
描述文件的类型(开发、Ad Hoc、App Store、企业)没有专门的字段,是从这些键推出来的:有设备列表且 get-task-allow 为 true 是开发版,有设备列表但没有它是 Ad Hoc,ProvisionsAllDevices 是企业版,一个设备都没有是 App Store。
写脚本时可以用 plutil -extract 取单个值:
$ plutil -extract ExpirationDate raw -o - profile.plist
2026-11-05T00:00:00Z
$ plutil -extract ProvisionedDevices raw -o - profile.plist
2
第二条我们一开始没反应过来:对数组用 raw,输出的是元素个数,不是内容。想看列表要把 raw 换成 xml1,再 grep 你关心的 UDID。另一个坑是带点号的键名。plutil -extract Entitlements.com.apple.developer.team-identifier raw 报 No value at that key path or invalid key path,因为 plutil 把键路径里的每个点都当成分隔符。PlistBuddy 用冒号分隔,就没有这个问题:
$ /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.developer.team-identifier" profile.plist
SYNTEAM042
开发者证书以 base64 存在 DeveloperCertificates 下。取出来交给 OpenSSL,就能看到证书属于谁、什么时候过期:
$ 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
这个 SHA-1 和钥匙串访问、security find-identity 里显示的签名身份哈希是同一个值,据此可以判断这台 Mac 上有没有对应的私钥。CMS 签名者的证书(真实文件里是 Apple 的证书链)是另一组,用 openssl pkcs7 -inform DER -in demo.mobileprovision -print_certs -noout 打印。我们先试的是 openssl cms -cmsout -print_certs,OpenSSL 4.0.3 回了一句 cms: Unknown option or cipher: print_certs:这个选项只有 pkcs7 子命令才有。
第 4 步:和 App 实际的签名对比
描述文件只规定“允许什么”,App 实际声明了什么要看它的代码签名:
$ 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 还会往 stderr 打一行 Executable=,把 plist 管道给其他工具时记得加 2>/dev/null。很多老教程写的是 --entitlements :-,在 Xcode 27 上还能用,但会提示 warning: Specifying ':' in the path is deprecated and will not work in a future release,所以改用 - 加 --xml。
想知道是哪张证书签的,-dv 不够。我们的包用 -dv 一行 Authority= 都没有,-dvvv 才显示:
$ 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 是合成证书带来的:codesign 从 Apple 证书特有的字段里取团队 ID,自签名证书没有这些字段。真实的包这里会显示你的团队 ID。entitlements 里 application-identifier 的前缀同样带着团队信息,这里仍然是 SYNTEAM042。
codesign --verify --verbose=2 对两个包都报 valid on disk 和 satisfies its Designated Requirement。这个校验和描述文件无关,所以设备会拒装的包它也照样放行。
把上面几步串起来,下面是我们只用系统自带工具对 bad.ipa 做的对比:
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
两处不一致,任何一处都足以让设备拒绝安装。这个循环只能处理不带点号的键,而且是字符串精确比较,TEAMID.* 这种通配符和数组都不认。拿来快速看一眼可以,当校验器用不行。
第 5 步:在浏览器里做同样的检查
iOS 描述文件查看器在 Web Worker 里读取 .mobileprovision 或整个 .ipa,做的就是上面的对比,并且处理了通配符和数组。我们在 Node.js 22.22.3 下直接调用本站仓库里这个工具的分析代码,跑了同一批文件(把“今天”设为 2026-10-20):
demo.mobileprovision及其 BER 版本:类型development,团队SYNTEAM042,App IDSYNTEAM042.com.example.demo,两个 UDID,开发者证书 SHA-1 为b0895541…69033,签名者Synthetic Profile Signer,以及一条警告:15 天后(2026-11-05)过期。没签名的profile.plist它也能直接读,签名者列表为空。ok.ipa:过期警告之外,bundle id、entitlements、团队、签名证书四项全部通过。证书检查比较的是代码签名里证书的 SHA-1 和描述文件DeveloperCertificates里的值。bad.ipa:bundle id 不通过(com.example.demo.beta对SYNTEAM042.com.example.demo),entitlements 有两个键不通过:application-identifier和aps-environment(production对development)。团队和证书通过。结论和上面的 shell 循环一样,而且不用解压。
它不做的事情是验证签名。我们不受信任的签名者它没提,改过一个字节的描述文件它也照常显示,名字是 Synthetic Demo Developmenx,除了过期警告什么都没报。读自己的文件没问题,但别把它当校验器。最终说了算的是设备。
报错和检查项的对应关系
这两个包我们都没有装到真机上:环境里没有设备,而且自签名的描述文件本来就会被拒。下表是根据各个报错的含义整理的,每一行对应可以静态检查的项目:
| 安装报错 | 去看 |
|---|---|
| “A valid provisioning profile for this executable was not found”(0xe8008015) | ExpirationDate;设备 UDID 在不在 ProvisionedDevices 里;签名证书 SHA-1 在不在 DeveloperCertificates 里;bundle id 和 application-identifier 是否匹配 |
| “The executable was signed with invalid entitlements”(0xe8008016) | codesign -d --entitlements - 列出的每个键都必须被描述文件的 Entitlements 允许,最常见的是 aps-environment 和 get-task-allow |
| 装上了,几周后打不开 | 开发版或 Ad Hoc 描述文件的 ExpirationDate,或开发者证书的 notAfter |
| Xcode 里能装,CI 打的包不行 | 实际嵌入的描述文件的 UUID 和 Name,CI 经常选中另一个 |
REA 对描述文件做了什么
REA(morluto 的开源项目)6.7.0 可以盘点 IPA 和 macOS App 包的结构。在已发布的 [email protected] 包里(我们用 npm pack 下载后读了 dist/domain/apple/appleBundleAnatomy.js 和 appleApplication.js,没有运行任何东西),embedded.mobileprovision 和 embedded.provisionprofile 作为签名相关路径列出,和 _CodeSignature/CodeResources 放在一起。我们在那里没找到解析 CMS 外壳或 entitlements 的代码,所以描述文件的内容还是用上面的命令或浏览器工具来看。等 App 能装上、要分析里面的可执行文件时,REA 才派上用场,见用 REA 逆向分析 macOS 应用。
本文的局限
样本是合成的,真实描述文件里 Apple 特有的部分没有覆盖到:Apple 的签名证书链、新版描述文件带的 DER-Encoded-Profile 键,以及 iCloud 容器、App Groups 这类能力键。这些键遵循同样的规则(App 里的值必须被描述文件允许),但本文没有实际测。另外,设备在开发者账号里是否仍然启用、证书有没有被吊销,任何文件里都看不出来,只有 Apple 的后台知道。
下一步
- 用 iOS 描述文件查看器打开你自己的描述文件或 IPA。
- 用 X.509 证书解码器完整查看开发者证书。
- 查看 App 可执行文件的架构和最低系统版本:这个 Mac 应用是不是通用二进制?讲了
lipo和 Mach-O 切片。 - 拿到一个文件,不确定它是不是描述文件?file 命令认错文件类型的那些情况对比了
file和本站识别工具的判断。