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
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 :Engine;
|
||||
export import :Store;
|
||||
export import :Tee;
|
||||
|
|
|
|||
Loading…
Reference in a new issue