// 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 . 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 b) { std::string s; for (std::byte x : b) s += std::format("{:02x}", std::to_integer(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(c[0]) == 0xA2, "map(2)"); Check(std::to_integer(c[1]) == 0x01, "key AttrUid = 1"); Check(std::to_integer(c[3]) == 0x06, "key AttrSystemTime = 6"); Check(std::to_integer(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 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(CredOp::GetLength) == 0, "IIO_OP_GET_LENGTH"); Check(static_cast(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(RootOp::RegisterLegacy) || op == static_cast(RootOp::Register) || op == static_cast(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; }