Skip to content

The FPC trustlet commits an enrolment only with a Gatekeeper/keymaster-signed auth token, which postmarketOS cannot produce

scope: soc:msm8998 · severity: finding · confidence: proven · subsystem: firmware

Everything below enrolment-commit works; the commit is the wall. On the Pixel 2 XL the FPC trustlet loads, initialises the sensor, captures a real finger, and accepts every enrolment sample – and then refuses END_ENROL with -201. The refusal is not ours: fprintd’s driver has built a complete template by then. It is the trustlet’s hardware-authorization gate.

The FPC secure-enrolment design (identical in Sony’s open vendor-sony-oss-fingerprint HAL) is: preEnroll gets a challenge (GET_ENROL_CHALLENGE {0x03,0x02}), Android’s Gatekeeper signs a 69-byte hardware auth token (HAT) carrying that challenge, AUTHORIZE_ENROL {0x03,0x03} verifies the signed token, and only an authorized session may END_ENROL. The HMAC key the trustlet verifies with is fetched from the keymaster64 trustlet and installed with FPC_SET_KEY_DATA; Gatekeeper holds the same key. Both live in TrustZone and neither exposes the raw key.

postmarketOS has no Gatekeeper, so it cannot produce a validly-signed HAT, so AUTHORIZE_ENROL cannot pass, so END_ENROL cannot commit. Setting the keymaster key would only enable verification, not bypass it. The gate is fail-closed (the verifier returns -201 when the token is absent, unsigned, or wrongly signed).

What this does NOT block: loading the trustlet, sensor init, capture, IDENTIFY, and the template database listeners – all proven. Matching a finger against an already-enrolled template needs no token. But templates can only be created by an authorized enrolment, so on a device that never enrolled under Android there is nothing to match against.

Update 2026-09-14 – most of the feared work turned out unnecessary, and the path is now blocked on one thing: RPMB. Measured on the device:

  • keymaster64 is ALREADY RESIDENT (the bootloader loads it): app_get_id("keymaster64") = id 65537, and it answers KEYMASTER_GET_AUTH_TOKEN_KEY (cmd 0x205) with a 152-byte sealed blob (per-call IV + counter, labelled “keymaster64” -> “fingerprint”). So no loading the 64-bit trustlet out of the bootloader keymaster partition, and no keymaster RPMB bring-up on our side, is needed for the key.
  • Handing that blob to the FPC trustlet as SET_KEY_DATA {0x03,0x05} – the vendor init step this stack never did – answers status 0. The verifier is then armed: AUTHORIZE_ENROL with an unsigned token still answers -201, now for the right reason (bad signature, not absent key).
  • There is no separate Gatekeeper trustlet: the vendor gatekeeper.msm8998.so (decompiled) drives the same keymaster64 app. Protocol: enrol {0x1001, uid, cur_handle, cur_pwd, pwd} -> a password handle; verify {0x1002, uid, u64 challenge, handle, pwd} -> the 69-byte HAT. req_len = (payload + 0x5f) & ~0x3f, response {status, off, len}.
  • Gatekeeper enrol answers -30 (nothing written): it stores its handle and throttle counters in RPMB, QSEE refuses a request for an unregistered listener, and no one registers RPMB. librpmb.so registers it with a 0x6400 buffer over UFS SG_IO. The running kernel has CONFIG_RPMB and CONFIG_SCSI_UFS_BSG both off; the UFS RPMB well-known LUN (0:0:0:49476) is present but unexposed.

So the remaining work is an RPMB relay: expose the UFS RPMB LUN (CONFIG_SCSI_UFS_BSG and/or CONFIG_RPMB), register listener 0x2000, and relay the trustlet’s RPMB frames (SECURITY PROTOCOL IN/OUT) to it – the supplicant pattern again, and shared infrastructure with disk encryption. Then: keymaster key -> FPC SET_KEY_DATA, gatekeeper enrol+verify a PIN -> HAT, FPC AUTHORIZE_ENROL(HAT) -> enrol. Probes: taimen bringup/qsee-km-probe/.

Related: a-qsee-trustlet-stores-files-through-two-listeners, a-trustzone-command-can-succeed-and-do-nothing.