一个由零件组装的USB指纹读取器,适用于Linux桌面。总零件成本不到15美元。可直接替代上游的fprintd——PAM、KDE设置、GNOME设置、fprintd-verify、使用手指的sudo、使用手指的屏幕解锁均可正常工作。
截至fw=1.0 / r503d 1.0.0,Arduino↔主机线路已认证:每个命令和响应都携带一个SipHash-2-4 MAC,其密钥基于EEPROM中TOFU配对的秘密。针对USB串行链路的重放和热插拔攻击被阻止。完整设计见SPEC.md §13,包括威胁模型不涵盖的内容。

真希望我有一台3D打印机……``` ┌──────────┐ UART ┌─────────────┐ USB-CDC ┌──────────────────┐ │ Grow │ 57600 8N1│ Arduino │ /dev/r503 │ r503d daemon │ │ R503 │◀─────────▶│ (firmware) │◀──────────▶│ net.reactivated │ │ sensor │ 3.3V TTL │ │ framed, │ .Fprint on D-Bus│ └──────────┘ └─────────────┘ MAC'd └──────────────────┘ │ ▼ PAM, KDE, GNOME, fprintd-verify, …
## 为什么
Linux 下的硬件 USB 指纹读取器稀缺、昂贵,并且现有的(如 Validity、Synaptics 等)都是通过不稳定的 libfprint 驱动逆向工程而来,这些驱动会在供应商固件更新时失效。Grow R503 的协议是 **公开** 的,Arduino 端是你自己的代码,而 libfprint 兼容层只是 D-Bus。
你最终还会得到一个从头到尾都能读到源代码的指纹读取器。
## 物料清单
| 部件 | 备注 | 大致成本 |
|------|-------|------|
| Grow R503 电容式指纹传感器 | 带有 RGB 环的圆形款 | ~$10 |
| Arduino Uno R3 / Nano / Mega / 任何 ATmega328 开发板 | 任何能运行 SoftwareSerial 的板子 | $5–$25 |
| 4–6 根杜邦线 | 杜邦 / 面包板 | 微不足道 |
就这样。**无需电平转换器,无需分压器** — 原因见 [`SPEC.md` §3.1](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/SPEC.md)(R503 的 RX 线在实践中可承受 5V;数据手册有误)。
## 接线```
R503 Arduino (Uno R3 / Nano / etc.)
---- ------------------------------
Red (VCC) 3V3
White (3.3VT) 3V3 (touch-IC supply; shares rail with red)
Black (GND) GND
Yellow (TXD) D2 ── SoftwareSerial RX
Brown (RXD) D3 ── SoftwareSerial TX (direct — no divider!)
Blue (WAKEUP) D4 (optional; not used by firmware yet)
如果您的 R503 配备了 JST-SH 连接器,请剪断一根 6 针 JST-SH 转杜邦线来分出线缆。棕色线有时会是绿色(取决于卖家)——请根据连接到 JST 接头的 RXD 针脚的那根线来验证,而不是根据颜色。
已在 Fedora 44 KDE 上测试;应适用于任何基于 systemd 的发行版,并包含 fprintd、pam_fprintd 以及较新的 Rust 工具链。
系统包:
| 发行版 | 构建依赖 | 运行时依赖 |
|---|---|---|
| Fedora / RHEL | rust cargo arduino-cli tpm2-tss-devel | fprintd pam fprintd-pam tpm2-tss |
| Debian / Ubuntu | rustc cargo arduino-cli libtss2-dev | fprintd libpam-fprintd libtss2-esys-3.0.2-0 |
仅当您计划使用 --pair --seal-tpm(规范 §13.12)时才需要 tss-esapi 包。否则守护进程无需 TPM 即可构建和运行——tss-esapi 是硬性构建依赖但属于软性运行时依赖(代码路径仅在 /var/lib/r503d/key.tpm 存在时才会进入)。
需要 Rust 1.95+ 以及 $PATH 中的 arduino-cli。
您是否有 TPM2?```bash ls /dev/tpmrm0 && tpm2_pcrread sha256:7 | head -3
如果两者都成功,你的主机可以使用密封密钥路径。如果 `/dev/tpmrm0` 缺失(旧硬件、BIOS中禁用了TPM,或没有虚拟TPM的虚拟机),则坚持使用默认的明文密钥流程。
## 构建与安装
### 1. 烧录固件
在Arduino IDE中打开 `firmware/r503fp/r503fp.ino` 并上传。或者使用 `arduino-cli`:```bash
# Uno R3:
arduino-cli compile --fqbn arduino:avr:uno firmware/r503fp/
arduino-cli upload --fqbn arduino:avr:uno --port /dev/ttyACM0 firmware/r503fp/
# Nano (modern Optiboot, including most Elegoo / WAVGAT clones):
arduino-cli compile --fqbn arduino:avr:nano:cpu=atmega328 firmware/r503fp/
arduino-cli upload --fqbn arduino:avr:nano:cpu=atmega328 --port /dev/ttyUSB0 firmware/r503fp/
# Nano with legacy 57600-baud bootloader (older clones):
# replace `cpu=atmega328` with `cpu=atmega328old`
固件使用了 Adafruit_Fingerprint。IDE 会在首次编译时提供安装。
如果 arduino-cli upload 失败并显示 not in sync: resp=0x7e,则你的 bootloader 是另一个变体——交换 atmega328 ↔ atmega328old 并重试。两者都有效;区别仅在于 bootloader 波特率。
需要 Rust 1.95+。```bash cd pcside/daemon cargo build --release
### 3. 安装```bash
sudo bash pcside/daemon/dist/install.sh
该脚本:
target/release/r503d 安装到 /usr/local/bin/r503d/var/lib/r503d/(权限 0700 root:root)用于存放密钥、状态和用户槽注册表/dev/r503,并将设备节点锁定为 root:root 0600(只有以 root 身份运行的守护进程需要它;这会关闭默认的 0660 root:dialout 路径,使其他本地用户无法打开端口 —— 安全审计 2026-05-28 / H1)。后果: 安装后,任何针对 /dev/r503 的手动 arduino-cli/串口监视器命令都需要 sudo。/etc/systemd/system/r503d.service)net.reactivated.Fprint/usr/share/polkit-1/actions/net.reactivated.fprint.device.r503d.policy),供调用者身份门使用/etc/dbus-1/system.d/net.reactivated.Fprint.conf)—— 只有 和 组成员可以与守护进程通信;其他所有人都会在代理处遇到 ,守护进程根本看不到调用该脚本是幂等的 —— 每次执行 cargo build --release 后重新运行它,即可重新部署新的二进制文件。
新刷写的 Nano 是未配对的 —— 守护进程可以跟它通信,但固件会拒绝每个帧命令。从以下两种流程中选择一种;两种流程最终都能得到配对的 Nano 和工作的守护进程。如果你的主机有 TPM2,推荐使用 TPM 密封流程(参见前提条件中的快速检查)。
两种流程中使用的 opt-in 文件(/etc/r503d/allow-pair)存在的目的是阻止攻击者将自己带来的 Nano 快速抢占到你的桌面上 —— 没有 root 权限就无法进行配对。r503d --pair 会在发送密钥到 Nano 之前删除该标记:如果主机在 Nano 端提交之后、主机端持久化之前崩溃,门已经关闭,因此下一次配对尝试需要管理员再次 touch 该标记。发送前的退出(没有标记,或“已经配对”)会保留标记以便重试。
如果你没有 TPM2 设备,或者不需要离线磁盘攻击防护,请使用此方式。```bash sudo systemctl stop r503d sudo mkdir -p /etc/r503d sudo touch /etc/r503d/allow-pair # opt-in (see SPEC §13.5) sudo r503d --pair # 128-bit key → /var/lib/r503d/key sudo systemctl start r503d
(此块内容为空)```bash
sudo r503d --status
# port: /dev/r503
# firmware: fw=1.1 fmt=2
# firmware paired: true
# firmware counter: 42
# host key.tpm: (absent)
# host key: /var/lib/r503d/key
# host key.bak: /var/lib/r503d/key.bak
# tpm device: (absent)
# allow-pair: (absent)
相同的流程,加上--seal-tpm。生成的密钥被密封到PCR7(安全启动策略+密钥)并写入/var/lib/r503d/key.tpm,而不是明文key文件。离线磁盘攻击者(对未挂载分区执行dd,将SSD交换到恶意主机)只能得到密文。```bash
sudo systemctl stop r503d
sudo mkdir -p /etc/r503d
sudo touch /etc/r503d/allow-pair
sudo r503d --pair --seal-tpm # seals new key to current PCR7
sudo systemctl start r503d
(由于输入内容为空,无翻译内容)```bash
sudo r503d --status
# port: /dev/r503
# firmware: fw=1.1 fmt=2
# firmware paired: true
# firmware counter: 12
# host key.tpm: /var/lib/r503d/key.tpm
# host key: (missing)
# host key.bak: (missing)
# tpm device: /dev/tpmrm0
# allow-pair: (absent)
内核更新、initrd更新、fwupd UEFI固件更新以及grub2更新 不会 改变PCR7,因此无需重新密封。PCR7仅会在安全启动策略编辑、MOK注册或将磁盘移动到其他主机时发生变化——此时守护进程会因TPM_RC_POLICY_FAIL而拒绝启动,而dist/reseal-tpm.sh会在约90秒内完成恢复。详见恢复:PCR7已更改。
fprintd-enroll mat
fprintd-verify mat
sudo whoami
Both KDE Settings (Plasma 6) and GNOME Control Center's user-account
fingerprint dialogs drive `r503d` exactly as they drive upstream `fprintd`.
### 重新配对 / 密钥轮换
如果你想要一个新的密钥(密钥泄露、计划更换硬件、偏执狂):```bash
sudo systemctl stop r503d
sudo r503d --unpair # framed; wipes Nano EEPROM + host key
sudo touch /etc/r503d/allow-pair
sudo r503d --pair # plaintext-key rotation
# - or -
sudo r503d --pair --seal-tpm # TPM-sealed rotation
sudo systemctl start r503d
匹配你原始的配对路径。 如果你最初使用了 --seal-tpm,轮换时也请使用 --seal-tpm — 否则轮换操作会静默地将你降级为磁盘上的明文密钥。
如果你使用了 --pair --seal-tpm,之后又更改了 PCR7 所测量的内容(例如关闭/开启 Secure Boot、注册新的 MOK、将磁盘移至其他主机),守护进程将拒绝启动,并在日志中显示 TPM_RC_POLICY_FAIL 的错误信息。恢复仅需一条命令:```bash
sudo bash pcside/daemon/dist/reseal-tpm.sh
该脚本会停止 `r503d`,重新刷写 `firmware/r503fp_wipe/` 来擦除 Nano EEPROM,然后重新刷写主固件,创建 `/etc/r503d/allow-pair`,运行 `r503d --reseal-tpm` 以生成一个绑定到*当前*PCR7的新密钥,最后重新启动守护进程。整个过程大约耗时90秒。已注册的指纹会被保留——模板存储在 R503 传感器的闪存中,而非 Nano 上。
该脚本需要 `arduino-cli` 可用。如果它安装在当前用户的 `$HOME/.local/bin` 目录下,则可通过 `$SUDO_USER` 自动检测;否则需要在运行前设置 `ARDUINO_CLI=/full/path/to/arduino-cli`。
### 恢复:`state.json` 丢失(计数器不同步)
如果主机密钥完好,但 `/var/lib/r503d/state.json` 丢失或回滚(例如恢复了旧备份或意外删除),守护进程的计数器会落后于 Nano 的 `last_seen`,导致每个帧命令都因 `ERR replay` 而被拒绝。`r503d --status` 会标记此问题;修复方法是一条命令:```bash
sudo systemctl stop r503d
sudo r503d --resync # reads Nano last_seen, sets host counter to last_seen+1
sudo systemctl start r503d
无需重新配对,无需重新烧录——密钥从不动移。status查询 --resync 所依赖的是未经身份验证的,但它只能将主机计数器向前移动以匹配Nano已提交的值,因此它永远无法使旧帧可重放(最坏情况下,恶意MITM强制另一个ERR replay,它可以通过破坏帧来实现同样的效果)。参见SPEC.md §13.11。
经过身份验证的--unpair需要密钥来进行授权。如果所有磁盘上的副本都已丢失(磁盘崩溃、意外删除、key和key.bak均被删除,或key.tpm对象丢失),你需要重新烧录以擦除的逃生舱——与dist/reseal-tpm.sh为上述PCR7变更情况自动执行的流程相同:```bash
sudo systemctl stop r503d
sudo arduino-cli upload --fqbn arduino:avr:nano:cpu=atmega328 --port /dev/r503 firmware/r503fp_wipe/
sudo arduino-cli upload --fqbn arduino:avr:nano:cpu=atmega328 --port /dev/r503 firmware/r503fp/ sudo touch /etc/r503d/allow-pair sudo r503d --pair sudo systemctl start r503d
如果 `sudo arduino-cli` 报告命令未找到(arduino-cli 位于你的 `~/.local/bin` 中,而不在 root 的 `PATH` 中),请以 `sudo env "PATH=$PATH" arduino-cli …` 运行它,或者给出绝对路径。
这不是攻击者可以利用的后门:重新配对需要主机上的 root 权限(可选文件及 `--pair` 命令行均需要 root 权限),因此,除非你已经拥有 root 权限,否则重新刷写的 Nano 无法被纳入信任。
### 卸载```bash
sudo bash pcside/daemon/dist/uninstall.sh
恢复所有内容,解除对 fprintd 的屏蔽,保留 /var/lib/r503d/(密钥、状态、用户)以备日后重新安装。如需彻底清理,请手动删除该目录。
Arduino 运行一个小型 ASCII 协议固件(firmware/r503fp/),该固件在 UART 端与 R503 的原生 R30x("同步字")二进制协议通信,并通过 USB-CDC 与主机交换面向行的文本命令:ping、info、enroll N、verify、delete N、clear、led off。完整 v1 协议见 SPEC.md §5。
自 fw=1.0(v2 认证通道工作的里程碑 E)起,每条命令和响应都被封装在 C <counter> <body> M <mac> / R <counter> <seq> <body> M <mac> 帧中,并使用基于 TOFU 配对的 128 位密钥通过 SipHash-2-4 进行 MAC 计算。Nano 在 EEPROM 中维护一个带磨损均衡的单调计数器;守护进程在 /var/lib/r503d/state.json 中维护一个匹配的计数器。重放尝试(固件端 incoming <= last_seen)将被拒绝,返回 ERR replay;篡改的帧将返回 ERR mac_invalid。完整规范、威胁模型和已知限制见 SPEC.md §13。
Rust 守护进程(r503d)在 net.reactivated.Fprint 上运行 D-Bus —— 与上游 fprintd 暴露的接口逐位相同 —— 因此每个 fprintd 客户端无需修改即可工作。位于 /var/lib/r503d/users.json 的 JSON 辅助文件将(用户、手指)映射到 R503 内部闪存中的槽位索引。
布局:``` firmware/r503fp/ Arduino firmware (v2 framed ASCII protocol) firmware/r503fp_wipe/ Emergency one-shot EEPROM wipe (lost-key recovery) firmware/* Diagnostic / development sketches (ping, loopback, ...) pcside/daemon/ Rust daemon (the fprintd replacement) pcside/daemon/src/{crypto,framing,keystore,state,pairing}.rs v2 wire protocol implementation pcside/daemon/src/auth.rs caller-identity gating for D-Bus methods pcside/daemon/dist/ udev rule, systemd unit, polkit + bus policy, install scripts docs/ Decision logs + troubleshooting SPEC.md Full architecture + protocol spec (§13 = v2 auth)
## 安全模型 — 快速总结
线路级别的认证针对的是特定威胁 —
**"恶意的女仆,带有五分钟和一个备用 Nano"** 加上一个在 `/dev/r503` 上的敌对本地进程 — 而不是拥有实验室的国家级或硬件攻击者。单用户桌面部署,附带已记录的不在范围内的列表。完整的威胁模型见 [`SPEC.md` §13.1](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/SPEC.md);实现与审查的证据见 [`docs/REVIEW-2026-05-28.md`](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/docs/REVIEW-2026-05-28.md)。一次独立的对抗性权限提升审计 (2026-05-28) 及其针对每个声明的验证/修复过程见 [`docs/SECURITY-AUDIT-2026-05-28.html`](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/docs/SECURITY-AUDIT-2026-05-28.html) 和 [`docs/SECURITY-AUDIT-2026-05-28-VALIDATION.html`](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/docs/SECURITY-AUDIT-2026-05-28-VALIDATION.html)。
**已防御:**
- 将 Nano 热替换为敌对单元(无密钥 → 所有帧通过 MAC 失败)。
- 本地进程在 `/dev/r503` 上注入虚假匹配响应。两层保护:
设备节点是 `root:root 0600`(udev 规则),并且守护进程使用 `TIOCEXCL` 持有它,因此非 root 进程无法打开它 — 即使能打开,它也没有密钥,所以帧无法通过 MAC 验证。
- 重放录制的 `OK match=...` 帧以用于未来会话。
- 任何帧字段的比特翻转篡改(恒定时间 MAC 比较)。
- 计数器耗尽阻断:一个对端(或 `--resync` 期间的一次性中间人)将单调计数器驱动到 `u64::MAX` 并永久阻塞通道,该行为被两端强制执行的保留计数器上限阻止 (`fw=1.1+`; SPEC §13.4 / 2026-05-28 审计 DoS-2)。
- 本地用户对传感器的拒绝服务攻击:单个捕获槽门限制并发注册/验证工作,且删除路径受操作门控,因此 `Start`/`Stop`(或并发删除)洪泛无法阻塞认证。
- 本地非 root 用户的跨用户指纹植入/清除/枚举(例如 `mallory` 调用 `Claim "root"` 然后注册自己的手指)——每次接受 `username` 的 D-Bus 方法都会检查调用者身份,且系统总线策略在代理层拒绝非 `wheel` 的调用者。
- **离线磁盘攻击主机密钥** *当与 `--seal-tpm` 结合使用时*:磁盘上的密钥使用 TPM2 密封到 PCR7,因此对未挂载分区进行 `dd` 或将 SSD 插入敌对主机只会得到密文。仅在同一台机器上、相同安全启动策略下才能解封。参见 [SPEC §13.12](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/SPEC.md)。
**未防御:**
- 主机 root 权限被攻破(密钥位于 `/var/lib/r503d/key`,`0600 root:root`)。运行中主机上的 root 也可以解封 TPM 密封的变体 — 密封削弱的是 *离线* 攻击,而非在线攻击。
- 对 Nano 的物理攻击(通过 ISP 回读 EEPROM 约 30 秒;芯片开盖等)。
- 固件重刷攻击(Arduino 引导加载程序没有签名 — 但重新配对需要主机上的 root 权限,所以没有主机被攻破,重刷过的 Nano 也无法被信任)。
- R503 侧被攻破(R30x 协议完全无认证;超出范围)。
- **密码学状态。** SipHash-2-4 MAC,128 位共享密钥,64 位 MAC 输出,域分离的 MAC 输入。两个独立的实现(AVR 上手写 C++,含启动时 KAT 自测试;主机上手写 Rust,在 CI 中针对第三方 `siphasher` 包的 1024 个随机向量进行逐位交叉验证)。主机 MAC 比较使用 `subtle::ConstantTimeEq`。线缆解析器在每个 CI 运行中进行属性模糊测试(约 135,000 个输入)。`cargo audit` 干净。SipHash 密钥包装在 `zeroize::Zeroizing<...>` 中,以便在 drop 时清除(每帧的 MAC 输入缓冲区也是如此)。一个 `cargo fuzz` libFuzzer 目标位于 `pcside/daemon/fuzz/`,用于在 nightly 上进行长语料库运行。没有付费的第三方人工审计 — 那仍然很有价值,欢迎 PR。
完整威胁模型及理由: [`SPEC.md` §13.1](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/SPEC.md)。
## 限制
- **多用户可行,但仅限于 `wheel` 组成员。** 每个接受 `username` 的 D-Bus 方法都会检查调用者身份(`Claim`、`EnrollStart`、`VerifyStart`、`ListEnrolledFingers`、`DeleteEnrolledFingers`);自身请求和 `uid 0` (PAM) 静默成功,来自非 root 调用者的跨用户请求被拒绝,返回 `net.reactivated.Fprint.Error.PermissionDenied`。系统总线策略进一步限制哪些帐户甚至可以开始对话:只有 `root` 和 `wheel` 成员能到达守护进程,其他人在代理层收到 `org.freedesktop.DBus.Error.AccessDenied`。需要跨用户注册?成为 root:`sudo fprintd-enroll target-user`。需要为自助服务终端/多用户实验室放宽跨用户限制?将一条 JS 规则放入 `/etc/polkit-1/rules.d/`,针对 [`net.reactivated.fprint.device.setusername`](https://gitlab.freedesktop.org/libfprint/fprintd/-/blob/master/src/net.reactivated.fprint.device.policy.in) — 动作名称与上游 fprintd 完全一致。
- **一个读取器。** 守护进程在 D-Bus 上暴露单个 Device 对象。多读取器设置需要对 Manager 进行扩展。
- **没有发出 `PropertiesChanged`** 针对 `finger-present` / `finger-needed` 提示属性。所有常见的 fprintd 客户端(PAM、KDE 设置、GNOME)都依赖 `EnrollStatus` / `VerifyStatus` 信号(这些信号会发出),而不是那些轮询的提示 — 但一个严格的客户端如果执行 `Get + PropertiesChanged` 将看到过时的值。
- **单个 Nano = 单点故障。** 如果 Nano 死了,指纹登录就不可用,直到你重新刷写一个备用件并重新配对。请保持启用密码认证方法作为备份。
- **State.json 丢失可通过一条命令恢复。** 如果 `state.json` 丢失而固件仍有较高的 `last_seen`,守护进程在首次发送时会遇到 `ERR replay`。运行 `sudo r503d --resync` 读取 Nano 的计数器并重新对齐主机 — 无需重新配对。参见 [`SPEC.md` §13.11](https://github.com/matpb/linux-fingerprint-r503/blob/HEAD/SPEC.md)。
## 故障排除```bash
# Daemon logs:
sudo journalctl -u r503d.service -f
# Confirm the sensor enumerates correctly:
ls -l /dev/r503
busctl --system call net.reactivated.Fprint /net/reactivated/Fprint/Device/0 \
net.reactivated.Fprint.Device ListEnrolledFingers s ""
# Confirm fprintd is masked and r503d owns the bus name:
systemctl is-enabled fprintd # should print "masked"
busctl --system list | grep -i fprint
如果守护进程无法启动或传感器始终无响应,最常见的解决方法是检查接线——参见SPEC.md §3,特别是§3.1中的**“无分压器”**说明。详细运行手册请参阅docs/TROUBLESHOOTING.md。
MIT — 参见 LICENSE.
fprintd 项目 — 设计了一个干净的D-Bus接口,使得此守护进程无需阅读libfprint源代码即可实现该接口。rootwheelAccessDeniedfprintd.servicer503d.service