28. Keystore与FIDO2:WebAuthn集成、CTAP协议、U2F密钥对生成、认证器注册
聊到移动端安全,FIDO2 是个绕不开的话题。说白了,它就是要干掉密码。我最早接触 FIDO 标准是在做企业级身份认证 SDK 的时候,客户要求支持无密码登录。当时我还在想,这玩意儿跟 Android Keystore 有什么关系?后来才发现,Keystore 就是 FIDO2 在 Android 上的硬件根基。
这一章,我们就把 FIDO2 的三大组件拆开来看:WebAuthn、CTAP、U2F。重点落在 Android Keystore 如何为它们提供密钥对生成和认证器注册的能力。
28.1 FIDO2 协议栈概览
先搭个框架。FIDO2 由两个核心规范组成:
- WebAuthn(Web Authentication):浏览器端的 JavaScript API,负责与服务器通信。
- CTAP(Client to Authenticator Protocol):客户端(浏览器/App)与认证器(硬件密钥/手机 TEE)之间的通信协议。
而 U2F 是 FIDO 第一代标准,现在被 FIDO2 兼容。Android 从 7.0 开始内置了 FIDO2 认证器,底层就是 Keystore + TEE。
核心关系:WebAuthn 是上层接口,CTAP 是传输协议,U2F 是密钥对生成规范。Android Keystore 是它们的硬件安全底座。
28.2 WebAuthn 集成:从浏览器到 Keystore
WebAuthn 的流程,我简化一下:
- 用户点击「登录」→ 服务器下发 challenge(随机数)。
- 浏览器调用
navigator.credentials.create()。 - Android 系统弹出认证器选择(内置 / 外接)。
- 认证器调用 Keystore 生成非对称密钥对。
- 返回公钥 + 签名给服务器。
这里的关键是第 4 步。Android 内置认证器会调用 KeyStore 的 generateKeyPair(),并且指定 KeyProperties.PURPOSE_SIGN。我习惯在生成时加上 setUserAuthenticationRequired(true),这样每次签名都需要用户验证指纹或锁屏。
我的经验:WebAuthn 的 challenge 必须足够随机,长度至少 32 字节。曾经有个项目用了 8 字节的 challenge,被安全审计直接打回。服务器端也要验证 challenge 的时效性,防止重放攻击。
28.3 CTAP 协议:客户端与认证器的桥梁
CTAP 分两个版本:
| 版本 | 对应标准 | 传输方式 | Android 支持 |
|---|---|---|---|
| CTAP1 | U2F | USB / NFC / BLE | Android 7.0+ |
| CTAP2 | FIDO2 | USB / NFC / BLE | Android 10+ |
CTAP 的消息格式是 CBOR 编码的。Android 平台通过 Fido2ApiClient 封装了这些细节。你不需要直接处理 CTAP 报文,但理解它的结构有助于调试。
举个例子,CTAP2 的 authenticatorMakeCredential 命令包含:
clientDataHash:WebAuthn 的 challenge 哈希rp:依赖方(你的服务器)信息user:用户信息(id、name、displayName)pubKeyCredParams:支持的算法类型(通常是 ES256)excludeList:排除已有的凭证 ID
Android 的 FIDO2 API 会把这些参数映射到 Keystore 的密钥生成参数上。嗯,这里要注意:pubKeyCredParams 如果指定了 -7(ES256),Keystore 就会生成 EC P-256 密钥对。
28.4 U2F 密钥对生成:Keystore 的硬核操作
U2F 的密钥对生成,说白了就是 Keystore 的 generateKeyPair 加上一些 FIDO 特定的参数。我直接贴一段代码,这是我在实际项目中用过的:
// 生成 U2F 兼容的密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
"fido2_key_" + System.currentTimeMillis(),
KeyProperties.PURPOSE_SIGN)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(30)
.setAttestationChallenge(challenge); // 关键:传入 challenge
keyPairGenerator.initialize(builder.build());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
这里有个坑:setAttestationChallenge() 是 Android 8.0 才加入的。如果你要兼容低版本,得用 KeyStore 的 setEntry() 手动注入 attestation 证书链。我曾经在 Android 7.1 的设备上踩过这个坑,折腾了两天才找到替代方案。
注意:U2F 要求密钥对必须绑定到特定的 AppID(WebAuthn 里叫 RP ID)。Android Keystore 的 setAttestationChallenge 可以携带这个信息,但你需要自己实现绑定逻辑。不要偷懒把 AppID 硬编码在代码里,否则换域名就全废了。
28.5 认证器注册:从生成到上报
认证器注册的完整流程,我画个图帮你理解:
注册完成后,认证器会返回一个 AuthenticatorAttestationResponse,里面包含:
- attestationObject:包含公钥、凭证 ID、attestation 证书链
- clientDataJSON:客户端数据(challenge、origin、type)
服务器收到后,需要验证 attestation 证书链是否可信。Android 的 attestation 证书由 Google 的 TEE 签名,你可以用 GoogleAttestationVerifier 来验证。我建议在服务器端缓存证书吊销列表,因为 Google 偶尔会更新签名密钥。
避坑指南:我曾经遇到过一个 case,某款手机的 TEE 固件版本太老,生成的 attestation 证书链不完整。解决方案是在客户端检测 isUserAuthenticationValid() 方法,如果返回 false,就提示用户更新系统安全补丁。
28.6 总结:Keystore 在 FIDO2 中的角色
最后总结一下。Android Keystore 在 FIDO2 里扮演了三个角色:
- 密钥生成器:生成符合 U2F/FIDO2 规范的 EC P-256 密钥对。
- 签名引擎:对 challenge 进行签名,证明用户持有私钥。
- attestation 提供者:提供硬件级证明,告诉服务器「这个密钥确实是在安全硬件里生成的」。
说白了,没有 Keystore,Android 的 FIDO2 认证器就是个软件模拟,安全性大打折扣。你想想看,如果私钥能被 App 直接读取,那跟用密码有什么区别?
我个人习惯在实现 FIDO2 时,始终把 Keystore 的 setUserAuthenticationRequired(true) 打开。虽然用户体验上多了一步验证,但安全性提升了一个量级。毕竟,FIDO2 的初衷就是「something you have + something you are」,缺一不可。
公众号:蓝海资料掘金营,微信deep3321