GitHub - wrobelda/goodix-fp-spi-linux: Driving Goodix SPI fingerprint sensors on Linux, including versions running in Qualcomm QSEE enclave.

GitHub

20 min read Original article ↗

A Goodix SPI fingerprint sensor — not the unrelated USB kind common in laptops — can be driven in two ways.

In the "traditional" one, the Rich Execution Environment (REE), the operating system's kernel driver handles all the hardware over SPI bus protocol, allowing the user space to talk to the sensor directly to do the imaging, to perform the imaging, the templating and the matching logic.

However, this is rather rare nowadays as it involves substantial security risk, since the images and the templates are handled by the operating system, where anything that compromises the kernel or the userspace stack can read or substitute them.

The alternative is running the fingerprint logic in a Trusted Execution Environment (TEE), such that the operating system never touches the imaging or the matching at all — which instead happen inside a trusted, proprietary, signed and closed-source application running in the SoC's secure world. The kernel driver is left with handling only the bare minimum of the hardware, without access to the SPI bus the scanner is connected to.

When it comes to Goodix scanners, two known implementations of such TEE applications exist, one for MediaTek's MicroTrust TEE, the other for Qualcomm's QSEE, and they ship in the operating system the device comes with.

Supporting the Goodix in TEE mode on Linux

TEE mode is much safer of the two for the user, but it also pretty much guarantees that the OEM of the — typically an Android — device keeps the Hardware Abstraction Layer (HAL) library closed, and it is that library that implements the entirety of the communication between the Operating System's permissions subsystem and the trusted application. The only open piece is the kernel sensor driver, which does the bare minimum of bringing the scanner up.

This unfortunately means that the only way to add support for these scanners in Linux is by reverse-engineering the Android implementation. A 2021 article, Fingerprint sensors on tuxified Android phones: Impossible? (archived), describes well what a port would have to do and why nobody had done it.

LLM disclaimer

The project was developed using Claude Opus 5 and Kimi K3 over a span of 10 days. Claude Fable 5 would likely speed things up a bit, but anything security-adjacent gets flagged by Anthropic and the model was getting routinely dropped to Opus 5, hence the Kimi K3 usage here.

This is not unverified AI slop — most of the architecture decisions and code layout were made by myself, as well as the disassembly/reverse-engineering approach and driving it.

The work here is tested on my hardware, with the hope of further refinement, testing on more devices and readying for mainlining into the Linux kernel.

Status

TEE support for the reference platform on Qualcomm QSEE is complete.

The Linux stack contains:

  • the two kernel interfaces,
  • a machine-wide QSEECOM supplicant and application loader,
  • a libfprint driver used by fprintd and GDM.

This repository contains the protocol documentation, tracing tools, and command-line hardware test client. The production userspace components use separate repositories, so they can be packaged and reviewed independently.

  • REE mode — the operating system does the imaging itself over SPI. This mode belongs in a separate libfprint image driver. Its kernel interface would be built by porting the SPI transfer and image scanning code from the MediaTek-lineage gf_spi_tee.c, where that code sits behind SUPPORT_REE_SPI

  • TEE mode on Qualcomm — the client reaches the trusted application over QSEECOM. Everything documented here was exercised in this mode:

  • TEE mode on MediaTek — a related kernel driver exists, but neither TA ABI compatibility nor the userspace/session boundary is established. Treat it as a separate libfprint driver unless evidence shows that a versioned protocol profile can be shared. This work requires reverse-engineering

  • The reference platform's trusted-application protocol — recovered for one versioned Goodix QSEE profile. Portability to other OEMs, parts, application names, or TEEs is not established:

  • GW in-display family — untested. It differs from the capacitive copies mostly in board power and pins

  • Upstreaming the kernel side — both drivers build and run, but neither has been posted. These points have to be settled with the maintainers first:

    • TEE_IMPL_ID_QSEECOM = 5 is new uapi — a client tells this driver apart from qcomtee by reading this value from TEE_IOC_VERSION. The number is therefore ABI, and it needs the TEE maintainer's ack in the same posting
    • Sessions are opened by name, not by UUID — QSEE matches on a string, and there is nothing to render a UUID into; the name therefore arrives in a parameter, and the UUID must be zero. amdtee renders its UUID into a firmware filename, while qcomtee hit the same wall and used objref parameters. This driver does not claim TEE_GEN_CAP_GP, but the TEE subsystem may prefer a generic "session by name" of its own over each backend overloading a parameter
    • The privileged device's open_session is polymorphic — it tells "register a listener" apart from "load an application" by whether parameter 0 is a value or a memref. Two separate func values would document themselves
    • CAP_SYS_ADMIN on the privileged device — no other TEE backend checks a capability; OP-TEE relies on the permissions of /dev/teepriv0. It is worth offering to drop this check rather than defending it
    • A TZ memory pool per invoke — the bounce buffer for each command gets its own pool. For the IRQ command that means a large allocation at interrupt rates, and -ENOMEM mid-capture under fragmentation
    • The mdt_loader extension needs a soc/qcom ack — it adds two functions and a Kconfig prompt to a file this project does not own; see what the rest of the kernel had to grow
    • A wedged supplicant blocks every QSEECOM user, in-kernel ones included — this decides who may be granted the privileged device
  • A machine-wide QSEECOM supplicant — implemented in wrobelda/qsee-supplicant. Its listener service belongs in one machine-wide process rather than inside the fprintd driver, because the kernel allows one receiver per device (not per trusted app!); a second QSEE client handling some other type of app could not register a handler of its own. Qualcomm's minkipc is the source-level protocol and dispatch reference, while the local daemon supplies:

    • the legacy QSEECOM transport,
    • the confined state root,
    • packaging,
    • the reconnect lifecycle,
    • hardware-free tests.

    On the reference device it replaced gfharness --supp; the tested restart and reconnect behaviour is listed in tests on the reference platform.

  • A libfprint Goodix QSEE driver — implemented on the goodix-qsee branch in the separate libfprint repository. The driver discovers the firmware-described misc device and reads the TA name from firmware_name. It implements:

    • initialisation,
    • list,
    • enroll,
    • identify/verify,
    • delete,
    • clear,
    • cancellation,
    • IRQ draining,
    • matched IDs,
    • quality feedback.

    Build and hardware-free protocol/core tests pass; the live fprintd and GDM behaviour verified on the reference device is listed in tests on the reference platform.

What is not understood

The stack is complete for the reference platform. The items below do not block its working lifecycle; they define portability and security work that is not claimed complete.

These items share one property: reading or porting the downstream code does not settle them. Each one needs a device to test against, a vendor to answer, or evidence from outside this project entirely.

  • Whether another TA or firmware generation requires a Gatekeeper-signed enrolment token. This TA accepts a token that carries only the fresh challenge, both before and after a lock-screen credential is provisioned ([our device]). Android supplies a signed, challenge-bound token; its UI's backup-PIN requirement is framework policy, not an additional Goodix protocol step.

  • Whether the trusted application enforces any lockout of its own. This was not observed (status); treat the absence as unverified and implement lockout regardless.

  • Whether the other listener services are ever needed. We never observed gfenu raising a request on any of them, across every flow we exercised ([our device]). That observation does not cover unexercised paths or other firmware versions.

  • How broadly the recovered trusted-application ABI applies. gfenu is the filename supplied by the reference platform's firmware description; it is not a known cross-device Goodix protocol name. We do not know whether the same command numbers, payload layouts, interrupt state machine, or storage conventions are used by:

    • another OEM's Goodix application,
    • another sensor generation,
    • a Goodix application on a MediaTek or newer Qualcomm TEE.

    A production client must therefore treat the protocol documented here as a versioned compatibility profile and select it from positive platform evidence. It must obtain the application name from the kernel's firmware_name sysfs attribute, which is populated from the device tree firmware-name property, rather than compiling gfenu into the driver identity.

  • Whether a trusted application image is bound to a model or a vendor, and who signs it. The image used here came from the stock OS of the model it runs on, and we never tried it anywhere else. Another unit of the same model is not the interesting case; a different model or manufacturer sharing the sensor and SoC is.

    The secure world rejects a malformed image. That shows it checks integrity, but it says nothing about device binding or about the signing chain. In particular, we do not know whether trusted applications can be redistributed the way other Qualcomm firmware is, or whether they are signed with keys held only by Qualcomm or the OEM.

    Nobody has checked whether linux-firmware already carries a trusted application; that would answer the question by example and is the cheapest place to start. This question decides whether any of this can be packaged for users rather than extracted per device.

  • What the sensor's own image data means. The reference client can save raw frames, but nothing here interprets them; all image processing happens inside the secure world.

What is where

There are eight pieces, and each is independently useful. The first four had to exist for any of it to work.

layer what it does where
Sensor driver owns the sensor's power, reset and interrupt; relays finger events — the secure world expects the hardware powered and out of reset docs/00-sensor-driver.md, branch goodix-fp-spi
TEE driver exposes QSEE trusted applications through Linux's TEE subsystem, so user space can reach them docs/01-kernel-tee-driver.md, branch qcom-qseecom-tee
TA protocol the command set the Goodix application speaks — undocumented, recovered by tracing the Android stack and reading the vendor's binaries docs/02-ta-protocol.md
Listener services the file service the application needs the normal world to run: it has no storage of its own and asks for reads and writes of its encrypted data — also undocumented docs/03-listener-services.md, the stored objects in docs/04-secure-storage.md
Client design investigation and architecture decisions for the supplicant and libfprint driver docs/05-writing-a-client.md
Gatekeeper the separate hardware-backed credential verifier Android uses to authorise enrolment with a signed token docs/06-Gatekeeper-protocol.md
Proof-of-concept client command-line hardware and protocol test tool that implements the complete client flow in one program docs/07-reference-client.md, code in harness/
Production userspace: supplicant and libfprint driver production implementation of the same client responsibilities, divided between machine-wide listener and TA-loading services and the fprintd/libfprint match-on-chip driver docs/08-production-client-stack.md, wrobelda/qsee-supplicant, branch goodix-qsee at wrobelda/libfprint

Read the documents in order. The TA protocol is meaningless without a running listener service, and the listener service cannot be registered without the kernel driver.

Quick start

There are two kernel drivers, with one branch each. Both are based off mainline v7.1 at wrobelda/linux:

branch provides config symbol
qcom-qseecom-tee the TEE driver, reaching trusted applications CONFIG_TEE_QSEECOM
goodix-fp-spi the sensor driver and its binding, powering the sensor CONFIG_INPUT_GOODIX_FP_SPI

The branches are independent series and do not depend on each other. The sequence below needs both of them, plus a device tree node for the sensor; the board support is not on either branch.

With those drivers in place:

The udev rule reads the trusted-application name from the sensor and starts the matching application-loader instance automatically. The supplicant must be ready before the loader and the biometric clients; see the production client lifecycle.

For command-line protocol and hardware testing, build gfharness and use the reference-client instructions. Do not run its listener mode at the same time as the machine-wide supplicant.

Reference platform

Everything documented here was verified on:

  • Device Xiaomi Pad 5 Pro 5G ("enuma"), postmarketOS
  • Sensor Goodix GF3626 (GF_CHIP_3626ZS1, part A005203), behind the power button
  • SoC Qualcomm SM8250, QSEE trusted execution environment
  • Application gfenu, loaded from /vendor/firmware_mnt/image/
  • Sensor driver drivers/input/misc/goodix_fp_spi.c, which owns the regulator, reset line and interrupt, and reports interrupts over netlink

Its device tree node, and the state of its binding, are documented with the driver.

On another model, expect the application and the sensor part to differ, and check the payload sizes; the transport and the listener protocols should not change.

None of what the sensor does is visible to the operating system. The sensor captures images and runs firmware of its own, but it does that across an SPI bus the secure world owns. The only thing that reaches the normal world is an interrupt line saying that something happened.

Every decision is made inside the trusted application:

  • image processing,
  • template construction,
  • matching.

That is why this is a protocol problem rather than a hardware-driver problem. A small hardware driver still exists; it owns the power, reset and interrupt that the secure world leaves to the normal world.

Scope

Three things are stacked here, and they generalise differently:

  • The QSEECOM transport and its listener services are Qualcomm-generic. Any device whose secure world is QSEE speaks this, whatever application is running on top. The kernel driver and listener service documents should apply unchanged.

  • The recovered command set is a versioned Goodix compatibility profile. It is implemented for the positively matched reference platform. ABI compatibility is not established by:

    • similar Goodix hardware,
    • another TA filename,
    • another OEM,
    • another TEE.

    Those cases need a separate profile or driver until tested.

  • What is per-model splits three ways, and each is configured somewhere different.

    • The sensor hardware — regulator, reset line, interrupt — is described in the device tree and driven by the companion char device. Board description belongs there, and this is the only one of the three that is in DT.
    • The trusted application ships with the device's stock operating system, and it was extracted from that system. How far one image travels between models, and whether it may be redistributed at all, is unresolved — see What is not understood.
    • The application name and its payload sizes belong to the application rather than the board. The client passes the name when opening a session, and it has to know the payload layout. The kernel TEE driver is application-agnostic and reads neither from the device tree.

Everything here was established on one device, the reference platform, by the means set out in How we know. Treat the layer boundaries as the guide to what should port and what needs re-checking.

How we know

Claims in these documents come from six different places, and those places are not equally strong. Where it matters, a claim carries a tag:

tag source what would overturn it
([our device]) observed on a Xiaomi Pad 5 Pro 5G running our own client another model, another firmware version, or a code path we never exercised
([vendor trace]) observed by instrumenting the stock Android HAL and qseecomd a vendor path we did not trigger, or a different HAL build
([libdrmfs.so disassembly]) read out of the vendor binary's code, not observed running misreading the binary; a different build of it
([vendor client disassembly]) read from the code, data tables, and exported symbols in this platform's libgf_ca.so, libgf_hal.so, and libgoodixhwfingerprint.so misreading a binary; a different build of the client libraries
([gfenu symbols]) symbol names in the trusted application image names that do not mean what they suggest
([Qualcomm minkipc]) Qualcomm's QTEE listener protocol headers and service implementations in qualcomm/minkipc a device carrying a modified Qualcomm listener ABI

Only the six labels in that table are tags, and each tag in the text links back here. Square brackets elsewhere — for instance "[gf_fts]" — are literal strings quoted from a binary, not provenance.

Untagged statements are either definitions or things that follow directly from tagged ones. Keep two conventions when editing:

  • an absence is almost always "we did not observe X", not "X does not exist";
  • anything learned from one device should say so.

Glossary

The secure world

term meaning
TrustZone / secure world the higher-privilege execution state the kernel cannot inspect; QSEE runs there
QSEE Qualcomm Secure Execution Environment, the TrustZone OS running in the secure world
normal world everything outside TrustZone: Linux, its drivers and user space. It cannot read secure-world memory, and reaches it only through SCM calls
trusted application abbreviated TA. A signed program running under QSEE. Here, gfenu, which owns all biometric logic
SCM call Secure Channel Manager call, the interface from the kernel into the secure world

The two interfaces, and the drivers that expose them

term meaning
QSEECOM / smcinvoke QSEE's two normal-world interfaces. An application is built against one or the other, so they are not alternative routes to the same thing:
QSEECOM — command-based: load an application by name, send it a command buffer, service the listener requests it raises. gfenu and its generation are built against this one.
smcinvoke — object-based: services are objects with operations that return results and further objects. Newer, but not a way to reach QSEECOM-era applications.
TEE subsystem Linux's generic trusted-execution framework (/dev/tee*), which our kernel driver plugs into
memref a TEE parameter that refers to shared memory rather than carrying a value; the request, response and payload buffers are all memrefs
qcomtee / qseecomtee / qcom_qseecom the three kernel drivers in play. The first two are TEE-subsystem backends, each exposing one of QSEE's interfaces to user space; the third is a kernel-internal client with no user-space face:
qcomtee — exposes smcinvoke (drivers/tee/qcomtee/), documented as QTEE. Reports TEE_IMPL_ID_QTEE, advertises TEE_GEN_CAP_OBJREF. This project never talks to it, but it is loaded on the same hardware and takes a TEE device node of its own.
qseecomtee — what this project adds (drivers/tee/qseecom/), exposing QSEECOM. Reports TEE_IMPL_ID_QSEECOM. Where this driver and qcomtee are both loaded there are two TEE devices, and which of them is /dev/tee0 depends on probe order — so a client finds the one it wants by reading impl_id from TEE_IOC_VERSION, never by node name.
qcom_qseecomdrivers/firmware/qcom/qcom_qseecom*.c, letting kernel code talk to a trusted application, qcom_qseecom_uefisecapp for EFI variables. Predates this work, offers user space nothing, gated on a machine allowlist.

Answering the secure world

term meaning
listener service a normal-world service a TA calls out to, identified by a small integer id. The TA blocks until one answers
supplicant the normal-world process that answers listener requests. Called that by the TEE subsystem; qseecomd is Android's
qseecomd the Android userspace daemon Qualcomm ships to answer listener requests — what this project replaces
gpfs GlobalPlatform file system, listener 28672: the sealed-object store

Storage

term meaning
sealed object data encrypted and integrity-protected by QSEE with a device-bound key. The normal world stores ciphertext it cannot read
qsee_sfs_* / qsee_fts_* / fts_* the three layers a stored object passes through, inside the secure world:
qsee_sfs_* — QSEE's secure file system, POSIX-shaped open/read/write/seek/close.
qsee_fts_* — QSEE's sealing API: encrypt, decrypt, integrity protect and verify. Qualcomm's, not Goodix's, despite what it stores.
fts_* — unprefixed, and Goodix's own: wrappers over the two above, and what gfenu calls directly. Attributed to Goodix from the GF_-prefixed constants and a "[gf_fts]" log string, not from the symbol names.
template the enrolled representation of a fingerprint, built inside the secure world and stored as a sealed object. Never an image, and never readable by the normal world
OTP one-time-programmable data fused into the sensor at manufacture, read out at initialisation and persisted as gf_otp_info.so

The applications here

term meaning
gfenu Goodix's fingerprint trusted application as shipped on the Xiaomi Pad 5 Pro 5G. Other models carry Goodix's implementation as their own application, under a different name
gatekeeper Android's credential authority: it verifies the PIN or password and issues signed tokens attesting that it happened. Biometric enrolment expects one of its tokens
auth token the hw_auth_token_t blob Gatekeeper signs and ENROLL carries, attesting that the user authenticated recently
Keymaster / km41.mbn the trusted application that implements Gatekeeper on this platform and signs those tokens. It lives in the dedicated keymaster_a/keymaster_b partition rather than among the dynamically loaded applications in NON-HLOS.bin
miriskm a separate Xiaomi risk-management trusted application. Its own diagnostics and strings concern device-status, registration-token, certificate and remote-auth operations; it is not Android Gatekeeper

Hardware and Linux fingerprint integration

term meaning
sensor driver the kernel driver owning the sensor's supply, reset line and interrupt. In TEE mode that is all it does, the trusted application doing the biometrics
MDT image Qualcomm firmware format: one ELF-shaped image split across files. .mdt holds the ELF header, program-header table and authentication hash; each .bNN holds the payload of program header NN. See the image format
HAL hardware abstraction layer, the Android userspace library a vendor ships to drive a device. The one this project replaces is Goodix's fingerprint HAL
fprintd / libfprint the Linux userspace fingerprint stack used by the implemented Goodix QSEE driver

Licence

GPL-2.0-only, as declared in the source files. The kernel work lives on its own branches and carries the kernel's own licensing: the driver is GPL-2.0-only, while its device tree binding keeps the dual GPL-2.0-only OR BSD-2-Clause that bindings use.

Provenance

The protocol documentation and the reference client are clean-room work for interoperability. They were produced on hardware owned by the author, with the bootloader unlocked by him, using his own fingerprints. They document how to talk to the trusted application; they do not:

  • extract biometric data,
  • recover keys,
  • defeat any check the application makes.

The sensor driver is not clean-room and is not claimed to be. It is a backport of Goodix's downstream driver, GPL-2.0-only, and it carries its original copyrights — see its provenance and licence.

Fingerprint templates are sealed with a device-bound key inside the secure world, and they remain opaque: the storage layer described here stores and returns ciphertext without being able to read it. Where the application enforces something, the documented answer is to satisfy it, never to bypass it.