使用 App Attest 保护你的 App
Secure your apps with App Attest
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:Apple 终于把 App Attest 搬上了 macOS 27,并引入了基于 30 天内唯一密钥数量的 Fraud Metric(欺诈指标),这意味着靠改机软件和设备农场刷接口的黑产要迎来降维打击了。
这场 Session 讲了什么
以前做防刷和防篡改,大家各显神通,搞设备指纹、越狱检测,结果往往是和黑客玩猫鼠游戏,道高一尺魔高一丈。App Attest 就是 Apple 给的官方“良民证”,利用 Secure Enclave(安全隔区)生成无法导出的密钥,向服务器证明“这个请求确实来自未被篡改的官方 App,且运行在真实的 Apple 硬件上”。这次 macOS 27 终于补齐了这块拼图,让 Mac 端的高价值业务(比如金融、游戏反作弊)也能用上这套机制。
对后端和风控同学影响最大。客户端只要按文档调几个 API 把 Attestation(证明)和 Assertion(断言)传上去就行,但服务端需要解析 CBOR 格式、验证证书链、校验 counter(计数器),还要把新加的 Fraud Metric 接入风控引擎。如果你们的服务端还没搞定 WebAuthn 标准的解析,现在得赶紧拉后端兄弟一起看文档了。
值得深挖的点
Fraud Metric:从“单点防御”到“群体画像”
以前 App Attest 只能证明“当前这个设备是不是真实的”,但黑客如果搞了几百台真实的二手 iPhone 组成设备农场(Device Farm),单看 Attestation 全是真机,服务端根本拦不住。这次 Apple 掏出了 Fraud Metric,直接告诉你“这台设备在过去 30 天内生成了多少个不同的 Attest 密钥”。
正常用户一台设备就一个密钥,如果这个指标飙升,说明这台设备在疯狂重置应用或者被当成了“代理(Broker)”在批量生成凭证。这个设计非常聪明,Apple 没有直接封杀设备(避免误伤),而是把风险评分(Risk Score)交给开发者。Trade-off 在于,这个指标是近似值(Approximate count),且有 30 天的延迟,你不能拿它做实时阻断,只能作为事后风控和封号的强力特征。
macOS 27 的 ACL Blob OID 与系统完整性验证
Mac 生态比 iOS 开放得多,用户可以随便关 SIP(System Integrity Protection)或者降级安全策略。如果只在 Mac 上跑 App Attest,黑客完全可以关掉 SIP 后注入动态库来劫持密钥。Apple 在 macOS 27 的证书里塞进了 ACL Blob OID(密钥访问控制属性),明确记录了生成密钥时设备是否开启了 Full Security(完全安全模式)和 SIP。
这解决了 Mac 端防篡改的关键短板。不过坑点在于,服务端解析这个 OID 需要额外写一套 ASN.1 解析逻辑,而且你要考虑那些为了开发调试 legitimately(合法地)关闭了 SIP 的内部测试员工,别把他们的请求也当成黑产给拦截了。
代码片段
1. 生成绑定安全隔区的密钥
场景:App 首次启动或用户登录时,生成并持久化 Key ID。
import DeviceCheck
// 调用 App Attest 生成密钥,私钥会安全地存储在 Secure Enclave 中
let keyID = try await DCAppAttestService.shared.generateKey()
// 坑:一定要把 keyID 存到 Keychain 里,千万别存 UserDefaults。
// 用户重装 App 或换设备会导致密钥失效,需要做好重新生成的兜底逻辑。
2. 请求 Attestation(证明)
场景:把生成的 Key ID 和服务器下发的 Challenge(挑战值)发给 Apple 验证。
import DeviceCheck
let keyId: String = fetchKeyIdFromKeychain()
// clientDataHash 必须是服务器下发的 challenge 的 SHA256 哈希
let clientDataHash: Data = sha256(serverChallenge)
let attestation = try await DCAppAttestService.shared.attestKey(
keyId: keyId,
clientDataHash: clientDataHash
)
// 坑:attestKey 会消耗 Apple 服务器的配额,必须加指数退避(Exponential Backoff)。
// 绝对不要在主线程或用户关键操作路径上同步调用,放后台任务里做。
3. 生成 Assertion(断言)保护业务 Payload
场景:在发送关键业务请求(如支付、提交答卷)前,对 Payload 进行签名。
import DeviceCheck
let keyId: String = fetchKeyIdFromKeychain()
// 对业务请求的 JSON 数据和服务器 challenge 拼接后求 SHA256
let clientDataHash: Data = sha256(payloadData + serverChallenge)
let assertion = try await DCAppAttestService.shared.generateAssertion(
keyId: keyId,
clientDataHash: clientDataHash
)
// 坑:服务端必须校验 assertion 里的 counter(计数器)是否单调递增。
// 如果 counter 变小或重复,说明请求被重放(Replay Attack)了,直接拒绝。
最佳实践
老项目接入 App Attest 最大的阻力往往在服务端。建议先别急着在客户端全量推开,先让后端把 WebAuthn 标准的 CBOR 解析和证书链验证写好,搭个 Mock 环境。客户端可以先在测试环境跑通,把 Attestation 和 Assertion 的 Base64 字符串抓出来给后端验。
对于新项目,把 DCAppAttestService.shared.isSupported 作为风控的一道门槛。如果一台声称是 iPhone 15 的设备返回 false,直接标记为高危。遇到不支持的设备或验证失败,不要直接弹 Alert 阻断用户,而是采用“降级服务”(比如限制提现额度、隐藏敏感数据),这样既能防黑产,又不会因为 Apple 服务器偶尔抽风而误伤正常用户。
还有什么值得关注
- iOS 27 新增 Authenticator-data Extensions:现在能直接拿到 App 的 Launch validation category(启动验证类别)和 Bundle version,服务端终于能精准识别出请求是来自 TestFlight 还是 App Store 正式版了。
- isSupported 的逆向思维:Apple 明确建议把
isSupported == false当作 Fraud signal(欺诈信号),这在越狱检测库满天飞的今天,算是官方给的最稳妥的“照妖镜”。 - 密钥不跟随 iCloud 备份:App Attest 的密钥是和设备硬件绑定的,用户从 iCloud 备份恢复新手机时,旧密钥直接作废,客户端必须老老实实重新走一遍生成和 Attest 流程。