iOS 测试包装不上的时候,报错很少直接告诉你签名的哪一环出了问题。“无法安装”“The executable was signed with invalid entitlements”(0xe8008016)“A valid provisioning profile for this executable was not found”(0xe8008015),归根到底都落在两处的几项信息上:描述文件,以及可执行文件的代码签名。在 Mac 上一般这样看:
security cms -D -i embedded.mobileprovision
codesign -d --entitlements :- Payload/MyApp.app
这个页面直接从文件里读出同样的数据,在浏览器里完成,并把两边并排对照。它面向的是检查自己的包和描述文件:测试机为什么装不上、导出的 IPA 里到底塞的是哪个描述文件、推送 entitlement 写的是 development 还是 production。
可以拖进来的文件
- 描述文件:
.mobileprovision(iOS、iPadOS、tvOS、watchOS、visionOS)和 macOS 的.provisionprofile。描述文件是一个 CMS SignedData,里面包着 XML plist;现在的描述文件用定长编码,早期的描述文件和openssl cms -stream生成的是 BER 不定长编码,两种都能读。security cms -D输出的 XML 也可以直接拖进来。 - 安装包:
.ipa、压缩过的.app(访达里的“压缩”)或压缩过的.xcarchive。先只读 ZIP 中央目录,然后只打开 App 的Info.plist、embedded.mobileprovision、PrivacyInfo.xcprivacy、各扩展的Info.plist和主可执行文件。几个 GB 的 IPA 也不会整个读进内存,支持 ZIP64。 - 单个文件:XML 或二进制(
bplist00)格式的Info.plist、.entitlements、PrivacyInfo.xcprivacy,或者一个单独的 Mach-O 可执行文件(单架构或通用二进制),用来列出签名里的 entitlements。
能看到什么
描述文件:名称、App ID 名称、App ID(application-identifier,带团队前缀)、UUID、团队 ID 和名称、平台、创建和过期时间(以及还剩几天)、是否由 Xcode 管理、完整的 Entitlements 字典、注册设备的 UDID 列表(带搜索框)、开发者证书(通用名、取自 OU 字段的团队、SHA-1、有效期),以及 CMS 签名者的证书。
描述文件类型并不写在文件里,这里按 Xcode 和 fastlane 的判断方式推出来:
| 描述文件包含 | 类型 |
|---|---|
ProvisionsAllDevices = true |
企业(In-House);macOS 上是 Developer ID |
ProvisionedDevices,且 get-task-allow = true |
开发(Development) |
ProvisionedDevices,get-task-allow = false |
Ad Hoc |
| 两者都没有 | App Store(macOS 上是 Mac App Store) |
App:Bundle ID、版本和 build 号、最低系统版本、SDK 和 Xcode 版本、设备类型、支持的平台、URL Scheme、必需的设备能力、App Transport Security 配置、每一条 NS…UsageDescription 及其文案、隐私清单(追踪开关、追踪域名、收集的数据类型,以及每个需要理由的 API 类别和理由代码,例如 CA92.1)、内嵌的框架和 dylib(标出自带隐私清单的那些),以及 App 扩展的 Bundle ID 和扩展点。
可执行文件:各架构切片(arm64、arm64e 等)及其 LC_BUILD_VERSION 里的平台和最低系统版本、LC_ENCRYPTION_INFO_64 的 cryptid(为 1 表示 App Store 下载的包,代码是加密的)、CodeDirectory 里的签名标识符和团队、是否为 ad hoc 签名、签名证书,以及代码签名里两份 entitlements:XML plist(slot 5)和 iOS 15 起实际校验的 DER 版本(slot 7)。两份不一致时页面会提示。
会做哪些检查
拖入的是带描述文件的包时,页面会把常见安装失败背后的几项逐一核对:
- 过期:描述文件已过期,或 30 天内过期。
- Bundle ID:
CFBundleIdentifier必须匹配描述文件 App ID 去掉团队前缀后的部分,*是通配符(TEAMID.com.example.*)。 - Entitlements:可执行文件签入的每一项 entitlement 都必须在描述文件允许的范围内。字符串和数组可以用描述文件里的通配符(钥匙串分组的
TEAMID.*、Associated Domains 的*);App Store 或 Ad Hoc 包里get-task-allow为 true、production描述文件配上development的aps-environment、别的团队的钥匙串分组、描述文件没有开通的能力(iCloud、App Groups),都会列出键名和两边的值。macOS 上com.apple.security.开头的沙盒和 Hardened Runtime 权限不需要描述文件,会单独标出。 - 团队:签入的
application-identifier(或 CodeDirectory)里的团队与描述文件的团队是否一致。 - 签名证书:代码签名里的证书必须在描述文件的
DeveloperCertificates里,按 SHA-1 比对。对应的就是“描述文件不包含签名证书”这种情况。 - 扩展:每个
.appex的 Bundle ID 都必须以主 App 的 Bundle ID 加一个点开头。
想确认某台设备在不在描述文件里,把它的 UDID 粘贴到设备列表上方的输入框。比对只在页面里进行,不会发送到任何地方。
我们是怎么验证的
测试样本都是合成的,由 test/ios/build.sh 在 macOS 27.0.1、Xcode 27.0(iPhoneOS 27.0 SDK)和 OpenSSL 4.0.3 上生成:一个一次性的 CA、一张“开发者”证书和一张描述文件签名证书;五个手写的描述文件(开发、BER 不定长编码的 Ad Hoc、已过期的 App Store、App ID 为 * 的企业版,以及一个 macOS 开发描述文件),用 openssl cms -sign -binary -nodetach 签名;再把一个很小的 C 程序编译成 arm64 和 arm64e,用 codesign --entitlements --generate-entitlement-der 签名后打包成 Payload/Fixture.app。另一个“错误”的包故意做错了一堆东西:Bundle ID 不对、对着 App Store 描述文件签了 get-task-allow true 和开发环境推送、用了别的团队的钥匙串分组、多了一个描述文件没有的 iCloud 权限、签名证书不在描述文件里、Widget 扩展的 Bundle ID 前缀不对,还有一个切片的 cryptid 被改成了 1。单元测试把解析结果和 security cms -D、plutil、codesign -d --entitlements(XML 和 --der 两种)、lipo -archs、otool -l 的输出逐项比对。我们还用解析器跑了一遍这台 Mac 上 Xcode 下载的 20 个真实描述文件:名称、UUID、团队、过期时间、设备列表、entitlements 和证书 SHA-1 全部 20 个都与 security cms -D 一致(这些数据不在本站和测试里出现)。
做样本时踩了两个坑。plutil -convert json 会拒绝解码后的描述文件,报 “Invalid object in plist for JSON format”,因为描述文件里有 JSON 表示不了的 <date> 和 <data>,所以构建脚本改用 plutil -p 生成参照输出。另外 codesign -dv 根本不打印 Authority= 行,要用 -dvvv 才能看到是哪张证书签的。如果把 security cms -D -i 直接用在 .ipa 上而不是里面的描述文件,会报 security: failed to add data to decoder: UNKNOWN (-8183(d):先解压,或者直接把 IPA 拖到这里。
你的文件如何保持私密
- 文件在你设备上的 Web Worker 里读取,不上传,这个页面也没有服务端。
- UDID、团队 ID、Bundle ID 和证书信息只显示在页面上。粘贴的 UDID 只在页面里比对,不保存。
- 统计事件只含粗粒度的值:成功或错误码、文件类型(
profile、ipa等)、推出的描述文件类型、未通过的检查项数量区间,以及文件大小和耗时区间。完整列表见隐私政策。 - 工具页不展示广告。
如何自己验证
- 打开浏览器开发者工具的 Network 面板,拖入一个 IPA。所有请求只有页面资源和 Worker 脚本
/_astro/ios-worker-*.js。 - 或者加载页面后断网,再拖入一个描述文件,照样能用。
- Worker 运行在内容安全策略
default-src 'none'; script-src 'self'; connect-src 'none'下,浏览器会拦下它发出的任何请求。 - 用你自己的工具对照:
security cms -D -i file.mobileprovision、codesign -d --entitlements :- MyApp.app和otool -l MyApp | grep -A4 LC_ENCRYPTION会给出同样的值。
局限
- 不做任何密码学校验。 既不校验 Apple 对描述文件的签名,也不校验 App 的代码签名;签名后被改过的描述文件或二进制在这里看着一切正常,但设备会拒绝。
- 不解密、不重签、不修改。App Store 下载的包只读取 load command 和签名。
- App 扩展只列出 Bundle ID,不交叉检查它们自己的描述文件和 entitlements。需要的话把扩展的
embedded.mobileprovision单独拖进来。 - Entitlements 的比对规则覆盖常见情况(精确值、
*通配符、数组取子集)。Apple 安装器还有一些特殊规则这里没有模拟,例如只授予 Apple 自己签名代码的权限。 - 设备列表来自描述文件本身。设备在开发者账号里是否仍处于启用状态,从文件里看不出来。
常见问题
我的 IPA 里用的是哪个描述文件?
就是 Payload/<App>.app/embedded.mobileprovision,页面在 描述文件 一节显示它的名称和 UUID。Xcode 导出时选中的描述文件可能和你以为的不一样,自动签名时尤其如此;看 UUID 就知道到底是哪一个。
描述文件里明明有这个 entitlement,还是装不上。
看 签入的值 那一列:不光键要有,值也必须被允许。用发布描述文件签名的包里 aps-environment 却是 development,是最常见的情况;钥匙串分组的团队前缀写错也很常见。再看签名证书那一行:证书续期后重新生成的描述文件里,已经没有旧证书了。
怎么查设备的 UDID?
连上设备,在访达里点设备名称下方那行文字,直到出现 UDID;或者在 Xcode 的 Window > Devices and Simulators 里查看。
相关工具和教程
- 原生二进制概览:Mach-O 可执行文件完整的 load command、段、符号和依赖库
- X.509 证书解析:证书的全部字段和扩展
- 用 REA + Ghidra 免费分析原生二进制
- .mobileprovision 里装了什么?描述文件的解码与核对:用
security cms、openssl、plutil解码描述文件,再和codesign -d --entitlements的结果对比