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:
parent
6c4622afff
commit
03023284ba
3 changed files with 432 additions and 7 deletions
|
|
@ -33,6 +33,8 @@ extern "C" {
|
|||
}
|
||||
|
||||
#include <linux/gpio.h>
|
||||
#include <linux/bsg.h>
|
||||
#include <scsi/sg.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
|
@ -50,6 +52,8 @@ namespace {
|
|||
constexpr const char* Version = "0.0.3";
|
||||
|
||||
bool g_verbose = false;
|
||||
bool g_listeners = false;
|
||||
std::uint32_t g_gid = 0;
|
||||
std::string g_taPath = "/lib/firmware/focal64.mbn";
|
||||
std::string g_cfgPath = "/lib/firmware/fingerprintd.json";
|
||||
|
||||
|
|
@ -218,6 +222,351 @@ qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) {
|
|||
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
|
||||
|
|
@ -483,8 +832,15 @@ void Report(fingerprintd::ta::Cmd cmd, const CommandResult& r) {
|
|||
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));
|
||||
// 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() {
|
||||
|
|
@ -512,6 +868,17 @@ int Probe() {
|
|||
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;
|
||||
|
|
@ -547,11 +914,6 @@ int Probe() {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// A storage read needs no sensor. It exercises the whole SFS listener path
|
||||
// if listeners are registered, and answers with no templates when they are
|
||||
// not.
|
||||
auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {});
|
||||
Report(fingerprintd::ta::Cmd::Enumerate, e);
|
||||
|
||||
// ---- The sensor, and the init chain that needs it powered
|
||||
Sensor sensor;
|
||||
|
|
@ -601,6 +963,24 @@ int Probe() {
|
|||
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
|
||||
|
|
@ -648,6 +1028,14 @@ int main(int argc, char** argv) {
|
|||
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.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)
|
||||
return Probe();
|
||||
|
|
|
|||
|
|
@ -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 ------------------------------------
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue