Serve QTEE's storage: the enrolled template loads

The whole storage path now works from the daemon. On the phone, against the
real store:

    listener 0x7000  sb=516096  -> result=0  REGISTERED
    listener 0x2000  sb=25600   -> result=0  REGISTERED
    SET_ACTIVE_GROUP gid=60 path='/data/vendor_de/0/fpdata'
      gpfile READ .../1lPrxAL0vXRvWPeDkW2c off=4096 len=252114
      ...
      CMD 0x2005 -> result=0 rc=1
      templates loaded: 1

QTEE read a 252114-byte enrolled template through our gpfile listener, verified
it, and loaded it. Since QTEE unlinks any container whose keyed integrity tag
fails, a load is proof the framing is right -- the read/write offset split, the
container chunking, and the RPMB anti-rollback read that has to succeed before
QTEE will trust any of it.

RPMB is served too: SECURITY PROTOCOL IN/OUT against the RPMB well-known LUN,
retrying the unit attention the LUN raises once after a reset. Writes are
refused unless asked for, because they advance a counter that cannot be moved
back, and key programming is refused unconditionally.

The store was served READ-ONLY throughout, which is the point. A listener that
serves bytes at the wrong offset does not merely fail: QTEE deletes the
container it cannot verify, and that is an enrolled fingerprint gone. Read-only
makes a wrong build harmless, so it is the default and writing is opt-in.

Two ordering facts, both of which produce -2 with no storage read at all --
indistinguishable from a broken listener:

  * a template reload needs the device init chain to have run FIRST, because
    that chain allocates the per-slot array the reload writes through;
  * SET_ACTIVE_GROUP's second field is a NAMESPACE path, not a filesystem one
    and not the gid again. The trustlet hashes it into the group's directory
    name, so it has to match what the store was written under.

Also: a positive rc is not an error code. ENUMERATE returns the template count
there, and running that through the error table printed "unknown" for a good
answer.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 18:42:20 +02:00
commit 03023284ba
3 changed files with 432 additions and 7 deletions

View file

@ -253,7 +253,27 @@ export namespace fingerprintd::ta {
// "templates with gid(%u != %u) hasn't been loaded." and returning -200 on
// a mismatch. So the two only have to agree with each other — the value
// itself is the caller's to choose.
//
// Its payload is {u32 gid; char path[]} and the path is NOT a filesystem
// path we control: the trustlet hashes it into the SFS group's directory
// name, so it is a namespace key and it has to match whatever the store
// was written under. The store on this device was written by the Android
// stack under its data directory, and every group in it derives from that
// string. Passing anything else resolves a different group, finds nothing
// and answers -2 -- with no storage read at all, which reads like a
// listener failure and is not one.
inline constexpr std::size_t SetActiveGroupGidOff = 0;
inline constexpr std::size_t SetActiveGroupPathOff = 4;
inline constexpr std::string_view GroupNamespacePath = "/data/vendor_de/0/fpdata";
inline std::vector<std::byte> BuildSetActiveGroup(
std::uint32_t gid, std::string_view path = GroupNamespacePath) {
std::vector<std::byte> out(SetActiveGroupPathOff + path.size() + 1, std::byte{0});
detail::StoreU32(out, SetActiveGroupGidOff, gid);
for (std::size_t i = 0; i < path.size(); i++)
out[SetActiveGroupPathOff + i] = static_cast<std::byte>(path[i]);
return out;
}
// ---- The request/response envelope ------------------------------------
//