每次往 TestFlight 上传完都会看到一条黄条:“缺少合规信息 —— 请提供出口合规信息”。在你点开那个多步问卷之前,build 一直卡在 TestFlight 的等待审核状态,外部测试者根本看不到。原因就是你的 Info.plist 没有 ITSAppUsesNonExemptEncryption 这一项,App Store Connect 无法替你回答美国出口管理局的问题。修复其实就一行 Info.plist;但选对值、判断是否真的需要每年提交自分类报告 (ERN),才是大多数团队会卡住的地方。
常见原因
按触发频率排序。
1. ITSAppUsesNonExemptEncryption 根本不在 Info.plist 里
key 不存在时 App Store Connect 默认”未知”,所以每次上传都弹问卷。老一些的 Xcode 模板创建的新工程不会自带这一项;SwiftUI App 模板也不会加。
如何识别:grep ITSAppUsesNonExemptEncryption Info.plist 没输出。处理完成后 TestFlight build 上立即出现黄色合规警告。
2. key 设成 true,但你只用了 HTTPS / 标准 iOS 加密
绝大多数 App 只用 URLSession(HTTPS)、Keychain、CryptoKit 的标准算法 —— 这些在美国 BIS 740.17(b)(1) 和苹果的”仅使用豁免加密”路径下都算豁免。明明没自带加密代码却把 key 写成 true(非豁免)是最差的回答:会强制你上传一份其实并不需要的 ERN 和首年 BIS 通知。
如何识别:你的代码从不 import OpenSSL、libsodium、BoringSSL、自写的 AES 表或 DRM 模块。所有加密逻辑都来自苹果的 framework 或 HTTPS。这种情况下你是豁免的 —— 但 key 现在却写着 true。
3. key 设成 false,但 App 实际上有非豁免加密
反过来的微妙情况:你打包了一个第三方 VPN SDK、一个自研文件加密库或一个 DRM 模块。这种情况下标 false 属于虚假陈述,审计时会出问题。
如何识别:Podfile.lock 中能搜到 OpenSSL-Universal、WireGuardKit、libsodium、BoringSSL-GRPC,或者你自己手写了 AES-GCM。诚实答案是 true 并走豁免声明流程。
4. key 设在了错的 target 里
包含 App + Widget + Watch + Share Extension 的 workspace,每个 target 都有自己的 Info.plist。只在 App target 设了、Extension target 没设,按 App Store Connect 当前版本逻辑还是可能在主 App 上弹警告。
如何识别:先 find . -name Info.plist,再对每个匹配 grep -l ITSAppUsesNonExemptEncryption,得到的条目比你的 target 数少。
5. 修完之后 CFBundleShortVersionString 反而降低了
你加了 key,但新 build 的 CFBundleShortVersionString 比 App Store Connect 上已有的某个 build 还旧。App Store Connect 按版本号保留合规状态,老版本号会重新触发问卷。
如何识别:明明最新 build 归档里 Info.plist 已经有 key,合规黄条还是反复出现。Build 号是新的,但版本字符串没变或更老。
6. 用了非豁免加密但没提交年度自分类
如果你的 App 用了非豁免加密,又不属于苹果列出的有限用途(认证、版权保护等),美国要求你在每年 2 月 1 日前向 BIS 提交一份年度自分类报告 (ERN)。不提交并不会卡你的 build —— 苹果不会执法 —— 但严格来说你违反了美国出口法。
如何识别:你把 ITSAppUsesNonExemptEncryption 标了 true、没交 ERN、App 不属于任何豁免类别,而且日历已经过了 2 月 1 日。
开始之前
- 从归档好的
.ipa中提取真正打包进去的Info.plist,不要只看 source 里的版本 —— build settings 有时会覆盖或删掉某些 key。 - 列出
Podfile.lock/Package.resolved里所有跟密码学相关的依赖。 - 列出最终交付包里包含哪些 target(App、Widgets、Watch、Intents、Share、NotificationService)。
需要收集的信息
grep -RE "ITSAppUsesNonExempt|ITSEncryption" .从项目根目录跑出的结果。- 加密库清单(
grep -RE "OpenSSL|BoringSSL|libsodium|WireGuard|crypto" Podfile.lock Package.resolved 2>/dev/null)。 - 你的 App 实际怎么用密码学(只用 HTTPS?CryptoKit?自写 AES?VPN?)。
- App Store Connect → TestFlight → 对应 build 上合规警告的原文。
- 上一个 build 的版本字符串以及当时是否已带 key。
修复步骤
绝大多数 App 走完 Step 1 + Step 2 就够了。
步骤 1:盘点实际的加密使用
三种情况:
- 豁免 —— 只用了标准 iOS 加密(HTTPS、Keychain、CryptoKit 标准算法):95% 的 App 在这一类。把
ITSAppUsesNonExemptEncryption设为false。 - 豁免 —— 用了非标准加密但符合豁免条件(仅用于认证、版权保护、加密全部来自苹果等):key 设为
true,并在通过一次性的法国 / 欧盟分类后通过ITSEncryptionExportComplianceCode声明豁免。 - 非豁免:key 设为
true,每年向美国 BIS 提交自分类报告,附上报告编号。
步骤 2:把 key 加到每个 target 的 Info.plist
App target:
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
如果你用的是新版 Xcode 把 Info.plist 写在 build settings 里的方式,到 target → Build Settings → Info.plist Values 加:
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO
每个 Extension target 都重复一次。当主 App 已声明时,extension 不需要单独发布加密声明,但每个 Info.plist 都带上 key 能避免边界情况。
步骤 3:如果必须回答 true,加上豁免码
在 App Store Connect → TestFlight → 对应 build → 提供出口合规中提交问卷后,会拿到一个 ITSEncryptionExportComplianceCode。把它写进 Info.plist,后续 build 就会跳过问卷:
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>ITSEncryptionExportComplianceCode</key>
<string>YOUR-CODE-FROM-APP-STORE-CONNECT</string>
这个码是按 App(不是按 build)发放的,在加密使用没实质变化前可以一直沿用。
步骤 4:重新构建、归档、上传
Info.plist 改动必须重新归档 —— App Store Connect 里不能编辑。先 bump build 号:
agvtool next-version -all
在 Xcode 里归档(Product → Archive)、校验、上传。处理完之后那条黄色合规栏就不应该再出现了。
步骤 5:确认 key 真的进了二进制
从 App Store Connect → TestFlight → Build → 下载 dSYM 处下载处理后的 .ipa(或从你的归档里提取),然后:
unzip -p YourApp.ipa "Payload/YourApp.app/Info.plist" \
| plutil -convert xml1 -o - - \
| grep -A1 ITSAppUsesNonExemptEncryption
应能看到你设置的值。如果 key 不见了,是 build settings 给剥掉了 —— 检查 Skip Install、Strip PNG Text 以及任何处理 Info.plist 的自定义 build phase。
步骤 6:如适用,向 BIS 提交年度自分类
非豁免加密每年 2 月 1 日前把年度自分类报告邮件发到 crypt-supp8@bis.doc.gov 和 enc@nsa.gov。报告是一份 CSV,列:产品名、型号、ECCN(一般是 5D002)、描述、特定授权、加密细节。免费;纯信息备案,能在 BIS 审计时保护你。
验证
- 新 TestFlight build 显示可供测试,没有黄色合规栏。
- 处理完成后外部测试者立即能看到 build,无需手动操作。
- 提取出的二进制
Info.plist里grep ITSAppUsesNonExemptEncryption能看到你设的值。 - App Store Connect → TestFlight → Build → 出口合规栏显示答案已锁定。
长期预防
- 把这个 key 加进你的 Xcode 项目模板,新建工程时默认就在。
- 把值锁定在版本管理里 —— 不要直接在 Xcode 里改而不 commit。
- 引入新 SDK 时,先问”它是否自带加密?“再 merge;加进项目级的依赖审查清单。
- 如果 App 从豁免升级到非豁免(比如加了 VPN 功能),更新 Info.plist key、重新拿合规码、提交年度报告。Build 处理依旧慢的话,参考TestFlight Build 卡在处理中。
- 在
README.md旁边维护一份一页的”加密清单”文档,记下二进制用了哪些加密以及法律分类 —— 这是 M&A 或安全审计时最有用的单一文档。
常见坑
- Xcode 还开着项目时,在 Finder 里手动改 Info.plist —— Xcode 下次 build 会重写,把你的改动覆盖掉。
- 把 key 设成字符串
"YES"/"NO"而不是布尔<true/>/<false/>。字符串形式 App Store Connect 会忽略。 - 以为 CryptoKit 算非标准加密。CryptoKit 用的是苹果提供的算法 —— 稳稳算”豁免”。
- 忽略了通过传递依赖(gRPC、Firebase Auth 套件)引入的
OpenSSL或libsodium会把你从豁免变成可能非豁免。 - 为了”过掉黄条”标个 true 就不交年度报告 —— 苹果不执法,但 BIS 会,罚款不轻。
- 把别的 App 或别的 team 的合规码复用过来 —— 这个码是按 App、按 team 发的。
FAQ
Q:我的 App 只用 HTTPS,该怎么填?
把 ITSAppUsesNonExemptEncryption 设为 false。HTTPS / URLSession / Keychain / CryptoKit 都属于”仅用于支持标准互联网通信的加密”豁免范围内。
Q:我每次发版都点一遍 App Store Connect 的问卷不行吗?
可以,但每次都会让外部测试者邀请暂停。把 Info.plist key 加好之后,该版本字符串永久跳过问卷。
Q:我打包了一个聊天 SDK,它内部封装了 libsodium。这算非豁免吗?
可能算。如果 libsodium 是用来给用户消息做端到端加密、并且这是 App 的核心功能,那是非豁免。如果 SDK 只在内部用 libsodium 做签名或密钥派生,是 API 的附带行为,通常仍可走豁免 —— 但最稳妥的做法是答 true + 通过问卷拿豁免码。
Q:我之前那版已经以 false 通过审核了,现在改成 true 会留下”案底”吗?
App Store Connect 按版本字符串记录合规状态。提交一个新版本号、新 key 的 build 就行,老 build 不用删。
Q:年度自分类要花钱吗?
不要。BIS 备案免费;只要每年 2 月 1 日前递交上一年度的产品清单即可。漏交是监管层面的处罚,跟商业无关 —— 苹果不会下架你的 App。
相关阅读
标签: #排查 #App Store #TestFlight #encryption #export-compliance