Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/44-fingerprint-fpc1020.md
Fimeg bde961c6f2 saf: one spine — device, state, and work under the index
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
2026-08-18 09:47:30 -04:00

433 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, `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 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
`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 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),
`saf/device/evidence/tz_listener_archaeology.md` (the TZ record that actually applies).