手上的 ASUS ROG Flow X13,指纹在 Windows 下秒开,到了 Linux 就从来没成功解锁过。lsusb 一看:

04f3:0c6e Elan Microelectronics Corp. ELAN:Fingerprint

这颗 PID 明明白白在 libfprint 的支持列表里,驱动、fprintd、polkit 代理全都装好,fprintd-enroll 也能录进去,可 fprintd-verify 永远回一句 verify-no-match。

症状:能录入,从不匹配

开 G_MESSAGES_DEBUG=all 跑一次验证,匹配阶段的日志是这样的:

libfprint-print-DEBUG: score 7/24
libfprint-print-DEBUG: score 6/24
libfprint-print-DEBUG: score 0/24

bozorth3 打分 0~7,阈值 24,差了一个数量级。原因其实 elan.c 自己就写了:

The algorithm which libfprint uses to match fingerprints doesn’t like small images like the ones these drivers produce. There’s just not enough minutiae …

这颗传感器内部只有 150×52 像素,是块窄条。libfprint 用细节点(minutiae)匹配,这么小的图提取不出几个点,匹配自然无从谈起。

社区现状:五年都没解决

翻上游 issue tracker,04f3:0c6e 相关的开着的 issue 一大把:#401、#402、#500、#732、#759、#808、#814……当年是有人"看到能出图"就把 PID 加进了支持列表,结果作者自己在 #401 里承认"它从来验证不过"。

有个研究仓库 ItzSkyeYT/elan-0c6e-fingerprint-linux 把各种死路都趟了一遍,结论是上游五年都不行。唯一有点希望的是树外的 elanpress 驱动——把传感器当按压式,匹配改用互相关(LCN),但作者实测最好也只有 FAR ~14%、FRR ~15%,自己标注"非认证级、别接 PAM"。

也就是说:Linux 这边要么凑合,要么换算法。既然 Windows 用得好好的,那答案多半在厂商驱动里。

换个思路:看 Windows 是怎么做的

从 Microsoft Update Catalog 按硬件 ID 搜 USB\VID_04F3&PID_0C6E,命中厂商包:

ELAN Finger Print - Biometric 3.5.12201.11508
  INF: USB\VID_04F3&PID_0C6E ; ELAN MCU: EMSE1000

解包后只有几个文件。WbfUsbDriver.dll 是 USB/采集,EngineAdapter.dll 是 WBF 的引擎适配器(导出 WbioQueryEngineInterface),匹配算法就在里面。

strings 一跑,参数名直接把我看笑了:

_SIGMA = %f
_CONTR_THR = %f
_CURV_THR = %d
_ORI_HIST_BINS = %d
_ORI_PEAK_RATIO = %f
_DESCR_SCL_FCTR = %f
_DESCR_MAG_THR = %f
_INT_DESCR_FCTR = %f
RANSAC_diff = %f
AcceptRANSACCnt = %d
InitialRadius = %d
MaxRadius = %d
TemplateSetTH = %d
TemplateNear = %d

这是 Lowe 的 SIFT 的参数名,后面还跟着 RANSAC 的几何校验参数。顺着初始化函数去 .rdata 里把常量抠出来,清一色标准默认值:

参数值
_SIGMA1.6
_CONTR_THR0.04
_CURV_THR10
_ORI_PEAK_RATIO0.8
_DESCR_MAG_THR0.2
_INT_DESCR_FCTR512
RANSAC_diff3.0 px
AcceptRANSACCnt3

也就是说:Windows 压根没用细节点匹配,用的是 SIFT 特征点 + RANSAC 几何校验。SIFT 对窄条、部分重叠的图像鲁棒得多,这才是它能工作的根本原因。而且这些参数和 OpenCV cv::SIFT 的默认值完全一致——厂商大概率就是拿 OpenCV(或同源实现)跑的。

先离线验证,再写代码

在动 libfprint 一行代码之前,得先确认 SIFT 在这颗传感器的真实图像上到底能不能区分真/假手指。

libfprint 自带 examples/img-capture,可以直接抓传感器原图(滑动时逐帧拼接出 225×272 的图)。同一根手指抓 7 张、另一根抓 5 张,用 Python + OpenCV 按提取到的参数跑 SIFT → BFMatcher(ratio 0.75) → findHomography(RANSAC, 3.0),以 RANSAC 内点数作为分数:

均值最大
真手指(同指配对)12.741
假手指(异指配对)1.75

分离度一目了然。再按 Windows 引擎的做法取"模板集最大分"(留一验证),阈值取 10 时 FAR 0%、FRR 8%。可以动手了。

移植进 libfprint

改动落在一个 fork 里,核心就几处:

  • 新增 fpi-sift-match.cpp:用 OpenCV 实现 SIFT+RANSAC 匹配,判定阈值 = RANSAC 内点数(默认 10,可用 FPRINT_SIFT_THRESHOLD 覆盖)。
  • fpi-image-device.c:图像设备改用 fpi_sift_match() 代替 fpi_print_bz3_match()。
  • FpPrint 保存全部录入图像:这是最绕的一处。libfprint 存盘时会丢掉图像(fp_print_serialize 注释白纸黑字写着 “the image data is discarded”),加载后的模板就没有图,SIFT 无从匹配。好在序列化结构里有个 “Unused a{sv} for expansion” 的空字典,把图像塞进去即可,向后兼容。同时 enroll 时 fpi_print_add_print 只复制细节点、不复制图像,得让它把每个阶段的图也累积下来,才能凑出"模板集"。
  • drivers/elan.c:瞬态的 0x00/0xaf “not ready” 状态对所有设备都重试,而不是只对 ELAN_0C58(上游 issue #879)。
  • drivers/elan.h:校准重试次数从 10 提到 100。

踩过的坑

校准卡死。 这颗传感器校准状态会长时间停在 0x01(约 2.5 秒)才变 0x03,而默认只重试 10 次(每次 50ms,共 0.5 秒),必然报 Calibration failed!。把次数拉到 100 就稳了。

协议错误。 采集时设备会回 0xaf(手指移开/忙)或 0x00(未就绪),这些是瞬态而非错误,上游只对 0x58 做了重试。去掉型号判断后,之前那个 protocol error 就没了。

它是滑动式。 一开始想当然地"按一下",结果全程 swipe-too-short。正确用法是:指肚平贴窄条一端,匀速滑到另一端,驱动在滑动中逐帧拍照再拼接成完整指纹图。这也解释了为什么拼接图比单帧信息量大得多——单帧(52px 高)的 SIFT 判别力实测只有 d′ 0.88,而拼接图有 1.52。

结果

录入 5 个阶段,验证连续三次全部命中(内点数 11 / 30 / 25,阈值 10)。锁屏这边用 hyprlock(原生支持 fprintd),顺手用 Pango markup 把指纹图标按状态上了色:等待白色、识别中蓝色、失败红色。

打包与发布

Arch 包(上游 tarball + 补丁,依赖 opencv)已经发布到自己的 Gitea:

xeonds/libfprint-elan-0c6e        # 源码 fork + packaging/
release v1.94.100-1.2             # libfprint-1.94.100-1.2-x86_64.pkg.tar.zst

记得在 /etc/pacman.conf 里加 IgnorePkg = libfprint,否则下次 pacman -Syu 会把补丁冲掉。

小结

  • 遇到"上游支持但就是不工作"的硬件,逆向厂商驱动往往比在开源实现里瞎调参高效得多。参数名、常量表、初始化函数,信息量大得惊人。
  • SIFT 这类经典特征算法,在窄条指纹上比细节点匹配靠谱得多。
  • 但要保持清醒:这是滑动式;按压式(elanpress)实测 FAR ~14%,不是认证级。个人锁屏够用,别接 PAM。

硬件没问题,只是算法没对上。逆向一轮,算是把 Windows 的答案抄了过来。