在 CI 中校验 iOS 应用签名链与证书有效期:从 WWDR 证书核验到 codesign 验证回归

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
440
文章
0
粉丝
安全开发215字数 3392阅读11分18秒阅读模式
AI智能摘要
CI 导出成功并不代表 iOS 应用一定能安装分发,本机钥匙串、构建机信任配置与最终产物可能并不一致。排查应从交付物出发逐层留证,而非仅凭“签名成功”下结论。文章给出了一套可复现的校验流程:固定变量复现故障、核对 WWDR 候选证书、用 codesign 验证签名链与嵌套代码,并对照 Entitlements 和描述文件。证书链是否受信任,最终仍取决于目标环境。
— AI 生成的文章内容摘要

本地构建成功、CI 导出的 iOS 应用却无法安装或分发时,一个容易忽略的假设是:只要 codesign 能找到签名身份,证书链、描述文件和最终产物就都没有问题。实际上,本机钥匙串、构建机信任配置、导出时使用的描述文件,以及签名后的产物内容都可能不同。排查应从最终交付物出发,逐层留下可复核的证据,而不是仅凭“签名成功”判断通过。

从一次最小复现开始

在隔离的 macOS 构建机上,选取一个已知可以正常构建和导出的测试应用,记录 Xcode、macOS、签名证书指纹和描述文件版本。随后分别比较:

在 CI 中校验 iOS 应用签名链与证书有效期:从 WWDR 证书核验到 codesign 验证回归
  • 使用原有签名环境导出的测试包。
  • 在不改变应用代码的前提下,将测试用钥匙串中的中间证书移除或替换后重新构建、导出的结果。

第二次操作可能在构建、导出或后续验证阶段失败,具体取决于钥匙串搜索路径、系统信任配置和工具版本;它不是保证复现某一种错误的脚本。不要在共享构建机的系统钥匙串中删除证书,也不要修改系统信任设置来制造故障。

关键是固定其他变量:同一源码、同一构建配置、同一签名身份和描述文件,只改变测试环境中的证书条件。比较两次日志和最终产物,才能判断问题发生在签名、证书路径构建,还是安装与分发所需的其他条件。

准备:确定检查对象和信任依据

以下命令针对 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 中引用这些材料的配置。先在隔离构建机导入新材料,用新身份和匹配的描述文件构建测试产物,再执行完整门禁,并在授权的目标环境中验证安装或既定分发流程。

回归样本至少覆盖:

  1. 正常样本:预期签名身份、描述文件和嵌套代码均匹配。
  2. 身份或授权不匹配样本:确认门禁能指出具体目标和失败规则。
  3. 签名后被修改的测试副本:确认完整性验证失败。
  4. 缺失材料或不可验证样本:确认结果是失败或未验证,而不是静默通过。
  5. 临近到期样本或可控测试证书:确认预警与阻断阈值按配置生效。

规则更新也应先以观察模式对历史样本和新构建运行,比较新增告警及漏报,再切换为阻断。保存规则版本、基线和审批记录,确保可以撤回单项变更。

回退前应保留上一版流水线配置及其可用性记录。若新规则误报,可回退到已验证的规则版本,并对受影响构建重新执行检查;若新证书材料有问题,只有在旧证书、私钥和描述文件仍有效、未吊销且获准使用时,才考虑暂时切回旧签名配置并重新构建验证。若旧身份已失效或存在泄露风险,应暂停发布并修复新链路,而不是为恢复流水线而继续使用旧凭据。

最后,避免把手工重新签名当作通用修复手段。Apple 的签名格式说明对命令行重新签名 iOS 等平台应用有明确限制;常规处置应回到受控的 Xcode 构建和导出流程,在正确配置下重新生成并验证最终交付物。

 
枫少@KillBoy
评论  2  访客  2
    • 醉卧花
      醉卧花 1

      扩展的签名确实容易漏查

      • Timeless Tides
        Timeless Tides 0

        失败日志归档可千万别挡住退出码

      匿名

      发表评论

      匿名网友

      拖动滑块以完成验证