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
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