Apple 证书从 0 到可发布:签名、Profile 与 CI 配置实战
讲清 Apple Development、Apple Distribution、App ID、Provisioning Profile 和私钥之间的关系,并完成创建、导出、验证、CI 导入与续期。
Apple 签名最容易出错的地方,不是证书创建按钮在哪里,而是没有分清证书、私钥、App ID、Entitlements 和 Provisioning Profile 分别负责什么。本文按一条可验证的发布链路整理,后台界面或规则变化时请以 Apple Developer 当前说明为准。
先建立正确的签名模型
一份可安装或可提交的 Apple 应用,至少需要下面几部分相互匹配:
- 证书:证明由哪个开发团队签名,以及签名用途是开发还是发布。
- 私钥:真正执行签名的密钥,只存在于生成 CSR 的机器或后续导入的位置。
- App ID:绑定 Team ID、Bundle ID 和应用能力。
- Provisioning Profile:把 App ID、证书、能力以及可选设备列表组合起来。
- Entitlements:最终写入应用签名的能力声明,例如推送、Associated Domains 和 App Groups。
证书文件只有公钥信息。下载一个 .cer 并不代表这台机器能签名,钥匙串中还必须存在与它配对的私钥。很多 No signing certificate 问题,本质上是只复制了证书,没有复制私钥。
Apple 常见证书类型
Apple Development
用于真机开发和调试。配合 Development Provisioning Profile 时,Profile 中还会包含允许安装的测试设备 UDID。
Apple Distribution
用于 App Store、TestFlight、Ad Hoc 或企业分发等发布场景。它通常属于团队级资产,不应按每个应用重复创建。
如果项目完全由 Xcode 管理,可以使用自动签名或 Xcode 管理的证书。需要接入 CI、多团队协作或严格控制签名材料时,建议明确记录证书、私钥和 Profile 的来源及用途。
第一步:在本机生成 CSR
在 macOS 打开“钥匙串访问”,进入“证书助理”,选择“从证书颁发机构请求证书”。填写团队可识别的邮箱和名称,并将请求保存到磁盘。
生成 CSR 时,本机会同时生成一对公私钥:
- CSR 中包含公钥和申请信息。
- 私钥保留在当前登录钥匙串中。
- Apple 根据 CSR 签发证书。
- 下载并安装证书后,钥匙串会把证书与对应私钥配对。
因此,谁生成 CSR,谁就应负责后续 .p12 导出。不要在一台机器生成 CSR,再期待另一台只靠 .cer 完成签名。
第二步:创建并安装证书
进入 Apple Developer 的 Certificates, Identifiers & Profiles:
- 创建 Apple Development 或 Apple Distribution 证书。
- 上传刚生成的 CSR。
- 下载
.cer文件并双击安装到钥匙串。 - 在“我的证书”中确认该证书下方可以展开私钥。
用命令行检查当前可用于代码签名的身份:
security find-identity -v -p codesigning
如果列表中没有目标证书,先检查证书是否过期、是否被撤销,以及钥匙串中是否存在配套私钥。
第三步:创建 App ID 与能力
创建显式 App ID,并使用稳定的 Bundle ID:
com.example.product
只启用应用实际需要的能力。推送通知、Associated Domains、Sign in with Apple、App Groups 等能力需要在三个位置保持一致:
- Apple Developer 后台的 App ID。
- Xcode Target 的 Signing & Capabilities。
- 构建产物最终签名中的 Entitlements。
修改后台能力后,已有 Provisioning Profile 不一定自动更新。手动签名项目通常需要重新生成并下载 Profile。
第四步:创建 Provisioning Profile
不同用途应使用不同 Profile:
| 场景 | Profile 类型 | 包含设备列表 |
|---|---|---|
| 真机开发 | iOS App Development | 是 |
| App Store / TestFlight | App Store | 否 |
| 指定设备分发 | Ad Hoc | 是 |
创建时依次选择 App ID、允许使用的证书,并在开发或 Ad Hoc 场景选择设备。下载后双击安装,或放入 Xcode 可识别的位置。
查看 Profile 的实际内容:
security cms -D -i MyApp.mobileprovision > profile.plist
/usr/libexec/PlistBuddy -c 'Print :Name' profile.plist
/usr/libexec/PlistBuddy -c 'Print :ExpirationDate' profile.plist
/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' profile.plist
检查时重点看名称、到期时间、App ID、Team ID、证书列表和 Entitlements,不要只根据文件名判断 Profile 是否正确。
第五步:导出可迁移的 p12
CI 或新开发机需要证书与私钥,因此应从钥匙串导出 .p12:
- 在“我的证书”中选中证书和其下方私钥。
- 导出为 Personal Information Exchange,也就是
.p12。 - 设置高强度导出密码。
- 将文件和密码分开保存到受控的凭据系统。
不要把 .p12、Profile 或密码提交到 Git。证书材料应放在团队密码库、CI Secret 或受权限控制的发布存储中,并记录恢复负责人。
在 CI 中导入证书
CI 应使用临时钥匙串,任务结束后删除:
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$P12_PATH" \
-P "$P12_PASSWORD" \
-A \
-t cert \
-f pkcs12 \
-k "$KEYCHAIN_PATH"
security set-key-partition-list \
-S apple-tool:,apple: \
-s \
-k "$KEYCHAIN_PASSWORD" \
"$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
环境变量必须来自 CI 的加密凭据,不要打印密码或 .p12 的 Base64 内容。构建结束后删除临时钥匙串和落盘文件。
用 p8 让脚本登录 App Store Connect
.p8 是 App Store Connect API Key 的私钥,不是代码签名证书。它通常与下面三项一起使用:
- API Key 文件,例如
AuthKey_XXXXXXXXXX.p8。 - Key ID。
- Issuer ID。
代码签名仍由 Apple Distribution 证书、私钥和 Provisioning Profile 完成;.p8 解决的是自动化工具如何获得 App Store Connect 上传权限。
项目可以把认证信息放在不提交到 Git 的本地环境文件或 CI Secret 中:
ASC_USE_API_KEY=true
ASC_KEY_ID=XXXXXXXXXX
ASC_ISSUER_ID=00000000-0000-0000-0000-000000000000
ASC_KEY_PATH=/secure/path/AuthKey_XXXXXXXXXX.p8
传给 xcodebuild 的认证参数如下:
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
-authenticationKeyPath "$ASC_KEY_PATH" \
-authenticationKeyID "$ASC_KEY_ID" \
-authenticationKeyIssuerID "$ASC_ISSUER_ID" \
-allowProvisioningUpdates
当 Export Options 使用 destination=upload 和 method=app-store-connect 时,xcodebuild -exportArchive 会把归档上传到 App Store Connect。脚本完成后可以在 TestFlight 或 App Store 的 Builds 中确认处理状态。
uploadSymbols 上传的不是运行日志
Export Options 中经常还有:
<key>uploadSymbols</key>
<true/>
它表示上传 dSYM 符号文件。App Store Connect 收到崩溃报告后,可以使用 dSYM 把内存地址还原成类名、方法名和代码位置,这个过程叫符号化。
因此更准确的关系是:
.p8为上传脚本提供 App Store Connect API 身份。uploadSymbols=true要求上传构建时同时处理符号文件。- Apple 后续使用符号文件解析崩溃日志。
.p8 本身既不生成日志,也不参与应用二进制签名。
p8 的安全要求
- API Key 私钥通常只能下载一次,创建后立即进入受控备份。
- 文件必须放在仓库之外,并限制文件读取权限。
- Key ID、Issuer ID 可以作为配置保存,但
.p8内容必须按密钥管理。 - CI 只给发布任务读取权限,普通测试任务不应获得上传密钥。
- 按最小权限创建 API Key,不使用个人账号密码代替。
- 怀疑泄露时立即在 App Store Connect 撤销并创建新 Key。
验证归档产物的签名
构建成功不代表签名正确。对 .app 检查签名主体与 Entitlements:
codesign -dvvv MyApp.app
codesign -d --entitlements :- MyApp.app
codesign --verify --deep --strict --verbose=2 MyApp.app
提交前还要确认:
- Release 配置使用正式 Bundle ID。
- Team 与 App Store Connect 中的应用一致。
- Profile 没有过期,并包含当前 Distribution 证书。
- 推送环境、App Groups、Associated Domains 等能力与预期一致。
- 嵌入式 Framework 和 Extension 也完成正确签名。
常见错误怎么排查
No signing certificate found
先运行 security find-identity。如果证书存在但没有私钥,重新导入包含私钥的 .p12,不要反复下载 .cer。
Provisioning profile doesn’t include signing certificate
Profile 创建时没有选中当前证书,或证书更新后 Profile 仍是旧版本。重新生成 Profile,并确认其中的 DeveloperCertificates。
Entitlement 不匹配
对比 Xcode 能力、工程 entitlements 和 codesign -d --entitlements :- 的最终结果。常见问题是后台开启能力后没有刷新 Profile。
CI 能看到证书但 codesign 仍失败
检查临时钥匙串是否解锁、是否加入搜索列表,以及是否执行了 security set-key-partition-list。这类问题通常不是证书失效,而是私钥访问权限不足。
续期与撤销策略
至少每月检查一次证书和 Profile 的到期时间,并在发布窗口前完成轮换:
- 先创建并验证新证书。
- 为正式应用重新生成 Profile。
- 在开发机和 CI 中并行验证新材料。
- 完成至少一次 Archive 和上传测试。
- 确认所有发布任务迁移后,再撤销旧证书。
不要为了“整理后台”随意撤销仍在使用的 Distribution 证书。撤销前先确认是否还有其他应用、CI 流水线或团队成员依赖它。
最终检查清单
- 证书未过期且未撤销。
- 钥匙串中的证书包含私钥。
- Bundle ID、Team ID 与 App ID 一致。
- Profile 类型符合开发或发布场景。
- Profile 包含当前证书与所需能力。
- CI 使用临时钥匙串和加密凭据。
- App Store Connect API Key 使用最小权限,
.p8未进入 Git。 - 上传配置已开启所需的 dSYM 符号处理。
- 已验证
.app的签名与 Entitlements。 -
.p12、密码和 Profile 有受控备份。
证书管理的目标不是“让 Xcode 不报红”,而是建立一条任何发布机器都能重复验证、出现问题也能快速恢复的签名链路。