diff --git a/implementations/main.cpp b/implementations/main.cpp index 6ceda8b..7c99736 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -45,7 +45,10 @@ import Fingerprintd; namespace { -constexpr const char* Version = "0.0.2"; +constexpr const char* Version = "0.0.3"; + +std::string g_taPath = "/lib/firmware/focal64.mbn"; +std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; qcomtee_object* g_root = QCOMTEE_OBJECT_NULL; @@ -212,6 +215,118 @@ qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) { return p[1].object; } +// ---- The trustlet +// +// The loader is IQSEEComCompatAppLoader (UID 122): op 1 loadFromBuffer, op 2 +// lookupTA. A stale instance from a crashed run is unloaded first, which is +// what stops a bad experiment costing a reboot. +constexpr const char* TaName = "focal64"; + +void UnloadStale(qcomtee_object* loader) { + qcomtee_param p[3] = {}; + std::array ob{}; + p[0].attr = QCOMTEE_UBUF_INPUT; + p[0].ubuf.addr = const_cast(TaName); + p[0].ubuf.size = std::strlen(TaName); + p[1].attr = QCOMTEE_UBUF_OUTPUT; + p[1].ubuf.addr = ob.data(); + p[1].ubuf.size = ob.size(); + p[2].attr = QCOMTEE_OBJREF_OUTPUT; + qcomtee_result_t result = 0; + if (qcomtee_object_invoke(loader, 2, p, 3, &result) || result) { + std::println("lookupTA('{}') -> result={} (nothing to unload)", TaName, + static_cast(result)); + return; + } + if (!qcomtee_object_invoke(p[2].object, 2, nullptr, 0, &result)) + std::println("unloaded a stale '{}' -> result={}", TaName, static_cast(result)); + qcomtee_object_refs_dec(p[2].object); +} + +qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) { + UnloadStale(loader); + + std::ifstream f(path, std::ios::binary); + if (!f) { + std::println(std::cerr, "cannot open {}", path); + return QCOMTEE_OBJECT_NULL; + } + std::vector image((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + if (image.empty()) { + std::println(std::cerr, "{} is empty", path); + return QCOMTEE_OBJECT_NULL; + } + + std::array distName{}; + qcomtee_param p[4] = {}; + p[0].attr = QCOMTEE_UBUF_INPUT; + p[0].ubuf.addr = image.data(); + p[0].ubuf.size = image.size(); + p[1].attr = QCOMTEE_UBUF_INPUT; + p[1].ubuf.addr = const_cast(TaName); + p[1].ubuf.size = std::strlen(TaName); + p[2].attr = QCOMTEE_UBUF_OUTPUT; + p[2].ubuf.addr = distName.data(); + p[2].ubuf.size = distName.size(); + p[3].attr = QCOMTEE_OBJREF_OUTPUT; + qcomtee_result_t result = 0; + if (qcomtee_object_invoke(loader, 1, p, 4, &result) || result) { + std::println(std::cerr, "loadFromBuffer failed, result={}", + static_cast(result)); + return QCOMTEE_OBJECT_NULL; + } + std::println("trustlet loaded from {} ({} bytes), distName='{}'", path, + image.size(), distName.data()); + return p[3].object; +} + +// sendRequest is op 0 with arity 0x0424: four input buffers, two output, four +// object slots. The request and response buffers go in and come back out; the +// trustlet's own return code rides in the returned request's header. +struct CommandResult { bool invoked = false; qcomtee_result_t result = 0; std::int32_t rc = 0; std::int32_t metric = 0; }; + +CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, + std::span payload) { + namespace ta = fingerprintd::ta; + static std::vector req(8192), rsp(16384), reqOut(8192), rspOut(16384); + std::ranges::fill(rsp, std::byte{0}); + std::ranges::fill(reqOut, std::byte{0}); + std::ranges::fill(rspOut, std::byte{0}); + ta::BuildRequest(req, cmd, payload); + + std::uint32_t is64 = 1; + qcomtee_param p[10] = {}; + p[0].attr = QCOMTEE_UBUF_INPUT; p[0].ubuf.addr = req.data(); p[0].ubuf.size = req.size(); + p[1].attr = QCOMTEE_UBUF_INPUT; p[1].ubuf.addr = rsp.data(); p[1].ubuf.size = rsp.size(); + p[2].attr = QCOMTEE_UBUF_INPUT; p[2].ubuf.addr = nullptr; p[2].ubuf.size = 0; + p[3].attr = QCOMTEE_UBUF_INPUT; p[3].ubuf.addr = &is64; p[3].ubuf.size = sizeof(is64); + p[4].attr = QCOMTEE_UBUF_OUTPUT; p[4].ubuf.addr = reqOut.data(); p[4].ubuf.size = reqOut.size(); + p[5].attr = QCOMTEE_UBUF_OUTPUT; p[5].ubuf.addr = rspOut.data(); p[5].ubuf.size = rspOut.size(); + for (int i = 6; i < 10; i++) { + p[i].attr = QCOMTEE_OBJREF_INPUT; + p[i].object = QCOMTEE_OBJECT_NULL; + } + + CommandResult out; + if (qcomtee_object_invoke(app, fingerprintd::tee::AppSendRequestOp, p, 10, &out.result)) + return out; + out.invoked = true; + out.rc = ta::ResultCode(reqOut); + out.metric = ta::CaptureMetric(reqOut); + return out; +} + +void Report(fingerprintd::ta::Cmd cmd, const CommandResult& r) { + namespace ta = fingerprintd::ta; + if (!r.invoked) { + std::println(" CMD 0x{:04x} -> INVOKE FAILED", static_cast(cmd)); + return; + } + std::println(" CMD 0x{:04x} -> result={} rc={} ({})", static_cast(cmd), + static_cast(r.result), r.rc, ta::StrError(r.rc)); +} + int Probe() { namespace tee = fingerprintd::tee; @@ -243,7 +358,41 @@ int Probe() { std::println("QSEECOM-compat app loader (UID {}) opened", tee::UidQseecomCompatAppLoader); - std::println("\nreached QTEE. Not driving the sensor yet."); + qcomtee_object* app = LoadTrustlet(loader, g_taPath); + if (app == QCOMTEE_OBJECT_NULL) + return 1; + + // SYNC_CONFIG first, always. The trustlet reads its whole configuration + // from this one JSON payload, and two keys in it are load-bearing: + // algorithm.enrolling_overlap_intervals must be PRESENT (its default is + // the empty string, which faults the trustlet's own sscanf), and + // device.preferred_device_id selects the chip driver. + std::ifstream cf(g_cfgPath); + if (!cf) { + std::println(std::cerr, "cannot open config {}", g_cfgPath); + return 1; + } + std::string json((std::istreambuf_iterator(cf)), + std::istreambuf_iterator()); + // The trustlet wants the terminating NUL counted. + std::vector cfg(json.size() + 1, std::byte{0}); + for (std::size_t i = 0; i < json.size(); i++) + cfg[i] = static_cast(json[i]); + std::println("config {}: {} bytes", g_cfgPath, cfg.size()); + + auto r = SendCommand(app, fingerprintd::ta::Cmd::SyncConfig, cfg); + Report(fingerprintd::ta::Cmd::SyncConfig, r); + if (!r.invoked || r.result != 0 || r.rc != 0) { + std::println(std::cerr, "SYNC_CONFIG did not succeed; stopping here"); + return 1; + } + + // A storage read needs no sensor. It exercises the whole SFS listener path + // if listeners are registered, and answers -2 when they are not. + auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {}); + Report(fingerprintd::ta::Cmd::Enumerate, e); + + std::println("\ntrustlet is up and configured. Sensor not powered yet."); pthread_cancel(th); pthread_join(th, nullptr); return 0; @@ -260,6 +409,8 @@ int main(int argc, char** argv) { return 0; } if (a == "--probe-tee") probe = true; + if (a.starts_with("--ta=")) g_taPath = a.substr(5); + if (a.starts_with("--config=")) g_cfgPath = a.substr(9); } if (probe) return Probe(); diff --git a/interfaces/Fingerprintd-Ta.cppm b/interfaces/Fingerprintd-Ta.cppm index 7fdce66..ff4cd81 100644 --- a/interfaces/Fingerprintd-Ta.cppm +++ b/interfaces/Fingerprintd-Ta.cppm @@ -233,6 +233,46 @@ export namespace fingerprintd::ta { // itself is the caller's to choose. inline constexpr std::size_t SetActiveGroupGidOff = 0; + // ---- The request/response envelope ------------------------------------ + // + // sendRequest carries two buffers in and two back. The request is: + // + // +0x00 u32 command id + // +0x04 u32 declared payload length + // +0x10 the payload + // + // and the returned copy of it carries the trustlet's own return code and + // the capture metric in the header, ahead of the payload: + // + // +0x08 i32 rc the trustlet's result, distinct from QTEE's + // +0x0c i32 metric CAPTURE_IMAGE's finger signal + // + // The metric is a HEADER field. It has been called "payload+12" in this + // project's notes and it is not; it tracks the finger reproducibly and + // every recorded number depends on reading it here. + inline constexpr std::size_t ReqCmdOff = 0x00; + inline constexpr std::size_t ReqLenOff = 0x04; + inline constexpr std::size_t ReqPayloadOff = 0x10; + inline constexpr std::size_t RespRcOff = 0x08; + inline constexpr std::size_t RespMetricOff = 0x0c; + + inline void BuildRequest(std::span req, Cmd cmd, + std::span payload) { + std::ranges::fill(req, std::byte{0}); + detail::StoreU32(req, ReqCmdOff, static_cast(cmd)); + if (!payload.empty()) { + detail::StoreU32(req, ReqLenOff, static_cast(payload.size())); + std::ranges::copy(payload, req.begin() + static_cast(ReqPayloadOff)); + } + } + + inline std::int32_t ResultCode(std::span reqOut) { + return static_cast(detail::LoadU32(reqOut, RespRcOff)); + } + inline std::int32_t CaptureMetric(std::span reqOut) { + return static_cast(detail::LoadU32(reqOut, RespMetricOff)); + } + // ---- Responses -------------------------------------------------------- // // THE TRAP. The buffer that comes back is the whole REQUEST, and the diff --git a/tests/Ta/main.cpp b/tests/Ta/main.cpp index b24251f..7b0f583 100644 --- a/tests/Ta/main.cpp +++ b/tests/Ta/main.cpp @@ -203,6 +203,38 @@ int main() { "reading at the payload offset directly gives the wrong word"); } + // ---- The request/response envelope + { + std::vector req(256); + std::array payload{ std::byte{1}, std::byte{2}, + std::byte{3}, std::byte{4} }; + BuildRequest(req, Cmd::SyncConfig, payload); + Check(Get32(req, ReqCmdOff) == 0x100d, "command id at +0"); + Check(Get32(req, ReqLenOff) == 4, "declared length at +4"); + Check(std::to_integer(req[ReqPayloadOff]) == 1, "payload at +0x10"); + Check(ReqPayloadOff == ResponsePayloadOff, "request and response payloads share the offset"); + + // An empty payload leaves the declared length zero rather than + // pointing at uninitialised bytes. + BuildRequest(req, Cmd::Enumerate, {}); + Check(Get32(req, ReqLenOff) == 0, "no payload, no declared length"); + Check(Get32(req, ReqCmdOff) == 0x2005, "ENUMERATE"); + + // rc and the metric are HEADER fields, ahead of the payload, and are + // distinct from each other. + std::vector out(256); + auto put = [&](std::size_t off, std::uint32_t v) { + for (std::size_t i = 0; i < 4; i++) + out[off + i] = static_cast((v >> (8 * i)) & 0xFF); + }; + put(RespRcOff, static_cast(-11)); + put(RespMetricOff, 345); + Check(ResultCode(out) == -11, "rc at +8, signed"); + Check(CaptureMetric(out) == 345, "metric at +0x0c"); + Check(RespRcOff != RespMetricOff && RespMetricOff < ResponsePayloadOff, + "both sit in the header, ahead of the payload"); + } + // ---- Poisoning { std::vector payload(64);