$$ \newcommand{\N}{\mathbb{N}} \newcommand{\C}{\mathbb{C}} \newcommand{\R}{\mathbb{R}} \newcommand{\Z}{\mathbb{Z}} \newcommand{\ZZ}{\ooalign{Z\cr\hidewidth\kern0.1em\raisebox{-0.5ex}{Z}\hidewidth\cr}} \newcommand{\colim}{\text{colim}} \newcommand{\weaktopo}{\tau_\text{weak}} \newcommand{\strongtopo}{\tau_\text{strong}} \newcommand{\normtopo}{\tau_\text{norm}} \newcommand{\green}[1]{\textcolor{ForestGreen}{#1}} \newcommand{\red}[1]{\textcolor{red}{#1}} \newcommand{\blue}[1]{\textcolor{blue}{#1}} \newcommand{\orange}[1]{\textcolor{orange}{#1}} \newcommand{\tr}{\text{tr}} \newcommand{\id}{\text{id}} \newcommand{\im}{\text{im}\>} \newcommand{\res}{\text{res}} \newcommand{\TopTwo}{\underline{\text{Top}^{(2)}}} \newcommand{\CW}[1]{\underline{#1\text{-CW}}} % specific for this document \newcommand{\cellOne}{\textcolor{green}{1}} \newcommand{\cellTwo}{\textcolor{red}{2}} \newcommand{\cellThree}{\textcolor{brown}{3}} \newcommand{\cellFour}{\textcolor{YellowOrange}{4}} $$

Writing Linux Drivers

Or: how to fix your fingerprint reader by writing the missing drivers

computer science
Linux
English
Author

Luca Leon Happel

Published

July 15, 2026

I own an Asus Rog Flow X13 with a 04f3:0c6e Elan Microelectronics Corp. ELAN:Fingerprint fingerprint reader, which currently does not work on Linux1.

In this blog post, we dive into how we can fix this ourselfes by reverse engineering the fingerprint reader and adding appropriate support to libfprint.

Project setup

I use NixOS (read more about this here), a Linux distribution with the pleasent benefit that you (the reader) can simply run

git clone https://github.com/quoteme/libfprint.git
cd libfprint
nix develop

to enter a completely reproducible development environment with all dependencies installed, perfectly fit for following this guide.

I decided to try Flake Parts this time around. Without diving too much into why, the reason being that it hopefully makes the development shell easier to extend for your future Linux driver.

Reading fingerprint data

Turns out 04f3:0c6e was already sitting in the elan driver’s device table. The catch is that driver treats every sensor as a swipe sensor and stitches consecutive frames of a moving finger into one image. The Flow X13’s sensor doesn’t move, though, you tap it and lift off, so feeding a stack of near-identical still frames through swipe-stitching logic just produces mush. Enrollment ran without complaint and quietly stored garbage.

So the fix is a separate driver, elanpress, that treats the sensor as what it actually is: a tap sensor (FP_SCAN_TYPE_PRESS). It averages the stable frames of a touch, subtracts a background frame, and normalizes the result with Elantech’s two-step curve to get a usable 8-bit image out of it.

Matching by correlation, not minutiae

The sensor only images about 7.5 by 2.6 mm, which is too small for minutiae matching. I tried it anyway with NBIS’s bozorth3, and genuine matches scored lower than impostor matches, so that approach was dead on arrival.

Windows gets around this by storing several raw touch images per finger and matching by image correlation instead of minutiae. I did the same: verification computes zero-mean normalized cross-correlation between the probe and each enrolled image, searching over small translations and rotations of up to ±12 degrees, since a finger never lands at exactly the same angle on an unconstrained pad twice. Genuine presses landed at 0.59 to 0.91 correlation, impostors stayed under 0.47, and 0.55 sits comfortably between the two as the acceptance threshold.

The reader that only worked once

Enrollment worked exactly once. The first touch of a fresh fprintd-enroll run went through fine, and every touch after that hung forever, both for the rest of that enrollment and on every run afterwards, until I rebooted the machine.

I wrote a small standalone libusb probe to bypass libfprint entirely and poll the sensor by hand. Turns out cmd_pre_scan, the command the driver used to ask “is a finger down yet”, only answers correctly once per power-up. After that it just times out, permanently. I tried everything short of a reboot to unwedge it: cmd_stop, a real libusb_reset_device(), clearing halted endpoints, resetting the USB configuration, even unbinding and rebinding the device through sysfs. None of it brought the status byte back.

Meanwhile cmd_get_image, the command that actually pulls a frame, worked fine no matter how many times I called it, finger down or not. And the image data itself gave a clean signal for whether a finger was touching the sensor: about 450 mean pixel delta from the background with nothing on the sensor, versus about 4800 with a finger pressed down.

So I ripped the pre-scan polling out of the capture state machine and replaced it with exactly that: keep calling cmd_get_image and check each frame against the background (elanpress_frame_has_touch()). The frame that shows a touch is also the frame you wanted to capture, so there’s no extra round trip, and the whole state machine shrank from 9 states to 5 once the separate “wait for the finger to lift” phase became unnecessary.

I deployed the fix by pointing my NixOS module (asusROGFlowX13.nix) at the new commit and rebuilding. fprintd-enroll now gets through all 8 enrollment stages without hanging.

Result

Enrollment and verification both work through fprintd on the Flow X13. libfprint now has a working tap sensor to go with its existing swipe sensors.