
问题背景证书过期为何难以提前发现iOS 开发者都有过这样的经历某天早上 CI 突然报错日志里显示code sign error排查半天才发现是 .p12 证书过期或者描述文件里的设备列表已经变更。证书过期不像代码编译错误那样有明确的报错位置它更像一颗定时炸弹——在签名那一刻才爆炸。更麻烦的是证书和描述文件并不是同一时间过期的。一个 .mobileprovision 描述文件的有效期通常取决于其中包含的证书而企业证书和开发证书的过期策略又不一致。手动打开 Keychain 逐个查看再对比描述文件里的ExpirationDate字段效率很低也容易遗漏。把证书检测做成一条 API目的就是让脚本能够在构建前主动检查证书状态而不是等签名失败之后再去抢救。接口能力边界POST https://v1.apizero.cn/api/ios-cert接收两个文件.p12证书文件和.mobileprovision描述文件。请求体以 Base64 编码传输响应中会给出以下信息证书的基本信息名称、有效期剩余天数、是否被吊销描述文件的类型Development开发、Distribution分发、Enterprise企业Team ID描述文件包含的设备列表25 项 entitlements 权限声明证书与描述文件的匹配性验证结果这个接口不负责生成证书也不提供签名服务它只做解析和校验。理解这一点很重要它是检查工具不是签名工具。请求参数与鉴权接口要求两个 HeaderHeader必填说明Authorization是接口鉴权凭证通常使用 API KeyContent-Type是固定为application/json请求体是一个 JSON 对象包含三个字段字段类型必填说明certstring是Base64 编码的 .p12 文件内容provisionstring是Base64 编码的 .mobileprovision 文件内容passwordstring否证书密码默认为空字符串注意.p12文件是二进制格式无法直接放进 JSON。需要先用命令行工具转成 Base64 字符串再作为cert字段的值发送。.mobileprovision文件本身是 XML 格式但同样建议用 Base64 传输避免 JSON 转义问题。还要注意.p12的密码问题。开发证书在创建时通常设置了密码导出.p12文件时也会要求输入密码。如果证书导出时使用了密码请求中的password字段就必须填写否则服务端无法解析 .p12 文件。用 curl 快速验证接口先写一个可复制的 curl 示例。实际使用前需要先执行 Base64 编码操作将文件转为字符串# 假设本地有 cert.p12 和 profile.mobileprovision 两个文件 export CERT_B64$(base64 cert.p12) export PROV_B64$(base64 profile.mobileprovision) curl -sS \ -X POST \ -H Authorization: Bearer $APIZERO_API_KEY \ -H Content-Type: application/json \ -d {\cert\: \$CERT_B64\, \provision\: \$PROV_B64\, \password\: \\} \ https://v1.apizero.cn/api/ios-cert这里有一个 shell 转义陷阱-d参数里的双引号必须用\转义否则 shell 会把 JSON 截断。如果你的 API 网关要求X-API-Key而不是 Bearer Token把Authorization那行替换成-H X-API-Key: $APIZERO_API_KEY即可具体以接口文档为准。如果希望响应更易读可以加上| jq .管道格式化 JSON 输出curl -sS \ -X POST \ -H Authorization: Bearer $APIZERO_API_KEY \ -H Content-Type: application/json \ -d {\cert\: \$CERT_B64\, \provision\: \$PROV_B64\} \ https://v1.apizero.cn/api/ios-cert | jq .返回字段解读成功的响应结构如下{ code: 0, msg: 成功, data: { certificate: { is_revoked: false, name: iPhone Developer: ..., status: 正常 }, mobileprovision: { cert_end_days: 350, cert_type: Development }, is_matching: true, permissions: { aps: true, debug: true, keychain: true } } }几个关键字段的工程含义certificate 对象is_revoked表示证书是否被 Apple 吊销。吊销的证书即使未过期也不能用于签名所以这个字段比有效期更值得关注。name是证书的 Common Name通常形如iPhone Developer: xxx (TEAMID)可以用来核对证书归属人。status是服务端对证书状态的汇总描述。mobileprovision 对象cert_end_days表示证书剩余有效期天数。cert_type返回Development、Distribution或Enterprise三者之一。这三种类型的描述文件使用场景差异很大Development 用于开发调试Distribution 用于 App Store 提交Enterprise 用于企业内部分发。拿到这个字段后可以判断当前描述文件是否被误用在错误的构建环境中。is_matching 字段is_matching是布尔值表示描述文件里包含的证书与传入的 .p12 证书是否匹配。这个验证解决了一个常见问题开发者手上有多个证书导出 .p12 时选错了或者 CI 配置里证书文件和描述文件来自不同的开发者账号。当is_matching为false时即使签名不报错最终产物也可能无法安装。permissions 对象permissions是一个扁平 JSON 对象包含 25 个布尔字段如aps推送、keychain钥匙串共享、debug调试权限。这些字段直接反映描述文件中声明的 entitlements 值。如果应用需要使用推送功能但检测结果显示aps为false说明描述文件中没有包含 Push Notification 能力需要去开发者后台重新生成描述文件。实际使用用脚本做证书巡检curl 适合手动调试。在持续集成场景中更好的做法是把检测逻辑封装成一个脚本函数在每次 CI 构建开始前执行。下面是一个用 Python 封装的最小示例import base64 import json import sys import urllib.request API_URL https://v1.apizero.cn/api/ios-cert def read_b64(path: str) - str: with open(path, rb) as fp: return base64.b64encode(fp.read()).decode(utf-8) def check_cert(cert_path: str, provision_path: str, api_key: str, password: str ): payload { cert: read_b64(cert_path), provision: read_b64(provision_path), password: password, } req urllib.request.Request( API_URL, datajson.dumps(payload).encode(utf-8), headers{ Authorization: fBearer {api_key}, Content-Type: application/json, }, methodPOST, ) with urllib.request.urlopen(req) as resp: result json.loads(resp.read().decode(utf-8)) if result.get(code) ! 0: print(fAPI 调用失败: {result.get(msg)}) sys.exit(1) data result[data] cert data[certificate] prov data[mobileprovision] print(f证书名称: {cert[name]}) print(f证书状态: {cert[status]}, 吊销: {cert[is_revoked]}) print(f剩余天数: {prov[cert_end_days]}) print(f证书类型: {prov[cert_type]}) print(f证书与描述文件匹配: {data[is_matching]}) # 可在这里添加阈值判断比如剩余天数小于 30 时告警 if prov[cert_end_days] 30: print([WARN] 证书将在 30 天内过期请安排更换) sys.exit(2) if data[is_matching] is False: print([ERROR] 证书与描述文件不匹配) sys.exit(3) if __name__ __main__: check_cert( cert_pathcert.p12, provision_pathprofile.mobileprovision, api_keyyour_api_key_here, password, )这个脚本做了几件在工程上有价值的事情把文件读取和 Base64 编码封装在内部调用方只需传文件路径。检查 API 返回的code字段业务错误直接退出。对cert_end_days设置告警阈值剩余天数不足 30 天时以非零退出码终止构建。对is_matching做硬校验不匹配时直接失败避免带病构建。在 CI 中只需在正式编译之前执行这个脚本就能把证书问题拦截在签名阶段之前。常见错误与处理401 UnauthorizedAuthorization 头缺失或 API Key 无效。检查环境变量是否设置以及请求中使用的 Header 格式是否和文档一致。400 Bad Request请求体 JSON 格式错误或者必填字段cert、provision缺失。常见原因是 Base64 字符串中包含换行符——使用base64命令时默认会按 76 字符换行需要去掉换行base64 cert.p12 | tr -d \n注意这里使用而不是cat避免 shell 将二进制文件内容解释为命令行参数。解析失败服务端无法解析 .p12 文件。最常见的原因有两种密码错误或未填写password字段传入的文件根本不是 .p12 格式例如把.cer或.pem文件误当作 .p12 提交证书已过期但接口返回代码正常接口只负责解析和检测不会因为证书过期而拒绝处理——这正是需要调用方自行判断cert_end_days字段的原因。如果希望“过期即报错”需要在客户端代码里判断就像上面的 Python 示例中那样。工程化注意事项不要在证书过期前一周才处理证书过期不是瞬时事件它有一个时间窗口。常见的做法是在 CI 中设置两级阈值剩余 60 天在构建日志中输出警告剩余 14 天发送报警通知并允许构建继续剩余 0 天构建失败多个证书如何管理一个 iOS 项目可能同时存在开发证书、发布证书、企业证书。建议把每个证书的检测结果输出到独立文件或者把 Team ID 作为标签放入文件名方便对照。Base64 传输的边界描述文件大小通常在几 KB 到几十 KB 之间Base64 编码后会膨胀约 33%在正常 HTTP 请求体积范围内没有问题。如果你的请求体达到数 MB需要确认网关是否有请求体大小限制以文档为准。不要把 API Key 提交到仓库这是老生常谈但在代码示例中仍然值得提醒curl命令和 Python 脚本中的api_key都应从环境变量读取不要硬编码。CI 平台一般内置 Secret 管理功能可以直接把 Key 注入到环境变量中。参考文档接口文档https://apizero.cn/aidocs/ios-cert原始文档https://apizero.cn/aidocs/ios-cert/raw.md