当 Chrome 用户享受着 WebUSB 带来的便利时,Firefox 用户只能望洋兴叹。Mozilla 出于安全考量,一直拒绝在 Firefox 中实现 WebUSB API,认为普通用户无法理解授予 USB 设备访问权限的潜在风险。然而,社区的黑客们从未放弃寻找替代方案。一位名为 ArcaneNibble(又称 R!)的开发者给出了出人意料的答案:既然 Firefox 支持 U2F 安全密钥,那为什么不把设备伪装成 U2F 密钥呢?
为什么 Firefox 拒绝 WebUSB
在深入技术细节之前,有必要理解 Mozilla 做出这一决策的背景。WebUSB API 允许网页直接与连接到用户计算机的 USB 设备交互,典型应用场景包括刷新微控制器固件、配置可编程键盘、连接标签打印机等。Chrome 在 2016 年率先实现了这一 API,但 Mozilla 很快表达了担忧。
Mozilla 的核心顾虑在于:USB 设备种类繁多,从存储设备到人机接口设备(HID)再到专业测量仪器,不一而足。许多设备的设计并未考虑过来自网页的潜在恶意交互。当用户点击授权对话框时,他们很难真正理解这一操作意味着什么 —— 网站可能正在读取设备数据、修改固件,甚至将设备变成了持久化的跟踪标识符。更令人担忧的是,WebUSB 的权限提示与 U2F 身份验证提示外观相似,用户可能在不知不觉中将自己的安全密钥暴露给钓鱼网站。
正是基于这些考量,Mozilla 在其官方标准立场文件中明确表态:暴露 USB 设备的风险过于宽泛,无法向终端用户解释清楚以获得有意义的知情同意。这一立场导致 Firefox 在 WebUSB、WebSerial、WebHID 等硬件相关 API 上全面落后于 Chromium 生态。
U2F 协议与 Firefox 的支持现状
尽管 Mozilla 拒绝实现 WebUSB,但 Firefox 仍然支持另一项与 USB 密切相关的协议:U2F(通用第二因素)。U2F 是 FIDO 联盟推出的双因素认证标准,用于实现无需密码的安全登录。用户只需将支持 U2F 的安全密钥(如 YubiKey)插入计算机,按下按钮确认,即可完成身份验证。
从技术实现角度,U2F 设备通过 USB HID(人机接口设备)协议与浏览器通信。浏览器通过 WebAuthn API 与这些设备交互,发起认证请求并接收响应。关键在于,Firefox 对 U2F 的支持是通过其 Credentials Management API 实现的,这一 API 允许网页请求访问存储在本地设备上的凭证信息。
ArcaneNibble 的创意正是基于这一点:如果我们能让一个微控制器模拟 U2F 安全密钥的行为,那么 Firefox 就会像对待真正的安全密钥一样与其通信。通过在固件层面精心构造符合 U2F 协议格式的数据,开发者可以在看似普通的身份验证流程中嵌入任意业务数据,从而实现浏览器与自定义硬件之间的双向通信。
固件层面的实现细节
要实现这一「伪冒」方案,首先需要一块支持 USB 功能的微控制器。ArcaneNibble 在其项目中选择了 WCH CH32V2xx 系列 RISC-V 微控制器 —— 这是一款来自中国厂商 WCH(南京沁恒)的低功耗芯片,内置 USB 控制器且价格极为低廉(单个芯片零售价往往不足一美元)。
固件实现的核心在于正确响应 U2F 协议中的几个关键命令。U2F 设备通过 HID 报告描述符与主机通信,当收到特定格式的请求时,设备需要返回符合协议规范的响应。最基本的 U2F 命令包括:注册(U2F_REGISTRATION)、认证(U2F_AUTHENTICATION)和版本查询(U2F_VERSION)。
在「I-cant-believe-its-not-webusb」项目中,固件被设计为接收来自浏览器的数据载荷,将其视为某种「应用 ID」,然后在响应中返回预先定义或动态生成的数据。实际上,浏览器发送的请求格式被巧妙地重新解读:原本用于身份验证的应用域名字段被用作数据编码通道,而设备的响应则携带了想要传回浏览器的信息。
具体实现时,固件需要处理 USB 枚举过程,声明自己为 HID 设备,并正确响应 HID 控制传输。CH32V2xx 的 USB 控制器支持全速(12 Mbps)模式,这对于模拟 U2F 设备来说已经绰绰有余。开发者使用了 Rust 语言编写固件,这得益于 Rust 生态中丰富的嵌入式开发资源和对 RISC-V 架构的良好支持。
浏览器端的 JavaScript 调用
在网页侧,开发者使用 Mozilla 的 WebAuthn 扩展 API 与伪冒设备通信。标准的 WebAuthn API 通常用于创建和验证凭据,其核心函数是 navigator.credentials.create() 和 navigator.credentials.get()。这两个函数分别用于注册新凭据和验证已有凭据。
对于「不是 WebUSB 的 WebUSB」使用场景,开发者主要调用 navigator.credentials.get() 函数,该函数会触发浏览器向 U2F 设备请求签名。关键参数是 publicKeyCredentialRequestOptions,其中的 challenge 字段尤其重要 —— 这个字段原本用于防止重放攻击,但在我们的场景中,它可以承载从网页发送到设备的数据。
当浏览器发起请求时,U2F 设备会对挑战数据进行处理并返回签名响应。固件层面的巧妙之处在于:它并不真正执行加密操作,而是将我们需要传递的数据编码到响应中。浏览器收到响应后,解析 authenticatorData 和 signature 字段,从中提取设备返回的数据。
需要注意的是,Firefox 对 WebAuthn 的支持通过 webauthn 实验性功能提供。开发者需要确保在 about:config 中启用相关偏好设置,或者使用标准的 WebAuthn API(这要求网站首先注册一个有效的域名)。在实际部署中,可能需要处理浏览器对设备过滤器的支持限制。
安全边界与风险评估
这一方案的安全性值得深入探讨。与真正的 WebUSB 不同,「伪冒 U2F」的方案有一个重要限制:设备必须是为这一特定目的专门设计的。攻击者无法利用这一机制访问用户已有的 YubiKey 或其他安全密钥 —— 他们只能访问被专门编程为模拟 U2F 设备的恶意硬件。
这从根本上改变了攻击模型。要发起攻击,攻击者需要同时做到两件事:让用户插入一个被预先编程的恶意设备,并诱骗用户访问一个恶意图网站。相比之下,真正的 WebUSB 漏洞利用只需要用户访问一个恶意网站(假设浏览器存在可利用的漏洞)。从这个角度看,「伪冒 U2F」的方案实际上比直接实现 WebUSB 更安全。
然而,这并不意味着方案完全没有风险。用户需要理解他们正在将某物插入计算机,并且该设备可能被用来与网页交互。如果用户被欺骗插入了一个看似无害但实际包含恶意固件的设备,攻击仍然可能发生。另外,由于这种通信方式绕过了浏览器对 WebUSB 的安全防护机制(如设备黑名单),理论上恶意设备可以获得比标准 WebUSB 更多的控制权限。
实用参数与工程考量
如果开发者希望在自己的项目中采用这一方案,以下参数值得注意:
在硬件选型上,CH32V203、CH32V103 等型号都是合适的选择,它们提供 USB 控制器和足够处理常见任务的计算能力。固件开发建议使用 Rust 配合 rust-usb 或类似的 HID 库,以获得更好的内存安全保证。
在浏览器兼容性方面,这一方案在 Firefox 中工作良好,因为它完整实现了 WebAuthn 规范。Chrome 和其他 Chromium 浏览器可能无法识别这类伪冒设备,因为它们的 WebUSB 实现与 U2F 实现是分离的。
在数据吞吐量上,由于 U2F 协议的消息长度有限制(通常单条消息不超过数百字节),这种方案不适合传输大量数据。对于需要更高带宽的应用,可以考虑将数据分块并通过多次请求序列传输。
在调试阶段,建议使用 Wireshark 配合 USB 协议分析插件来观察 HID 流量,这有助于理解浏览器发送的具体请求格式和设备响应的结构。
应用场景与局限
「不是 WebUSB 的 WebUSB」方案最适合以下场景:开发者需要为自定义硬件提供跨浏览器(特别是 Firefox)的配置界面,但不愿依赖 Electron 等重量级框架;硬件设备已经使用了支持 USB 的微控制器,且愿意将部分资源用于模拟 U2F 协议;用户群体对安全有较高要求,无法接受直接暴露的 USB 访问。
该方案的局限性同样明显:它无法访问已有的标准 USB 设备(如串口转换器、 HID 键盘鼠标等);每次通信都涉及完整的 U2F 握手流程,引入了不必要的延迟;固件开发复杂度较高,需要深入理解 USB 协议和 U2F 规范;浏览器的 WebAuthn 实现可能随版本变化,需要持续测试兼容性。
小结
「I-cant-believe-its-not-webusb」项目展示了在浏览器功能受限的情况下,社区如何通过创造性思维找到替代方案。虽然这种「曲线救国」的方式无法完全替代原生 WebUSB 的功能和便利性,但它为那些坚持使用 Firefox 同时需要与自定义硬件通信的开发者提供了一条可行路径。
这一案例也提醒我们:浏览器的安全决策往往是在便利性与风险之间寻找平衡。当一种途径被封锁时,更具创造力的替代方案可能会出现 —— 它们可能带来新的安全考量,也可能只是在当前约束下的务实选择。对于硬件开发者而言,理解这些底层机制有助于构建更健壮的跨平台解决方案。
资料来源