PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
433 lines
24 KiB
Markdown
433 lines
24 KiB
Markdown
# 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 81–84.
|
||
|
||
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, `saf/device/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 gpio0–3), 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
|
||
`saf/device/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 **`saf/device/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 entirely~~ — **AMENDED 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 81–84. 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),
|
||
`saf/device/evidence/tz_listener_archaeology.md` (the TZ record that actually applies).
|