最快修复: 拒回邮件会点名是哪个产物。写 “signing certificate… no longer valid”(多半是 ITMS-90165)= distribution 证书过期,要新建一张 iOS Distribution 证书 + 一份绑定它的新 App Store profile,再重 Archive。写 “provisioning profile… has expired”(ITMS-90161)= 只是 profile 到期,在 Portal 里重生 profile 即可。不要两个一起盲改:重生那个其实还有效的产物会白白浪费 20 分钟,还无谓地重置了 CI secrets。
你在 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 一过期它立刻作废),以及给整条链签名的 Apple WWDR 中间证书。CI/CD 经常用几个月前 check 进 config 仓库或 secret manager 的旧 profile 签名。修复是机械操作——重生过期那个产物再 rebuild——但先搞清是哪个过期能省一小时。一个常踩的坑:过期的 distribution 证书无法”续期”,只能新建一张(Apple 在证书支持页里明确说明了这点)。
先判断你属于哪一类
动手前先把邮件文案对到具体产物上。
| 邮件 / 日志文案 | 大概率错误码 | 要修的产物 | 对应章节 |
|---|---|---|---|
| ”signing certificate… no longer valid” | ITMS-90165 | Distribution 证书 | 原因 1、Step 2 |
| ”provisioning profile… has expired” | ITMS-90161 | Provisioning profile | 原因 2、Step 3 |
| 本机能签、只在 CI 失败 | (无邮件,签名时挂) | CI 缓存的旧 profile/cert | 原因 3、Step 4 |
| ”doesn’t include the entitlement” / “doesn’t support capability” | ITMS-90164 系列 | App ID 漂移 | 原因 4 |
| ”membership has expired” / Portal 被锁 | 无 | 会员到期 | 原因 5 |
errSecInternalComponent / codesign 时链不受信 | 无 | WWDR 中间证书 | 原因 6 |
常见原因
按命中率排序。
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 会员到期
Apple Developer Program 会员(截至 2026 年 6 月仍是 99 美元/年,符合条件的非营利组织、认证院校、政府机构可申请费用豁免)续费没成功,通常是卡被拒或卡过期。会员到期那一刻起所有 cert 和 profile 全失效。
如何判断:Apple Developer → Membership。续费日已过且状态不是 “Active”,整个账号都被锁了,得续费才能动。
6. WWDR 中间证书过期或本机没有
Apple 的 WWDR (Worldwide Developer Relations) 中间证书给你的 distribution cert 签名,它有自己的有效期。你本机 Keychain 或 CI runner 镜像里可能没装最新版。Distribution cert 即使有效,证书链也验不过,codesign 会报错(常见 errSecInternalComponent)。当给 profile 签名的那张 WWDR 证书过期时,还会触发 ITMS-90165 “Invalid Provisioning Profile Signature”。
如何判断:打开 Apple 证书颁发机构页。原始 WWDR 中间证书已于 2023 年 2 月 7 日过期,之后 Apple 分拆出几张中间证书:续期后的 WWDR G3(截至 2026 年 6 月,2030 年 2 月 20 日到期)给 Apple Distribution / iOS Distribution 证书签名,G6 是较新的 ECDSA 软件签名中间证书(2036 年 3 月 19 日到期)。Xcode 15+ 会自动安装符合条件的 WWDR 中间证书,所以缺证书几乎都意味着是无头 CI runner 或老镜像。下载 AppleWWDRCAG6.cer(如果你的 distribution 证书链经过 G3,再下载 G3 证书),导入构建 Keychain,确认有效期在未来。
动手前先确认
- 完整记下拒回邮件原文。“expired” 和 “no longer valid” 指向不同产物。
- 确认你在哪个 team 下签名。多 team 账号会悄悄用错 cert 签名而完全不报警。
- 改任何东西前先把当前 cert 和 profile 备份到本地(导出
.p12),这样轮换搞砸了还能回退。 - 弄清问题是只在 CI 出现还是本机也复现。只在 CI 出,直指某个旧的 secret。
需要收集的信息
- 拒回邮件完整原文 + 错误码(如
ITMS-90161、ITMS-90165)。 - 你的 iOS Distribution 证书名、指纹、到期日。
- build 用的 provisioning profile 名、UUID、到期日。
- CI 构建日志里 code signing 那几行。
- Apple Developer Program 会员状态和续费日。
最短修复路径
Step 1:识别哪个产物过期
拒回邮件里搜关键词:
- “signing certificate… no longer valid”(
ITMS-90165)→ cert 问题(Step 2)。 - “provisioning profile… has expired”(
ITMS-90161)→ profile 问题(Step 3)。 - “doesn’t include the entitlement / capability” → App ID 漂移(见原因 4;先改 App ID,再重做 Step 3)。
- “membership” / Portal 被锁 → 会员到期(见原因 5;先续费,再重做 Step 2-3)。
文案模糊就 Step 2 和 Step 3 都跑一遍。
Step 2:新建 distribution 证书
过期证书无法续期,只能新建一张替换:Apple Developer → Certificates → ”+” → Apple Distribution(这一种类型现在统管 iOS / tvOS / watchOS / macOS App Store;只有你专门维护平台专属证书时才选 “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 安装。磁盘上的位置在 Xcode 16 变了:profile 现在存在 ~/Library/Developer/Xcode/UserData/Provisioning Profiles/(Xcode 15 及更早是 ~/Library/MobileDevice/Provisioning Profiles/)。如果你脚本化复制,要按机器上的 Xcode 版本写对路径;注意有些工具(fastlane、Rider、.NET MAUI)当初要更新才认得新文件夹。
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
# Xcode 16+ runner 路径;Xcode 15 镜像用 ~/Library/MobileDevice/Provisioning Profiles
mkdir -p ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles
cp profile.mobileprovision ~/Library/Developer/Xcode/UserData/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 + 命令行上传工具:
xcodebuild -workspace Acme.xcworkspace -scheme Acme -archivePath build/Acme.xcarchive archive
xcodebuild -exportArchive -archivePath build/Acme.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist
上传环节优先用 App Store Connect API key,而不是 Apple ID + app-specific password(在 CI 上更稳,没有 2FA 弹窗):
# 现代做法:App Store Connect API key(.p8)——notarytool 和 fastlane 也用这个
xcrun altool --upload-app -f build/ipa/Acme.ipa -t ios \
--apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
# 旧的回退方式(截至 2026 年 6 月仍可用于 App Store 上传):
xcrun altool --upload-app -f build/ipa/Acme.ipa -t ios -u "$ASC_USER" -p "@keychain:ASC_PASSWORD"
注意:altool 已对公证 (notarization) 弃用(2023 年 11 月 1 日起不再接受其上传,公证改用 xcrun notarytool),但用于上传 App Store 二进制仍受支持。.p8 key 文件需放在 ~/.appstoreconnect/private_keys/,或用 --apiKeyPath 显式指定。
Step 6:长期改用 fastlane match
手动管理 cert 是反复过期痛的根源。fastlane match 把 cert 和 profile 加密存进你选定的后端(私有 git 仓库、Amazon S3 或 Google Cloud Storage),任何开发者或 CI runner 都能拉到当前状态:
brew install fastlane
fastlane match init
fastlane match appstore
任何机器上一行 fastlane match appstore 拉到当前 cert + profile;fastlane match nuke distribution 后再重生,变成一行命令的年度仪式。CI 上务必加 --readonly,让 runner 永远不去创建或吊销证书,只拉取已有的:
fastlane match appstore --readonly
如果 profile 或 cert 确实过期了,先在开发机上跑一次完整(非 readonly)的 fastlane match 重生,再让 CI 回到只读模式。
怎么确认已经修好
- 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。删掉缓存(Xcode 16+ 在
~/Library/Developer/Xcode/UserData/Provisioning Profiles/,Xcode 15 及更早在~/Library/MobileDevice/Provisioning Profiles/),重启 Xcode。
常见问题
邮件说证书过期,但 Portal 里显示它有效,为什么?
几乎可以肯定是 build 用了本机或 CI runner 上缓存的一张更旧的 cert 签名,不是你在 Portal 看到的那张。跑 security find-identity -v -p codesigning,删掉所有过期身份,再确认构建日志引用的是新 cert 的指纹。
能直接续期那张过期的 distribution 证书吗? 不能。Apple 不支持证书续期,只能新建一张,让旧的过期或主动吊销。然后重生所有引用旧 cert 的 profile(profile 永远指向它生成时绑定的那张具体 cert)。
重新上传一定要改 build number 或版本号吗?
如果被拒的那次上传已经进入 processing,就要。App Store Connect 要求每次上传的 CFBundleVersion(build number)唯一。如果是在 processing 前的 validation 阶段被拒,同一个号通常还能用,但加 1 是更稳的默认做法。
之前一直好好的、我什么都没改,现在却被拒了,是什么过期了? 时间过期了。Cert 和 profile 有硬性的 1 年有效期,到点就失效,与你是否活跃无关。去 Apple Developer → Certificates 和 → Profiles 看有没有到期日已经过去的,那个静默倒计时是最常见的原因。
本机能签、只有 CI 失败,先查哪里?
查 CI secret。runner 在用几个月前存的 profile 或 .p12 签名。在 CI 里解码内嵌 profile 并打印到期日(security cms -D -i embedded.mobileprovision | grep -A1 ExpirationDate),和 Portal 对比。这是原因 3。
预防建议
- 任何 cert 或 profile 到期前 30 天加日历提醒;Apple 会发警告邮件但常进垃圾箱。
- 第一天就用
fastlane match——免去手动.p12折腾,CI 轮换变得无痛。 - 在团队共享文档里固定记录 profile 和 cert 到期日;月初续不是到期当天。
- App Store 分发最多留 2 张 distribution cert(“当前”+“备用”),轮换避免依赖快过期的那张。
- CI 加一步在每次构建打印 profile 到期日,过期 profile 当场报警。
相关阅读
标签: #排查