你在 Xcode 里 Archive(或推 CI),通过 Organizer 或 Transporter 上传,几分钟内收到 Apple 自动邮件,标题 “App Store Connect: Issues with your app”,正文写 “the provisioning profile included in the bundle has expired” 或 “the signing certificate used to sign your app is no longer valid”。build 根本没出现在 TestFlight,处理阶段就被拒了,审核员都没看到。
iOS 签名有两个有效期独立的产物:distribution 证书(建立后 1 年),和 provisioning profile(建立后 1 年,绑定的 cert 过期也作废)。CI/CD 经常用几个月前 check 进 config 仓库的旧 profile。修复是机械操作——重生过期那个产物再 rebuild——但先搞清是哪个过期能省一小时。
常见原因
按命中率排序。
1. Distribution certificate 过期
你的 iOS Distribution 证书 1 年到了。所有绑定它的 provisioning profile 同一刻全失效。你没做任何事;cert 静默过期。
如何判断:Apple Developer → Certificates。找 “iOS Distribution” 行,看到期日是不是已经过去;状态列写 Expired。
2. Provisioning profile 1 年到期
cert 还有效但 profile 是一年多前生成的。Profile 不自动续期,必须手动重生。
如何判断:Apple Developer → Profiles → 过滤 App Store / Ad Hoc。看 “Expires” 列,过了今天的就是死的。
3. CI/CD 缓存了几个月前的 profile
CI 把 profile 文件放在私有仓库或 secret manager 里没重拉。即使你在 Developer Portal 重生了新 profile,CI 还在用老的。
如何判断:SSH 进 CI runner(或查 secret manager)。打开 .mobileprovision:
security cms -D -i embedded.mobileprovision | grep -A1 "ExpirationDate"
文件里的到期日比 Developer Portal 上的早,CI 就是过期的。
4. App ID 变了但 profile 没跟上
改了 bundle ID 或加了新 entitlement(Push、HealthKit)。原有 profile 的能力和 App ID 的 capabilities 不再匹配,Apple 拒签。
如何判断:Apple Developer → Identifiers → 你的 App ID → 看 Capabilities。再 Profiles → 点你的 distribution profile → 确认列出的 Capabilities 完全一致。漂移 = 拒回。
5. Apple Developer Program 会员到期
99 美元/年续费没成功(卡被拒、卡过期)。会员到期那一刻起所有 cert 和 profile 全失效。
如何判断:Apple Developer → Membership。续费日已过且状态不是 “Active”,整个账号都死了,得续费才能动。
6. WWDR 中间证书过期或本机没有
Apple 的 WWDR (Worldwide Developer Relations) 中间证书有自己的有效期。你本机 Keychain 或 CI runner 里可能没装最新版。Distribution cert 即使有效,证书链验不过。
如何判断:去 Apple WWDR 页。下载当前 G3 / G4 中间证书。导入 Keychain,看有效期;很多 CI 镜像里还是老版本。
动手前先确认
- 完整记下拒回邮件原文;“expired” 和 “invalid” 对应不同修法。
- 确认你在哪个 team 下签名——多 team 账号能用错 cert 签名而不报警。
- 改之前把当前 cert 和 profile 文件备份到本地(
.p12导出)。 - 弄清问题是只在 CI 出现还是本机也复现;只在 CI 出就先查 secret manager。
需要收集的信息
- 拒回邮件完整原文 + 错误码(如 ITMS-90161、ITMS-90201)。
- 你的 iOS Distribution 证书名、指纹、到期日。
- build 用的 provisioning profile 名、UUID、到期日。
- CI 构建日志里 code signing 那几行。
- Apple Developer Program 会员状态和续费日。
最短修复路径
Step 1:识别哪个产物过期
拒回邮件里搜关键词:
- “signing certificate… expired” → cert 问题(Step 2)。
- “provisioning profile… expired” → profile 问题(Step 3)。
- “missing entitlements” → App ID 漂移(Step 5)。
- “membership” → 会员到期(Step 6)。
文案模糊就 Step 2 和 Step 3 都跑一遍。
Step 2:重生 distribution 证书
Apple Developer → Certificates → ”+” → iOS Distribution → Create。
需要从 Keychain Access 生成 CertificateSigningRequest (CSR):
Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority
Email: your@team.com
Common Name: Your Name
Saved to disk → CertificateSigningRequest.certSigningRequest
上传 CSR。下载新 .cer,双击装到 Keychain。把 cert + 私钥一起导出 .p12 备份 + 给 CI 用:
Keychain Access → My Certificates → 右键 → Export
Step 3:重生 provisioning profile
Apple Developer → Profiles → ”+” → App Store(或 Ad Hoc / In House,按分发方式)。
- 选你的 App ID。
- 选 Step 2 的新 distribution cert。
- 选设备(仅 Ad Hoc)。
- 命名带日期后缀:
AppStore_Acme_2027-05。 - Generate,Download。
Xcode 里双击 .mobileprovision 安装。或放到 ~/Library/MobileDevice/Provisioning Profiles/。
Step 4:更新 CI/CD secrets
CI 按文件引用 profile:
# 在 secret manager / git repo 里替换文件
# 例如 GitHub Actions
gh secret set IOS_PROVISIONING_PROFILE_BASE64 --body "$(base64 -i AppStore_Acme_2027-05.mobileprovision)"
gh secret set IOS_CERT_P12_BASE64 --body "$(base64 -i DistributionCert.p12)"
gh secret set IOS_CERT_P12_PASSWORD --body "your-cert-password"
CI workflow 里每次跑都解码并导入:
- name: Install signing assets
run: |
echo "${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}" | base64 -d > profile.mobileprovision
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
echo "${{ secrets.IOS_CERT_P12_BASE64 }}" | base64 -d > cert.p12
security create-keychain -p "" build.keychain
security import cert.p12 -k build.keychain -P "${{ secrets.IOS_CERT_P12_PASSWORD }}" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
Step 5:重 Archive 并上传
Xcode:
- Product → Clean Build Folder。
- Product → Archive(Generic iOS Device)。
- Distribute App → App Store Connect → Upload。
- 自动签名或手动选新 profile。
或 xcodebuild + xcrun altool / Transporter:
xcodebuild -workspace Acme.xcworkspace -scheme Acme -archivePath build/Acme.xcarchive archive
xcodebuild -exportArchive -archivePath build/Acme.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist
xcrun altool --upload-app -f build/ipa/Acme.ipa -u "$ASC_USER" -p "@keychain:ASC_PASSWORD"
Step 6:长期改用 fastlane match
手动管理 cert 是反复过期痛的根源。fastlane match 把 cert 和 profile 加密存进私有 git 仓库(或 S3),任何开发者或 CI runner 都能拉到当前状态:
brew install fastlane
fastlane match init
fastlane match appstore
任何机器上一行 match appstore 拉到当前 cert + profile;match nuke distribution + 重生变成一行命令的年度仪式。
怎么确认已经修好
- Xcode 里重 Archive 不报签名错误。
- 上传到 App Store Connect 顺利过 validation 步。
- 30-60 分钟内收到 processing 成功邮件。
- 新 build 出现在 App Store Connect → TestFlight → Builds。
- 2 小时内没收到后续 ITMS-90xxx 拒回邮件。
如果还是没修好
- 打开 Xcode → Settings → Accounts → Manage Certificates,确认看到新 cert 不是旧的。
- 跑
security find-identity -v -p codesigning看本机能用哪些 cert;旧的删掉。 - 检查
Info.plist里的 bundle ID 和 profile 绑的 App ID 是否完全匹配——大小写不一致都会挂。 - 把构建日志里的 profile UUID 和你下载的 UUID 对一遍;不一样说明 Xcode 缓存了老 profile。删掉
~/Library/MobileDevice/Provisioning Profiles/里旧的,重启 Xcode。
预防建议
- 任何 cert 或 profile 到期前 30 天加日历提醒;Apple 会发警告邮件但常进垃圾箱。
- 第一天就用
fastlane match——免去手动.p12折腾,CI 轮换变得无痛。 - 在团队共享文档里固定记录 profile 和 cert 到期日;月初续不是到期当天。
- App Store 分发最多留 2 张 distribution cert(“当前”+“备用”),轮换避免依赖快过期的那张。
- CI 加一步在每次构建打印 profile 到期日,过期 profile 当场报警。
相关阅读
标签: #排查