青

一言

FIDO2 无密码认证技术

FIDO2 无密码认证技术

AIGC 提醒

本文 超过80% 内容使用 AIGC 技术生成,使用的模型包括: Claude-4-sonnet,Deepseek-R1
在发布之前已进行人工整理修正,过程中尽可能确保信息真实可信,但还请在参考采用前更多进行验证。

快速了解

如果你赶时间,或者不愿意阅读太多技术细节,直接阅读本部分内容即可

FIDO2 是一套开放的、免密码的强身份验证行业标准,由 WebAuthn(W3C标准)CTAP2(通信协议) 组成。它通过加密密钥对实现抗钓鱼攻击的无密码认证,核心优势包括:

  1. 安全性:抗钓鱼/重放攻击,无共享秘密(服务器仅存公钥)
  2. 隐私保护:生物特征不离设备,防跨站追踪
  3. 用户体验:单手势认证(指纹/面部/按键),跨设备兼容
  4. 标准化支持:所有主流浏览器及操作系统(Win/macOS/Android/iOS)原生集成

关键技术特性:

  • 认证器类型:平台认证器(内置) vs 漫游认证器(外置)
  • 核心配置
    • authenticatorAttachment(认证器类型选择)
    • residentKey(常驻密钥实现无用户名登录)
    • userVerification(生物识别/PIN验证强度)
  • 企业级能力:设备证明(attestation)、交易授权扩展(txAuthSimple

FIDO2 无密码认证技术

FIDO2的基本介绍

一句话介绍:FIDO2 是一套开放的、免密码的、强身份验证的行业标准。

FIDO2代表着身份认证技术的根本性转变,它是由FIDO联盟开发的最新开放认证标准,旨在实现跨Web和移动应用的无密码、抗钓鱼认证。

与传统基于知识的认证(密码)不同,FIDO2采用基于拥有的认证方式,使用加密密钥对来确保安全性。

定义与核心组件

FIDO2由两个互补的规范组成:

WebAuthn(Web认证API):由W3C与FIDO联盟合作开发的JavaScript API,允许浏览器访问强认证功能。这个API已于2019年3月4日成为W3C官方推荐标准,并获得所有主流浏览器支持。

CTAP2(客户端到认证器协议2):FIDO联盟开发的通信协议,支持外部认证器通过USB、NFC或蓝牙低功耗(BLE)与FIDO2兼容的浏览器和平台进行通信。

简单说,WebAuthn 是网站支持 FIDO2 登录的编程接口,CTAP 是硬件设备与登录平台沟通的规则,两者共同实现了 FIDO2 无密码认证。

发展历程

FIDO发展历程

FIDO联盟的演进反映了行业对更强大身份认证解决方案的迫切需求:

  • 2012年7月:FIDO联盟由PayPal、联想、Nok Nok Labs等公司创立
  • 2014年12月:发布FIDO 1.0规范,包括UAF和U2F
  • 2016年2月:W3C基于FIDO2 2.0 Web API启动WebAuthn标准制定
  • 2018年4月10日:FIDO2正式发布,WebAuthn达到W3C候选推荐状态
  • 2019年3月4日:WebAuthn成为W3C官方推荐标准
  • 2020年至今:Safari、iOS和macOS加入支持,实现真正的跨平台兼容

与 FIDO U2F 的区别

U2F 是一种硬件安全密钥的物理双因素认证(2FA)标准,是 FIDO联盟推出的第一代免密码认证标准,专注于增强传统密码登录的安全性。

FIDO2 是 FIDO U2F 的继承与发展,在很多方面进行了升级和改进:

特性 FIDO U2F FIDO2
认证模式 仅作为第二因素(2FA) 支持单因素、2FA和MFA
密码依赖 增强密码安全性 无需密码
用户体验 需要用户名+密码+密钥 生物识别/PIN实现无密码登录
设备支持 主要是硬件安全密钥 内置认证器+漫游密钥
用户验证 仅基本用户在场验证 支持生物识别、PIN等高级验证
核心协议 CTAP1 WebAuthn (API) + CTAP2 (协议,兼容CTAP1)

U2F 为更安全、更便捷的 FIDO2 无密码未来铺平了道路。如今,许多服务同时支持 U2F(作为2FA选项)和 FIDO2(作为无密码或2FA选项)。

主要优势

安全性提升

  • 抗钓鱼攻击:加密密钥与原始域绑定,无法在欺诈网站使用
  • 无共享秘密:服务器仅存储公钥,数据泄露影响最小化
  • 防重放攻击:每次认证包含唯一的挑战-响应机制

隐私保护

  • 防跨站追踪:每个网站使用唯一的加密密钥对
  • 生物特征数据保护:生物识别数据永不离开用户设备

用户体验改善

  • 便捷性:无需记忆和输入密码
  • 速度提升:单手势认证(指纹、面部扫描、按键)
  • 跨设备工作:在所有设备和平台上无缝使用

FIDO2支持的设备类型和兼容性

认证器类型

平台认证器

  • 内置于用户设备中(Touch ID、Face ID、Windows Hello)
  • 始终可用,无需额外硬件
  • 通过硬件安全元件保护私钥

漫游认证器

  • 独立的外部设备(USB安全密钥、NFC设备、智能手机)
  • 可跨多个设备使用
  • 通过USB、NFC、蓝牙与客户端设备通信

浏览器兼容性

  • Google Chrome:67+版本起完全支持FIDO2/WebAuthn
  • Mozilla Firefox:60+版本起完整支持,增强的移动支持
  • Microsoft Edge:与Windows Hello和外部密钥全面集成
  • Apple Safari:macOS Mojave/iOS 13起支持WebAuthn,PIN设置有限制

操作系统支持

  • Windows 10/11:原生Windows Hello集成,最新更新中增强了安全功能
  • macOS:Touch ID/Face ID集成,Safari WebAuthn支持,NFC/BLE支持有限
  • Android:Android 7.0+完整的NFC和USB支持,PIN保护的NFC密钥有些限制,但需要注意,部分手机厂商如果解锁了Bootloader,可能会永久禁用安全芯片,导致无法使用FIDO2认证器。
  • iOS:iOS13.3+之后的系统在 Safari 中提供NFC支持,并与支持USB-C,Lightning连接器兼容
  • Linux:PAM框架支持,Ubuntu和Fedora集成

FIDO2 配置参数解析

认证器选择标准(authenticatorSelection)

认证器选择是FIDO2实现中最关键的配置决策,直接影响用户体验和安全性:

1
2
3
4
5
6
authenticatorSelection: {
authenticatorAttachment: "", // 值:"platform"、"cross-platform"或空
userVerification: "preferred", // 值:"required"、"preferred"、"discouraged"
requireResidentKey: false, // 布尔值:true/false(已废弃)
residentKey: "preferred" // 值:"required"、"preferred"、"discouraged"
}

authenticatorAttachment 认证器类型选择

authenticatorAttachment 是一个在注册 (navigator.credentials.create()) 和认证 (navigator.credentials.get()) 过程中,依赖方(RP,即网站/应用)可以向浏览器/平台指定的提示或要求。它指示 RP 偏好或要求用户使用哪种认证器。

  • "platform" (平台认证器):
    • 平台认证器是集成在用户当前正在使用的设备内部的硬件或软件模块。
    • 常见例子:
      • 笔记本电脑/台式机的 TPM 芯片 + Windows Hello (PIN/人脸/指纹)
      • Mac/iPhone/iPad 的 T2(Mac)/Secure Enclave + Touch ID/Face ID
      • Android 设备的 Titan M/安全芯片 + 指纹/人脸识别
    • 特点:
      • 无需额外硬件: 用户无需购买或携带额外的安全密钥。
      • 无缝集成: 通常与操作系统登录体验深度集成(如 Windows Hello 弹窗、iOS 面容 ID 提示)。
      • 凭证绑定设备: 生成的密钥对(凭证)默认存储在该设备内部的安全区域。用户只能在这台设备上使用该凭证。
      • 可用性限制: 用户很可能无法在其他设备上使用该凭证登录(FIDO 本身不定义同步,但实际实现确有同步机制)。
  • "cross-platform" (跨平台认证器):
    • 跨平台认证器是一个物理的、可移动的外部设备,可以通过标准接口(USB、NFC、蓝牙)连接到多种不同的设备(平台)。
    • 常见例子:
      • YubiKey、Google Titan Security Key、SoloKey、Feitian 等品牌的 USB-A/USB-C/NFC/蓝牙安全密钥。
    • 特点:
      • 设备无关性/可移植性: 用户可以将同一个安全密钥插入/触碰不同的电脑、手机或平板来使用同一个凭证。密钥对存储在密钥内部。
      • 高安全性与隔离性: 私钥物理隔离在专用硬件中,即使连接的设备被恶意软件感染,私钥也难以被提取。
设置值 验证方式 特性
"platform" 内置认证器 更便捷
"cross-platform" 外部安全密钥 需物理密钥
undefined(或为空) 由用户进行选择 更灵活

userVerification 用户验证方法

userVerification 用于指定在认证过程中验证用户身份的严格程度。它决定了认证器(安全密钥、手机、指纹识别器等)在执行操作前,需要以何种方式确认操作者确实是“真人”且是“本人”。

  1. 用户在场验证

    • 概念: 这是最基础的验证级别。它只要求证明用户“物理在场”并“同意”了这次认证操作。
    • 实现方式:
      • 物理按键: 最常见的方式。用户需要按一下安全密钥上的物理按钮。这证明用户确实拿着密钥并且有意进行了操作。
      • 简单的生物识别接触: 在某些设备上,可能只需要用户将手指短暂轻触指纹传感器(不需要完整的指纹匹配验证过程),或者让摄像头检测到人脸(不需要进行精确的人脸识别匹配)。目的是检测到有“人”在操作设备。
    • 目的: 防止远程攻击或无意的操作。确保认证操作不是由恶意软件在后台偷偷发起的,也不是用户不小心碰触触发的。
    • 不验证身份: 它不验证操作者是谁。只要有人(任何能接触到设备的人)按下按钮或触发简单的接触检测,验证就算通过。它不能防止设备被盗用后的操作。
    • 对应 userVerification 设置: 当设置为 discouraged 时,通常只需要这种级别的验证。在 preferredrequired 时,它可能是验证流程的一部分(例如,先按按钮表示同意,然后再输入PIN/生物识别),但单独使用时不够。
  2. 用户验证

    • 概念: 这是更高级别的验证。它要求证明操作者不仅是“在场”的真人,而且是该认证器的合法拥有者本人。
    • 实现方式: 这是通过要求用户提供只有本人知道的秘密或本人特有的生物特征来实现:
      • PIN 码: 用户在认证器上输入一个预先设置好的、只有本人知道的数字密码。
      • 生物识别:
      • 密码: (较少见) 这“密码”不是指网站登录密码,而是指在平台认证器上解锁本地用户账户的凭证。例如:
        • 在 Windows 电脑上使用 Windows Hello,选择“使用我的 Windows 登录密码”作为验证方式。这实际上是用你的 Windows 账户密码来解锁本地存储的 FIDO2 凭据。
        • 在 Mac 上使用 Touch ID 或 Apple Watch 时,背后可能需要输入 macOS 用户账户密码来授权某些操作。
    • 目的: 提供强身份验证,确保操作者是设备的合法所有者/用户本人。防止设备丢失、被盗或被他人短暂借用时进行未经授权的操作。

理解 userVerification 和这些验证方式的区别,对于设计和实现安全且用户体验良好的无密码认证流程至关重要。服务端需要根据应用的风险等级来选择合适的验证级别。

简言之,用户在场验证 “有真人同意,不验证是谁,用户验证 通过 输入PIN、完整生物识别匹配、或输入本地账户密码 确认 “是本人同意”

设置值 描述 验证机制 安全级别 用户体验 失败处理 适用场景
required 必须进行用户验证 生物识别、PIN、密码 中等 认证失败 金融交易、敏感操作
preferred 优先进行用户验证 任何可用方法 中等到高 降级到 discouraged 一般Web应用、平衡安全性
discouraged 不建议用户验证 仅用户在场验证 中等 最高 仅物理交互 快速认证、低风险操作

residentKey 常驻密钥策

residentKey (常驻密钥 / Discoverable Credential)是一个在注册 (navigator.credentials.create()) 过程中,依赖方(RP)设置的参数。它决定了 RP 是否要求(或偏好)认证器将生成的公钥凭证存储(常驻)在认证器本地,以及存储的偏好程度。

  • 关键概念 - 常驻凭证 (Resident Credential) / 可发现凭证 (Discoverable Credential):
    • 定义: 当 residentKey 要求存储时,认证器会在其有限的本地存储空间中保存一个包含以下关键信息的凭证:
      • 用户对应的私钥 (始终安全存储)
      • 关联的依赖方 ID (RP ID) (例如 google.com)
      • 用户句柄 (User Handle) (RP 提供的一个代表该用户账户的不透明唯一标识符)
      • (可选) 用户显示名 (User Display Name)
  • 可选值及其含义:
    • "required" (必须常驻):
      • RP 严格要求认证器必须将生成的凭证存储为常驻凭证(可发现凭证)。
      • 意义: RP 旨在提供最佳的无密码无用户名体验。用户登录时完全不需要输入用户名或密码。
      • 要求: 认证器必须支持存储常驻凭证,且必须有足够的存储空间。如果认证器不支持或空间不足,注册会失败。
    • "preferred" (优先常驻):
      • RP 希望认证器存储常驻凭证,但这不是强制要求。
        • 如果认证器支持且有空闲空间,它会存储为常驻凭证。
        • 如果认证器不支持或空间不足,它会回退到存储为服务端凭证 (Server-side Credential)。
    • "discouraged" (不鼓励常驻):
      • RP 明确要求认证器不要将凭证存储为常驻凭证(可发现凭证)。
      • 意义: RP 选择使用服务端凭证模式。
      • 行为: 认证器总是生成服务端凭证。只存储私钥和 RP ID。不存储 User Handle 或用户显示名。
  • 服务端凭证 (Server-side Credential) / 非可发现凭证 (Non-Discoverable Credential):
    • 认证器只存储私钥和 RP ID。不存储 User Handle 或用户显示名。
    • 在登录时,用户必须先输入用户名。

物理安全密钥的存储空间通常非常有限(很多设备只能存储 25-100 个常驻凭证),需要权衡用户体验和用户可能拥有的密钥数量。

设置值 存储方式 认证流程 功能限制 用户体验 认证器要求
required 必须在认证器中存储用户信息 无用户名认证流程 受认证器存储容量限制 最佳用户体验,真正无密码 必须支持常驻密钥功能
preferred 优先创建常驻密钥,但可降级 支持有/无用户名两种流程 功能可能因设备而异 平衡功能和兼容性 兼容更多认证器类型
discouraged 不存储用户信息在认证器中 传统用户名+认证器验证 需要用户输入用户名 节省认证器存储空间 所有认证器都支持

authenticatorAttachment 和 residentKey 共同决定了认证器(安全设备)的类型以及如何存储和管理用户的凭证(密钥对),直接影响用户体验和安全性设计。

算法选择和安全参数

支持的公钥算法:

1
2
3
4
5
6
7
8
9
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256 (ECDSA P-256 SHA-256)
{ alg: -257, type: "public-key" }, // RS256 (RSASSA-PKCS1-v1_5 SHA-256)
{ alg: -8, type: "public-key" }, // EdDSA (Ed25519)
{ alg: -35, type: "public-key" }, // ES384 (ECDSA P-384 SHA-384)
{ alg: -36, type: "public-key" }, // ES512 (ECDSA P-521 SHA-512)
{ alg: -258, type: "public-key" }, // RS384 (RSASSA-PKCS1-v1_5 SHA-384)
{ alg: -259, type: "public-key" } // RS512 (RSASSA-PKCS1-v1_5 SHA-512)
]

决策矩阵:

算法 密钥长度 签名长度 性能 安全性 兼容性
ES256 (-7) 32字节 64字节 优秀 最佳
EdDSA (-8) 32字节 64字节 优秀 最高 良好
ES384 (-35) 48字节 96字节 良好 很高 良好
RS256 (-257) 256字节 256字节 一般 最佳

选择策略:

安全需求 首选算法 备选算法 适用场景
高安全 EdDSA ES384 政府、金融机构
平衡模式 ES256 EdDSA 一般企业应用
兼容优先 ES256 RS256 传统环境

证明和隐私

attestation(证明)和attestationLevels(证明级别)是 FIDO2 中一个更偏技术实现和安全审计的功能 ,普通用户通常感知不到,但对于服务提供方(RP,即网站/应用)和安全审计人员非常重要。

证明 (Attestation) 是在用户注册一个新的认证器(安全密钥、手机等)时,向依赖方(RP)提供强有力的证据,证明这个新注册的认证器是真实的、未被篡改的,并且是由可信赖的制造商生产的符合 FIDO 标准的硬件或软件。

  • 解决的问题: 防止攻击者使用伪造的、不安全的或恶意修改的认证器进行注册,从而试图绕过安全机制或植入后门。
  • 工作原理: 认证器在注册过程中会生成一个特殊的签名数据包,称为 Attestation Statement(证明声明)。这个数据包包含:
    1. 认证器的唯一标识信息。
    2. 一个由认证器制造商或认证器内部的可信根密钥对该次注册操作(包含新生成的公钥)进行的数字签名。
    3. (可选)指向制造商根证书的证书链(Attestation Certificate Chain)。
  • RP 的验证: RP 服务器收到这个 Attestation Statement 后,可以:
    1. 验证数字签名的有效性(使用制造商公开的根证书或 FIDO 联盟维护的元数据服务)。
    2. 检查证书链是否有效且未被吊销。
    3. 确认认证器的型号、固件版本等是否符合 RP 的安全策略(例如,是否允许使用某特定型号或固件版本的密钥)。

在 WebAuthn 中,RP 在发起注册请求 (PublicKeyCredentialCreationOptions) 时,可以通过 attestation 参数来指定它希望从认证器那里获得哪种详细程度的证明信息。

  1. "none" (无证明): RP 明确表示不需要任何形式的证明信息。

    • 认证器不会生成 Attestation Statement
    • 注册响应中只包含最基本的公钥凭证信息。
    • 特点: 注册过程最快,数据传输量最小,隐私性最好,但无法验证认证器的真伪和来源,安全性最低(无法防御伪造认证器注册)。
  2. "indirect" (间接证明):RP 希望获得证明信息,但允许认证器或客户端(浏览器/平台)对证明数据进行匿名化处理或使用一个通用的、不标识具体设备的证书(如批次证书)进行签名。

    • 认证器或客户端可能会生成一个 Attestation Statement,这个声明可能经过处理,使得 RP 只能确认认证器是“某一类”符合标准的设备(例如,所有 YubiKey 5 NFC),而不能精确定位到具体某个设备。
    • RP 通常使用 FIDO 联盟的 Metadata Service (MDS) 来验证这类证明的有效性。MDS 存储了已知认证器厂商的根证书和相关信息。
    • 特点: 提供了基本的认证器来源保证,隐私性比 direct 好(设备唯一性信息可能被隐藏),验证过程比 none 复杂,依赖于外部的 MDS;不能精确识别单个设备。
  3. "direct" (直接证明):RP 希望获得最原始、最详细的证明信息,通常包含完整的、能唯一标识特定设备的证书链。

    • 认证器必须生成一个 Attestation Statement,这个声明包含完整的证书链(如果认证器支持),允许 RP 精确定位到注册所使用的具体设备型号和实例。
    • RP 可以使用自己信任的根证书库或 MDS 来严格验证签名和证书链。
    • 特点: 提供最高级别的认证器真实性和来源验证,支持最严格的安全策略,隐私性最低(因为 RP 可以获取设备的唯一标识信息),注册过程最慢,数据传输量最大,验证逻辑最复杂。
    • 并非所有认证器都支持生成完整的证书链(尤其是平台认证器和较旧的密钥)。
  4. "enterprise" (企业证明):这是一个非官方但被广泛理解和支持(尤其是在企业级安全密钥中)的扩展级别。它本质上是一种特殊形式的 direct 证明,允许企业大规模部署和管理安全密钥时,将企业特定的标识信息嵌入到证明数据中。

    • 安全密钥制造商(如 Yubico)提供工具,允许企业客户在密钥出厂后或部署前,向密钥批量注入一个企业特定的证书/标识符。
    • 在注册时,密钥生成的 Attestation Statement 会包含这个企业标识信息。
    • 特点: 在 direct 的基础上,增加了对企业资产管理的支持,防止员工使用个人密钥注册企业高权限系统,但需要特定的支持企业证明的安全密钥型号(如 YubiKey Enterprise Series)且需要企业建立额外的密钥预配置和管理流程,也无法避免 direct 的隐私和复杂性缺点。

总结:

证明级别 (attestation) 核心目的 隐私性 安全性 复杂性 典型应用场景
"none" 最大隐私,不要证明。 RP 不关心认证器来源。 最高 最低 最低 大众化服务、新闻、博客、测试环境。
"indirect" 平衡。 需要证明认证器是某类合规设备,但不需唯一标识。隐私较好。 较高 中等 中等 需要基本保证但对隐私有要求的服务,常见默认选择。
"direct" 高安全审计。 需要精确证明认证器来源和型号,支持设备名单管控。隐私较低。 较低 最高 最高 银行、金融交易、政府系统、企业特权账户、强制设备策略场景。
"enterprise" 企业资产管理。 在 direct 基础上增加企业标识,验证是企业发放的密钥。 最低 最高 最高 大型企业员工统一密钥管理、需要验证密钥是企业资产而非个人资产的内部/合作系统。

关键点:

  1. 默认行为: 如果 RP 不指定 attestation 参数,WebAuthn 规范定义其行为等同于 "none"
  2. 用户同意: 即使 RP 要求 directenterprise 证明,浏览器/平台可能会在发送详细的证明数据给 RP 前,弹窗征求用户同意,告知用户设备标识信息将被共享。这是隐私保护的重要环节。
  3. 元数据服务 (MDS): FIDO 联盟维护一个公开的 FIDO Metadata Service。RP 可以利用这个服务来验证 indirectdirect attestation 中证书链的有效性,并获取认证器的详细信息(如厂商、型号、安全认证状态、已知漏洞等)。
  4. 实际选择: 绝大多数面向公众的互联网服务出于隐私和性能考虑,会选择 "none""indirect"。只有对安全性和设备管控有极高要求的特定场景(如企业内网、高价值金融账户)才会采用 "direct""enterprise"

超时

注册超时认证超时是 FIDO2 中两个重要的安全参数,用于防止重放攻击和确保认证过程的及时性。

配置建议

使用场景 注册超时 认证超时
移动设备 120秒 60秒
桌面环境 300秒 180秒
企业环境 600秒 180秒
金融交易 120秒 30秒

扩展功能配置

主要扩展功能

扩展名称 功能用途 应用场景
txAuthSimple 交易授权确认 金融支付
uvm 验证方法查询 合规审计
credProps 凭据属性查询 功能检测
hmacCreateSecret 密钥派生 端到端加密

1. txAuthSimple (简单交易授权扩展)

  • 在用户进行敏感操作(如确认支付、签署合同、授权交易)时,明确告知用户当前操作的具体内容,并要求用户在认证器上进行确认。防止恶意软件篡改用户意图(例如,修改支付金额或收款账户)。
  • 工作原理:
    1. RP 设置: 依赖方(RP)在发起认证请求时 (navigator.credentials.get()),通过 extensions 参数包含 txAuthSimple 扩展,并提供一个简短的、纯文本的 transactionPrompt(交易提示信息)。例如:"Pay $100 to ExampleCorp"
    2. 浏览器/平台传递: 浏览器或平台将这个提示信息传递给认证器。
    3. 认证器显示: 支持此功能的认证器(通常是带屏幕的安全密钥或手机认证器)在其自身安全的显示区域上显示这个提示信息。
    4. 用户确认: 用户必须在认证器上阅读显示的信息,并执行验证操作(按按钮、输PIN、刷指纹)。这个验证动作不仅证明用户在场,也证明用户确认了屏幕上显示的特定交易内容。
    5. 结果返回: 认证器生成的签名数据中会隐式包含用户确认了该交易提示的事实。
  • 关键点:
    • 防篡改: 提示信息由 RP 生成,在认证器安全显示。恶意软件无法篡改认证器屏幕上显示的内容。
    • 用户感知: 用户必须阅读并确认屏幕上显示的具体内容。
    • 纯文本限制: transactionPrompt 必须是纯文本,且长度非常有限(通常不超过 128 字节)。无法显示富文本或复杂图形。
    • 认证器支持: 需要认证器有屏幕和相应的固件支持。常见的带屏安全密钥(如 YubiKey Bio Series, YubiKey 5 FIPS Series, Feitian BioPass K27/K26)支持此功能。手机认证器理论上也可支持。
    • 适用场景: 高价值金融交易确认、关键操作授权(如修改账户设置、提现)、法律电子签名确认。

2. uvm (用户验证方法扩展)

  • 在认证响应中,向 RP 报告用户在执行本次认证时实际使用的验证方法(如指纹、PIN、密码、人脸等)以及该方法的强度信息。用于 RP 进行更细粒度的风险评估、审计和策略执行。
  • 认证响应包含一个 uvm (User Verification Method) 数据结构。这个结构是一个数组,每个元素包含:
    • userVerificationMethod (UVM 类型): 一个数字标识,代表使用的验证方法。
    • keyProtectionType (可选): 表示密钥如何被保护(如硬件、软件、TEE)。
    • matcherProtectionType (可选): 表示验证器(如指纹传感器)如何被保护。
  • 认证器支持: 主流认证器(平台如 Windows Hello/Apple Touch ID/Face ID/Android Biometrics,安全密钥如 YubiKey)通常支持报告基本UVM信息(指纹=1, PIN=2)。

3. credProps (凭证属性扩展)

  • 在注册响应中,向 RP 明确指示本次注册生成的凭证是否是“常驻凭证”。解决 RP 在注册时设置了 residentKey: "preferred" 后,无法从标准响应中直接得知认证器最终是否成功存储了常驻凭证的问题。
  • 解决 preferred 的模糊性: 这是 credProps 最主要的价值。当 RP 设置 residentKey: "preferred" 时,认证器可能存成 RK 也可能存成 Server-side。RP 需要知道确切结果来调整后续的登录流程(是否要求输入用户名)。

4. hmacCreateSecret (HMAC 密钥派生扩展)

  • 允许 RP 和认证器协同派生一个唯一的、受认证器保护的共享密钥 (Secret)。这个密钥不用于 FIDO2 登录认证本身,而是作为应用层密钥,用于后续的安全数据加解密、消息认证码 (HMAC) 生成或其他加密操作。
  • 应用层密钥: 派生的密钥不用于 FIDO2 登录的签名,专供应用业务逻辑使用。
  • 密钥安全: 子密钥的明文只在认证器内部和 RP 的安全存储中存在。在通信过程中,只传递盐值、挑战值和 HMAC 结果。
  • 认证器支持: 需要认证器硬件支持安全密钥存储和 HMAC 计算能力。支持 FIDO2 的现代安全密钥(如 YubiKey 5 系列)通常支持。

FIDO2设备的特点和管理方式

设备寿命问题

硬件寿命方面:

  • 主流FIDO2设备(如YubiKey)通常设计寿命5-10年
  • USB接口是最容易损坏的部分,NFC版本相对更耐用
  • 内部安全芯片和存储器稳定性很高,正常使用下很少出现故障

技术演进影响:

  • FIDO2标准相对稳定,向后兼容性良好
  • 新算法支持可能需要固件更新,但老设备通常能继续工作

丢失风险管理

风险评估:

  • 比传统密码丢失影响更严重,因为无法”记起来”
  • 但比手机丢失影响小,因为FIDO2设备通常只用于认证

缓解策略:

  • 多设备备份:每个账户注册2-3个FIDO2设备
  • 恢复代码:保存服务提供的一次性恢复代码
  • 混合认证:保留其他认证方式作为备选
  • 企业管理:IT部门统一管理和替换

被盗用可能性

技术防护:

  • 用户验证要求:设置PIN或生物识别,即使设备被盗也需要额外验证
  • 设备绑定:私钥无法导出,克隆设备在技术上极其困难

实际风险对比:

  • 被盗FIDO2设备的利用难度远高于密码泄露
  • 社会工程学攻击更可能针对恢复流程而非设备本身
  • 企业环境中可以通过设备管理系统远程禁用丢失设备

企业级管理

  • 设备生命周期管理
  • 集中配置
  • 使用分析和报告
  • 替换密钥管理
  • 安全审计和合规性

个人使用管理策略

  • 每个账户注册多个密钥(主要+备份)
  • 安全存储备份密钥,恢复代码

FIDO2验证的完整工作流程

WebAuthn API核心机制

WebAuthn基于三个核心实体:依赖方(RP)用户代理(浏览器)认证器。理解它们之间的交互是掌握FIDO2的关键。

核心数据结构

  • PublicKeyCredential:WebAuthn操作的主要返回类型
  • AuthenticatorData:认证器生成的数据结构,包含RP ID哈希、标志位、计数器等
  • ClientDataJSON:浏览器生成的客户端数据,包含challenge、origin、type等

注册过程分析

阶段1:挑战生成与选项配置

服务器生成注册选项时,每个参数都有其特定的安全作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 关键参数深度分析
const publicKeyCredentialCreationOptions = {
// 防重放攻击的随机挑战
challenge: crypto.getRandomValues(new Uint8Array(32)), // 32字节随机数

// 依赖方信息,决定凭据的绑定域
rp: {
id: "example.com", // 必须匹配或为current origin的后缀
name: "Example Corp" // 用户界面显示名称
},

// 用户标识符,在认证器中唯一标识用户
user: {
id: new Uint8Array(64), // 最大64字节,应该是随机且唯一的
name: "user@example.com", // 用户可读标识符
displayName: "John Doe" // 用户友好显示名称
},

// 支持的公钥算法,按优先级排序
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256:ECDSA P-256 + SHA-256
{ alg: -257, type: "public-key" }, // RS256:RSASSA-PKCS1-v1_5 + SHA-256
{ alg: -8, type: "public-key" } // EdDSA:Ed25519签名算法
],

// 排除已有凭据,防止重复注册
excludeCredentials: existingCredentials.map(cred => ({
id: cred.credentialId,
type: "public-key",
transports: ["usb", "nfc", "ble", "internal"]
})),

// 认证器选择标准
authenticatorSelection: {
authenticatorAttachment: "cross-platform", // "platform" | "cross-platform" | undefined
userVerification: "required", // "required" | "preferred" | "discouraged"
requireResidentKey: true, // 是否需要常驻密钥
residentKey: "required" // "required" | "preferred" | "discouraged"
},

// 证明偏好,影响隐私和验证强度
attestation: "direct", // "none" | "indirect" | "direct" | "enterprise"

// 超时设置(毫秒)
timeout: 300000,

// 扩展功能
extensions: {
credProps: true, // 获取凭据属性信息
hmacCreateSecret: true // 支持HMAC密钥派生
}
};

参数选择的安全考量

  1. Challenge设计

    • 必须使用密码学安全的随机数生成器
    • 长度建议32字节以上
    • 服务器需要验证challenge的时效性(通常5分钟内)
  2. User ID设计

    • 不应包含PII(个人可识别信息)
    • 应该在RP内全局唯一
    • 推荐使用UUID或加密哈希值
  3. 算法优先级

    • ES256(-7):椭圆曲线算法,计算效率高,广泛支持
    • RS256(-257):RSA算法,兼容性好但计算量大
    • EdDSA(-8):现代椭圆曲线,安全性高但支持有限

阶段2:认证器交互与密钥生成

当浏览器调用navigator.credentials.create()时,触发复杂的认证器交互:

1
2
3
4
// 浏览器端调用
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});

认证器内部处理流程

  1. 用户验证

    • 平台认证器:Touch ID、Face ID、Windows Hello PIN
    • 漫游认证器:物理按键、PIN输入、生物识别
  2. 密钥对生成

    1
    2
    Private Key (32 bytes) → 存储在安全元件/TEE中
    Public Key (33/65 bytes) → 包含在注册响应中
  3. 凭据ID生成

    • 唯一标识这个密钥对
    • 通常包含加密的密钥材料或密钥索引
    • 长度可变,但建议不超过1023字节
  4. 证明生成(如果需要):

    • 证明密钥对是由特定认证器生成的
    • 包含认证器证书链
    • 用于企业环境的设备验证

阶段3:服务器端验证流程(21步详细验证)

服务器收到注册响应后,必须执行严格的验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 验证步骤详解
func VerifyRegistrationResponse(response *webauthn.CredentialCreationResponse,
sessionData *webauthn.SessionData) error {

// 步骤1-3:基础验证
if !bytes.Equal(response.Response.ClientDataJSON.Challenge, sessionData.Challenge) {
return errors.New("challenge mismatch")
}

if response.Response.ClientDataJSON.Origin != sessionData.RelyingParty.RPOrigins[0] {
return errors.New("origin mismatch")
}

if response.Response.ClientDataJSON.Type != "webauthn.create" {
return errors.New("ceremony type mismatch")
}

// 步骤4-8:AuthenticatorData解析与验证
authData, err := ParseAuthenticatorData(response.Response.AttestationObject.AuthData)
if err != nil {
return err
}

// 验证RP ID哈希
expectedRPIDHash := sha256.Sum256([]byte(sessionData.RelyingParty.RPID))
if !bytes.Equal(authData.RPIDHash, expectedRPIDHash[:]) {
return errors.New("RP ID hash mismatch")
}

// 验证用户在场标志
if !authData.Flags.UserPresent() {
return errors.New("user not present")
}

// 如果需要用户验证,检查UV标志
if sessionData.UserVerification == "required" && !authData.Flags.UserVerified() {
return errors.New("user not verified")
}

// 步骤9-14:凭据数据验证
if !authData.Flags.AttestedCredentialDataIncluded() {
return errors.New("attested credential data not included")
}

credData := authData.AttestedCredentialData

// 验证算法标识符
if !isAlgorithmSupported(credData.CredentialPublicKey.Algorithm) {
return errors.New("unsupported algorithm")
}

// 验证公钥格式
if err := validatePublicKey(credData.CredentialPublicKey); err != nil {
return err
}

// 步骤15-18:证明验证
if err := verifyAttestation(response.Response.AttestationObject,
authData,
response.Response.ClientDataJSON); err != nil {
return err
}

// 步骤19-21:业务逻辑验证
if credentialExists(credData.CredentialID) {
return errors.New("credential already exists")
}

// 存储凭据
return storeCredential(userID, credData)
}

认证过程分析

阶段1:认证选项生成

认证过程比注册更加复杂,因为需要处理多种认证模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const publicKeyCredentialRequestOptions = {
// 新的随机挑战
challenge: crypto.getRandomValues(new Uint8Array(32)),

// 允许的凭据列表(可选)
allowCredentials: [{
id: storedCredentialId, // 特定凭据ID
type: "public-key",
transports: ["usb", "nfc", "ble", "internal"] // 传输方式提示
}],

// 用户验证要求
userVerification: "preferred", // "required" | "preferred" | "discouraged"

// 超时设置
timeout: 180000,

// RP ID(必须与注册时一致)
rpId: "example.com",

// 扩展功能
extensions: {
txAuthSimple: "Transfer $100 to Alice", // 交易授权
uvm: true, // 用户验证方法
hmacGetSecret: { // HMAC密钥获取
salt1: new Uint8Array(32),
salt2: new Uint8Array(32)
}
}
};

allowCredentials的智能选择策略

  1. 无密码认证(allowCredentials为空):

    • 依赖常驻密钥功能
    • 认证器展示所有可用账户
    • 用户选择要认证的账户
  2. 特定凭据认证(指定allowCredentials):

    • 更快的认证体验
    • 适用于已知用户身份的场景
    • 减少用户选择负担

阶段2:认证器断言生成

1
2
3
const assertion = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});

认证器内部断言生成过程

  1. 凭据查找

    • 如果指定了allowCredentials,查找匹配的凭据
    • 如果未指定,展示所有RP的常驻密钥
  2. 用户验证

    • 根据userVerification参数决定验证强度
    • 可能包括生物识别、PIN或仅用户在场确认
  3. 签名计算

    1
    2
    signatureBase = authenticatorData || sha256(clientDataJSON)
    signature = sign(privateKey, signatureBase)
  4. 计数器递增

    • 每次认证后递增签名计数器
    • 用于检测凭据克隆攻击

阶段3:断言验证的安全机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func VerifyAuthenticationResponse(assertion *webauthn.CredentialAssertionResponse,
credential *webauthn.Credential,
sessionData *webauthn.SessionData) error {

// 验证客户端数据
if assertion.Response.ClientDataJSON.Type != "webauthn.get" {
return errors.New("invalid ceremony type")
}

// 解析认证器数据
authData, err := ParseAuthenticatorData(assertion.Response.AuthenticatorData)
if err != nil {
return err
}

// 验证签名计数器(防克隆攻击)
if authData.SignCount <= credential.Authenticator.SignCount {
// 可能的凭据克隆攻击
log.Warn("Potential credential cloning detected",
"user", sessionData.UserID,
"current_count", authData.SignCount,
"stored_count", credential.Authenticator.SignCount)

// 企业策略:可能选择拒绝或要求额外验证
if strictSecurityMode {
return errors.New("signature counter regression detected")
}
}

// 构造签名验证数据
clientDataHash := sha256.Sum256(assertion.Response.ClientDataJSON.Raw)
signatureBase := append(assertion.Response.AuthenticatorData, clientDataHash[:]...)

// 验证签名
if !verifySignature(credential.PublicKey, signatureBase, assertion.Response.Signature) {
return errors.New("signature verification failed")
}

// 更新签名计数器
credential.Authenticator.SignCount = authData.SignCount

return nil
}

其他安全特性

1. 证明(Attestation)机制

证明允许RP验证认证器的真实性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 证明类型及其安全含义
type AttestationType int

const (
// Basic: 自签名证明,提供最低验证级别
AttestationBasic AttestationType = iota

// Self: 使用认证器自有密钥签名
AttestationSelf

// AttCA: 通过认证机构证明
AttestationAttCA

// AnonCA: 匿名证明,平衡隐私和验证
AttestationAnonCA

// None: 无证明信息
AttestationNone
)

企业环境证明验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func VerifyEnterpriseAttestation(attestationObject *protocol.AttestationObject) error {
// 获取认证器证书链
certChain := extractCertificateChain(attestationObject)

// 验证证书链到可信根CA
if err := verifyCertificateChain(certChain, trustedRootCAs); err != nil {
return err
}

// 检查认证器是否在企业白名单中
aaguid := attestationObject.AuthData.AttestedCredentialData.AAGUID
if !isApprovedAuthenticator(aaguid) {
return errors.New("authenticator not approved for enterprise use")
}

return nil
}

2. 扩展(Extensions)功能

扩展为FIDO2提供了额外的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 高级扩展使用示例
const extensions = {
// 交易确认扩展
txAuthSimple: JSON.stringify({
prompt: "确认转账",
transaction: {
amount: "1000.00",
currency: "CNY",
recipient: "张三",
account: "6222***1234"
}
}),

// 用户验证方法查询
uvm: true,

// HMAC密钥派生
hmacCreateSecret: true,

// 凭据属性查询
credProps: true,

// 大blob存储
largeBlob: {
support: "required"
}
};

3. 常驻密钥(Resident Keys)的实现细节

常驻密钥是实现真正无用户名认证的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 常驻密钥的存储策略
type ResidentKeyStrategy int

const (
// 在认证器中存储用户信息和私钥
ResidentKeyFull ResidentKeyStrategy = iota

// 仅存储私钥,用户信息由RP提供
ResidentKeyMinimal

// 混合策略:根据存储空间动态选择
ResidentKeyHybrid
)

func HandleResidentKeyCreation(req *CredentialCreationRequest) error {
if req.AuthenticatorSelection.ResidentKey == "required" {
// 检查认证器存储容量
if !authenticator.HasAvailableStorage() {
return errors.New("insufficient storage for resident key")
}

// 创建常驻密钥
return authenticator.CreateResidentKey(req.User, req.CredentialParams)
}

// 创建非常驻密钥
return authenticator.CreateNonResidentKey(req.CredentialParams)
}

其他设备认证方式

QR码认证

QR码认证通过跨设备认证(CDA)实现,允许用户使用存储在另一设备上的凭据在一个设备上进行认证。

技术实现

  • 混合传输:在CTAP 2.2规范中定义
  • 双设备模型:CDA客户端(需要认证的设备)和CDA认证器(存储密钥的设备)

工作流程

  1. 客户端设备生成包含会话标识符、加密密钥交换信息和中继服务器路由数据的QR码
  2. 用户使用认证器设备扫描QR码
  3. 使用蓝牙低功耗(BLE)进行邻近验证
  4. 通过中继服务器建立加密网络连接
  5. 通过安全隧道进行标准FIDO CTAP操作

蓝牙认证(caBLE协议)

caBLE是一种通过蓝牙实现FIDO2认证的协议,具有增强的可靠性和安全性。

技术规格

  • 传输类型:CTAP 2.2中的”hybrid”传输
  • 通信:结合BLE广播与基于网络的隧道
  • 密钥交换:用于安全通信的加密密钥协议

支持的设备和平台

  • iOS:iOS 16+(开发者设置),iOS 17+(普遍可用)
  • Android:Android 9+配合Google Play服务24+
  • Windows:Windows 10 1903+用于安全密钥,Windows 11 22H2+用于密钥

非浏览器环境使用FIDO2进行认证

移动应用集成

iOS实现

  • iOS 17.1+:带企业SSO插件的完整密钥支持
  • iOS 16.0+:原生应用中的基本密钥支持
  • 安全隔区:硬件支持的密钥存储
  • 生物识别集成:Touch ID/Face ID用于用户验证

Android实现

  • Android 13+:FIDO2安全密钥的完整支持
  • Android 14+:Microsoft Authenticator中的密钥支持
  • Google Play服务:FIDO2 API集成(版本24+)
  • 硬件安全:带TEE/安全元件的Android密钥库

SSH认证

OpenSSH集成

1
2
3
4
5
# 生成FIDO2 SSH密钥
ssh-keygen -t ecdsa-sk -f ~/.ssh/id_ecdsa_sk

# 生成常驻密钥(推荐)
ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk -O resident -O verify-required

安全配置

1
2
3
4
# 服务器配置(/etc/ssh/sshd_config)
PubkeyAuthentication yes
PubkeyAuthOptions verify-required
PasswordAuthentication no

操作系统登录

Windows Hello for Business

  • Azure AD企业身份集成
  • 本地AD混合部署场景
  • 基于证书的传统密码替代
  • 组策略和MDM控制

Linux PAM集成

  • pam_fido2:FIDO2认证的自定义PAM模块
  • 桌面登录:图形登录管理器集成
  • 控制台认证:命令行登录支持

基于Go的实现示例

FIDO2后端实现的核心在于正确处理WebAuthn协议的注册和认证流程。以下展示关键实现要点:

用户模型设计

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
ID []byte `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Credentials []webauthn.Credential `json:"credentials"`
}

// 实现WebAuthn用户接口
func (user *User) WebAuthnID() []byte { return user.ID }
func (user *User) WebAuthnName() string { return user.Name }
func (user *User) WebAuthnDisplayName() string { return user.DisplayName }
func (user *User) WebAuthnCredentials() []webauthn.Credential { return user.Credentials }

WebAuthn配置

1
2
3
4
5
6
7
8
9
10
11
12
13
func initWebAuthn() *webauthn.WebAuthn {
wconfig := &webauthn.Config{
RPDisplayName: "FIDO2 Demo Application",
RPID: "localhost",
RPOrigins: []string{"https://localhost:8080"},
AuthenticatorSelection: protocol.AuthenticatorSelection{
UserVerification: protocol.UserVerificationPreferred,
ResidentKey: protocol.ResidentKeyRequirementPreferred,
},
Timeout: 300000, // 5分钟
}
return webauthn.New(wconfig)
}

注册流程核心逻辑

注册过程分为两个阶段:开始注册和完成注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 开始注册:生成挑战和选项
func BeginRegistration(w http.ResponseWriter, r *http.Request) {
username := mux.Vars(r)["username"]
displayName := mux.Vars(r)["displayName"]

user := getOrCreateUser(username, displayName)
options, sessionData, err := webAuthn.BeginRegistration(user)
if err != nil {
http.Error(w, "Registration failed", http.StatusInternalServerError)
return
}

// 存储会话数据用于验证
storeSession(r, "registration", sessionData)
json.NewEncoder(w).Encode(options)
}

// 完成注册:验证响应并存储凭据
func FinishRegistration(w http.ResponseWriter, r *http.Request) {
username := mux.Vars(r)["username"]
user := getUser(username)
sessionData := getSession(r, "registration")

credential, err := webAuthn.FinishRegistration(user, sessionData, r)
if err != nil {
http.Error(w, "Registration verification failed", http.StatusBadRequest)
return
}

// 存储凭据到数据库
saveCredential(user.ID, *credential)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}

认证流程核心逻辑

认证过程同样分为两个阶段,但重点在于验证现有凭据的签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 开始认证:生成挑战
func BeginLogin(w http.ResponseWriter, r *http.Request) {
username := mux.Vars(r)["username"]
user := getUser(username)

options, sessionData, err := webAuthn.BeginLogin(user)
if err != nil {
http.Error(w, "Login failed", http.StatusInternalServerError)
return
}

storeSession(r, "authentication", sessionData)
json.NewEncoder(w).Encode(options)
}

// 完成认证:验证断言签名
func FinishLogin(w http.ResponseWriter, r *http.Request) {
username := mux.Vars(r)["username"]
user := getUser(username)
sessionData := getSession(r, "authentication")

credential, err := webAuthn.FinishLogin(user, sessionData, r)
if err != nil {
http.Error(w, "Authentication failed", http.StatusBadRequest)
return
}

// 更新凭据使用计数器
updateCredential(user.ID, *credential)
json.NewEncoder(w).Encode(map[string]string{"status": "authenticated"})
}

安全注意事项

实现FIDO2后端时,需要特别关注以下安全要点:

  1. HTTPS必需:WebAuthn只能在安全上下文中工作
  2. 会话管理:正确存储和验证会话数据,防止重放攻击
  3. 挑战验证:确保每次挑战都是唯一的且有时效性
  4. 来源验证:严格验证请求来源与配置的RP Origins匹配
  5. 计数器检查:监控签名计数器,检测可能的凭据克隆

数据库集成要点

生产环境中应使用可靠的数据库存储用户和凭据信息,要确保凭据ID的唯一性,并正确更新签名计数器以防止重放攻击。

JavaScript前端实现示例

前端实现的关键在于正确处理WebAuthn API调用和数据格式转换。浏览器原生支持WebAuthn,但需要处理Base64URL编码转换和错误处理。

核心工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class WebAuthnUtils {
// Base64URL与ArrayBuffer相互转换
static base64urlToBuffer(base64url) {
const padding = '='.repeat((4 - base64url.length % 4) % 4);
const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/');
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

static bufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binaryString = '';
for (let i = 0; i < bytes.byteLength; i++) {
binaryString += String.fromCharCode(bytes[i]);
}
const base64 = window.btoa(binaryString);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

static isWebAuthnSupported() {
return !!(navigator.credentials && navigator.credentials.create);
}
}

主要WebAuthn客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class WebAuthnClient {
constructor(options = {}) {
this.apiBase = options.apiBase || '/api/v1';
this.timeout = options.timeout || 300000;
this.checkSupport();
}

// 注册新凭据的完整流程
async register(username, displayName) {
try {
// 1. 获取服务器生成的注册选项
const response = await fetch(`${this.apiBase}/register/begin/${username}/${displayName}`, {
method: 'POST',
credentials: 'same-origin'
});
const options = await response.json();

// 2. 转换数据格式供WebAuthn使用
const createOptions = this.transformCredentialCreateOptions(options.publicKey);

// 3. 调用浏览器WebAuthn API创建凭据
const credential = await navigator.credentials.create({
publicKey: createOptions
});

// 4. 转换响应数据并发送到服务器完成注册
const credentialResponse = this.transformCredentialResponse(credential);
await fetch(`${this.apiBase}/register/finish/${username}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialResponse),
credentials: 'same-origin'
});

return { success: true, message: '注册成功!' };
} catch (error) {
throw new Error(`注册失败:${this.getErrorMessage(error)}`);
}
}

// 认证流程
async authenticate(username) {
try {
// 1. 获取认证挑战
const response = await fetch(`${this.apiBase}/login/begin/${username}`, {
method: 'POST',
credentials: 'same-origin'
});
const options = await response.json();

// 2. 转换格式并调用WebAuthn API
const requestOptions = this.transformCredentialRequestOptions(options.publicKey);
const assertion = await navigator.credentials.get({
publicKey: requestOptions
});

// 3. 完成认证
const assertionResponse = this.transformCredentialResponse(assertion);
await fetch(`${this.apiBase}/login/finish/${username}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assertionResponse),
credentials: 'same-origin'
});

return { success: true, message: '认证成功!' };
} catch (error) {
throw new Error(`认证失败:${this.getErrorMessage(error)}`);
}
}
}

数据格式转换

WebAuthn API使用ArrayBuffer,而服务器通信使用Base64URL编码,需要正确转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
transformCredentialCreateOptions(options) {
return {
...options,
challenge: WebAuthnUtils.base64urlToBuffer(options.challenge),
user: {
...options.user,
id: WebAuthnUtils.base64urlToBuffer(options.user.id)
},
excludeCredentials: options.excludeCredentials?.map(cred => ({
...cred,
id: WebAuthnUtils.base64urlToBuffer(cred.id)
}))
};
}

transformCredentialResponse(credential) {
const response = {
id: credential.id,
rawId: WebAuthnUtils.bufferToBase64url(credential.rawId),
type: credential.type,
response: {
clientDataJSON: WebAuthnUtils.bufferToBase64url(credential.response.clientDataJSON)
}
};

// 注册响应包含attestationObject
if (credential.response.attestationObject) {
response.response.attestationObject =
WebAuthnUtils.bufferToBase64url(credential.response.attestationObject);
}

// 认证响应包含authenticatorData和signature
if (credential.response.authenticatorData) {
response.response.authenticatorData =
WebAuthnUtils.bufferToBase64url(credential.response.authenticatorData);
response.response.signature =
WebAuthnUtils.bufferToBase64url(credential.response.signature);

if (credential.response.userHandle) {
response.response.userHandle =
WebAuthnUtils.bufferToBase64url(credential.response.userHandle);
}
}

return response;
}

用户界面集成

实际应用中需要提供友好的用户界面和错误处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class WebAuthnUI {
constructor() {
this.client = new WebAuthnClient({ debug: true });
this.initEventListeners();
}

initEventListeners() {
document.getElementById('register-btn')?.addEventListener('click', (e) => {
this.handleRegister(e);
});

document.getElementById('login-btn')?.addEventListener('click', (e) => {
this.handleLogin(e);
});
}

async handleRegister(event) {
event.preventDefault();

const username = document.getElementById('username').value.trim();
const displayName = document.getElementById('displayName').value.trim();

if (!username || !displayName) {
this.showMessage('请填写用户名和显示名称', 'error');
return;
}

this.showLoading('正在注册FIDO2凭据...');

try {
await this.client.register(username, displayName);
this.showMessage('注册成功!', 'success');
} catch (error) {
this.showMessage(error.message, 'error');
} finally {
this.hideLoading();
}
}

async handleLogin(event) {
event.preventDefault();

const username = document.getElementById('login-username').value.trim();
if (!username) {
this.showMessage('请填写用户名', 'error');
return;
}

this.showLoading('正在进行FIDO2认证...');

try {
await this.client.authenticate(username);
this.showMessage('认证成功!', 'success');
} catch (error) {
this.showMessage(error.message, 'error');
} finally {
this.hideLoading();
}
}
}

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
if (!WebAuthnUtils.isWebAuthnSupported()) {
document.body.innerHTML = '<h1>您的浏览器不支持WebAuthn</h1>';
return;
}
new WebAuthnUI();
});

错误处理最佳实践

WebAuthn可能产生多种错误,需要为用户提供友好的错误提示:

1
2
3
4
5
6
7
8
9
10
11
getErrorMessage(error) {
const errorMessages = {
'NotSupportedError': 'WebAuthn不被当前浏览器支持',
'NotAllowedError': '用户取消了操作或操作超时',
'InvalidStateError': '认证器已被注册或状态无效',
'NetworkError': '网络连接错误,请检查网络连接',
'SecurityError': '安全错误,请确保在HTTPS环境下使用'
};

return errorMessages[error.name] || error.message || '发生未知错误,请重试';
}

前端实现的关键要点是正确处理异步操作、数据转换和用户体验。通过合理的错误处理和加载状态提示,可以为用户提供流畅的无密码认证体验。

总结

FIDO2代表着成熟的、标准化的无密码认证方法,同时加强了安全性。通过WebAuthn和CTAP2的结合,它为在Web和移动应用中实施抗钓鱼、用户友好的认证提供了全面的框架。

随着2024-2025年市场的快速增长,FIDO2已经获得了广泛的行业采用、通用浏览器支持和强大的安全基础,使其成为现代认证策略的重要组成部分。从个人用户到大型企业,FIDO2提供了一条经过验证的通向无密码未来的道路。

实施FIDO2时,重要的是要考虑具体的使用案例、平台要求和安全需求。成熟的库和SDK在多种编程语言和平台上的可用性促进了采用并降低了实施复杂性。随着标准的持续演进和平台支持的增长,FIDO2将继续在塑造数字认证的未来中发挥关键作用。

本文作者:
本文链接:https://tdh6.top/%E6%9D%82%E9%A1%B9/fido2/
版权声明:本站文章采用 CC BY-NC-SA 3.0 CN 协议进行许可,翻译文章遵循原文协议。
图片来源:本站部分图像来源于网络,前往查看 相关说明。