I am LAZY bones?
AN ancient AND boring SITE

Apple App Attest简介

在这个AI时代,越来越多的应用(APP)是和AI相关的,其中有不少,对用户的请求需要调用LLM来处理,也就是要消耗token。如果这个应用,是对免费用户甚至未注册用户有一定的体验使用量的话,就要考虑怎么防止token被刷爆的问题了。恰巧我就在做一个这样的应用。

此时,一个自然而然的问题是:要怎么证明”这是正版 App 发来的”?

Apple App Attest 就是来解决这个问题的。(PS:大家如果有安卓的、PWA的解决方案,可以留言)

我想实现的是”匿名用户每天送一次 AI 评分”,不登录就能用,体验好、转化高。可这接口裸奔在公网上,谁拿 curl 写个循环都能把额度刷爆。加验证码太伤体验,强制登录又把”尝鲜”这个卖点废了。

我想要的其实是一句话:能不能让服务器确信”这条请求确实是从我那个正版 App、在一台真机上发出来的”?Apple 的 App Attest 就是干这个的。这篇把它的原理、整体流程,以及服务端到底该怎么验,讲清楚;我自己趟过的几个坑放在最后当佐料。

App Attest 解决的是什么问题

传统的”防刷”思路是给请求带个密钥或 token,可只要密钥在客户端,逆向、抓包、改包就能仿造,挡不住有心人。App Attest 换了个思路:它借助 Secure Enclave(设备上独立的安全芯片),由苹果来给你的 App 背书,证明两件事——这是从 App Store 渠道的正版 App 发出的,且跑在一台真实的苹果设备上。

这个背书是密码学保证的:签名私钥生成在 Secure Enclave 里、永远导不出来,连越狱也偷不走。所以它特别适合”匿名但要防滥用”的场景:免费额度、防注册机刷号、防接口被脚本薅。它不是用户身份认证(那是 Sign in with Apple 的活),它认的是”设备 + App”这个组合可信。

代价是它只在真机、正版渠道下成立——模拟器用不了,这点后面会再提。

两段式:attestation 和 assertion

理解 App Attest,关键是分清它的两段,这俩验证逻辑完全不同。

第一段 attestation,一次性的。App 首次要证明自己时,在 Secure Enclave 里生成一对密钥,请苹果给这把公钥签发一张证书;证书连同一坨 authenticator data 打包成 attestation 对象,发给你的服务器。服务器验完,把这把公钥存进库,跟这台设备绑定。这一步只做一次。

第二段 assertion,每次请求都做。App 用第一段那把私钥,对”本次请求的内容”签个名,随请求发出。服务器用之前存下的公钥验签——对得上,就说明这条请求确实来自那台被证明过的设备,且内容没被篡改。

客户端的代码很薄,DCAppAttestService 几个方法调一调就行。真正有讲究的是服务端这两段验证,下面分开说。

服务端怎么验 attestation

attestation 对象 CBOR 解开后,核心是一条证书链 x5c 和一段 authData。要验的东西不少,挑要点说:

证书链。x5c 里只有两张证书:给设备公钥签的叶子证书,和一张中间证书。你要做的是把它验到苹果的 App Attest 根证书。这里有个反直觉的点——根证书不在链里。别去比对”链里最后一张是不是根”,那是中间证书。正确做法是把苹果根证书内嵌进代码,用它的公钥验中间证书,再用中间证书验叶子。根证书是信任锚,得自己持有,不能从对方给的链里取。苹果根证书在 certificate authority 页面下载,建议顺手核对哈希:

nonce。光证明”证书合法”挡不住重放——截一个合法 attestation 反复发也行。苹果的办法是:服务器先发一个随机 challenge,苹果会在叶子证书的扩展里(OID 1.2.840.113635.100.8.2)塞进 nonce = SHA256(authData ‖ SHA256(challenge))。服务器照样算一遍比对,对上才说明这份证明是冲着你这次的 challenge 来的,authData 也没被动过。

剩下几项是常规校验:rpIdHash 要等于 SHA256(appId)(appId = Team ID 加 Bundle ID);新鲜的 attestation 计数器必须是 0;aaguid 标明这是 App Attest(正式环境是 appattest,Xcode 调试走的开发环境是 appattestdevelop,两者都要放行);最后把 authData 里的公钥 SHA256 一下,应当等于凭证 ID。全过了,把这把公钥存库。

证书链验签自己撸 ASN.1 容易出隐蔽 bug,我直接用了 @peculiar/x509,它在 Cloudflare Worker 的 WebCrypto 环境里能跑。

服务端怎么验 assertion

每次请求的验证简单些:CBOR 解开 assertion 拿到签名和 authenticatorData,先验 rpIdHash、再查计数器是否比库里存的大(防重放,每签一次苹果会自增),最后用存下的公钥验签。

验签的消息构造是这段里唯一的”暗礁”。苹果文档说签名覆盖的是 authenticatorData ‖ clientDataHash,但你要是把这俩直接拼起来交给 ECDSA-SHA256 去验,会失败。真相是:苹果用 ES256 签的是 nonce = SHA256(authData ‖ clientDataHash),而 ES256 自己还会再 hash 一层,所以最终参与 ECDSA 的摘要是 SHA256(nonce)。WebCrypto 的 ECDSA 必定做一次 hash、跳不过,因此正确写法是先把 nonce 算出来,再把 nonce 当消息传进去,让它在上面再 hash 一次:

这层”看不见的 hash”我是把原始字节 dump 出来在本地穷举才定位到的——推理走不动时,让事实说话往往更快。

实现时几个容易绊倒的点

主线讲完了,把我真机联调时踩到的坑列一下,纯属佐料,但能省你几个小时:

1. Team ID 不一定是你以为的那个。报 “RP ID hash mismatch” 时我很懵,appId 明明拼对了。后来去构建产物里一看,签名证书的 team 和描述文件的 team 是两个,App Attest 取的是 application-identifier 里那个:

2. COSE 公钥是整数键。authData 里那把公钥是 COSE 格式,x、y 的键是 -2、-3 这种整数。cborg 默认解对象会直接抛 “non-string keys not supported”,得开 useMaps 解成 Map 再 get:

3. 模拟器测不了。App Attest 在模拟器上直接不支持,匿名链路只能上真机。开发期可以在服务端留个开关跳过验证方便联调,但上线前务必删掉。

值不值得用

如果你有”匿名 / 低门槛、但又怕被脚本滥用”的接口,App Attest 是目前苹果生态里最硬的一道闸:信任根在苹果、私钥锁在 Secure Enclave,比任何塞在客户端的密钥都难仿造。代价是只覆盖真机正版、客户端服务端都得改、还得忍受一段真机调试的来回。

它的坑也基本都不在文档主线上,而在那些”想当然”的接缝处——根证书的位置、Team ID 的来源、COSE 的键类型、ES256 那层默认的 hash。单看每个都不难,叠在一起就够耗你一天。提前知道它们长什么样,就能少趟很多。

全文完。

最后修改时间: 2026年06月14日 20:28

本文章发表于: 2026年06月14日 20:28 | 所属分类:经验技巧. | 您可以在此订阅本文章的所有评论. | 您也可以发表评论, 或从您的网站trackback.

一个评论 关于: “Apple App Attest简介”

  1. 依云 在 2026年06月14日 22:32 说:回复

    Android和YubiKey也都有自己的attestation的,网上很容易就能搜到。这东西要和硬件密码学设备绑定,而且依赖设备厂商的签名。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注