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.
151 lines
6.7 KiB
C++
151 lines
6.7 KiB
C++
// 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;
|
|
}
|