webauthn公钥凭证不包含生物识别信息,仅含加密签名等元数据;attestationobject为cbor编码,须用cbor库解析;authdata中嵌入公钥,需正确提取并验证;credentialid和publickey必须以二进制形式存储。

WebAuthn 公钥凭证不是“生物识别信息”本身
很多人误以为 AuthenticatorAttestationResponse 或 AuthenticatorAssertionResponse 文件里存着指纹/人脸图像,其实完全不是。它只包含加密签名、挑战响应、公钥、认证器标识等元数据——生物特征始终留在设备 Secure Enclave / TPM 里,从不离开。C# 程序处理的只是这些密码学结构,不是生物模板。
所以你不需要也**不应该**去“解密生物信息”,而是要正确解析、验证、序列化和持久化标准 WebAuthn 二进制结构(如 CBOR 编码的 attestationObject 或 authenticatorData)。
C# 解析 attestationObject 必须用 CBOR 库,不能用 json
浏览器传来的 attestationObject 是 CBOR 编码的二进制 blob,不是 JSON 字符串。直接用 JsonSerializer.Deserialize 会报错或静默损坏数据——这是最常踩的坑。
- 用
System.Text.Json解析必然失败,CBOR 和 JSON 语义不兼容(比如字节串、标签、未定义长度数组) - 推荐用
microsoft.Cswpf.Cbor(轻量、.NET 6+ 原生支持)或Newtonsoft.Json+Newtonsoft.Json.Cbor - 关键字段如
authData是字节数组,必须原样保留;fmt字段决定验证逻辑分支("fido-u2f"、"packed"、"android-key"等)
示例:用 CborReader 提取 authData:
var cbor = CborReader.Read(bytes); // bytes = request.Response.AttestationObject var authDataBytes = cbor["authData"].AsByteArray(); // 不要 ToString() 或 Encoding.UTF8.GetString()
验证 signature 时,publicKey 必须从 authData 解析,不能从 x5c 直接取
尤其在 "packed" 格式下,证书链(x5c)可选,而公钥一定嵌在 authData 的扩展字段(attestedCredentialData)里。跳过这步会导致验签失败或接受伪造凭证。
-
authData前 32 字节是 RP ID hash,接着 1 字节标志位,再 16 字节 AAGUID,然后才是可变长的attestedCredentialData - 其中第 17–20 字节是 credential ID 长度,之后是 credential ID(需原样存储用于后续登录),再之后 1 字节是公钥长度,紧接着才是 DER 编码的 EC public key(
SubjectPublicKeyInfo) - 用
System.Security.Cryptography.ECDsa.Create()加载该 DER 数据,再用它验证signature对clientDataHash+authData的签名
存储 credentialId 和 publicKey 时,别用 Base64Url 就完事
很多教程只说“存 credentialId”,但漏了两个关键点:长度和编码一致性。数据库字段类型、索引策略、比较逻辑都会因此出问题。
-
credentialId是原始字节数组,长度通常 16–64 字节(不同认证器差异大),**必须存为 VARBINARY / BYTEA / BLOB**,而非 VARCHAR;否则可能被截断或乱码 - 如果非要转字符串(比如日志或调试),统一用
Base64UrlEncoder.Encode(不是Convert.ToBase64String),避免 + / = 字符引发路由或 URL 问题 -
publicKey同理:存 DER 二进制,不要存 PEM;验证时也必须用原始 DER,PEM 头尾会破坏 ASN.1 结构
真正麻烦的是跨平台兼容性:Android SafetyNet 和 Apple Secure Enclave 返回的 fmt 和签名算法完全不同,一套验证逻辑跑不通所有设备——得按 fmt 分支写,且每个分支都要单独测试真实设备。