Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/44-fingerprint-fpc1020.md
2026-08-10 20:46:28 -04:00

24 KiB
Raw Blame History

TASK 44 — Fingerprint (FPC1020) as a capability factor

Status: the TrustZone wall is down (2026-08-01 evening) and the command protocol is recovered (2026-08-09). The kernel loads and authenticates a signed TA, fpctzappfingerprint runs, and its SPI traffic clears the interrupt latch that killed piece 1 — the sensor now sits armed with the line idle-low. The open question — does a finger raise an edge — now has a direct instrument instead of a correlational one: target 10 command 1 is fpc_ta_check_finger_lost. Raised 2026-07-28 (Casey: "a huge win possible"). Size: two separable pieces — one was a session, the other is a research frontier. Do not scope them as one thing. Repos: Pixel3Arch (kernel + DT), souveraine (the factor, if the second piece lands).

The protocol is in Pixel3Arch/docs/fpc-ta-protocol.md. The application is not stripped, so its dispatch tables resolve to real function names — eight targets, and the full bio lifecycle (begin_enrol, enrol, end_enrol, identify, get_template_ids, delete_template) at target 11. Recovered statically from the vendor image on disk: no device, no Android, no Frida. Two names below are wrong and the doc corrects them — sensor command 3 is wakeup_setup and command 5 is deep_sleep, not "idle".

Read the 18:5x section below before the 16:0x one. The earlier measurement concluded piece 1 "does not exist on this hardware"; that conclusion was correct about the AP acting alone and is superseded by the TA path.

The substrate was extracted — but from the wrong driver

Pixel3Arch/staging/fingerprint/ held this from 2026-07-07 and was never referenced by a build. Two corrections came out of actually wiring it (2026-08-01):

1. The staged fpc1020_platform_tee.c is the variant blueline does not build. LOS carries two copies, and staging/'s is byte-identical to drivers/input/misc/fpc_fingerprint/. b1c1_defconfig sets CONFIG_FPR_FPC=y — the other one, drivers/input/misc/fpr_FingerprintCard/. The difference is load-bearing: the shipping driver's vreg_conf[] is empty, so ARRAY_SIZE is 0 and device_prepare() manages no regulators at all. The rail is always-on and the vendor deliberately gutted the table. Anything scoped around this driver's power management was scoped around code that does not run on this phone.

2. The pin list is confirmed, and the FPC1075 caution below is resolved. This unit's own DTBO (~/pixel3-work/dtbo_9.dts, fragment@40, fp_fpc1020) carries compatible = "fpc,fpc1020", fpc,gpio_rst = <… 0x07 …>, fpc,gpio_irq = <… 0x79 …> (121), vdd_io-supply = <&pm8998_s4>. Identical to LOS's own sdm845-b1c1-fingerprint.dtsi. There is no FPC1075 on this board and nothing reserves GPIO 8184.

Note pm8998_s4 is a downstream label that does not exist in our tree — the same rail is vreg_s4a_1p8 (a regulator-fixed, always-on, boot-on 1.8 V). The staged .dtsi also uses the retired mux {}/config {} pinctrl style and fpc,gpio_* properties that gpiod cannot resolve, so it could never have been included as written.

Read the driver before planning around it

fpc1020_platform_tee.c is not a fingerprint driver. It is a power, reset and interrupt shim with a sysfs face: clk_enable, pinctl_set, regulator_enable, hw_reset, device_prepare, wakeup_enable, irq. There is no image capture in it and no matching. On Android the sensor's SPI data path belongs to a TrustZone application under QSEECOM — the AP never sees a ridge image; it sees "a finger arrived" and later "the TA says yes."

That single fact splits this task, and it is why the two halves must not share a schedule.

Piece 1 — the sensor as an input and a wake source (BUILT 2026-08-01)

Written as drivers/input/misc/fpc1020.c on the fingerprint-fpc1020 branch of linux-blueline (6f637c4c2), not as a copy of the staged vendor file. The 2015 original cannot compile here: of_gpio.h is gone, wakeup_source_init /_trash are gone, platform_driver.remove returns void. The port uses descriptor GPIOs, devm_pm_set_wake_irq, and an implicit default pinctrl state in place of the three named reset/irq states.

The vendor sysfs surface is not shipped — no device_prepare, hw_reset, regulator_enable, clk_enable, irq. That is a control interface for a TA which is not loaded here, and a poll()-driven sysfs node is not how input is routed on this device. The driver registers an input device reporting KEY_WAKEUP instead, which puts the reader on the same evdev path as everything else — so it becomes a row in the existing gesture table rather than a new hardcoded branch (INTERFACE-ARCHITECTURE §4). TASK-13 fought exactly this and the rule came out of it.

DT is in sdm845-google-blueline.dts: interrupts-extended = <&tlmm 121 IRQ_TYPE_EDGE_RISING>, reset-gpios = <&tlmm 7 GPIO_ACTIVE_LOW> (active-low — the polarity trap that cost a day on FTS, PAF/touch.md), vdd_io-supply = <&vreg_s4a_1p8>, plus an fp-default-state pinctrl group. CONFIG_INPUT_FPC1020=m is in kernel/config-blueline.aarch64 and survives olddefconfig in place, so the PKGBUILD drift guard stays quiet.

Measured on glass 2026-08-01 — the AP half is complete, and it is not enough

Everything the AP owns is correct and verified on the running kernel (7.1.1-sdm845-g8eebcaff65ef): module loaded, driver bound at /sys/bus/platform/drivers/fpc1020, FPC1020 Fingerprint Sensor registered as input4/event4 advertising KEY_WAKEUP, line 7: output active-low consumer="reset", and IRQ 168 wired as msmgpio 121 Edge fpc1020.

And a touch produces nothing. evtest on event4 for 60 s across repeated touches: zero events, and the interrupt count never moved.

The reason is measurable, not inferred:

  • gpioget --chip gpiochip2 121 reads active — the IRQ line is stuck high, so an IRQ_TYPE_EDGE_RISING request can only ever see one edge.
  • Unbind the driver and the line stays high, against the DT's bias-pull-down. The sensor is driving it, not a pull or a stale config.
  • Rebind and the count goes 1 → 2, one edge per reset, then latched again.

So the FPC1020 asserts its interrupt at power-up and holds it until the host clears it over SPI. That is the "half a driver" this task already named, now with a number on it: without the SPI side the line latches once and the pin is dead thereafter.

There is no SPI path to it from the AP. The device's own DTBO gives fp_fpc1020 exactly interrupts, fpc,gpio_rst, fpc,gpio_irq and vdd_io-supply — no SPI node, no bus. The one nearby SPI, qupv3_se0_spi (fragment@36, TLMM gpio03), hosts intel,mnh-spi — the Pixel Visual Core. The fingerprint's SPI is not in the AP's device tree anywhere.

Therefore piece 1, as scoped above, does not exist on this hardware. "Wire the DT and the phone gains a real button under the reader" was the premise, and it is wrong: a finger cannot raise an edge the AP can see, because the first edge is already spent and only a TrustZone SPI transaction rearms it. The driver is correct and worth keeping — it is the AP half of whatever piece 2 becomes — but it is not an input, not a wake source, and must not be described as either.

SUPERSEDED the same evening — and by its own prediction. "Only a TrustZone SPI transaction rearms it" was the exit condition, and the TA path below satisfies it: the latch clears and the line returns to idle-low. What stays true is the narrower claim that the AP alone can never rearm the sensor. Piece 1 does not exist without TZ; with the TA loaded it is no longer blocked on the hardware.

What this does to Mis012's GPIO-mode lead. Driving the sensor in GPIO mode means bit-banging its SPI from TLMM pins, which requires knowing which pins carry it. On blueline they are absent from the DTBO, so the lead is not bounded from here — it needs a schematic, a probe, or a downstream source that names the pins. Worth exactly that much investigation and no more before it is believed.

This piece does not authenticate anything. Do not let it get described as fingerprint unlock, in a commit or a settings string. An input that looks like an auth factor and is not is worse than no input.

Measured 2026-08-01 18:5x — the latch clears, and the sensor does not free-run

First cold boot on 7.1.1-sdm845-g08e9db54cfce. The TA loads from a cold start with nothing resident: app_id=2, and init (0), idle (5) and arm (3) each return 0x00000000.

The interrupt latch clears. This is the measurement that moves the task. At 16:0x the line read active and stayed high with the driver unbound — the sensor driving it against the DT's bias-pull-down. After the TA sequence, /sys/kernel/debug/gpio reads:

gpio121 : in  low  func0 2mA pull down

Idle-low, pull-down effective, and stable across seconds. An IRQ_TYPE_EDGE_RISING request can fire again, which it could not before. Read the level through debugfs, not gpioget — while the driver holds the line, a request returns EBUSY.

Edges are TA-driven SPI traffic, and every one is accounted for. Counted from /proc/interrupts around each command:

command IRQs line after
init (0) +68 low
idle (5) +1 low
arm (3) +1 low

68 edges on init is a real register conversation with the sensor, not noise. The boot count went 1 → 91 over the first sequence and 91 → 161 over a second.

Free-running is refuted. Armed once, never re-armed, 15 s with nothing on the reader: 0 hardware IRQs, 0 poll notifications, and the count held flat per-second for the whole window. The 1444-event run that raised this doubt re-armed on every interrupt, so one assert became a loop — the loop was the signal, not the sensor. Any future instrument must arm exactly once per window; fp-window.py on the phone does, and prints per-second deltas so a real detection can be seen to cluster rather than spread.

The chain reaches userspace. With the sensor armed and idle-low, an assert was caught end to end:

EDGE  at t+94.73s  +1  total=169  line=high
EVDEV at t+94.73s  code=143 value=1   <-- KEY_WAKEUP
EVDEV at t+94.73s  code=143 value=0   <-- KEY_WAKEUP

Sensor → IRQ 168 → driver → KEY_WAKEUP on /dev/input/event4, same timestamp. That is the evdev path hyprland.lua's existing XF86WakeUp bind already listens on, so the routing question is answered. Across ~120 s of armed-but-untouched windows the count never moved; every assert has come in a window where a touch was being attempted. Attribution to a finger still rests on the human confirming the touch — the rig cannot tell a finger from any other cause, so say "an assert" until someone says "I touched it then".

wakeup_enable and device_prepare take the vendor's enable/disable strings, not 0/1 — a 1 returns EINVAL (wakeup_enable_store). The driver gates the key on it, so KEY_WAKEUP reaches evdev only after echo enable > wakeup_enable. Anything productionising this must set it.

Why the 1444-event run happened — solved, and it constrains the design

arm (cmd 3) generates its own edge, +1, measured. So a handler that re-arms on every interrupt feeds itself: arm → edge → handler → arm → edge, without end. The 1444 events were the instrument, not the sensor and not a finger.

That is a design constraint, not a footnote. The sensor asserts once per arm and then latches high until the TA clears it, so a usable input must re-arm after every event — and a naive re-arm is exactly the storm. Whatever lands has to suppress the arm's own edge (ignore the edge that follows an arm within a few ms, or ask the TA for finger state instead of re-arming blind). Do not ship a re-arm loop that has not solved this.

Piece 2 — biometrics, which is a TrustZone question

Matching needs either QSEECOM and Google's TA, or an open SPI image path with our own matcher. Neither exists here today.

Correction 2026-08-01 — this section used to point at PAF/evidence/secure_load_tz_frontier.md. That is the wrong doc. It is marked SUPERSEDED at the top and it is about qcom_scm_assign_mem returning 22 and the modem's XPU faulting — memory ownership transfer, a different SCM surface from loading and calling a TA. It answers nothing here. The standing record for this question is PAF/evidence/tz_listener_archaeology.md plus the four qseecom: commits on the blueline branch.

Read together, they say the question is half answered, and better than expected:

  • google,blueline is in qcom_scm_qseecom_allowlist; the QSEECOM version query already succeeds on this device (2e172dcb3). TZ answers us.
  • Listener registration works against this device's real signed TZ image — id 0x5000 (ssd) returns QSEECOM_RESULT_SUCCESS, live-tested 2026-07-02. The archaeology doc had listed "whether this exact signed TZ image accepts registration from a differently-built AP driver" as unknown and untestable without trying it. It was tried. It works.
  • /dev/qseecom exists with GET_APP_ID + SEND_CMD wrapping qcom_scm_qseecom_app_get_id/_app_send (5fa36934b) — but that commit says plainly: compiles clean, never run on hardware.

So what is actually missing is narrower than "can we talk to TZ":

  1. There is no app-loading path. BUILT 2026-08-01. qcom_scm_qseecom_app_load() — app-mgr command 1 under QSEE_OS, three VAL params (mdt_len, img_len, phys), downstream's TZ_OS_APP_START_ID (qseecomi.h:456) — plus a LOAD_APP ioctl that reassembles the QSEE image the way TZ expects: the .mdt ELF header blob plus one .bNN per program header, laid out contiguously at p_paddr-relative offsets, staged with dma_alloc_coherent under a 32-bit mask because the request carries a 32-bit physical address. Commits d7d2e1222, cb150d71d, 4d52867fe.
  2. Nothing has ever exercised app_send live. DONE. Three commands round-tripped to a real TA on hardware, all 0x00000000.
  3. The FPC TA blob stays vendor-opaque, but its command protocol is no longer entirely so: 0 init, 3 arm, 5 idle, sent through a 0x58-byte shared aux buffer (fpc_tac_alloc_shared) whose first two words are 0x0A and the command. Payload-in-a-second-buffer is ac9a4dd2a.

The prediction below was tested and resolved. Every GET_APP_ID returned -ENOENT — for all names, not just uefisecapp — because TZ only resolves apps something already started, and on Android that something is qseecomd. So "apps must be loaded before they can be called" was the right branch, and loading is now built rather than a frontier.

Cheapest next probe, and it is small: run the existing /dev/qseecom GET_APP_ID against candidate names on the device. uefisecapp is already confirmed ENOENT; downstream loads keymaster as 'keymaste'. If any name resolves, resident-app calling is real and only FP-specific work remains. If none does, apps must be loaded before they can be called, and that is the frontier — not the SCM channel.

If the answer stays no, the honest outcome is Piece 1 plus a note, and face auth (TASK-12) is the cheaper capability factor. Say so rather than leaving this open forever.

Where it plugs in — and it is a daemon, not a script

The capability tier model already has the shape — StepUpAuth.qml, LockContext.qml, and the souveraine-stepup PAM service named in sessiond/protocol.rs:36. TASK-12 scopes face auth against exactly those primitives and explicitly refuses to wrap gazed: "we own the source — anything touching the permission/capability gating is in-tree, not vendored." A fingerprint factor is symmetric with it and inherits that decision.

So the deliverable is souveraine-fpd: a system daemon owning /dev/qseecom and input4, driving the TA through the protocol in Pixel3Arch/docs/fpc-ta-protocol.md, exposing enrol/verify over D-Bus the way gazed does, and minting through souveraine-stepup. The fp-*.py tools are instruments, not the factor.

Temporary post-login Polkit factor — built 2026-08-10

The smaller blueline-fingerprintd producer now has two bounded outputs from the same real FPC assertion:

  • /run/blueline-fingerprintd/preview-pulse is root-owned and world-readable. The Souveraine lock surface may use it only for the optional wiring exercise.
  • /run/blueline-fingerprintd/polkit-approval is root-only, expires after eight seconds, and is atomically consumed exactly once by pam_souveraine_fpc.so. It exists only long enough to satisfy a blank polkit-1 PAM conversation after the fullscreen Polkit surface has shown its three-second confirmation interval.

This is an intentional bridge while match-on-chip verification lands. It is a real PAM result, not a shell-side boolean: the fullscreen Polkit surface continues to own the prompt, PAM decides success, and the ordinary PIN/password conversation remains its fallback. It covers user-facing Polkit only. It cannot satisfy first login, first storage unlock after boot, screen-lock PAM, sudo, SSH, remote admission, or household/device enrollment. A reboot erases the record and returns the device to PIN-required.

The bridge does not claim continuous finger presence or a verified fingerprint. The current hardware proof is one re-arm-safe FPC IRQ, not a template match; the visible three seconds make the temporary interaction deliberate rather than pretending to add biometric evidence. The package inserts a narrowly marked pam_souveraine_fpc.so line ahead of the existing polkit-1 auth stack and saves the preimage. If a Polkit upgrade produces a .pacnew, merge the marked three-line block deliberately; do not let a package upgrade silently erase the factor.

The reader's KEY_WAKEUP stays mapped to panel wake only: the FTS touch driver emits the same generic key, so binding that key inside Hyprland would falsely call a touchscreen double-tap an FPC event. The FPC daemon record, not the generic key, is the only source the Polkit UI observes.

The intended enduring flow remains PIN after boot, verified FPC after that. The PIN performs the first unlock that makes Personal-class key material and the enrolled FPC template database available. souveraine-fpd will replace this raw-IRQ bridge with a verified match result without changing the Polkit surface or its PAM ownership. A verified match may then unlock a later screen lock or mint a narrow read/step-up grant, according to the requested operation; it never replaces first-boot storage unlock, household membership, or remote-device admission.

Prior art, checked 2026-08-09

sailfishos-open/sailfish-fpd-community Live (Nov 2025). Implements Jolla's org.sailfishos.fingerprint1, but reaches hardware through an Android library (libbiometry_fp_api, built in HADK, descended from Ubuntu Touch's biometryd). Unusable here — Souveraine has no hybris. Its D-Bus surface and state machine are worth copying, and its README doubles as a reverse-engineering method note.
wrobelda/libfprint branch goodix-qsee The only working example of a libfprint driver doing match-on-chip over QSEE. Different TA, same architecture. The template if we ever want fprintd.
libfprint match-on-chip Supported upstream — device features cover on-device storage and identify, so match-on-chip is not fighting the framework.
FPC over QSEE, natively on Linux Nobody has done it. No prior art found.

The primitives all agree, which settles the API

Cloned to Pixel3Arch/references/biometrics/: fprintd, libfprint (with wrobelda/goodix-qsee fetched as a remote branch), sailfish-fpd-community, biometryd.

Reading their interfaces side by side, they are the same API three times over — and TASK-12's gazed is a fourth. fprintd has Claim/Release, VerifyStart/VerifyStop, EnrollStart/EnrollStop, ListEnrolledFingers, DeleteEnrolledFinger, signals VerifyStatus/VerifyFingerMatched/ EnrollStatus, properties finger-present/finger-needed/ num-enroll-stages. gazed has claim/release, verify_start/stop, enroll_start/stop with a status signal carrying capture quality. Sailfish adds the enum worth stealing wholesale: FPACQUIRED_GOOD/PARTIAL/ INSUFFICIENT/IMAGER_DIRTY/TOO_SLOW/TOO_FAST.

So there is no API to design. Take that shape, and the face factor and the finger factor become the same surface with two backends — which is what symmetry with TASK-12 was supposed to mean.

It maps onto the recovered protocol without a gap:

daemon call TA commands
EnrollStart 11/6 load_empty_db (first run), 11/9 set_active_fingerprint_set, 11/0 begin_enrol
enroll stage 10/4 qualify_capture → 11/1 enrol; remaining count drives num-enroll-stages
EnrollStop 11/2 end_enrol
VerifyStart 10/3 wakeup_setup, then on IRQ 10/1 check_finger_lost → 10/4 → 11/3 identify
ListEnrolledFingers 11/7 get_template_ids
DeleteEnrolledFinger 11/8 delete_template
finger-present 10/1 check_finger_lost
acquired quality capture/qualify return codes → the Sailfish enum
persistence target 2, via the supplicant — see below

wrobelda/goodix-qsee is the structural template for the backend: 1,418 lines split into -transport.c (the TEE channel), -sensor.c (the misc device), -token.c (the auth token), -proto.h (the command table) and the driver proper. Those four files are exactly our four layers — /dev/qseecom, the fpc1020 input device, target 3, and fpc-ta-protocol.md.

The listener is a separate owner, and that is a constraint

Templates only persist through target 2, which goes out to a QSEE listener. The kernel allows one receiver per device, not per application, so whoever registers the file-service listener locks out every other QSEE client. That argues the listener does not belong inside souveraine-fpd — it belongs in one machine-wide supplicant, with the fingerprint daemon as a client of it. wrobelda/qsee-supplicant is that component, is TA-agnostic, and made this argument first.

Order that falls out: sensor attribution (fp-finger.py) → RAM-only enrol and identify → supplicant + target 2 for persistence → daemon → PAM factor. Only the last two are the "app".

Do not

  • Do not build Piece 2 first. The cheap win is real and independent.
  • Do not ship the shim's sysfs to userspace as an API. The port drops that surface entirelyAMENDED 2026-08-01 (329737a23): the surface is back, because the premise it rested on ("a vendor interface for a TA that does not exist here") stopped being true the moment the TA loaded. The TZ stack needs device_prepare/hw_reset/wakeup_enable to sequence the sensor. The prohibition still holds where it was aimed: these are root-only and write-only on the device (--w-------; only irq is readable), and none of them may become a settings switch or an agent verb. A control that reports success while authenticating nothing is exactly the failure this bullet was written against — that argument survives, only its factual premise changed.
  • Do not confuse FPC1020 with the FPC1075 — RESOLVED 2026-08-01. This unit's own DTBO carries FPC1020 on gpio7/gpio121 and nothing reserves GPIO 8184. TASK-45's pin-conflict problem (GPIO 85/86, SE5) is real and separate; it is a neighbouring range, not this one.
  • Do not take staging/fingerprint/ as the reference. It is the fpc_fingerprint variant, which blueline does not build, and its .dtsi uses downstream labels and retired pinctrl syntax. The authority is references/los-kernel-blueline-4.9/drivers/input/misc/fpr_FingerprintCard/ plus this unit's DTBO.

Connects to

TASK-12 (face auth — the other capability factor, and the fallback if TZ says no), TASK-40 (what a biometric stream permits inferring), SECURITY-AUDIT.md, SESSION-AUTHORITY-DOCTRINE.md §2 (reveal-on-lock-surface — "a fingerprint touch reveals personal content in place, without unlocking the session" — is gated on this sensor; piece 1 does not deliver it, piece 2 does), PAF/evidence/tz_listener_archaeology.md (the TZ record that actually applies).