Add enrolment, and let it choose its own namespace

Enrolment is the first thing here that writes: template containers through the
gpfile listener and counter records through RPMB. It refuses to run unless both
--sfs-writable and --rpmb-write are given, and it refuses to call SAVE_DATA if
the sample count did not reach zero, because a partial template is worse than
none.

The sequence is stock's: cancel, reset-lockout, authenticate, cancel,
reset-lockout, PRE_ENROLL, authenticate, cancel, ENROLL, the sample loop,
POST_ENROLL, SAVE_DATA with bit 30 set. AUTHENTICATE is what arms the capture
session, which is why it appears in an enrolment at all.

Enrolment takes one sample per PRESS: touch on the rising edge, release on the
falling one, nothing in between. Stock's entire enrolment trace contains no
image-ready event, and feeding every held frame gives the algorithm
near-duplicate images from a single press.

Two things named honestly. The ENROLL payload's u32 at +69 was recorded here as
a "timeout"; the trustlet reports it back as the GROUP ID, and filling a
mislabelled field with a plausible number is the entire provenance of gid 60.
It is the gid now, so an enrolment can choose its own group.

And --group-path exposes the namespace key the trustlet hashes into the group's
directory name. It defaults to Android's, which is where this device's existing
store lives and how that template is readable. But SAVE_DATA rewrites the
group's index container, and an index QTEE later fails to verify takes every
template listed in it -- so enrolling into a DIFFERENT namespace is complete
isolation from a store we did not write.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 20:12:24 +02:00
commit 4e8a763cf9
3 changed files with 146 additions and 9 deletions

View file

@ -41,6 +41,7 @@ export namespace fingerprintd::ta {
Enroll = 0x2001,
PostEnroll = 0x2002,
Cancel = 0x2004,
ResetLockout = 0x200a,
Enumerate = 0x2005,
SetActiveGroup = 0x2007,
Authenticate = 0x2008,
@ -208,11 +209,17 @@ export namespace fingerprintd::ta {
// ---- ENROLL / AUTHENTICATE payloads -----------------------------------
// ENROLL takes a 69-byte hw_auth_token, a u32 timeout at +69 and a u8 flag
// at +73 (stub 0xa0c8).
// ENROLL takes a 69-byte hw_auth_token, a u32 at +69 and a u8 flag at +73
// (stub 0xa0c8).
//
// The u32 at +69 was recorded in this project as a "timeout". It is not:
// the trustlet reports it back as the GROUP ID. Setting it to 60 is where
// `gid = 60` came from, and the whole gid-60 store exists because a
// mislabelled field was filled with a plausible-looking number. Naming it
// honestly is what makes an enrolment able to choose its own group.
inline constexpr std::size_t EnrollPayloadSize = 74;
inline constexpr std::size_t EnrollTokenSize = 69;
inline constexpr std::size_t EnrollTimeoutOff = 69;
inline constexpr std::size_t EnrollGidOff = 69;
// No Gatekeeper is needed. ff_trustlet_enroll reads config
// trustlet.enable_trusted_enrollment and, when false, skips the version
@ -220,9 +227,9 @@ export namespace fingerprintd::ta {
// outright (0xce34 tbz -> 0xd198), so an all-zero token is accepted.
// pmOS has no Gatekeeper to mint one and nothing there verifies auth
// tokens anyway.
inline void BuildEnrollPayload(std::span<std::byte> out, std::uint32_t timeoutSeconds) {
inline void BuildEnrollPayload(std::span<std::byte> out, std::uint32_t gid) {
std::ranges::fill(out.first(EnrollPayloadSize), std::byte{0});
detail::StoreU32(out, EnrollTimeoutOff, timeoutSeconds);
detail::StoreU32(out, EnrollGidOff, gid);
}
// AUTHENTICATE (TA 0xea88 takes these as x0/w1/w2/w3):