青

一言

使用 ESP32-S3 或 树莓派 RP2040/RP2530 的 FIDO2 安全密钥(PicoKey)

使用 ESP32-S3 或 树莓派 RP2040/RP2530 的 FIDO2 安全密钥(PicoKey)

使用 ESP32-S3 或 树莓派 RP2040/RP2530 的 FIDO2 安全密钥(PicoKey)

FIDO2 是一种基于公钥加密的身份验证标准,旨在提供更安全和便捷的身份验证方式。它由 FIDO 联盟开发,主要包括两个组件:WebAuthn(Web Authentication)和 CTAP(Client to Authenticator Protocol)。FIDO2 允许用户使用硬件安全密钥或生物识别技术(如指纹或面部识别)来替代传统的密码,从而提高账户的安全性。

你可以阅读我先前发布的文章 FIDO2 无密码认证技术 或者使用自己的方式了解更多 FIDO2 的信息。

硬件选择

在 PicoKey 项目中,推荐选用下面几个系列硬件:

  • Raspberry Pi Pico (RP2040)
  • Raspberry Pi Pico2 (RP2350)
  • ESP32-S3

You can acquire multiple boards mounting a RP2040, RP2350 or ESP32-S3 chip from different vendors. Our firmwares are ready to run on any board.

根据作者的说明和实际测试体验,这是在它们当中挑选一个时候需要注意的:

  • Raspberry Pi Pico (RP2040): 相比 RP2350,RP2040 的成本低很多,但是,它缺乏硬件安全功能,因此不适合需要高安全性的应用场景。
  • ESP32-S3: 这款芯片内置了丰富的安全功能(如安全启动、加密存储和硬件加速的加密算法),且 ESP32-S3 的价格也相对较低,是一个性价比很高的选择。不过实际测试中,ESP32-S3 在当前版本下似乎不如树莓派稳定(仅个别设备使用体验,没有经过充分测试,仅供参考)。

这里推荐两个具体开发板型号: ESP32-S3 SuperMini,RP2350Zero,它们都具有较小的体积(大约 23 mm * 18 mm,不同供应商的尺寸可能有细微差距,在选择 3D 打印外壳的时候需要特别注意)和良好的 USB 支持。

分析和建议:

RP2040 存在的最大问题是存储和计算分离,存储内容可被直接读取和复制,攻击者只需要将闪存芯片取下,然后用一个标准的 SPI 编程器完整地读取闪存芯片中的所有内容,这会直接导致密钥被提取。
所以对于安全密钥这种需要保护核心机密的场景,在实际使用过程中,不应该选择 RP2040,它只适合用于学习、原型验证或非安全应用。

RP2350 的核心优势是 Arm TrustZone-M 安全架构,是一个“标准”的选择,比起 ESP32-S3 的 权限控制 & 双核隔离 方案,更加成熟和完善,不过在密钥安全存储,硬件加密加速(性能)方面,ESP32-S3 也有不错的表现,都是可以考虑的选择,在对比这两个选择的时候,更多的考虑不再是“更安全”,而是“更适合”。

PicoKey 版本

PicoKey 中,提供了四种不同的版本:

  • Pico HSM 一个完整的硬件安全模块,可存储多个密钥和证书,支持多种加密算法和协议。
  • Pico FODI 一个兼容 FIDO2 标准的安全密钥。
  • Pico OpenPGP 一个支持 OpenPGP 标准的安全密钥,可用于加密、签名和认证。
  • Pico TRNG 一个硬件随机数生成器(仅此而已)

作者给出了选择建议:

If you need to generate and store dozens of keys, then go for Pico HSM. If you are looking for a personal Passkey (Fido2) for secure logging, then go for Pico Fido. If you need to interface with PGP keys for secure e-mail, then go for Pico OpenPGP.

如果您需要生成和存储数很多密钥,选择 Pico HSM。如果您正在寻找用于安全登录的个人 Passkey 则是 Pico Fido。如果需要与 PGP 密钥进行交互实现电子邮件签名加密等功能,使用 Pico OpenPGP。

此外,在 GitHub 仓库 中,还提供了 Pico FIDO2 ,简单的理解即 Pico Fido + Pico OpenPGP 的组合。

固件烧录

针对树莓派 Pico 设备,前往 开始页面 或对应的 Github 仓库(如果 Pico FIDO2 必须)下载对应的 uf2 文件,以 DFU 模式进行连接(按住 BOOTSEL 按钮插入 USB)将下载的 UF2 文件拖放到设备的存储盘中即可完成。

针对 ESP32-S3 设备,如果需要 Pico FIDO2 固件,需要从前述 Github 仓库中下载预编译好的 bin 文件(不考虑自编译的情况),随后使用 esptool 或者 ESPHome-Flasher 等工具进行烧录。

如果需要 Pico HSM、Pico FIDO 或 Pico OpenPGP 固件,可直接通过 ESP32 Flasher 在线完成烧录。

功能设置(可选)

推荐在固件烧录完成后,对其功能进行设置,最简单的方式是使用 Pico Commissioner 页面直接进行。

设置所需参数,通过 WebUSB 或者 WebAuthn 可以直接进行设置,但需要注意的是,ESP32 的安全启动和安全锁定必须使用 USB 连接进行设置。

在测试过程中,Pico FIDO2 固件在部分设备和浏览器中无法通过 WebUSB 进行连接,建议更换设备尝试、等待更新,或暂时使用 WebAuthn 方式进行初始化。

可能会有疑问,WebAuthn 不是用来进行身份验证的吗?为什么还能传递配置呢?

这里有一些 Gemini 分析的通过 WebAuthn 传递配置的JS函数的分析,如果想要了解每一个选项具体含义,也可以查看:

这段代码的核心功能是通过 WebAuthn (Web Authentication API) 协议来配置(或称为 “Commissioning” / “烧录配置”)一个硬件安全密钥,比如 Nitrokey 或 Yubikey。这是一个非常巧妙的用法,它利用了 WebAuthn 注册流程的一个特性来向设备传递配置数据,而不是创建真正的登录凭证。

我将分步解析:

1. 整体目标:利用 WebAuthn 进行设备配置

通常,WebAuthn 用于网站的无密码登录。用户注册时,网站(Relying Party)向浏览器请求创建一个新的公钥凭证,浏览器与硬件安全密钥(Authenticator)通信,密钥生成一对新的公私钥,并将公钥返回给网站。

这段代码“劫持”了这个流程。它构造了一个特殊的注册请求,硬件密钥(需要有特定的固件支持)在收到这个请求后,不会创建新的登录凭证,而是会解析请求中附带的数据,并用这些数据来配置自身的参数,例如USB设备ID、LED行为等。


2. getPhyData() 函数详解

这是整个逻辑的核心。它的作用是从网页上的一个表单中读取用户选择的配置项,并将它们编码成一个特定格式的二进制字节数组 (Uint8Array)。这个字节数组就是最终要发送给硬件密钥的配置指令。

2.1. 常量定义 (Constants)

代码开头定义了大量的常量,这些都是**协议的“键” (Key) 或“标志位” (Flag)**。这种结构非常类似于 TLV (Type-Length-Value) 编码格式。

  • PHY_... (配置项类型): 这些十六进制数代表了不同配置项的标识符(Type)。

    • PHY_VID: USB 厂商 ID (Vendor ID) 和产品 ID (Product ID)。
    • PHY_LED_GPIO: 控制 LED 的 GPIO 引脚编号。
    • PHY_LED_BTNESS: LED 亮度。
    • PHY_OPTS: 一个包含多个开关选项的“位掩码” (bitmask)。
    • PHY_UP_BUTTON: 用户存在(触摸按钮)的超时时间。
    • PHY_USB_PRODUCT: 自定义的 USB 产品名称字符串。
    • PHY_ENABLED_CURVES: 启用的加密曲线的位掩码。
    • PHY_LED_DRIVER: LED 驱动类型(例如,单个LED或WS2812灯带)。
  • PHY_OPT_... (选项标志位): 这些是用于构建 PHY_OPTS 位掩码的值。例如,PHY_OPT_DIMM = 0x2 意味着在选项字节中,如果第1位(从0开始数)被设置为1,就代表启用了“DIMM”这个选项。

  • PHY_CURVE_... (加密曲线标志位): 同理,这些是用于构建 PHY_ENABLED_CURVES 位掩码的值,用来告诉设备支持哪些椭圆曲线加密算法。

2.2. 数据编码过程

函数按顺序读取表单字段,并按照 [Type, Length, Value] 的格式将数据推入 byteArray

  1. VID 和 PID (厂商/产品ID):

    • 首先尝试从一个下拉菜单 (field_8) 中获取预设的设备。
    • 如果用户没有选择预设设备,则会尝试从一个文本框 (field_1) 中读取用户手动输入的 VID:PID 字符串,并将其解析为十六进制数。
    • 如果成功获取到 vidpid,则将其编码并推入数组:
      • byteArray.push(PHY_VID, 4, ...)
      • Type: PHY_VID (0x0)
      • Length: 4 (因为 VID 和 PID 各占2个字节,总共4字节)
      • Value: (vid >> 8) & 0xFF (VID 的高8位), vid & 0xFF (VID 的低8位), (pid >> 8) & 0xFF (PID 的高8位), pid & 0xFF (PID 的低8位)。这是将16位整数拆分成两个8位字节的标准做法。
  2. LED GPIO 引脚 (field_2):

    • 读取引脚号,编码为 [PHY_LED_GPIO, 1, gpio_value]。长度为1,值就是引脚号本身。
  3. LED 亮度 (field_3):

    • 读取亮度值,编码为 [PHY_LED_BTNESS, 1, brightness_value]
  4. 选项 (Options) (field_4_...):

    • 通过检查一系列复选框的状态,构建一个名为 opts位掩码
    • opts |= PHY_OPT_DIMM; 这行代码使用按位或 (Bitwise OR) 操作。如果复选框被选中,就将对应的标志位“合并”到 opts 变量中。
    • 最后,将 opts 编码为 [PHY_OPTS, 2, opts_high_byte, opts_low_byte]。这是一个2字节的值。
  5. 启用的曲线 (Enabled Curves) (field_4_9):

    • 与选项类似,构建一个 curves 位掩码。目前代码只检查了一个复选框 (SECP256K1)。
    • 最后,将 curves 编码为一个4字节的值。
  6. 按钮超时 (field_11):

    • 读取超时值,编码为 [PHY_UP_BUTTON, 1, timeout_value]
  7. USB 产品名称 (field_12):

    • 读取自定义的产品名称字符串。
    • 将其转换为一个字节数组 (每个字符的ASCII码)。
    • 编码为 [PHY_USB_PRODUCT, length+1, ...char_bytes, 0x00]。这里 length+1 和末尾的 0x00 表明它被编码为一个 **C 语言风格的空字符结尾字符串 (null-terminated string)**。
  8. LED 驱动类型 (field_13):

    • 从下拉菜单读取驱动类型,并映射到对应的常量值。
    • 编码为 [PHY_LED_DRIVER, 1, driver_value]

2.3. 返回值

  • return new Uint8Array(byteArray);
  • 函数最后将包含所有编码数据的普通 JavaScript 数组转换为 Uint8Array。这是一种专门用来处理原始二进制数据的类型,是 Web API (包括 WebAuthn) 中处理二进制数据的标准格式。

3. createUserDetails() 函数

这个函数是连接配置数据和 WebAuthn 协议的桥梁

  1. const byteArray = getPhyData();: 首先调用 getPhyData() 获取配置数据的二进制字节数组。
  2. const userId = btoa(String.fromCharCode.apply(null, byteArray));:
    • 这一步是关键。它将二进制数据 (byteArray) 转换为 Base64 编码的字符串
    • String.fromCharCode.apply(null, byteArray) 是一个将字节数组转换为字符串的经典技巧。
    • btoa() 是浏览器内置的 Base64 编码函数。
    • 这个 Base64 字符串将被用作 WebAuthn 注册流程中的 user.id
  3. const userName = "+picoCommissionProfile";:
    • 这是一个“魔法字符串”。硬件密钥的固件被设计成会识别这个特殊的用户。当它收到一个用户名为 +picoCommissionProfile 的注册请求时,它就知道这不是一个普通的注册请求,而是一个配置指令。
  4. return { id: userId, ... };:
    • 函数返回一个符合 WebAuthn PublicKeyCredentialUserEntity 规范的对象。
    • 最巧妙的地方在于:配置数据被隐藏在了 user.id 字段里

4. createCredential() 函数

这个函数负责发起 WebAuthn 注册流程。

  1. const user = createUserDetails();: 获取包含配置数据的特殊 user 对象。
  2. const publicKey = { ... };: 构造 WebAuthn navigator.credentials.create() 所需的参数对象。
    • rp: { name: "Pico Keys" }: rp 指 Relying Party,也就是网站的身份。
    • user: { id: Uint8Array.from(atob(user.id), c => c.charCodeAt(0)), ... }: 这里将 Base64 编码的 user.id 解码Uint8Array 二进制格式,因为 WebAuthn API 要求 user.idArrayBuffer 或其视图(如 Uint8Array)。
    • 其他参数 (challenge, pubKeyCredParams 等) 都是 WebAuthn 的标准配置。
  3. await navigator.credentials.create({ publicKey }):
    • 这是触发浏览器与硬件密钥交互的核心调用。
    • 浏览器将 publicKey 对象中的信息(包括那个特殊的 user 对象)发送给硬件密钥。
    • 硬件密钥的固件接收到数据,检查到 userName+picoCommissionProfile,于是触发配置模式。
    • 它会解码 user.id 中的二进制数据,解析出 [Type, Length, Value] 指令,然后将这些设置写入自己的非易失性存储(如闪存)中。
    • 如果配置成功,硬件密钥可能会返回一个成功的响应,整个 create 调用就成功了。如果失败,则会抛出错误。
  4. .then(...)catch(...):
    • 这部分代码用于处理 WebAuthn 调用的结果。虽然在这个配置场景下,返回的凭证数据 (attestationObject, clientDataJSON 等) 可能没什么用,但代码仍然打印了它们用于调试。
    • catch 块则用于捕获并显示可能发生的错误。

总结

这段代码实现了一个基于网页的硬件设备配置工具。它通过一种非常创新的方式,将配置指令打包到 WebAuthn 注册请求的 user.id 字段中,并使用一个特殊的 user.name 作为触发器,让定制固件的硬件安全密钥进入配置模式并应用这些设置。

使用

现在就可以将 PicoKey 连接到设备,在支持 FIDO2 的网站上进行身份验证了,比如 GitHub、Google、Microsoft 等等。

Windows 系统会在首次验证时强制要求设置 PIN,这里也强烈推荐设置 PIN 以提升安全性,否则如果设备丢失,任何人都可以直接使用它进行身份验证,驻留密钥会直接暴露用户名和网站信息,从而直接登录并通过验证。

其他

三方管理工具

将设备的 PID/VID 设置为 Yubico 或 Nitrokey 的值,可以让对应的管理软件识别,从而使用它们的软件进行管理和配置,但是需要注意:这可能会违反某些软件的使用条款,分发这些 PID/VID 的固件也可能涉及法律风险。

例如,将 VID 设置为 Yubico 的 0x1050,PID 设置为 Yubico FIDO2 的 0x0407,就可以使用 Yubico Authenticator,Yubico Manager 等软件进行管理。

外壳

搜索社区可以找到不少分享的 3D 打印的外壳设计文件,但需要根据自己的实际设备进行选择。

写在最后的话

PicoKey 是一个非常有趣且实用的项目,利用廉价的硬件实现了 FIDO2 安全密钥的功能。

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