Reach QTEE: credentials, client env and the app loader, with no QCBOR
fingerprintd's own code now talks to QTEE. On the phone:
root object on /dev/tee0
client env obtained (uid 0, 13-byte credentials)
QSEECOM-compat app loader (UID 122) opened
The credentials object is ours rather than libqcomtee's. Upstream's exists only
to build a thirteen-byte CBOR map and drags in QCBOR to do it, so
packaging/make-libqcomtee.sh compiles the two sources that matter and drops
credentials_obj.c entirely -- nothing else references it, and the library then
has no dependency beyond libc. The map is built in Fingerprintd:Tee where it is
pinned byte-for-byte against the string verified on-device, and the object's
two-op read protocol is served here.
Three interop details, all of which cost a build cycle:
* libqcomtee's headers carry no extern "C" guard, having only ever been
consumed from C, so everything came out C++-mangled. They also pull in
<stdatomic.h> and <stdio.h>, which under libc++ drag in templates that may
not appear inside extern "C" -- so those are included first.
* tee_call_t's second parameter is unsigned long on glibc and int on musl.
The native build is glibc and the phone is musl; both forms are compiled.
* On the callback path a UBUF_OUTPUT param arrives with addr = NULL. The
dispatcher supplies the buffer, so a handler POINTS the param at its own
storage rather than writing through the incoming address. Doing the latter
is a null dereference that takes the supplicant thread with it, which is
how the first run against real QTEE ended -- with the correct behaviour
already spelled out in the module comment above the code that ignored it.
That comment now says so in as many words.
This commit is contained in:
parent
93692c9505
commit
a91fb2ff58
6 changed files with 708 additions and 25 deletions
|
|
@ -2,49 +2,270 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
// lint-disable-file fixed-width-types
|
// lint-disable-file fixed-width-types
|
||||||
|
// lint-disable-file no-char-pointer
|
||||||
/*
|
/*
|
||||||
fingerprintd — the daemon shell.
|
fingerprintd — the daemon shell.
|
||||||
|
|
||||||
Nothing here talks to hardware yet. The core (Fingerprintd:Sfs and the modules
|
Everything that touches hardware lives here; the decisions live in
|
||||||
that follow it) is being ported from the fp6 harness one wire format at a time,
|
fingerprintd-core, which is tested without a phone. Right now this reaches QTEE
|
||||||
each landing with tests that run without a phone; this entry point exists so
|
and stops: root object, credentials, client env, the QSEECOM-compat loader.
|
||||||
the executable target builds alongside them and so the version constant that
|
Enough to prove the transport, not yet to drive the sensor.
|
||||||
gates a package publish has a home.
|
|
||||||
|
|
||||||
What this will own, and why it has to be one long-lived process:
|
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 sensor rail (gpio29), reset (gpio74) and IRQ (gpio75) — one sensor
|
the boot, and one sensor reset buys exactly one trustlet init. So the process
|
||||||
reset buys exactly one trustlet init, so whatever powers the sensor must
|
that powers the sensor has to be the process that holds the session.
|
||||||
also hold the session;
|
|
||||||
* the QTEE session on /dev/tee0: client env, the QSEECOM-compat loader, and
|
|
||||||
the focal64 trustlet, loaded once;
|
|
||||||
* the storage listeners QTEE calls back into — gpfile 0x7000 and RPMB
|
|
||||||
0x2000 — served from a supplicant thread that must outlive every request,
|
|
||||||
because QTEE's listener table is global and an id is taken for as long as
|
|
||||||
the registration is held;
|
|
||||||
* net.reactivated.Fprint, so pam_fprintd and the desktop need no changes.
|
|
||||||
*/
|
*/
|
||||||
|
// 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 std;
|
||||||
import Fingerprintd;
|
import Fingerprintd;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Bumping this is what publishes a package: the registry answers 409 for a
|
|
||||||
// version it already has, which a build treats as a no-op.
|
constexpr const char* Version = "0.0.2";
|
||||||
constexpr const char* Version = "0.0.1";
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
std::println("\nreached QTEE. Not driving the sensor yet.");
|
||||||
|
pthread_cancel(th);
|
||||||
|
pthread_join(th, nullptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
||||||
|
bool probe = false;
|
||||||
for (std::string_view a : args.subspan(1)) {
|
for (std::string_view a : args.subspan(1)) {
|
||||||
if (a == "--version") {
|
if (a == "--version") {
|
||||||
std::println("fingerprintd {}", Version);
|
std::println("fingerprintd {}", Version);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (a == "--probe-tee") probe = true;
|
||||||
}
|
}
|
||||||
|
if (probe)
|
||||||
|
return Probe();
|
||||||
|
|
||||||
std::println(std::cerr,
|
std::println(std::cerr,
|
||||||
"fingerprintd {}: no runtime yet -- the core is still being "
|
"fingerprintd {}: no runtime yet. --probe-tee reaches QTEE; "
|
||||||
"ported. Run `crafter-build test` for what does work.",
|
"`crafter-build test` covers the core.", Version);
|
||||||
Version);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
195
interfaces/Fingerprintd-Tee.cppm
Normal file
195
interfaces/Fingerprintd-Tee.cppm
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
// lint-disable-file fixed-width-types
|
||||||
|
/*
|
||||||
|
Fingerprintd:Tee — the QTEE object protocol's constants and its one encoded
|
||||||
|
structure.
|
||||||
|
|
||||||
|
Reaching the trustlet is a chain of object invocations over /dev/tee0: get the
|
||||||
|
root object, exchange credentials for a client env, open the QSEECOM-compat
|
||||||
|
loader on that env, load the trustlet, then invoke it. Registering the storage
|
||||||
|
listeners QTEE calls back into runs off the same env.
|
||||||
|
|
||||||
|
The transport is libqcomtee's. What lives here is the part that is ours to get
|
||||||
|
right: the service and op numbers, the listener table, and the credentials blob
|
||||||
|
— a CBOR map QTEE parses, so its bytes have to be exact.
|
||||||
|
|
||||||
|
Building that blob here rather than with QCBOR drops the only dependency
|
||||||
|
libqcomtee has beyond libc, and puts a reverse-engineered byte structure under
|
||||||
|
test instead of inside a vendored library.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export module Fingerprintd:Tee;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace fingerprintd::tee {
|
||||||
|
|
||||||
|
inline constexpr std::string_view DevTee = "/dev/tee0";
|
||||||
|
|
||||||
|
// ---- Services ---------------------------------------------------------
|
||||||
|
//
|
||||||
|
// QTEE service UIDs, opened on a client env.
|
||||||
|
inline constexpr std::uint32_t UidQseecomCompatAppLoader = 122;
|
||||||
|
inline constexpr std::uint32_t UidListenerCbo = 87;
|
||||||
|
|
||||||
|
// Root object ops (IClientEnv). SAFETY: never invoke 4, 8 or 9 —
|
||||||
|
// NOTIFY_DOMAIN_CHANGE, ADCI_ACCEPT and ADCI_SHUTDOWN. ADCI_ACCEPT donates
|
||||||
|
// a thread to QTEE and does not return; an ADCI arc cost a string of
|
||||||
|
// forced reboots and was ultimately refuted as an explanation for
|
||||||
|
// anything. qcomtee_root_object_check() blocks them as destructive.
|
||||||
|
enum class RootOp : std::uint32_t {
|
||||||
|
RegisterLegacy = 1, // credentials as a CBOR BUFFER
|
||||||
|
Register = 2, // credentials as a live callback OBJECT
|
||||||
|
RegisterWithCredentials = 5, // the kernel's privileged NULL path
|
||||||
|
};
|
||||||
|
|
||||||
|
// We use Register (op 2) with a live credentials object, because that is
|
||||||
|
// what the harness that actually enrolled and matched a finger used. The
|
||||||
|
// journal records op 1 working too, and all three constructions behaving
|
||||||
|
// identically as far as storage is concerned — but the proven path is the
|
||||||
|
// one that produced the working flow, and this is not the place to change
|
||||||
|
// a known-good variable.
|
||||||
|
inline constexpr RootOp ClientEnvOp = RootOp::Register;
|
||||||
|
|
||||||
|
// The app object's sendRequest, arity 0x0424 = 4 input buffers, 2 output
|
||||||
|
// buffers, 4 object slots.
|
||||||
|
inline constexpr std::uint32_t AppSendRequestOp = 0;
|
||||||
|
|
||||||
|
// ---- Listeners --------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Ids and shared-buffer sizes are qseecomd's, decoded from its service
|
||||||
|
// table. The share column is measured across 224 trustlet commands during
|
||||||
|
// a real enrolment on stock Android.
|
||||||
|
struct Listener {
|
||||||
|
std::uint32_t id;
|
||||||
|
std::size_t bufferSize;
|
||||||
|
std::string_view name;
|
||||||
|
};
|
||||||
|
inline constexpr std::array<Listener, 3> Listeners = {{
|
||||||
|
{ 0x7000, 516096, "gpfile" }, // 47 of 66 callbacks
|
||||||
|
{ 0x2000, 25600, "rpmb" }, // 16 of 66
|
||||||
|
{ 10, 20480, "fs" }, // 3 of 66; never called on our path
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Registration semantics, settled by measurement. A fresh id returns 0 in
|
||||||
|
// under 2 ms; the SAME id again returns -99 in 21-47 ms, identically from
|
||||||
|
// another env in the same process or from a different process — so the
|
||||||
|
// table is GLOBAL to QTEE, not per-env. QTEE releases the callback object
|
||||||
|
// it was handed on every -99 and keeps it on every 0.
|
||||||
|
//
|
||||||
|
// The consequence for a daemon: a registration is held for the life of the
|
||||||
|
// process, so exactly one process may own these ids, and it must not exit
|
||||||
|
// and restart casually within a boot.
|
||||||
|
inline constexpr std::int32_t ResultIdAlreadyTaken = -99;
|
||||||
|
|
||||||
|
// One callback object PER registration. Sharing a single object across
|
||||||
|
// registrations overwrites its id and buffer, and every multi-listener
|
||||||
|
// result taken that way was void — six sessions of hypotheses rested on it.
|
||||||
|
inline constexpr bool OneObjectPerRegistration = true;
|
||||||
|
|
||||||
|
// ---- Credentials ------------------------------------------------------
|
||||||
|
//
|
||||||
|
// A CBOR map QTEE parses. Attribute keys come from SmcInvokeCredAPI.h via
|
||||||
|
// minkipc's TZCom, and libqcomtee builds the same two-entry map:
|
||||||
|
//
|
||||||
|
// { AttrUid: <uid>, AttrSystemTime: <ms> }
|
||||||
|
//
|
||||||
|
// encoded as a2 01 <uid> 06 1b <u64 ms> for a small uid. Only 1 and 6 have
|
||||||
|
// ever been used; 2-5 are untried and there is a measured 10-byte read cap
|
||||||
|
// that makes larger content doubtful.
|
||||||
|
enum class CredAttr : std::uint8_t {
|
||||||
|
Uid = 1, PkgFlags = 2, PkgName = 3, PkgCert = 4, Permissions = 5, SystemTime = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Minimal CBOR unsigned encoder (major type 0). QTEE's parser wants the
|
||||||
|
// shortest form, which is what QCBOR emits.
|
||||||
|
inline void AppendCborUint(std::vector<std::byte>& out, std::uint64_t v) {
|
||||||
|
auto push = [&](std::uint8_t b) { out.push_back(static_cast<std::byte>(b)); };
|
||||||
|
auto pushBe = [&](std::uint64_t x, int n) {
|
||||||
|
for (int i = n - 1; i >= 0; i--) push(static_cast<std::uint8_t>((x >> (8 * i)) & 0xFF));
|
||||||
|
};
|
||||||
|
if (v < 24) push(static_cast<std::uint8_t>(v));
|
||||||
|
else if (v <= 0xFF) { push(0x18); pushBe(v, 1); }
|
||||||
|
else if (v <= 0xFFFF) { push(0x19); pushBe(v, 2); }
|
||||||
|
else if (v <= 0xFFFFFFFFull) { push(0x1A); pushBe(v, 4); }
|
||||||
|
else { push(0x1B); pushBe(v, 8); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The credentials blob. `systemTimeMs` is wall-clock milliseconds, which is
|
||||||
|
// the only value that changes between sessions — it was the leading
|
||||||
|
// suspect for a session-scoped storage key and is REFUTED: pinning it to a
|
||||||
|
// constant changed nothing. Do not re-test that.
|
||||||
|
inline std::vector<std::byte> BuildCredentials(std::uint32_t uid, std::uint64_t systemTimeMs) {
|
||||||
|
std::vector<std::byte> out;
|
||||||
|
out.push_back(std::byte{0xA2}); // map(2)
|
||||||
|
AppendCborUint(out, static_cast<std::uint64_t>(CredAttr::Uid));
|
||||||
|
AppendCborUint(out, uid);
|
||||||
|
AppendCborUint(out, static_cast<std::uint64_t>(CredAttr::SystemTime));
|
||||||
|
// Always the 8-byte form for the timestamp: it is what the reference
|
||||||
|
// emits and what the on-device verification used.
|
||||||
|
out.push_back(std::byte{0x1B});
|
||||||
|
for (int i = 7; i >= 0; i--)
|
||||||
|
out.push_back(static_cast<std::byte>((systemTimeMs >> (8 * i)) & 0xFF));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The credentials object's protocol --------------------------------
|
||||||
|
//
|
||||||
|
// QTEE does not take the blob directly on the Register path: it takes an
|
||||||
|
// object and calls back into it to read the bytes. Two ops, confirmed
|
||||||
|
// against both our reversal of libminkdescriptor.so and libqcomtee's own
|
||||||
|
// source:
|
||||||
|
//
|
||||||
|
// op 0 GET_LENGTH one output buffer of exactly 8: write the length
|
||||||
|
// op 1 READ_AT_OFFSET one input buffer of 8 (u64 offset) + one output;
|
||||||
|
// serve min(out.size, len - offset) bytes and set
|
||||||
|
// the output size to what was served
|
||||||
|
//
|
||||||
|
// The reference does not memcpy into QTEE's buffer — it points the output
|
||||||
|
// at its own storage and lets the framework marshal. That is correct for
|
||||||
|
// the userspace layer, and it is not optional:
|
||||||
|
//
|
||||||
|
// ON THE CALLBACK PATH A UBUF_OUTPUT PARAM ARRIVES WITH addr = NULL.
|
||||||
|
//
|
||||||
|
// `size` is the capacity QTEE will accept; the dispatcher supplies the
|
||||||
|
// buffer. A handler that writes through the incoming address dereferences
|
||||||
|
// NULL and takes the supplicant thread with it. That is exactly how the
|
||||||
|
// first run of this daemon against real QTEE died, with the correct
|
||||||
|
// behaviour already written in this comment. An INPUT param does carry a
|
||||||
|
// real address; only outputs arrive empty.
|
||||||
|
//
|
||||||
|
// The storage a handler points at must outlive the dispatch, so it belongs
|
||||||
|
// to the object, not to the stack frame.
|
||||||
|
enum class CredOp : std::uint32_t { GetLength = 0, ReadAtOffset = 1 };
|
||||||
|
|
||||||
|
inline constexpr std::size_t CredLengthReplySize = 8;
|
||||||
|
|
||||||
|
// How many bytes to serve for a read at `offset`, and whether the request
|
||||||
|
// is valid at all. An offset at or past the end is an error, not an empty
|
||||||
|
// read — the reference returns QCOMTEE_ERROR_INVALID there. An earlier
|
||||||
|
// reversal of ours guessed 10; libqcomtee settled it.
|
||||||
|
struct ReadPlan { bool valid = false; std::size_t offset = 0; std::size_t count = 0; };
|
||||||
|
inline ReadPlan PlanRead(std::size_t blobLen, std::uint64_t offset, std::size_t outCapacity) {
|
||||||
|
if (offset >= blobLen) return {};
|
||||||
|
std::size_t avail = blobLen - static_cast<std::size_t>(offset);
|
||||||
|
return { true, static_cast<std::size_t>(offset), std::min(avail, outCapacity) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Shared memory for capture ----------------------------------------
|
||||||
|
//
|
||||||
|
// CAPTURE_IMAGE needs a real memory REGION or the trustlet returns success
|
||||||
|
// having done nothing but a raw scan — the entire flat-metric history.
|
||||||
|
// The region goes in an object slot, and IB2 carries embeddedBufOffsets:
|
||||||
|
// u32 offsets into the request naming where an embedded pointer sits.
|
||||||
|
// 0x10 means "at request+0x10", which is payload+0x00, which the trustlet
|
||||||
|
// reads as its output-buffer pointer.
|
||||||
|
inline constexpr std::size_t CaptureRegionSize = 16384;
|
||||||
|
inline constexpr std::uint32_t EmbeddedBufOffsetValue = 0x10;
|
||||||
|
|
||||||
|
// Two traps, both paid for. The offsets array applies to EVERY command in
|
||||||
|
// a run, and patching a pointer into SYNC_CONFIG's request breaks it — so
|
||||||
|
// it is scoped to one command id. And an invoke CONSUMES its input
|
||||||
|
// objects, so the region must be allocated fresh per command.
|
||||||
|
inline constexpr std::uint32_t RegionScopedToCommand = 0x1013; // CAPTURE_IMAGE
|
||||||
|
}
|
||||||
|
|
@ -17,3 +17,4 @@ export import :Rpmb;
|
||||||
export import :Ta;
|
export import :Ta;
|
||||||
export import :Engine;
|
export import :Engine;
|
||||||
export import :Store;
|
export import :Store;
|
||||||
|
export import :Tee;
|
||||||
|
|
|
||||||
82
packaging/make-libqcomtee.sh
Executable file
82
packaging/make-libqcomtee.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
# make-libqcomtee.sh — build libqcomtee for a target, without QCBOR.
|
||||||
|
#
|
||||||
|
# packaging/make-libqcomtee.sh [--target=<triple> --sysroot=<dir>] [outdir]
|
||||||
|
#
|
||||||
|
# libqcomtee is Qualcomm's BSD-3 userspace client for QTEE (quic/quic-teec).
|
||||||
|
# Upstream's CMake does find_package(QCBOR REQUIRED) and falls back to libcbor,
|
||||||
|
# but that dependency exists for exactly one file, src/objects/credentials_obj.c,
|
||||||
|
# which builds the {AttrUid, AttrSystemTime} CBOR map.
|
||||||
|
#
|
||||||
|
# fingerprintd builds that map itself in Fingerprintd:Tee, where it is thirteen
|
||||||
|
# bytes under test rather than a library dependency, and implements the
|
||||||
|
# credentials object's two-op read protocol in its own shell. So only two of
|
||||||
|
# the three sources are needed:
|
||||||
|
#
|
||||||
|
# src/qcomtee_object.c the object/marshalling core
|
||||||
|
# src/objects/mem_obj.c memory objects (CAPTURE_IMAGE needs a real region)
|
||||||
|
#
|
||||||
|
# Nothing in either references credentials_obj, so dropping it leaves the
|
||||||
|
# library with no dependency beyond libc.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO="${QUIC_TEEC_REPO:-https://github.com/quic/quic-teec.git}"
|
||||||
|
# Pinned. Upstream is small and moves rarely; an unpinned clone would change
|
||||||
|
# the marshalling under us without notice.
|
||||||
|
COMMIT="${QUIC_TEEC_COMMIT:-736419e}"
|
||||||
|
SRC="${QUIC_TEEC_DIR:-$HOME/.cache/fingerprintd/quic-teec}"
|
||||||
|
|
||||||
|
TARGET=""
|
||||||
|
SYSROOT=""
|
||||||
|
MARCH=""
|
||||||
|
OUT=""
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
--target=*) TARGET="${a#--target=}" ;;
|
||||||
|
--sysroot=*) SYSROOT="${a#--sysroot=}" ;;
|
||||||
|
--march=*) MARCH="${a#--march=}" ;;
|
||||||
|
-*) echo "unknown option: $a" >&2; exit 1 ;;
|
||||||
|
*) OUT="$a" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$OUT" ] || OUT="$HOME/.cache/fingerprintd/libqcomtee${TARGET:+-$TARGET}"
|
||||||
|
|
||||||
|
if [ ! -d "$SRC/.git" ]; then
|
||||||
|
echo ">> cloning $REPO -> $SRC"
|
||||||
|
mkdir -p "$(dirname "$SRC")"
|
||||||
|
git clone -q "$REPO" "$SRC"
|
||||||
|
git -C "$SRC" checkout -q "$COMMIT"
|
||||||
|
elif [ -n "${QUIC_TEEC_DIR:-}" ]; then
|
||||||
|
# A checkout the caller pointed us at is THEIRS. Never move its HEAD --
|
||||||
|
# silently checking out a pin in someone's working tree is how you lose
|
||||||
|
# uncommitted work. Report the mismatch and let them decide.
|
||||||
|
have=$(git -C "$SRC" rev-parse --short HEAD)
|
||||||
|
case "$COMMIT" in
|
||||||
|
"$have"*) : ;;
|
||||||
|
*) echo "!! $SRC is at $have, not the pinned $COMMIT." >&2
|
||||||
|
echo "!! Building from it anyway; unset QUIC_TEEC_DIR to use a pinned clone." >&2 ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
git -C "$SRC" fetch -q --all 2>/dev/null || true
|
||||||
|
git -C "$SRC" checkout -q "$COMMIT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
set -- -O2 -Wall -fPIC
|
||||||
|
[ -n "$TARGET" ] && set -- "$@" --target="$TARGET"
|
||||||
|
[ -n "$SYSROOT" ] && set -- "$@" --sysroot="$SYSROOT"
|
||||||
|
[ -n "$MARCH" ] && set -- "$@" -march="$MARCH"
|
||||||
|
|
||||||
|
echo ">> building libqcomtee.a (no QCBOR) for ${TARGET:-native}"
|
||||||
|
for f in src/qcomtee_object.c src/objects/mem_obj.c; do
|
||||||
|
clang "$@" -I"$SRC/libqcomtee/include" -I"$SRC/libqcomtee/src" \
|
||||||
|
-c "$SRC/libqcomtee/$f" -o "$OUT/$(basename "$f" .c).o"
|
||||||
|
done
|
||||||
|
rm -f "$OUT/libqcomtee.a"
|
||||||
|
llvm-ar rcs "$OUT/libqcomtee.a" "$OUT"/*.o
|
||||||
|
mkdir -p "$OUT/include"
|
||||||
|
cp "$SRC/libqcomtee/include/"*.h "$OUT/include/"
|
||||||
|
|
||||||
|
echo ">> done: $OUT/libqcomtee.a"
|
||||||
35
project.cpp
35
project.cpp
|
|
@ -8,6 +8,34 @@ import Crafter.Build;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// libqcomtee — Qualcomm's BSD-3 userspace client for QTEE, built by
|
||||||
|
// packaging/make-libqcomtee.sh into a per-target cache dir. It is not vendored
|
||||||
|
// here: it is upstream code we pin, and the script builds it WITHOUT QCBOR
|
||||||
|
// (see the script for why that dependency is avoidable).
|
||||||
|
static void ApplyQcomteeFlags(Configuration& cfg) {
|
||||||
|
fs::path base = fs::path(std::getenv("HOME") ? std::getenv("HOME") : ".")
|
||||||
|
/ ".cache" / "fingerprintd";
|
||||||
|
// make-libqcomtee.sh names its output dir after --target, and plain
|
||||||
|
// "libqcomtee" when built for the host. cfg.target is always populated
|
||||||
|
// (it defaults to the host triple), so try the target-specific dir first
|
||||||
|
// and fall back to the host one.
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path root = base / ("libqcomtee-" + cfg.target);
|
||||||
|
if (!fs::exists(root / "libqcomtee.a", ec))
|
||||||
|
root = base / "libqcomtee";
|
||||||
|
|
||||||
|
if (!fs::exists(root / "libqcomtee.a", ec)) {
|
||||||
|
std::println(std::cerr,
|
||||||
|
"libqcomtee not built for '{}'. Run:\n"
|
||||||
|
" packaging/make-libqcomtee.sh{}{}",
|
||||||
|
cfg.target.empty() ? std::string("native") : cfg.target,
|
||||||
|
cfg.target.empty() ? std::string() : " --target=" + cfg.target,
|
||||||
|
cfg.sysroot.empty() ? std::string() : " --sysroot=" + cfg.sysroot);
|
||||||
|
}
|
||||||
|
cfg.compileFlags.push_back("-I" + (root / "include").string());
|
||||||
|
cfg.linkFlags.push_back((root / "libqcomtee.a").string());
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
// fingerprintd-core — the wire formats and state machines as a static
|
// fingerprintd-core — the wire formats and state machines as a static
|
||||||
// library of pure C++ modules. Deliberately free of GLib, libqcomtee and
|
// library of pure C++ modules. Deliberately free of GLib, libqcomtee and
|
||||||
|
|
@ -21,13 +49,14 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
ApplyStandardArgs(*Core, args);
|
ApplyStandardArgs(*Core, args);
|
||||||
Core->type = ConfigurationType::LibraryStatic;
|
Core->type = ConfigurationType::LibraryStatic;
|
||||||
{
|
{
|
||||||
std::array<fs::path, 6> ifaces = {
|
std::array<fs::path, 7> ifaces = {
|
||||||
"interfaces/Fingerprintd",
|
"interfaces/Fingerprintd",
|
||||||
"interfaces/Fingerprintd-Sfs",
|
"interfaces/Fingerprintd-Sfs",
|
||||||
"interfaces/Fingerprintd-Rpmb",
|
"interfaces/Fingerprintd-Rpmb",
|
||||||
"interfaces/Fingerprintd-Ta",
|
"interfaces/Fingerprintd-Ta",
|
||||||
"interfaces/Fingerprintd-Engine",
|
"interfaces/Fingerprintd-Engine",
|
||||||
"interfaces/Fingerprintd-Store",
|
"interfaces/Fingerprintd-Store",
|
||||||
|
"interfaces/Fingerprintd-Tee",
|
||||||
};
|
};
|
||||||
std::array<fs::path, 0> impls = {};
|
std::array<fs::path, 0> impls = {};
|
||||||
Core->GetInterfacesAndImplementations(ifaces, impls);
|
Core->GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
@ -48,11 +77,15 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplyQcomteeFlags(cfg);
|
||||||
|
cfg.linkFlags.push_back("-lpthread"); // the supplicant thread
|
||||||
|
|
||||||
cfg.AddTest("Sfs").Dependencies({ Core.get() });
|
cfg.AddTest("Sfs").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Rpmb").Dependencies({ Core.get() });
|
cfg.AddTest("Rpmb").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Ta").Dependencies({ Core.get() });
|
cfg.AddTest("Ta").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Engine").Dependencies({ Core.get() });
|
cfg.AddTest("Engine").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Store").Dependencies({ Core.get() });
|
cfg.AddTest("Store").Dependencies({ Core.get() });
|
||||||
|
cfg.AddTest("Tee").Dependencies({ Core.get() });
|
||||||
|
|
||||||
ProjectLint::AddProjectLintRules(cfg);
|
ProjectLint::AddProjectLintRules(cfg);
|
||||||
|
|
||||||
|
|
|
||||||
151
tests/Tee/main.cpp
Normal file
151
tests/Tee/main.cpp
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
// lint-disable-file fixed-width-types
|
||||||
|
/*
|
||||||
|
Fingerprintd:Tee unit tests.
|
||||||
|
|
||||||
|
The credentials blob is CBOR that QTEE parses, so it is pinned against the
|
||||||
|
exact byte string verified on the device: a2 01 00 06 1b <u64 ms>. Building it
|
||||||
|
here instead of with QCBOR is what lets libqcomtee be compiled with no
|
||||||
|
dependency beyond libc, so the encoder has to be right rather than
|
||||||
|
approximately right.
|
||||||
|
|
||||||
|
The rest is constants that were expensive to establish and are cheap to undo by
|
||||||
|
accident: the listener table, and the three root ops that must never be
|
||||||
|
invoked.
|
||||||
|
*/
|
||||||
|
import std;
|
||||||
|
import Fingerprintd;
|
||||||
|
|
||||||
|
using namespace fingerprintd::tee;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int Failures = 0;
|
||||||
|
void Check(bool cond, std::string_view msg) {
|
||||||
|
if (!cond) {
|
||||||
|
std::println(std::cerr, "FAIL: {}", msg);
|
||||||
|
++Failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string Hex(std::span<const std::byte> b) {
|
||||||
|
std::string s;
|
||||||
|
for (std::byte x : b) s += std::format("{:02x}", std::to_integer<unsigned>(x));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// ---- The credentials blob, against the byte string verified on-device
|
||||||
|
{
|
||||||
|
// {AttrUid: 0, AttrSystemTime: 0} -> a2 01 00 06 1b 00*8
|
||||||
|
auto c = BuildCredentials(0, 0);
|
||||||
|
Check(Hex(c) == "a20100061b0000000000000000", "root, zero time: exact bytes");
|
||||||
|
Check(c.size() == 13, "13 bytes for a small uid");
|
||||||
|
|
||||||
|
// The map header and both keys are fixed.
|
||||||
|
Check(std::to_integer<unsigned>(c[0]) == 0xA2, "map(2)");
|
||||||
|
Check(std::to_integer<unsigned>(c[1]) == 0x01, "key AttrUid = 1");
|
||||||
|
Check(std::to_integer<unsigned>(c[3]) == 0x06, "key AttrSystemTime = 6");
|
||||||
|
Check(std::to_integer<unsigned>(c[4]) == 0x1B, "timestamp is the 8-byte form");
|
||||||
|
|
||||||
|
// A real timestamp, big endian.
|
||||||
|
auto t = BuildCredentials(0, 0x0000019283746555ull);
|
||||||
|
Check(Hex(t) == "a20100061b0000019283746555", "timestamp big endian");
|
||||||
|
|
||||||
|
// A non-root uid takes the shortest CBOR form, which is what QCBOR
|
||||||
|
// emits and what QTEE's parser expects.
|
||||||
|
Check(Hex(BuildCredentials(23, 0)).starts_with("a20117"), "uid 23 is one byte");
|
||||||
|
Check(Hex(BuildCredentials(24, 0)).starts_with("a2011818"), "uid 24 needs 0x18");
|
||||||
|
Check(Hex(BuildCredentials(1000, 0)).starts_with("a2011903e8"), "uid 1000 needs 0x19");
|
||||||
|
Check(Hex(BuildCredentials(70000, 0)).starts_with("a2011a00011170"), "uid 70000 needs 0x1a");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The CBOR uint encoder's boundaries
|
||||||
|
{
|
||||||
|
auto enc = [](std::uint64_t v) {
|
||||||
|
std::vector<std::byte> o; AppendCborUint(o, v); return Hex(o);
|
||||||
|
};
|
||||||
|
Check(enc(0) == "00", "0");
|
||||||
|
Check(enc(23) == "17", "23 is the last single-byte value");
|
||||||
|
Check(enc(24) == "1818", "24 crosses into the one-byte form");
|
||||||
|
Check(enc(255) == "18ff", "255");
|
||||||
|
Check(enc(256) == "190100", "256 crosses into the two-byte form");
|
||||||
|
Check(enc(65535) == "19ffff", "65535");
|
||||||
|
Check(enc(65536) == "1a00010000", "65536 crosses into the four-byte form");
|
||||||
|
Check(enc(0xFFFFFFFFull) == "1affffffff", "u32 max");
|
||||||
|
Check(enc(0x100000000ull) == "1b0000000100000000", "past u32 takes eight bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The credentials object's read protocol
|
||||||
|
{
|
||||||
|
// A 13-byte blob, QTEE offering a 4096-byte buffer.
|
||||||
|
auto p = PlanRead(13, 0, 4096);
|
||||||
|
Check(p.valid && p.offset == 0 && p.count == 13, "whole blob in one read");
|
||||||
|
|
||||||
|
// A short output buffer serves what fits.
|
||||||
|
auto q = PlanRead(13, 0, 8);
|
||||||
|
Check(q.valid && q.count == 8, "clamped to the output capacity");
|
||||||
|
|
||||||
|
// Continuing from where that stopped.
|
||||||
|
auto r = PlanRead(13, 8, 8);
|
||||||
|
Check(r.valid && r.offset == 8 && r.count == 5, "the remainder");
|
||||||
|
|
||||||
|
// At or past the end is an ERROR, not an empty read. libqcomtee
|
||||||
|
// returns QCOMTEE_ERROR_INVALID; an earlier reversal of ours guessed
|
||||||
|
// 10 and was wrong.
|
||||||
|
Check(!PlanRead(13, 13, 8).valid, "offset == length is invalid");
|
||||||
|
Check(!PlanRead(13, 99, 8).valid, "offset past the end is invalid");
|
||||||
|
Check(!PlanRead(0, 0, 8).valid, "an empty blob has nothing to read");
|
||||||
|
|
||||||
|
Check(CredLengthReplySize == 8, "GET_LENGTH answers into exactly 8 bytes");
|
||||||
|
Check(static_cast<std::uint32_t>(CredOp::GetLength) == 0, "IIO_OP_GET_LENGTH");
|
||||||
|
Check(static_cast<std::uint32_t>(CredOp::ReadAtOffset) == 1, "IIO_OP_READ_AT_OFFSET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The listener table
|
||||||
|
{
|
||||||
|
Check(Listeners.size() == 3, "three listeners");
|
||||||
|
Check(Listeners[0].id == 0x7000 && Listeners[0].bufferSize == 516096, "gpfile");
|
||||||
|
Check(Listeners[1].id == 0x2000 && Listeners[1].bufferSize == 25600, "rpmb");
|
||||||
|
Check(Listeners[2].id == 10 && Listeners[2].bufferSize == 20480, "fs");
|
||||||
|
// The buffer sizes are qseecomd's and are not ours to round off.
|
||||||
|
Check(Listeners[0].bufferSize > Listeners[1].bufferSize, "gpfile's buffer is the large one");
|
||||||
|
// Ids must be distinct: the table is global to QTEE and a collision
|
||||||
|
// means one registration silently loses.
|
||||||
|
Check(Listeners[0].id != Listeners[1].id && Listeners[1].id != Listeners[2].id,
|
||||||
|
"ids are distinct");
|
||||||
|
Check(ResultIdAlreadyTaken == -99, "-99 means the id is taken, not a storage error");
|
||||||
|
Check(OneObjectPerRegistration, "one callback object per registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Services and ops
|
||||||
|
{
|
||||||
|
Check(UidQseecomCompatAppLoader == 122, "the loader UID");
|
||||||
|
Check(UidListenerCbo == 87, "CListenerCBO");
|
||||||
|
Check(AppSendRequestOp == 0, "sendRequest is op 0");
|
||||||
|
Check(ClientEnvOp == RootOp::Register, "we use the proven op-2 path");
|
||||||
|
|
||||||
|
// The three destructive root ops must not appear in the enum we can
|
||||||
|
// reach for. This is a guard against a future sweep.
|
||||||
|
auto isDefined = [](std::uint32_t op) {
|
||||||
|
return op == static_cast<std::uint32_t>(RootOp::RegisterLegacy)
|
||||||
|
|| op == static_cast<std::uint32_t>(RootOp::Register)
|
||||||
|
|| op == static_cast<std::uint32_t>(RootOp::RegisterWithCredentials);
|
||||||
|
};
|
||||||
|
Check(!isDefined(4), "NOTIFY_DOMAIN_CHANGE is not reachable");
|
||||||
|
Check(!isDefined(8), "ADCI_ACCEPT is not reachable");
|
||||||
|
Check(!isDefined(9), "ADCI_SHUTDOWN is not reachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The capture region
|
||||||
|
{
|
||||||
|
Check(CaptureRegionSize == 16384, "16 KB region");
|
||||||
|
Check(EmbeddedBufOffsetValue == 0x10, "request+0x10 is payload+0x00");
|
||||||
|
Check(RegionScopedToCommand == 0x1013,
|
||||||
|
"scoped to CAPTURE_IMAGE; applying it to SYNC_CONFIG breaks that command");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Failures == 0) std::println("Tee: all tests passed");
|
||||||
|
return Failures;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue