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

@ -202,6 +202,23 @@ int main() {
Check(tokenZero, "the 69-byte auth token is all zero");
}
// ---- SET_ACTIVE_GROUP: a gid and a NAMESPACE path, not a file path
{
auto sag = BuildSetActiveGroup(60);
Check(Get32(sag, SetActiveGroupGidOff) == 60, "gid at +0");
std::string path;
for (std::size_t i = SetActiveGroupPathOff; i < sag.size() - 1; i++)
path.push_back(static_cast<char>(std::to_integer<unsigned char>(sag[i])));
Check(path == "/data/vendor_de/0/fpdata", "the Android namespace path");
Check(sag.back() == std::byte{0}, "NUL-terminated");
Check(sag.size() == SetActiveGroupPathOff + GroupNamespacePath.size() + 1,
"length is 4 + path + NUL");
// The path is a key the trustlet hashes into the group directory name,
// so it is not ours to invent. A gid rendered as text is not it.
Check(GroupNamespacePath != "60", "the second field is not the gid again");
Check(GroupNamespacePath.starts_with('/'), "it looks like a path because it is one");
}
// ---- Responses: the payload starts at +0x10, and forgetting that reads
// a confident zero.
{