本地构建成功、CI 导出的 iOS 应用却无法安装或分发时,一个容易忽略的假设是:只要 codesign 能找到签名身份,证书链、描述文件和最终产物就都没有问题。实际上,本机钥匙串、构建机信任配置、导出时使用的描述文件,以及签名后的产物内容都可能不同。排查应从最终交付物出发,逐层留下可复核的证据,而不是仅凭“签名成功”判断通过。
从一次最小复现开始
在隔离的 macOS 构建机上,选取一个已知可以正常构建和导出的测试应用,记录 Xcode、macOS、签名证书指纹和描述文件版本。随后分别比较:

- 使用原有签名环境导出的测试包。
- 在不改变应用代码的前提下,将测试用钥匙串中的中间证书移除或替换后重新构建、导出的结果。
第二次操作可能在构建、导出或后续验证阶段失败,具体取决于钥匙串搜索路径、系统信任配置和工具版本;它不是保证复现某一种错误的脚本。不要在共享构建机的系统钥匙串中删除证书,也不要修改系统信任设置来制造故障。
关键是固定其他变量:同一源码、同一构建配置、同一签名身份和描述文件,只改变测试环境中的证书条件。比较两次日志和最终产物,才能判断问题发生在签名、证书路径构建,还是安装与分发所需的其他条件。
准备:确定检查对象和信任依据
以下命令针对 macOS 构建机和解压后的 .app。如果交付物是 IPA,应先在临时目录解压,再检查 Payload 内的应用;若存在多个 .app,不要靠通配符猜测,应明确指定目标。也应分别检查扩展、内嵌应用及 framework 等嵌套代码。
APP="/path/to/Payload/Example.app"
test -d "$APP" || { echo "应用目录不存在" >&2; exit 1; }
sw_vers
xcodebuild -version
security list-keychains -d user证书来源和预期链应以 Apple 的 WWDR 证书说明及其链接的 Certificate Authority 页面为准。该说明中提到的 2030 年到期日针对其中所述的续期证书,不应作为所有 WWDR 证书的统一硬编码阈值。记录实际证书的颁发者、序列号、指纹和有效期,并将其与当前签名证书的实际链对应起来。
系统时间也属于检查前提:时间异常会影响有效期判断。钥匙串密码、私钥和导出的 .p12 不应进入日志或诊断制品。
执行:分层检查证书、签名和授权
1. 查看钥匙串中的 WWDR 候选证书
先确认构建机实际搜索哪些钥匙串,再检查指定钥匙串中的候选证书:
KEYCHAIN="$HOME/Library/Keychains/login.keychain-db"
security find-certificate -a -c "Worldwide Developer Relations" \
-p "$KEYCHAIN" > wwdr-candidates.pem
test -s wwdr-candidates.pem || {
echo "指定钥匙串未找到 WWDR 候选证书" >&2
exit 1
}find-certificate 只证明指定钥匙串中存在匹配名称的证书,不证明它就是最终签名链采用的中间证书,也不证明链受信任。存在多个候选时,应逐张提取并核对;不要让脚本默认把 PEM 文件中的第一张当成正确证书。
对已确认的单张候选证书,可以用构建机上的 OpenSSL 查看有效期和指纹:
openssl x509 -in wwdr-one.pem -noout \
-subject -issuer -serial -dates -fingerprint -sha256这里的 wwdr-one.pem 应由前一步的候选集合中明确选择,不包含私钥。还可用以下命令检查当前可用的代码签名身份:
security find-identity -v -p codesigning身份列表有助于定位构建机条件,但仍需将列出的身份与最终产物使用的签名证书核对;它不能代替对最终产物的验证。
2. 检查最终产物的签名与实际证书
codesign -dv --verbose=4 "$APP" 2>&1
codesign --verify --strict --verbose=4 "$APP"第一条用于记录签名标识、Authority 等诊断信息;第二条检查当前环境下的签名完整性及相关验证结果。应以退出状态判断命令是否成功,同时保存标准错误输出。codesign -dv 的展示信息不是独立的完整信任结论;不同 macOS 和 Xcode 版本的输出也可能不同。
如需精确关联叶证书和中间证书,可在隔离临时目录提取签名中携带的证书,再逐张查看:
mkdir -p cert-evidence
(
cd cert-evidence
codesign -d --extract-certificates "$APP"
)提取出的文件命名和内容应以当前工具版本的实际结果为准。用 openssl x509 -inform DER -in 文件名 -noout -subject -issuer -serial -dates -fingerprint -sha256 检查各证书,并与 Apple 官方资料、钥匙串候选证书和预期签名身份比对。证书链能否构建、是否受信任,仍需结合目标环境的验证结果判断;仅靠 Subject 或 Authority 字符串相同不足以下结论。
对嵌套代码逐项执行同样的展示和验证,例如:
NESTED="$APP/PlugIns/Example.appex"
codesign -dv --verbose=4 "$NESTED" 2>&1
codesign --verify --strict --verbose=4 "$NESTED"应由构建配置或经过测试的枚举逻辑确定检查清单,避免只验证主应用就漏掉扩展。验证成功也不等于设备安装、描述文件授权或分发必然成功。
3. 对照 Entitlements 和描述文件
分别导出主应用及各扩展的签名 Entitlements:
codesign -d --entitlements :- "$APP" \
> app-entitlements.plist 2> app-entitlements.log
plutil -p app-entitlements.plist对包含 embedded.mobileprovision 的应用或扩展,可解码描述文件以供比对:
security cms -D -i "$APP/embedded.mobileprovision" \
> profile.plist
plutil -p profile.plist重点核对应用标识、团队标识、实际签名证书是否属于描述文件允许的证书,以及签名 Entitlements 是否符合该目标和描述文件的授权范围。通配符、数组及不同分发方式的规则不能简单用两个 plist 全文相等判断。描述文件不存在时,应结合交付类型判断,而不是一律当作解析错误。
自动规则适合检查项目明确约定的标识、必需或禁止的 entitlement,以及描述文件的有效期;复杂授权关系应通过经过测试的解析器和真实样本验证。描述文件和 entitlement 可能暴露团队标识、设备标识等信息,诊断制品应限制访问并设置保留期限。
4. 不要把时间戳显示当成 RFC 3161 验证
codesign -dv --verbose=4 的输出可作为时间相关信息的排查线索,但出现签名时间或时间戳字段,不足以单独证明 RFC 3161 时间戳令牌有效。时间戳策略也应按产物类型和分发方式制定:不能假设所有 iOS 签名都必须出现同一字段,更不能把缺失字段直接解释为签名链断裂。
如果组织的交付策略确实要求 RFC 3161 时间戳,应先在隔离环境确认该产物类型实际包含可提取的令牌,再使用能够验证该令牌的工具检查消息摘要绑定、令牌签名、时间戳机构证书链及相应信任策略。把工具版本、验证策略和结果一起记录。若尚未具备可靠提取与验证能力,应将该项标为“未验证”,而不是输出通过。
配置:把检查变成可失败的 CI 门禁
可以先将检查分成三类,再决定阻断范围:
| 检查项 | 建议处理 |
|---|---|
| 最终产物不存在、签名验证失败、必需嵌套代码验证失败 | 阻断发布,保留命令退出码和日志 |
| 签名身份、团队标识或明确禁止的 Entitlements 与发布配置不符 | 阻断发布;配置基线变更须经审批 |
| 证书或描述文件接近到期 | 按团队维护周期设置预警;到期或不满足明确发布策略时阻断 |
| 无法确认的证书链、时间戳或工具输出解析结果 | 标记未验证并进入人工复核;关键发布策略不允许将其当作通过 |
下面是最小门禁骨架,故意只负责主应用存在性和签名验证;嵌套代码、身份基线、描述文件和时间戳检查必须另行实现,不能因为示例通过就认定整个发布流程合规。
#!/bin/bash
set -u
APP="${1:?请传入最终导出的 .app 路径}"
EVIDENCE="${2:-signing-evidence}"
mkdir -p "$EVIDENCE"
test -d "$APP" || {
echo "应用目录不存在" >&2
exit 1
}
codesign -dv --verbose=4 "$APP" \
> "$EVIDENCE/display.log" 2>&1
display_status=$?
codesign --verify --strict --verbose=4 "$APP" \
> "$EVIDENCE/verify.log" 2>&1
verify_status=$?
printf 'display=%s\nverify=%s\n' \
"$display_status" "$verify_status" > "$EVIDENCE/status.txt"
if [ "$display_status" -ne 0 ] || [ "$verify_status" -ne 0 ]; then
echo "签名检查失败,停止发布" >&2
exit 1
fi在流水线中应把该脚本放在最终导出和任何会修改产物的步骤之后。如果后续重新打包或修改 bundle,应再次验证交付物。日志归档步骤需要在门禁失败时仍执行,同时确保发布步骤不会因归档成功而继续运行。
预警阈值应覆盖证书申请、描述文件更新、测试和发布窗口,并留出组织自己的缓冲时间;不要把某个固定天数包装成 Apple 的通用要求。证书吊销或在线信任检查是否可用,也应根据构建机网络和实际验证策略单独确认,离线环境中的“无法检查”不能默认为“未吊销”。
失败证据与误报处理
每次失败至少保留以下信息,并关联构建编号与产物摘要:
- macOS、Xcode 和签名检查工具版本;检查时间及时间同步状态。
- 最终 IPA 或
.app的可追溯摘要、检查对象清单,以及各命令的退出码和原始输出。 - 实际签名证书及相关中间证书的公开字段:Subject、Issuer、序列号、指纹和有效期。
- 经访问控制的 Entitlements、描述文件解析结果,以及用于判断的规则版本。
- 如果失败发生在安装或分发阶段,保留对应阶段的原始错误与目标环境信息;不要只保存 CI 最后一行摘要。
排查误报时,先确认脚本检查的是最终导出物、正确的嵌套目标和预期钥匙串,再核对工具升级是否改变了输出格式。尽量依赖命令退出码和结构化数据,不要只用 grep 匹配一行易变化的展示文本。
确需例外时,应记录失败项、影响范围、负责人、补偿措施和到期时间。只对已识别的规则和目标作短期例外,不要全局关闭代码签名验证;无法验证的关键条件也不应被改写成“通过”。
证书轮换与规则调整后的回归和回退
轮换应同时检查签名身份、相关中间证书、描述文件及 CI 中引用这些材料的配置。先在隔离构建机导入新材料,用新身份和匹配的描述文件构建测试产物,再执行完整门禁,并在授权的目标环境中验证安装或既定分发流程。
回归样本至少覆盖:
- 正常样本:预期签名身份、描述文件和嵌套代码均匹配。
- 身份或授权不匹配样本:确认门禁能指出具体目标和失败规则。
- 签名后被修改的测试副本:确认完整性验证失败。
- 缺失材料或不可验证样本:确认结果是失败或未验证,而不是静默通过。
- 临近到期样本或可控测试证书:确认预警与阻断阈值按配置生效。
规则更新也应先以观察模式对历史样本和新构建运行,比较新增告警及漏报,再切换为阻断。保存规则版本、基线和审批记录,确保可以撤回单项变更。
回退前应保留上一版流水线配置及其可用性记录。若新规则误报,可回退到已验证的规则版本,并对受影响构建重新执行检查;若新证书材料有问题,只有在旧证书、私钥和描述文件仍有效、未吊销且获准使用时,才考虑暂时切回旧签名配置并重新构建验证。若旧身份已失效或存在泄露风险,应暂停发布并修复新链路,而不是为恢复流水线而继续使用旧凭据。
最后,避免把手工重新签名当作通用修复手段。Apple 的签名格式说明对命令行重新签名 iOS 等平台应用有明确限制;常规处置应回到受控的 Xcode 构建和导出流程,在正确配置下重新生成并验证最终交付物。
- it2021
- it2021.com
- test
- 访问控制





山东 烟台 1F
扩展的签名确实容易漏查
重庆 重庆 2F
失败日志归档可千万别挡住退出码