PoisonFid takes the payload and offsets to the fid field internally. It was being handed a span already offset by the payload offset, so the poison landed at payload+0x20 and the real fid field stayed zero. A frame where the matcher never ran then looks exactly like a frame where it ran and rejected -- which is the specific failure this project has recorded three times and is precisely what the poison exists to prevent. Visible in a real run: the frames marked REJECTED were 138, 138, 133, 137, 134 against a floor of 136, i.e. every one of them was a finger-RELEASE frame with nothing on the sensor. Five rejections that never happened. The two offsets are numerically equal, which is why double-applying is silent, so the test now pins both directions: poisoning the payload marks the fid field, and poisoning an already-offset span leaves it zero and misclassifies. Also adds --rescan=N, which patches common.max_authentication_rescan_times into the config. The stock budget lets a whole run end with no terminal verdict -- correct for shipping, useless as a measurement, because a wrong-finger control that never reaches a verdict has not demonstrated a rejection. Forcing 0 makes every frame terminal. It prints MEASUREMENT ONLY because a rate taken that way is a per-frame figure with the retry mechanism disabled, and is not a shipping reject rate.
1209 lines
50 KiB
C++
1209 lines
50 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 <linux/gpio.h>
|
|
#include <linux/bsg.h>
|
|
#include <scsi/sg.h>
|
|
#include <pthread.h>
|
|
#include <fcntl.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";
|
|
|
|
bool g_verbose = false;
|
|
bool g_listeners = false;
|
|
bool g_auth = false;
|
|
int g_frames = 40;
|
|
int g_frameGapMs = 500;
|
|
std::string g_logDir = "/var/log/fingerprintd";
|
|
int g_rescan = -1; // -1 = leave the config's value alone
|
|
std::uint32_t g_gid = 0;
|
|
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.
|
|
//
|
|
// Every run writes its own timestamped transcript. Not a convenience: a run
|
|
// whose result nobody recorded is a run that has to be repeated on a human's
|
|
// finger. And a SINGLE shared log path is worse than none -- the next run,
|
|
// including a quick control, destroys the interesting one, which is how the
|
|
// first successful authentication in this project was very nearly lost.
|
|
//
|
|
// Done at the file-descriptor level rather than by wrapping a stream, because
|
|
// std::println writes to stdout through C stdio: an ostream wrapper would
|
|
// capture nothing. Routing fd 1 through tee catches every line including the
|
|
// ones libqcomtee prints.
|
|
bool StartTranscript(const std::string& dir) {
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(dir, ec);
|
|
auto now = std::chrono::system_clock::now();
|
|
std::string path = std::format("{}/{:%Y%m%d-%H%M%S}.log", dir,
|
|
std::chrono::floor<std::chrono::seconds>(now));
|
|
FILE* t = ::popen(std::format("tee {}", path).c_str(), "w");
|
|
if (!t) return false;
|
|
::dup2(::fileno(t), 1);
|
|
::setvbuf(stdout, nullptr, _IOLBF, 0);
|
|
std::println("transcript: {}", path);
|
|
return true;
|
|
}
|
|
|
|
// 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 storage listeners
|
|
//
|
|
// QTEE cannot reach a filesystem, so it calls back into the normal world for
|
|
// every template read and write. This serves those callbacks. The framing is
|
|
// Fingerprintd:Sfs; what lives here is the file I/O and the registration.
|
|
//
|
|
// READ-ONLY MODE EXISTS FOR A REASON. QTEE deletes a container whose keyed
|
|
// integrity tag does not verify, so a listener that serves bytes at the wrong
|
|
// offset does not merely fail -- it makes QTEE unlink an enrolled template.
|
|
// That is unrecoverable. Until a build has been shown to round-trip a
|
|
// container, it should serve read-only, where an unlink is refused with EROFS
|
|
// and the store cannot be damaged.
|
|
bool g_sfsReadOnly = true;
|
|
std::string g_sfsRoot = "/var/lib/fingerprintd/sfs";
|
|
|
|
struct ListenerObject {
|
|
qcomtee_object object; // must be first
|
|
std::uint32_t id = 0;
|
|
qcomtee_object* shared = QCOMTEE_OBJECT_NULL;
|
|
std::array<std::array<std::byte, 64>, 8> outBufs{};
|
|
};
|
|
|
|
void ListenerRelease(qcomtee_object* object) {
|
|
delete reinterpret_cast<ListenerObject*>(object);
|
|
}
|
|
|
|
// Serve one gpfile request out of the shared buffer, in place.
|
|
void ServeGpFile(std::span<std::byte> sb) {
|
|
namespace sfs = fingerprintd::sfs;
|
|
|
|
auto req = sfs::ParseRequest(sb);
|
|
if (!req) {
|
|
std::println(" gpfile: undecodable request");
|
|
sfs::WriteReply(sb, EINVAL, 0);
|
|
return;
|
|
}
|
|
if (req->op == sfs::OpConfigPathInit) {
|
|
// Asked first, with an empty frame. The answer is LATCHED for the
|
|
// whole boot, so an experiment on its value needs a fresh boot.
|
|
std::println(" gpfile op 12 (path init) -> {}", sfs::ConfigPathInitReply);
|
|
sfs::WriteConfigPathInitReply(sb);
|
|
return;
|
|
}
|
|
|
|
auto full = sfs::ResolvePath(g_sfsRoot, req->root, req->path);
|
|
if (!full) {
|
|
std::println(" gpfile: refusing path '{}' under root {}", req->path, req->root);
|
|
sfs::WriteReply(sb, EINVAL, 0);
|
|
return;
|
|
}
|
|
|
|
switch (req->action) {
|
|
case sfs::Action::Read: {
|
|
std::println(" gpfile READ {} off={} len={}", *full, req->offset, req->length);
|
|
std::ifstream f(*full, std::ios::binary);
|
|
if (!f) { sfs::WriteReply(sb, ENOENT, 0); return; }
|
|
if (req->offset > 0) f.seekg(req->offset);
|
|
std::size_t want = std::min<std::size_t>(req->length,
|
|
sfs::Capacity(sb, sfs::Action::Read));
|
|
f.read(reinterpret_cast<char*>(sb.data() + sfs::ReadDataOff),
|
|
static_cast<std::streamsize>(want));
|
|
auto got = static_cast<std::uint32_t>(f.gcount());
|
|
std::println(" read {} bytes into +0x{:03x}", got, sfs::ReadDataOff);
|
|
sfs::WriteReply(sb, 0, got);
|
|
return;
|
|
}
|
|
case sfs::Action::Write: {
|
|
std::println(" gpfile WRITE {} off={} len={}", *full, req->offset, req->length);
|
|
if (g_sfsReadOnly) {
|
|
std::println(" REFUSED: read-only");
|
|
sfs::WriteReply(sb, EROFS, 0);
|
|
return;
|
|
}
|
|
// O_RDWR | O_CREAT | O_SYNC and never O_TRUNC: QTEE writes a container
|
|
// as write(0,4096), write(4096,N), write(0,4096), so truncating on open
|
|
// leaves 4096 bytes where a 258850-byte template belongs.
|
|
int fd = ::open(full->c_str(), O_RDWR | O_CREAT | O_SYNC, 0600);
|
|
if (fd < 0) { sfs::WriteReply(sb, errno, 0); return; }
|
|
if (req->offset > 0 && ::lseek(fd, req->offset, SEEK_SET) < 0) {
|
|
int e = errno; ::close(fd); sfs::WriteReply(sb, e, 0); return;
|
|
}
|
|
std::size_t want = std::min<std::size_t>(req->length,
|
|
sfs::Capacity(sb, sfs::Action::Write));
|
|
std::size_t done = 0;
|
|
while (done < want) { // short writes are real; the reference loops
|
|
ssize_t n = ::write(fd, sb.data() + sfs::WriteDataOff + done, want - done);
|
|
if (n <= 0) break;
|
|
done += static_cast<std::size_t>(n);
|
|
}
|
|
::fsync(fd);
|
|
::close(fd);
|
|
sfs::WriteReply(sb, 0, static_cast<std::uint32_t>(done));
|
|
return;
|
|
}
|
|
case sfs::Action::Unlink:
|
|
std::println(" gpfile UNLINK {}", *full);
|
|
if (g_sfsReadOnly) {
|
|
std::println(" REFUSED: read-only (this is what protects an enrolled template)");
|
|
sfs::WriteReply(sb, EROFS, 0);
|
|
return;
|
|
}
|
|
sfs::WriteReply(sb, ::unlink(full->c_str()) ? errno : 0, 0);
|
|
return;
|
|
case sfs::Action::Rename: {
|
|
auto to = sfs::ResolvePath(g_sfsRoot, req->root, req->path2);
|
|
std::println(" gpfile RENAME {} -> {}", *full, to ? *to : std::string("?"));
|
|
if (g_sfsReadOnly || !to) { sfs::WriteReply(sb, EROFS, 0); return; }
|
|
sfs::WriteReply(sb, ::rename(full->c_str(), to->c_str()) ? errno : 0, 0);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- RPMB
|
|
//
|
|
// The anti-rollback half. QTEE will not trust a container until it has read
|
|
// its counter record out of the UFS device's replay-protected area, and it
|
|
// cannot reach the device itself. This serves that read.
|
|
//
|
|
// A WRITE advances a monotonic counter that can never be moved back, so it is
|
|
// refused unless explicitly enabled. Key programming is refused ALWAYS -- the
|
|
// RPMB key is one-time programmable and relaying such a frame destroys this
|
|
// part's RPMB permanently.
|
|
bool g_rpmbWrite = false;
|
|
|
|
// SECURITY PROTOCOL IN/OUT against the RPMB well-known LUN. Returns 0 on
|
|
// success, 1 on unit attention (retryable), -1 on error.
|
|
int SecurityProtocol(int fd, bool isIn, std::byte* buf, std::uint32_t len) {
|
|
namespace rp = fingerprintd::rpmb;
|
|
std::array<unsigned char, 12> cdb{};
|
|
std::array<unsigned char, 64> sense{};
|
|
|
|
cdb[0] = isIn ? 0xA2 : 0xB5;
|
|
cdb[1] = rp::SecurityProtocolUfs;
|
|
cdb[2] = (rp::SecurityProtocolSpecific >> 8) & 0xFF;
|
|
cdb[3] = rp::SecurityProtocolSpecific & 0xFF;
|
|
cdb[4] = 0; // INC_512 = 0: the length is in bytes
|
|
cdb[6] = (len >> 24) & 0xFF;
|
|
cdb[7] = (len >> 16) & 0xFF;
|
|
cdb[8] = (len >> 8) & 0xFF;
|
|
cdb[9] = len & 0xFF;
|
|
|
|
sg_io_v4 io{};
|
|
io.guard = 'Q';
|
|
io.protocol = BSG_PROTOCOL_SCSI;
|
|
io.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD;
|
|
io.request_len = cdb.size();
|
|
io.request = reinterpret_cast<std::uintptr_t>(cdb.data());
|
|
io.max_response_len = sense.size();
|
|
io.response = reinterpret_cast<std::uintptr_t>(sense.data());
|
|
io.timeout = 15000;
|
|
if (isIn) {
|
|
io.din_xfer_len = len;
|
|
io.din_xferp = reinterpret_cast<std::uintptr_t>(buf);
|
|
} else {
|
|
io.dout_xfer_len = len;
|
|
io.dout_xferp = reinterpret_cast<std::uintptr_t>(buf);
|
|
}
|
|
|
|
if (::ioctl(fd, SG_IO, &io) < 0) {
|
|
std::println(" SP{} ioctl failed: {}", isIn ? "I" : "O", ::strerror(errno));
|
|
return -1;
|
|
}
|
|
if (io.driver_status || io.transport_status || io.device_status) {
|
|
unsigned key = sense[2] & 0x0F;
|
|
std::println(" SP{} status drv={} trans={} dev={} sense key={} asc=0x{:02x}/{:02x}",
|
|
isIn ? "I" : "O", io.driver_status, io.transport_status,
|
|
io.device_status, key, sense[12], sense[13]);
|
|
// The RPMB LUN raises UNIT ATTENTION on the first command after a
|
|
// reset and clears it by reporting it once. Retryable, not an error.
|
|
return key == fingerprintd::rpmb::SenseKeyUnitAttention ? 1 : -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SecurityProtocolRetry(int fd, bool isIn, std::byte* buf, std::uint32_t len) {
|
|
for (int t = 0; t < 4; t++) {
|
|
int rc = SecurityProtocol(fd, isIn, buf, len);
|
|
if (rc != 1) return rc;
|
|
std::println(" (unit attention cleared, retrying)");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void ServeRpmb(std::span<std::byte> sb) {
|
|
namespace rp = fingerprintd::rpmb;
|
|
|
|
auto req = rp::ParseRequest(sb);
|
|
if (!req) { rp::WriteReply(sb, rp::StatusRefused, 0); return; }
|
|
if (g_verbose)
|
|
std::println(" rpmb op=0x{:x} nblocks={} framesz={} dataoff=0x{:x}",
|
|
static_cast<unsigned>(req->op), req->nblocks, req->frameSize,
|
|
req->dataOff);
|
|
|
|
if (!rp::FramesInBounds(sb, *req)) {
|
|
std::println(" rpmb: frames out of bounds, refusing");
|
|
rp::WriteReply(sb, rp::StatusRefused, 0);
|
|
return;
|
|
}
|
|
|
|
// NEVER RELAYED, whatever the write policy says. The RPMB authentication
|
|
// key is one-time programmable: reprogramming it destroys this part's RPMB
|
|
// permanently and no reflash recovers it. QTEE has no legitimate reason to
|
|
// send one.
|
|
if (rp::AnyKeyProgramming(sb, *req)) {
|
|
std::println(" *** REFUSED: RPMB KEY PROGRAMMING frame. Irreversible. ***");
|
|
rp::WriteReply(sb, rp::StatusRefused, 0);
|
|
return;
|
|
}
|
|
|
|
if (req->op == rp::Op::Write && !g_rpmbWrite) {
|
|
std::println(" rpmb WRITE refused (advances an irreversible counter)");
|
|
rp::WriteReply(sb, rp::StatusRefused, 0);
|
|
return;
|
|
}
|
|
if (req->op != rp::Op::Read && req->op != rp::Op::Write) {
|
|
rp::WriteReply(sb, rp::StatusRefused, 0);
|
|
return;
|
|
}
|
|
|
|
int fd = ::open(std::string(rp::BsgDevice).c_str(), O_RDWR);
|
|
if (fd < 0) {
|
|
std::println(" rpmb: open {}: {}", rp::BsgDevice, ::strerror(errno));
|
|
rp::WriteReply(sb, rp::StatusRefused, 0);
|
|
return;
|
|
}
|
|
std::byte* frames = sb.data() + req->dataOff;
|
|
std::uint32_t total = req->nblocks * static_cast<std::uint32_t>(rp::FrameSize);
|
|
|
|
int rc = -1;
|
|
if (req->op == rp::Op::Read) {
|
|
// A read posts ONE request frame however large nblocks is, then
|
|
// collects nblocks * 512 back.
|
|
if (SecurityProtocolRetry(fd, false, frames, rp::FrameSize) == 0)
|
|
rc = SecurityProtocolRetry(fd, true, frames, total);
|
|
}
|
|
::close(fd);
|
|
|
|
if (rc != 0) {
|
|
rp::WriteReply(sb, rp::StatusRefused, 0);
|
|
return;
|
|
}
|
|
if (g_verbose)
|
|
std::println(" rpmb read ok: resp=0x{:04x} result=0x{:04x} counter={}",
|
|
rp::ReqRespOf(std::span(frames, rp::FrameSize)),
|
|
rp::ResultOf(std::span(frames, rp::FrameSize)),
|
|
rp::WriteCounterOf(std::span(frames, rp::FrameSize)));
|
|
// +0x08 is an OUT parameter QTEE checks against what it expected to be
|
|
// transferred; leaving the request's frame size there fails every
|
|
// transaction. +0x0c is left exactly as the request supplied it.
|
|
rp::WriteReply(sb, rp::StatusOk, rp::BytesTransferred(req->op, req->nblocks));
|
|
}
|
|
|
|
qcomtee_result_t ListenerDispatch(qcomtee_object* object, qcomtee_op_t op,
|
|
qcomtee_param* params, int num) {
|
|
auto* self = reinterpret_cast<ListenerObject*>(object);
|
|
if (g_verbose)
|
|
std::println(" *** QTEE called listener 0x{:x} op={} params={}", self->id,
|
|
static_cast<unsigned>(op), num);
|
|
|
|
for (int i = 0; i < num; i++) {
|
|
switch (params[i].attr) {
|
|
case QCOMTEE_UBUF_OUTPUT: {
|
|
// addr arrives NULL on the callback path; point it at our own
|
|
// storage. Zeros are the answer QTEE expects here.
|
|
std::size_t want = std::min<std::size_t>(params[i].ubuf.size,
|
|
self->outBufs[0].size());
|
|
if (i < 8) {
|
|
self->outBufs[i].fill(std::byte{0});
|
|
params[i].ubuf.addr = self->outBufs[i].data();
|
|
params[i].ubuf.size = want;
|
|
}
|
|
break;
|
|
}
|
|
case QCOMTEE_OBJREF_OUTPUT:
|
|
// MUST be set. cb_marshal_in leaves .object uninitialised and
|
|
// marshal_out then calls typeof() on stack garbage -- a SIGSEGV in
|
|
// the supplicant the moment QTEE first dispatches.
|
|
params[i].object = QCOMTEE_OBJECT_NULL;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The request itself rides in the registered shared buffer, not in params.
|
|
void* addr = qcomtee_memory_object_addr(self->shared);
|
|
std::size_t size = qcomtee_memory_object_size(self->shared);
|
|
if (addr) {
|
|
std::span<std::byte> sb(static_cast<std::byte*>(addr), size);
|
|
if (self->id == 0x7000)
|
|
ServeGpFile(sb);
|
|
else if (self->id == 0x2000)
|
|
ServeRpmb(sb);
|
|
else
|
|
std::println(" (listener 0x{:x}: no handler yet)", self->id);
|
|
}
|
|
return QCOMTEE_OK;
|
|
}
|
|
|
|
qcomtee_object_ops g_listenerOps = {
|
|
/* release */ ListenerRelease,
|
|
/* dispatch */ ListenerDispatch,
|
|
/* error */ nullptr,
|
|
/* supported */ nullptr,
|
|
};
|
|
|
|
// One callback object PER registration. Sharing one across registrations
|
|
// overwrites its id and buffer, and every multi-listener result taken that way
|
|
// is void -- six sessions of hypotheses rested on exactly that bug.
|
|
bool RegisterListener(qcomtee_object* env, std::uint32_t id, std::size_t bufSize) {
|
|
qcomtee_object* svc = OpenService(env, fingerprintd::tee::UidListenerCbo);
|
|
if (svc == QCOMTEE_OBJECT_NULL) return false;
|
|
|
|
qcomtee_object* shared = QCOMTEE_OBJECT_NULL;
|
|
if (qcomtee_memory_object_alloc(bufSize, g_root, &shared)) {
|
|
std::println(std::cerr, "listener 0x{:x}: shared buffer alloc failed", id);
|
|
return false;
|
|
}
|
|
auto* lo = new ListenerObject{};
|
|
lo->id = id;
|
|
lo->shared = shared;
|
|
if (qcomtee_object_cb_init(&lo->object, &g_listenerOps, g_root)) {
|
|
delete lo;
|
|
return false;
|
|
}
|
|
|
|
std::uint32_t lid = id;
|
|
qcomtee_param p[3] = {};
|
|
p[0].attr = QCOMTEE_UBUF_INPUT; p[0].ubuf.addr = &lid; p[0].ubuf.size = sizeof(lid);
|
|
p[1].attr = QCOMTEE_OBJREF_INPUT; p[1].object = &lo->object;
|
|
p[2].attr = QCOMTEE_OBJREF_INPUT; p[2].object = shared;
|
|
qcomtee_result_t result = 0;
|
|
if (qcomtee_object_invoke(svc, 0, p, 3, &result)) {
|
|
std::println(std::cerr, "listener 0x{:x}: invoke failed", id);
|
|
return false;
|
|
}
|
|
std::println("listener 0x{:<5x} sb={:<7} -> result={}{}", id, bufSize,
|
|
static_cast<int>(result),
|
|
result == 0 ? " REGISTERED"
|
|
: static_cast<int>(result) == fingerprintd::tee::ResultIdAlreadyTaken
|
|
? " (id already taken)" : "");
|
|
return result == 0;
|
|
}
|
|
|
|
// ---- The sensor rail
|
|
//
|
|
// GPIO v2 chardev ioctls directly: libgpiod is not on the phone and this is
|
|
// three lines. The chip is found by LABEL, never by index -- /dev/gpiochipN
|
|
// ordering is not stable and driving the wrong controller's pins is the kind
|
|
// of mistake that is not recoverable over ssh.
|
|
class Sensor {
|
|
public:
|
|
~Sensor() { PowerOff(); }
|
|
|
|
bool Open() {
|
|
namespace sn = fingerprintd::sensor;
|
|
chip_ = FindChip(sn::ChipLabel);
|
|
if (chip_ < 0) {
|
|
std::println(std::cerr, "no gpiochip labelled '{}'", sn::ChipLabel);
|
|
return false;
|
|
}
|
|
power_ = RequestLine(sn::PowerLine, GPIO_V2_LINE_FLAG_OUTPUT, "fpd-pwr");
|
|
reset_ = RequestLine(sn::ResetLine, GPIO_V2_LINE_FLAG_OUTPUT, "fpd-rst");
|
|
irq_ = RequestLine(sn::IrqLine, GPIO_V2_LINE_FLAG_INPUT, "fpd-irq");
|
|
return power_ >= 0 && reset_ >= 0 && irq_ >= 0;
|
|
}
|
|
|
|
// Rail up, settle, release reset, settle. Both lines are driven low first
|
|
// so a warm restart starts where a cold one does.
|
|
bool PowerOn() {
|
|
namespace sn = fingerprintd::sensor;
|
|
if (!Set(power_, 0) || !Set(reset_, 0)) return false;
|
|
if (!Set(power_, 1)) return false;
|
|
std::this_thread::sleep_for(sn::PowerSettle);
|
|
if (!Set(reset_, 1)) return false;
|
|
std::this_thread::sleep_for(sn::ResetSettle);
|
|
on_ = true;
|
|
return true;
|
|
}
|
|
|
|
void PowerOff() {
|
|
if (!on_) return;
|
|
Set(reset_, 0);
|
|
Set(power_, 0);
|
|
on_ = false;
|
|
}
|
|
|
|
std::optional<int> ReadIrq() const { return Get(irq_); }
|
|
|
|
private:
|
|
static int FindChip(std::string_view label) {
|
|
for (int i = 0; i < 32; i++) {
|
|
std::string path = std::format("/dev/gpiochip{}", i);
|
|
int fd = ::open(path.c_str(), O_RDWR | O_CLOEXEC);
|
|
if (fd < 0) continue;
|
|
gpiochip_info info{};
|
|
if (::ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info) == 0
|
|
&& label == info.label
|
|
&& info.lines >= fingerprintd::sensor::MinChipLines) {
|
|
std::println("gpiochip '{}' is {} ({} lines)", info.label, path,
|
|
info.lines);
|
|
return fd;
|
|
}
|
|
::close(fd);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int RequestLine(unsigned line, std::uint64_t flags, const char* consumer) {
|
|
// The guard, enforced where the line is actually opened rather than
|
|
// only asserted in the core. gpio8-11 are XPU-protected and touching
|
|
// one is an immediate SError, not an error return.
|
|
if (!fingerprintd::sensor::IsSafeLine(line)) {
|
|
std::println(std::cerr,
|
|
"REFUSING to open gpio{}: XPU-protected fingerprint SPI", line);
|
|
return -1;
|
|
}
|
|
gpio_v2_line_request req{};
|
|
req.offsets[0] = line;
|
|
req.num_lines = 1;
|
|
req.config.flags = flags;
|
|
std::snprintf(req.consumer, sizeof(req.consumer), "%s", consumer);
|
|
if (::ioctl(chip_, GPIO_V2_GET_LINE_IOCTL, &req) < 0) {
|
|
std::println(std::cerr, "gpio{} request failed: {}", line, ::strerror(errno));
|
|
return -1;
|
|
}
|
|
return req.fd;
|
|
}
|
|
|
|
static bool Set(int fd, int v) {
|
|
if (fd < 0) return false;
|
|
gpio_v2_line_values vals{};
|
|
vals.mask = 1;
|
|
vals.bits = v ? 1 : 0;
|
|
return ::ioctl(fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) == 0;
|
|
}
|
|
|
|
static std::optional<int> Get(int fd) {
|
|
if (fd < 0) return std::nullopt;
|
|
gpio_v2_line_values vals{};
|
|
vals.mask = 1;
|
|
if (::ioctl(fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) return std::nullopt;
|
|
return static_cast<int>(vals.bits & 1);
|
|
}
|
|
|
|
int chip_ = -1, power_ = -1, reset_ = -1, irq_ = -1;
|
|
bool on_ = false;
|
|
};
|
|
|
|
// ---- 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;
|
|
// Only meaningful for REPORT_EVENT: the matcher's verdict rides in the
|
|
// returned request's payload.
|
|
std::uint32_t gid = 0;
|
|
std::uint32_t fid = 0;
|
|
std::int32_t samplesRemaining = -1;
|
|
};
|
|
|
|
CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd,
|
|
std::span<const std::byte> payload) {
|
|
namespace ta = fingerprintd::ta;
|
|
namespace tee = fingerprintd::tee;
|
|
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);
|
|
|
|
// CAPTURE_IMAGE's flags word sits at payload+0x18, PAST the declared
|
|
// length of 0x14 -- the trustlet range-checks the length to exactly that
|
|
// and reads the flags anyway. Without bit 1 or bit 30 it skips
|
|
// preprocessing, the classifier and the enrol grouper entirely and returns
|
|
// success having done nothing but a raw scan.
|
|
if (cmd == ta::Cmd::CaptureImage) {
|
|
for (std::size_t i = 0; i < 4; i++)
|
|
req[ta::ReqPayloadOff + ta::CaptureFlagsOff + i] =
|
|
static_cast<std::byte>((ta::CaptureFlagsEnrol >> (8 * i)) & 0xFF);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// A capture needs a real shared memory REGION or the trustlet answers
|
|
// -201: it reads an output-buffer pointer out of payload+0x00, and QTEE
|
|
// only patches an address in there if we name the location in
|
|
// embeddedBufOffsets (IB2) and hand it the region in an object slot.
|
|
// Without that the pointer is NULL. This is the whole difference between a
|
|
// flat metric and a real scan.
|
|
//
|
|
// Two traps: the offsets array applies to EVERY command in a run, so it is
|
|
// scoped to this one command -- patching a pointer into SYNC_CONFIG's
|
|
// request breaks it. And an invoke CONSUMES its input objects, so the
|
|
// region is allocated fresh each time.
|
|
qcomtee_object* region = QCOMTEE_OBJECT_NULL;
|
|
std::uint32_t offsets = tee::EmbeddedBufOffsetValue;
|
|
if (cmd == static_cast<ta::Cmd>(tee::RegionScopedToCommand)) {
|
|
if (qcomtee_memory_object_alloc(tee::CaptureRegionSize, g_root, ®ion)) {
|
|
std::println(std::cerr, " memory region alloc failed");
|
|
region = QCOMTEE_OBJECT_NULL;
|
|
} else {
|
|
void* addr = qcomtee_memory_object_addr(region);
|
|
std::size_t sz = qcomtee_memory_object_size(region);
|
|
if (g_verbose)
|
|
std::println(" region: addr={} size={} offsets=[0x{:x}] slot=IO0",
|
|
addr, sz, offsets);
|
|
std::memset(addr, 0, sz);
|
|
p[2].ubuf.addr = &offsets;
|
|
p[2].ubuf.size = sizeof(offsets);
|
|
p[6].object = region;
|
|
}
|
|
}
|
|
|
|
CommandResult out;
|
|
if (qcomtee_object_invoke(app, tee::AppSendRequestOp, p, 10, &out.result)) {
|
|
if (region != QCOMTEE_OBJECT_NULL)
|
|
qcomtee_memory_object_release(region);
|
|
return out;
|
|
}
|
|
out.invoked = true;
|
|
out.rc = ta::ResultCode(reqOut);
|
|
out.metric = ta::CaptureMetric(reqOut);
|
|
if (cmd == ta::Cmd::ReportEvent) {
|
|
out.gid = ta::MatchedGid(reqOut);
|
|
out.fid = ta::MatchedFid(reqOut);
|
|
out.samplesRemaining = ta::SamplesRemaining(reqOut);
|
|
}
|
|
if (g_verbose && cmd == ta::Cmd::CaptureImage) {
|
|
std::string hex;
|
|
for (std::size_t i = 0; i < 0x30; i++)
|
|
hex += std::format("{:02x}{}", std::to_integer<unsigned>(reqOut[i]),
|
|
(i % 16 == 15) ? "\n " : " ");
|
|
std::println(" reqOut[0x00..0x2f]:\n {}", hex);
|
|
}
|
|
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;
|
|
}
|
|
// A POSITIVE rc is not an error code. ENUMERATE returns the template
|
|
// count there, so running it through the error table prints "unknown" for
|
|
// a perfectly good answer.
|
|
if (r.rc > 0)
|
|
std::println(" CMD 0x{:04x} -> result={} rc={}", static_cast<unsigned>(cmd),
|
|
static_cast<int>(r.result), r.rc);
|
|
else
|
|
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());
|
|
|
|
// Register the storage listeners BEFORE loading the trustlet, so any
|
|
// storage QTEE wants during init has somewhere to go.
|
|
if (g_listeners) {
|
|
for (const auto& l : tee::Listeners) {
|
|
if (l.id == 10) continue; // never called on the fingerprint path
|
|
RegisterListener(env, l.id, l.bufferSize);
|
|
}
|
|
std::println("SFS root {} ({})", g_sfsRoot,
|
|
g_sfsReadOnly ? "READ-ONLY" : "writable");
|
|
}
|
|
|
|
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>());
|
|
|
|
// common.max_authentication_rescan_times bounds how many frames the
|
|
// matcher may answer "not identified yet" before it has to produce a
|
|
// verdict. At the stock default a whole run can end undecided, which is
|
|
// the right shipping behaviour and useless as a measurement: a
|
|
// wrong-finger control that never reaches a verdict has not demonstrated
|
|
// a rejection. Setting it to 0 forces every frame terminal.
|
|
//
|
|
// MEASUREMENT ONLY. A rate measured this way is a per-frame figure taken
|
|
// with the retry mechanism disabled and is not a shipping reject rate.
|
|
if (g_rescan >= 0) {
|
|
auto at = json.find("\"common\":{");
|
|
if (at == std::string::npos) at = json.find("\"common\": {");
|
|
if (at == std::string::npos) {
|
|
std::println(std::cerr, "config has no \"common\" object to patch");
|
|
return 1;
|
|
}
|
|
auto brace = json.find('{', at);
|
|
json.insert(brace + 1,
|
|
std::format("\"max_authentication_rescan_times\":{},", g_rescan));
|
|
std::println("forcing max_authentication_rescan_times={} (MEASUREMENT ONLY)",
|
|
g_rescan);
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
|
|
// ---- The sensor, and the init chain that needs it powered
|
|
Sensor sensor;
|
|
if (!sensor.Open()) {
|
|
std::println(std::cerr, "sensor lines unavailable; stopping before init");
|
|
return 1;
|
|
}
|
|
if (!sensor.PowerOn()) {
|
|
std::println(std::cerr, "sensor power-up failed");
|
|
return 1;
|
|
}
|
|
auto irq = sensor.ReadIrq();
|
|
std::println("sensor powered, reset released, irq={}",
|
|
irq ? std::to_string(*irq) : std::string("?"));
|
|
|
|
// The chain, in order. Every step answers rc=0 on a healthy sensor and the
|
|
// last one is not optional: without SYNC_STATISTICS the trustlet's
|
|
// g_statistics stays NULL and the first enrol frame that gets far enough
|
|
// writes through it.
|
|
//
|
|
// One reset buys one init. If this fails, the rail has to go down and come
|
|
// back up -- re-running the chain answers -205.
|
|
bool ok = true;
|
|
for (fingerprintd::ta::Cmd c : fingerprintd::ta::InitChain) {
|
|
std::vector<std::byte> payload;
|
|
if (c == fingerprintd::ta::Cmd::WorkMode) {
|
|
// WORK_MODE takes a u32 mode; 1 = WAIT_TOUCH.
|
|
payload.assign(0x10, std::byte{0});
|
|
payload[0] = static_cast<std::byte>(
|
|
static_cast<std::uint32_t>(fingerprintd::ta::WorkMode::WaitTouch));
|
|
} else if (c == fingerprintd::ta::Cmd::SyncStatistics) {
|
|
payload.assign(fingerprintd::ta::SyncStatisticsPayloadSize, std::byte{0});
|
|
}
|
|
auto ir = SendCommand(app, c, payload);
|
|
Report(c, ir);
|
|
if (!ir.invoked || ir.result != 0 || ir.rc != 0) {
|
|
ok = false;
|
|
if (ir.rc == fingerprintd::sensor::RcDeviceNotFound)
|
|
std::println(std::cerr,
|
|
" -205: a second init in one power cycle. "
|
|
"Power-cycle the rail, do not retry.");
|
|
break;
|
|
}
|
|
}
|
|
if (!ok) {
|
|
std::println(std::cerr, "init chain did not complete");
|
|
return 1;
|
|
}
|
|
|
|
// NOW the store can be read. A template reload needs the device init
|
|
// chain to have run first: the per-slot enroll-template array is allocated
|
|
// by that chain, and without it FtInitEnrollTplData writes through a NULL
|
|
// the moment a template becomes reachable. Running SET_ACTIVE_GROUP before
|
|
// the chain answers -2 and loads nothing, which reads like a missing
|
|
// container and is an ordering bug.
|
|
if (g_listeners) {
|
|
auto sag = fingerprintd::ta::BuildSetActiveGroup(g_gid);
|
|
std::println("\nSET_ACTIVE_GROUP gid={} path='{}'", g_gid,
|
|
fingerprintd::ta::GroupNamespacePath);
|
|
auto g = SendCommand(app, fingerprintd::ta::Cmd::SetActiveGroup, sag);
|
|
Report(fingerprintd::ta::Cmd::SetActiveGroup, g);
|
|
|
|
auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {});
|
|
Report(fingerprintd::ta::Cmd::Enumerate, e);
|
|
std::println(" templates loaded: {}", e.rc);
|
|
}
|
|
|
|
// With the sensor initialised and a region supplied, a capture returns a
|
|
// real metric. No finger is needed to establish the idle floor, and the
|
|
// floor is the only meaningful reference: the metric is per frame and
|
|
// drifts, so a fixed threshold is wrong by construction.
|
|
fingerprintd::engine::Baseline baseline;
|
|
std::println("calibrating the idle floor ({} samples)",
|
|
fingerprintd::engine::Baseline::DefaultSamples);
|
|
for (std::size_t i = 0; i < fingerprintd::engine::Baseline::DefaultSamples; i++) {
|
|
std::vector<std::byte> cap(fingerprintd::ta::CaptureDeclaredLen);
|
|
fingerprintd::ta::BuildCapturePayload(cap);
|
|
auto c = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap);
|
|
if (!c.invoked || c.result != 0) {
|
|
Report(fingerprintd::ta::Cmd::CaptureImage, c);
|
|
std::println(std::cerr, "capture failed during calibration");
|
|
return 1;
|
|
}
|
|
std::println(" idle {}/{}: rc={} metric={}", i + 1,
|
|
fingerprintd::engine::Baseline::DefaultSamples, c.rc, c.metric);
|
|
baseline.Observe(c.metric);
|
|
}
|
|
if (!baseline.Ready()) {
|
|
std::println(std::cerr, "baseline did not calibrate (floor stayed 0)");
|
|
return 1;
|
|
}
|
|
std::println("idle floor = {}, finger threshold = {}", baseline.Floor(),
|
|
baseline.Threshold());
|
|
|
|
// ---- Authentication
|
|
//
|
|
// Needs no writes of any kind: no SAVE_DATA, no RPMB write, no SFS write.
|
|
// So it runs safely against an existing template with the store read-only,
|
|
// which is what makes it the right thing to try before enrolment.
|
|
if (g_auth) {
|
|
namespace ta = fingerprintd::ta;
|
|
namespace en = fingerprintd::engine;
|
|
|
|
// AUTHENTICATE arms the scan session. Its gid must match the one
|
|
// SET_ACTIVE_GROUP used or the trustlet answers -200.
|
|
std::vector<std::byte> au(ta::AuthPayloadSize);
|
|
ta::BuildAuthPayload(au, 1, g_gid);
|
|
std::println("\nAUTHENTICATE gid={}", g_gid);
|
|
auto a = SendCommand(app, ta::Cmd::Authenticate, au);
|
|
Report(ta::Cmd::Authenticate, a);
|
|
if (!a.invoked || a.result != 0 || a.rc != 0) {
|
|
std::println(std::cerr, "could not arm authentication");
|
|
return 1;
|
|
}
|
|
|
|
for (int c = 3; c > 0; c--) {
|
|
std::println("*** press and lift your finger in {}... ***", c);
|
|
std::fflush(stdout);
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
}
|
|
std::println("\n*** GO -- {} frames, about {} seconds ***\n", g_frames,
|
|
(g_frames * g_frameGapMs) / 1000);
|
|
en::TouchTracker tracker;
|
|
en::AuthTally tally;
|
|
|
|
for (int i = 0; i < g_frames; i++) {
|
|
std::vector<std::byte> q(0x10, std::byte{0});
|
|
SendCommand(app, ta::Cmd::QueryEventStatus, q);
|
|
|
|
std::vector<std::byte> cap(ta::CaptureDeclaredLen);
|
|
ta::BuildCapturePayload(cap);
|
|
auto c = SendCommand(app, ta::Cmd::CaptureImage, cap);
|
|
bool finger = baseline.IsFinger(c.metric);
|
|
|
|
auto events = tracker.Observe(finger, en::Mode::Authenticate);
|
|
std::string verdicts;
|
|
for (ta::Event ev : events) {
|
|
std::vector<std::byte> evbuf(ta::EventContextSize);
|
|
ta::BuildEventContext(evbuf, { .event = ev });
|
|
// Poison the fid field before the call. A zero-initialised
|
|
// buffer cannot tell "the matcher never ran" from "the matcher
|
|
// ran and rejected the finger" -- the failure path writes zero
|
|
// there too, so zero is ambiguous and 0xAAAAAAAA is not.
|
|
// PoisonFid already writes at RespFidOff within the PAYLOAD.
|
|
// Handing it a span that is itself already offset by the
|
|
// payload offset double-counts and poisons payload+0x20, so
|
|
// the real fid field stays zero -- and a released finger then
|
|
// classifies as a REJECTION, inventing failures that never
|
|
// happened.
|
|
ta::PoisonFid(evbuf);
|
|
|
|
auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf);
|
|
if (!r.invoked) continue;
|
|
|
|
ta::Verdict v = ta::Classify(r.rc, r.fid);
|
|
tally.Observe(v, finger);
|
|
verdicts += std::format(" {}", [&] {
|
|
switch (v) {
|
|
case ta::Verdict::Match:
|
|
return std::format("*** MATCH *** gid={} fid={}", r.gid, r.fid);
|
|
case ta::Verdict::Rejected: return std::string("REJECTED");
|
|
case ta::Verdict::NotIdentifiedYet: return std::string("not identified yet");
|
|
case ta::Verdict::MatcherNeverRan: return std::string("released");
|
|
}
|
|
return std::string("?");
|
|
}());
|
|
}
|
|
std::println(" frame {:2}/{}: metric={:<4}{}{}", i + 1, g_frames, c.metric,
|
|
finger ? " FINGER" : " ", verdicts);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs));
|
|
}
|
|
// Only a terminal verdict is an attempt. Counting rescan frames as
|
|
// rejections invents failures that never happened.
|
|
std::println("\n=== {} MATCH / {} REJECTED over {} terminal frames ===",
|
|
tally.Matches(), tally.Rejections(), tally.TerminalFrames());
|
|
std::println(" ({} answered 'not identified yet', {} never reached the matcher)",
|
|
tally.NotIdentifiedYet(), tally.NeverRan());
|
|
if (tally.Presses() > 0)
|
|
std::println(" presses: {} total, {} reached a verdict, {} matched",
|
|
tally.Presses(), tally.PressesDecided(), tally.PressesMatched());
|
|
std::println(" {}", tally.Identified() ? "FINGER IDENTIFIED" : "no match");
|
|
}
|
|
|
|
std::println("\ntrustlet initialised against a powered sensor.");
|
|
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 (a == "--verbose") g_verbose = true;
|
|
if (a == "--listeners") g_listeners = true;
|
|
// Serving the store writable lets QTEE UNLINK a container it rejects,
|
|
// which destroys an enrolled template. Opt in explicitly.
|
|
if (a == "--sfs-writable") g_sfsReadOnly = false;
|
|
if (a == "--rpmb-write") g_rpmbWrite = true;
|
|
if (a == "--auth") { g_auth = true; g_listeners = true; }
|
|
if (a.starts_with("--frames=")) g_frames = std::stoi(std::string(a.substr(9)));
|
|
if (a.starts_with("--log-dir=")) g_logDir = a.substr(10);
|
|
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
|
|
if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11);
|
|
if (a.starts_with("--gid=")) g_gid = static_cast<std::uint32_t>(
|
|
std::stoul(std::string(a.substr(6))));
|
|
}
|
|
if (probe) {
|
|
StartTranscript(g_logDir);
|
|
return Probe();
|
|
}
|
|
|
|
std::println(std::cerr,
|
|
"fingerprintd {}: no runtime yet. --probe-tee reaches QTEE; "
|
|
"`crafter-build test` covers the core.", Version);
|
|
return 1;
|
|
}
|