fingerprintd/tests/Tee/main.cpp

151 lines
6.7 KiB
C++
Raw Permalink Normal View History

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.
2026-09-02 18:02:28 +02:00
// 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;
}