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.
422 lines
16 KiB
C++
422 lines
16 KiB
C++
// SPDX-License-Identifier: GPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// lint-disable-file fixed-width-types
|
|
// lint-disable-file no-char-pointer
|
|
/*
|
|
fingerprintd — the daemon shell.
|
|
|
|
Everything that touches hardware lives here; the decisions live in
|
|
fingerprintd-core, which is tested without a phone. Right now this reaches QTEE
|
|
and stops: root object, credentials, client env, the QSEECOM-compat loader.
|
|
Enough to prove the transport, not yet to drive the sensor.
|
|
|
|
Why the process must be long-lived, once it does more: a listener registration
|
|
is held for as long as the process lives and QTEE's listener table is global to
|
|
the boot, and one sensor reset buys exactly one trustlet init. So the process
|
|
that powers the sensor has to be the process that holds the session.
|
|
*/
|
|
// libqcomtee is a C library and its headers carry no extern "C" guard -- it
|
|
// has only ever been consumed from C. Without one every symbol would be
|
|
// C++-mangled and none would link.
|
|
//
|
|
// The headers pull in <stdarg.h>, <stdatomic.h> and <stdio.h>, and under
|
|
// libc++ those drag in C++ templates, which may not appear inside an
|
|
// extern "C" block. Including them first makes the nested includes no-ops.
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdatomic.h>
|
|
extern "C" {
|
|
#include <qcomtee_object.h>
|
|
#include <qcomtee_object_types.h>
|
|
#include <qcomtee_errno.h>
|
|
}
|
|
|
|
#include <pthread.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
|
|
import std;
|
|
import Fingerprintd;
|
|
|
|
namespace {
|
|
|
|
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;
|
|
|
|
// The ioctl trampoline libqcomtee calls. Cancellation is made asynchronous
|
|
// around it so the supplicant thread can be stopped while blocked in the
|
|
// kernel waiting for QTEE.
|
|
//
|
|
// tee_call_t's second parameter is `unsigned long` on glibc and `int` on musl
|
|
// (qcomtee_object.h keys it off __GLIBC__), so the signature has to match or
|
|
// the function pointer will not convert. The native build is glibc and the
|
|
// phone is musl, so both forms are compiled here.
|
|
#ifdef __GLIBC__
|
|
int TeeCall(int fd, unsigned long op, ...) {
|
|
#else
|
|
int TeeCall(int fd, int op, ...) {
|
|
#endif
|
|
va_list ap;
|
|
va_start(ap, op);
|
|
void* arg = va_arg(ap, void*);
|
|
va_end(ap);
|
|
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, nullptr);
|
|
int ret = ::ioctl(fd, static_cast<unsigned long>(op), arg);
|
|
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
|
|
return ret;
|
|
}
|
|
|
|
// QTEE's callbacks are serviced here. Nothing QTEE asks of us happens without
|
|
// this running.
|
|
void* Supplicant(void*) {
|
|
for (;;) {
|
|
pthread_testcancel();
|
|
if (qcomtee_object_process_one(g_root))
|
|
break;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::uint64_t NowMs() {
|
|
timeval tv{};
|
|
::gettimeofday(&tv, nullptr);
|
|
return static_cast<std::uint64_t>(tv.tv_sec) * 1000
|
|
+ static_cast<std::uint64_t>(tv.tv_usec) / 1000;
|
|
}
|
|
|
|
// ---- The credentials object
|
|
//
|
|
// QTEE will not take the credentials blob directly on the Register path: it
|
|
// takes an object and calls back into it, twice, while our invoke is still in
|
|
// flight. Two ops, GET_LENGTH then READ_AT_OFFSET.
|
|
//
|
|
// libqcomtee ships one of these, but only by pulling in QCBOR to build the
|
|
// map. The map is thirteen bytes and lives in Fingerprintd:Tee under test, so
|
|
// this serves it and the library needs no dependency beyond libc.
|
|
struct CredentialsObject {
|
|
qcomtee_object object; // must be first: we cast between them
|
|
std::vector<std::byte> blob;
|
|
std::uint64_t lenStorage = 0; // op 0's answer, pointed at not copied
|
|
};
|
|
|
|
void CredentialsRelease(qcomtee_object* object) {
|
|
delete reinterpret_cast<CredentialsObject*>(object);
|
|
}
|
|
|
|
qcomtee_result_t CredentialsDispatch(qcomtee_object* object, qcomtee_op_t op,
|
|
qcomtee_param* params, int num) {
|
|
auto* self = reinterpret_cast<CredentialsObject*>(object);
|
|
|
|
// On the CALLBACK path a QCOMTEE_UBUF_OUTPUT param arrives with
|
|
// addr = NULL and size = the capacity QTEE will accept: the dispatcher
|
|
// supplies the buffer, so the handler POINTS the param at storage of its
|
|
// own and lets the framework marshal it. Writing through the incoming addr
|
|
// is a null dereference, which is exactly how this crashed the first time
|
|
// it ran against real QTEE.
|
|
if (op == static_cast<qcomtee_op_t>(fingerprintd::tee::CredOp::GetLength)) {
|
|
if (num != 1 || params[0].attr != QCOMTEE_UBUF_OUTPUT)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
if (params[0].ubuf.size < fingerprintd::tee::CredLengthReplySize)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
self->lenStorage = static_cast<std::uint64_t>(self->blob.size());
|
|
params[0].ubuf.addr = &self->lenStorage;
|
|
params[0].ubuf.size = sizeof(self->lenStorage);
|
|
return QCOMTEE_OK;
|
|
}
|
|
|
|
if (op == static_cast<qcomtee_op_t>(fingerprintd::tee::CredOp::ReadAtOffset)) {
|
|
if (num != 2 || params[0].attr != QCOMTEE_UBUF_INPUT
|
|
|| params[1].attr != QCOMTEE_UBUF_OUTPUT)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
// An INPUT param does carry a real address; only outputs arrive NULL.
|
|
if (params[0].ubuf.size < sizeof(std::uint64_t) || !params[0].ubuf.addr)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
std::uint64_t offset = 0;
|
|
::memcpy(&offset, params[0].ubuf.addr, sizeof(offset));
|
|
|
|
auto plan = fingerprintd::tee::PlanRead(self->blob.size(), offset,
|
|
params[1].ubuf.size);
|
|
if (!plan.valid)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
// Same again: point at the blob, do not copy into QTEE's buffer. The
|
|
// storage has to outlive the dispatch, which the object owns.
|
|
params[1].ubuf.addr = self->blob.data() + plan.offset;
|
|
params[1].ubuf.size = plan.count;
|
|
return QCOMTEE_OK;
|
|
}
|
|
|
|
return QCOMTEE_ERROR_INVALID;
|
|
}
|
|
|
|
qcomtee_object_ops g_credOps = {
|
|
/* release */ CredentialsRelease,
|
|
/* dispatch */ CredentialsDispatch,
|
|
/* error */ nullptr,
|
|
/* supported */ nullptr,
|
|
};
|
|
|
|
qcomtee_object* MakeCredentials(std::uint32_t uid) {
|
|
auto* c = new CredentialsObject{};
|
|
c->blob = fingerprintd::tee::BuildCredentials(uid, NowMs());
|
|
if (qcomtee_object_cb_init(&c->object, &g_credOps, g_root)) {
|
|
delete c;
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
return &c->object;
|
|
}
|
|
|
|
// ROOT op 2: hand QTEE a live credentials object and get a client env back.
|
|
// QTEE calls into the object while this invoke is outstanding, which is why
|
|
// the supplicant has to be running first.
|
|
qcomtee_object* GetClientEnv(std::uint32_t uid) {
|
|
qcomtee_object* creds = MakeCredentials(uid);
|
|
if (creds == QCOMTEE_OBJECT_NULL) {
|
|
std::println(std::cerr, "credentials object init failed");
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
qcomtee_param p[2] = {};
|
|
p[0].attr = QCOMTEE_OBJREF_INPUT;
|
|
p[0].object = creds;
|
|
p[1].attr = QCOMTEE_OBJREF_OUTPUT;
|
|
qcomtee_result_t result = 0;
|
|
if (qcomtee_object_invoke(g_root,
|
|
static_cast<qcomtee_op_t>(fingerprintd::tee::ClientEnvOp),
|
|
p, 2, &result) || result) {
|
|
std::println(std::cerr, "ROOT op {} failed, result={}",
|
|
static_cast<unsigned>(fingerprintd::tee::ClientEnvOp),
|
|
static_cast<int>(result));
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
return p[1].object;
|
|
}
|
|
|
|
// IClientEnv op 0: open a service by UID on the env.
|
|
qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) {
|
|
qcomtee_param p[2] = {};
|
|
p[0].attr = QCOMTEE_UBUF_INPUT;
|
|
p[0].ubuf.addr = &uid;
|
|
p[0].ubuf.size = sizeof(uid);
|
|
p[1].attr = QCOMTEE_OBJREF_OUTPUT;
|
|
qcomtee_result_t result = 0;
|
|
if (qcomtee_object_invoke(env, 0, p, 2, &result) || result) {
|
|
std::println(std::cerr, "IClientEnv.open({}) failed, result={}", uid,
|
|
static_cast<int>(result));
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
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;
|
|
|
|
std::string dev(tee::DevTee);
|
|
g_root = qcomtee_object_root_init(dev.c_str(), TeeCall, nullptr, nullptr);
|
|
if (g_root == QCOMTEE_OBJECT_NULL) {
|
|
std::println(std::cerr, "root object on {}: {}", tee::DevTee,
|
|
::strerror(errno));
|
|
return 1;
|
|
}
|
|
std::println("root object on {}", tee::DevTee);
|
|
|
|
pthread_t th{};
|
|
if (pthread_create(&th, nullptr, Supplicant, nullptr) != 0) {
|
|
std::println(std::cerr, "supplicant thread failed to start");
|
|
return 1;
|
|
}
|
|
|
|
std::uint32_t uid = ::getuid();
|
|
qcomtee_object* env = GetClientEnv(uid);
|
|
if (env == QCOMTEE_OBJECT_NULL)
|
|
return 1;
|
|
std::println("client env obtained (uid {}, {}-byte credentials)", uid,
|
|
tee::BuildCredentials(uid, 0).size());
|
|
|
|
qcomtee_object* loader = OpenService(env, tee::UidQseecomCompatAppLoader);
|
|
if (loader == QCOMTEE_OBJECT_NULL)
|
|
return 1;
|
|
std::println("QSEECOM-compat app loader (UID {}) opened",
|
|
tee::UidQseecomCompatAppLoader);
|
|
|
|
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;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
|
bool probe = false;
|
|
for (std::string_view a : args.subspan(1)) {
|
|
if (a == "--version") {
|
|
std::println("fingerprintd {}", Version);
|
|
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();
|
|
|
|
std::println(std::cerr,
|
|
"fingerprintd {}: no runtime yet. --probe-tee reaches QTEE; "
|
|
"`crafter-build test` covers the core.", Version);
|
|
return 1;
|
|
}
|