Load the trustlet and configure it

On the phone, end to end from the daemon:

    lookupTA('focal64') -> result=23 (nothing to unload)
    trustlet loaded from /lib/firmware/focal64.mbn, distName='fingerprint'
    config /lib/firmware/fingerprintd.json: 349 bytes
      CMD 0x100d -> result=0 rc=0 (Success)
      CMD 0x2005 -> result=0 rc=0 (Success)

The config is the one fp6fpcfg.py --daemon generates, so the reduction of nine
hand-edited ffcfg files to one generator is confirmed against the trustlet
rather than only against the files it replaced.

A stale instance is unloaded before loading, which is what stops a crashed
experiment costing a reboot; result=23 is the clean-slate answer.

The request envelope moves into Fingerprintd:Ta with the rest of the layouts:
command id at +0, declared length at +4, payload at +0x10, and on the way back
the trustlet's own rc at +8 and the capture metric at +0x0c. Both are HEADER
fields ahead of the payload -- the metric has been miscalled "payload+12" in
this project's notes, and every recorded finger number depends on reading it
where it actually is.

ENUMERATE answering rc=0 is correct here and not a regression: no group is
active and no storage listeners are registered yet, so there are no templates
to count.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 18:19:26 +02:00
commit 2648e46d43
3 changed files with 225 additions and 2 deletions

View file

@ -45,7 +45,10 @@ import Fingerprintd;
namespace {
constexpr const char* Version = "0.0.2";
constexpr const char* Version = "0.0.3";
std::string g_taPath = "/lib/firmware/focal64.mbn";
std::string g_cfgPath = "/lib/firmware/fingerprintd.json";
qcomtee_object* g_root = QCOMTEE_OBJECT_NULL;
@ -212,6 +215,118 @@ qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) {
return p[1].object;
}
// ---- The trustlet
//
// The loader is IQSEEComCompatAppLoader (UID 122): op 1 loadFromBuffer, op 2
// lookupTA. A stale instance from a crashed run is unloaded first, which is
// what stops a bad experiment costing a reboot.
constexpr const char* TaName = "focal64";
void UnloadStale(qcomtee_object* loader) {
qcomtee_param p[3] = {};
std::array<std::byte, 4> ob{};
p[0].attr = QCOMTEE_UBUF_INPUT;
p[0].ubuf.addr = const_cast<char*>(TaName);
p[0].ubuf.size = std::strlen(TaName);
p[1].attr = QCOMTEE_UBUF_OUTPUT;
p[1].ubuf.addr = ob.data();
p[1].ubuf.size = ob.size();
p[2].attr = QCOMTEE_OBJREF_OUTPUT;
qcomtee_result_t result = 0;
if (qcomtee_object_invoke(loader, 2, p, 3, &result) || result) {
std::println("lookupTA('{}') -> result={} (nothing to unload)", TaName,
static_cast<int>(result));
return;
}
if (!qcomtee_object_invoke(p[2].object, 2, nullptr, 0, &result))
std::println("unloaded a stale '{}' -> result={}", TaName, static_cast<int>(result));
qcomtee_object_refs_dec(p[2].object);
}
qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) {
UnloadStale(loader);
std::ifstream f(path, std::ios::binary);
if (!f) {
std::println(std::cerr, "cannot open {}", path);
return QCOMTEE_OBJECT_NULL;
}
std::vector<char> image((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
if (image.empty()) {
std::println(std::cerr, "{} is empty", path);
return QCOMTEE_OBJECT_NULL;
}
std::array<char, 128> distName{};
qcomtee_param p[4] = {};
p[0].attr = QCOMTEE_UBUF_INPUT;
p[0].ubuf.addr = image.data();
p[0].ubuf.size = image.size();
p[1].attr = QCOMTEE_UBUF_INPUT;
p[1].ubuf.addr = const_cast<char*>(TaName);
p[1].ubuf.size = std::strlen(TaName);
p[2].attr = QCOMTEE_UBUF_OUTPUT;
p[2].ubuf.addr = distName.data();
p[2].ubuf.size = distName.size();
p[3].attr = QCOMTEE_OBJREF_OUTPUT;
qcomtee_result_t result = 0;
if (qcomtee_object_invoke(loader, 1, p, 4, &result) || result) {
std::println(std::cerr, "loadFromBuffer failed, result={}",
static_cast<int>(result));
return QCOMTEE_OBJECT_NULL;
}
std::println("trustlet loaded from {} ({} bytes), distName='{}'", path,
image.size(), distName.data());
return p[3].object;
}
// sendRequest is op 0 with arity 0x0424: four input buffers, two output, four
// object slots. The request and response buffers go in and come back out; the
// trustlet's own return code rides in the returned request's header.
struct CommandResult { bool invoked = false; qcomtee_result_t result = 0; std::int32_t rc = 0; std::int32_t metric = 0; };
CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd,
std::span<const std::byte> payload) {
namespace ta = fingerprintd::ta;
static std::vector<std::byte> req(8192), rsp(16384), reqOut(8192), rspOut(16384);
std::ranges::fill(rsp, std::byte{0});
std::ranges::fill(reqOut, std::byte{0});
std::ranges::fill(rspOut, std::byte{0});
ta::BuildRequest(req, cmd, payload);
std::uint32_t is64 = 1;
qcomtee_param p[10] = {};
p[0].attr = QCOMTEE_UBUF_INPUT; p[0].ubuf.addr = req.data(); p[0].ubuf.size = req.size();
p[1].attr = QCOMTEE_UBUF_INPUT; p[1].ubuf.addr = rsp.data(); p[1].ubuf.size = rsp.size();
p[2].attr = QCOMTEE_UBUF_INPUT; p[2].ubuf.addr = nullptr; p[2].ubuf.size = 0;
p[3].attr = QCOMTEE_UBUF_INPUT; p[3].ubuf.addr = &is64; p[3].ubuf.size = sizeof(is64);
p[4].attr = QCOMTEE_UBUF_OUTPUT; p[4].ubuf.addr = reqOut.data(); p[4].ubuf.size = reqOut.size();
p[5].attr = QCOMTEE_UBUF_OUTPUT; p[5].ubuf.addr = rspOut.data(); p[5].ubuf.size = rspOut.size();
for (int i = 6; i < 10; i++) {
p[i].attr = QCOMTEE_OBJREF_INPUT;
p[i].object = QCOMTEE_OBJECT_NULL;
}
CommandResult out;
if (qcomtee_object_invoke(app, fingerprintd::tee::AppSendRequestOp, p, 10, &out.result))
return out;
out.invoked = true;
out.rc = ta::ResultCode(reqOut);
out.metric = ta::CaptureMetric(reqOut);
return out;
}
void Report(fingerprintd::ta::Cmd cmd, const CommandResult& r) {
namespace ta = fingerprintd::ta;
if (!r.invoked) {
std::println(" CMD 0x{:04x} -> INVOKE FAILED", static_cast<unsigned>(cmd));
return;
}
std::println(" CMD 0x{:04x} -> result={} rc={} ({})", static_cast<unsigned>(cmd),
static_cast<int>(r.result), r.rc, ta::StrError(r.rc));
}
int Probe() {
namespace tee = fingerprintd::tee;
@ -243,7 +358,41 @@ int Probe() {
std::println("QSEECOM-compat app loader (UID {}) opened",
tee::UidQseecomCompatAppLoader);
std::println("\nreached QTEE. Not driving the sensor yet.");
qcomtee_object* app = LoadTrustlet(loader, g_taPath);
if (app == QCOMTEE_OBJECT_NULL)
return 1;
// SYNC_CONFIG first, always. The trustlet reads its whole configuration
// from this one JSON payload, and two keys in it are load-bearing:
// algorithm.enrolling_overlap_intervals must be PRESENT (its default is
// the empty string, which faults the trustlet's own sscanf), and
// device.preferred_device_id selects the chip driver.
std::ifstream cf(g_cfgPath);
if (!cf) {
std::println(std::cerr, "cannot open config {}", g_cfgPath);
return 1;
}
std::string json((std::istreambuf_iterator<char>(cf)),
std::istreambuf_iterator<char>());
// The trustlet wants the terminating NUL counted.
std::vector<std::byte> cfg(json.size() + 1, std::byte{0});
for (std::size_t i = 0; i < json.size(); i++)
cfg[i] = static_cast<std::byte>(json[i]);
std::println("config {}: {} bytes", g_cfgPath, cfg.size());
auto r = SendCommand(app, fingerprintd::ta::Cmd::SyncConfig, cfg);
Report(fingerprintd::ta::Cmd::SyncConfig, r);
if (!r.invoked || r.result != 0 || r.rc != 0) {
std::println(std::cerr, "SYNC_CONFIG did not succeed; stopping here");
return 1;
}
// A storage read needs no sensor. It exercises the whole SFS listener path
// if listeners are registered, and answers -2 when they are not.
auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {});
Report(fingerprintd::ta::Cmd::Enumerate, e);
std::println("\ntrustlet is up and configured. Sensor not powered yet.");
pthread_cancel(th);
pthread_join(th, nullptr);
return 0;
@ -260,6 +409,8 @@ int main(int argc, char** argv) {
return 0;
}
if (a == "--probe-tee") probe = true;
if (a.starts_with("--ta=")) g_taPath = a.substr(5);
if (a.starts_with("--config=")) g_cfgPath = a.substr(9);
}
if (probe)
return Probe();