diff --git a/implementations/main.cpp b/implementations/main.cpp index a0c3693..15dcb20 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -49,6 +49,7 @@ namespace { constexpr const char* Version = "0.0.3"; +bool g_verbose = false; std::string g_taPath = "/lib/firmware/focal64.mbn"; std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; @@ -396,12 +397,24 @@ struct CommandResult { bool invoked = false; qcomtee_result_t result = 0; std::i CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, std::span payload) { namespace ta = fingerprintd::ta; + namespace tee = fingerprintd::tee; 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); + // CAPTURE_IMAGE's flags word sits at payload+0x18, PAST the declared + // length of 0x14 -- the trustlet range-checks the length to exactly that + // and reads the flags anyway. Without bit 1 or bit 30 it skips + // preprocessing, the classifier and the enrol grouper entirely and returns + // success having done nothing but a raw scan. + if (cmd == ta::Cmd::CaptureImage) { + for (std::size_t i = 0; i < 4; i++) + req[ta::ReqPayloadOff + ta::CaptureFlagsOff + i] = + static_cast((ta::CaptureFlagsEnrol >> (8 * i)) & 0xFF); + } + 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(); @@ -415,12 +428,52 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, p[i].object = QCOMTEE_OBJECT_NULL; } + // A capture needs a real shared memory REGION or the trustlet answers + // -201: it reads an output-buffer pointer out of payload+0x00, and QTEE + // only patches an address in there if we name the location in + // embeddedBufOffsets (IB2) and hand it the region in an object slot. + // Without that the pointer is NULL. This is the whole difference between a + // flat metric and a real scan. + // + // Two traps: the offsets array applies to EVERY command in a run, so it is + // scoped to this one command -- patching a pointer into SYNC_CONFIG's + // request breaks it. And an invoke CONSUMES its input objects, so the + // region is allocated fresh each time. + qcomtee_object* region = QCOMTEE_OBJECT_NULL; + std::uint32_t offsets = tee::EmbeddedBufOffsetValue; + if (cmd == static_cast(tee::RegionScopedToCommand)) { + if (qcomtee_memory_object_alloc(tee::CaptureRegionSize, g_root, ®ion)) { + std::println(std::cerr, " memory region alloc failed"); + region = QCOMTEE_OBJECT_NULL; + } else { + void* addr = qcomtee_memory_object_addr(region); + std::size_t sz = qcomtee_memory_object_size(region); + if (g_verbose) + std::println(" region: addr={} size={} offsets=[0x{:x}] slot=IO0", + addr, sz, offsets); + std::memset(addr, 0, sz); + p[2].ubuf.addr = &offsets; + p[2].ubuf.size = sizeof(offsets); + p[6].object = region; + } + } + CommandResult out; - if (qcomtee_object_invoke(app, fingerprintd::tee::AppSendRequestOp, p, 10, &out.result)) + if (qcomtee_object_invoke(app, tee::AppSendRequestOp, p, 10, &out.result)) { + if (region != QCOMTEE_OBJECT_NULL) + qcomtee_memory_object_release(region); return out; + } out.invoked = true; out.rc = ta::ResultCode(reqOut); out.metric = ta::CaptureMetric(reqOut); + if (g_verbose && cmd == ta::Cmd::CaptureImage) { + std::string hex; + for (std::size_t i = 0; i < 0x30; i++) + hex += std::format("{:02x}{}", std::to_integer(reqOut[i]), + (i % 16 == 15) ? "\n " : " "); + std::println(" reqOut[0x00..0x2f]:\n {}", hex); + } return out; } @@ -548,12 +601,32 @@ int Probe() { return 1; } - // With the sensor initialised, a capture returns a real metric. No finger - // is needed to see the idle floor. - std::vector cap(fingerprintd::ta::CaptureDeclaredLen, std::byte{0}); - auto c1 = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap); - Report(fingerprintd::ta::Cmd::CaptureImage, c1); - std::println(" idle capture metric = {}", c1.metric); + // With the sensor initialised and a region supplied, a capture returns a + // real metric. No finger is needed to establish the idle floor, and the + // floor is the only meaningful reference: the metric is per frame and + // drifts, so a fixed threshold is wrong by construction. + fingerprintd::engine::Baseline baseline; + std::println("calibrating the idle floor ({} samples)", + fingerprintd::engine::Baseline::DefaultSamples); + for (std::size_t i = 0; i < fingerprintd::engine::Baseline::DefaultSamples; i++) { + std::vector cap(fingerprintd::ta::CaptureDeclaredLen); + fingerprintd::ta::BuildCapturePayload(cap); + auto c = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap); + if (!c.invoked || c.result != 0) { + Report(fingerprintd::ta::Cmd::CaptureImage, c); + std::println(std::cerr, "capture failed during calibration"); + return 1; + } + std::println(" idle {}/{}: rc={} metric={}", i + 1, + fingerprintd::engine::Baseline::DefaultSamples, c.rc, c.metric); + baseline.Observe(c.metric); + } + if (!baseline.Ready()) { + std::println(std::cerr, "baseline did not calibrate (floor stayed 0)"); + return 1; + } + std::println("idle floor = {}, finger threshold = {}", baseline.Floor(), + baseline.Threshold()); std::println("\ntrustlet initialised against a powered sensor."); pthread_cancel(th); @@ -574,6 +647,7 @@ int main(int argc, char** argv) { 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 (a == "--verbose") g_verbose = true; } if (probe) return Probe(); diff --git a/interfaces/Fingerprintd-Ta.cppm b/interfaces/Fingerprintd-Ta.cppm index ff4cd81..f2325ec 100644 --- a/interfaces/Fingerprintd-Ta.cppm +++ b/interfaces/Fingerprintd-Ta.cppm @@ -173,6 +173,28 @@ export namespace fingerprintd::ta { // is written past the declared length on purpose. 0x20 gives -201. inline constexpr std::uint32_t CaptureDeclaredLen = 0x14; + // Two fields inside that payload which an all-zero request leaves unset. + // + // +0x0c frame count how many frames this capture takes + // +0x10 branch selector which capture path runs; 0 returns metric 0 + // + // Both matter for reading the result as much as for getting one: the + // metric is PER FRAME, so a count of 4 reads roughly four times a count of + // 1 and a threshold calibrated at one count is meaningless at another. + // Sending zeros gets -201. + inline constexpr std::size_t CaptureFrameCountOff = 0x0c; + inline constexpr std::size_t CaptureSelectorOff = 0x10; + inline constexpr std::uint32_t CaptureFrameCountDefault = 1; + inline constexpr std::uint32_t CaptureSelectorDefault = 1; + + inline void BuildCapturePayload(std::span out, + std::uint32_t frames = CaptureFrameCountDefault, + std::uint32_t selector = CaptureSelectorDefault) { + std::ranges::fill(out.first(CaptureDeclaredLen), std::byte{0}); + detail::StoreU32(out, CaptureFrameCountOff, frames); + detail::StoreU32(out, CaptureSelectorOff, selector); + } + // ---- SAVE_DATA -------------------------------------------------------- // // payload+0x00 is a bitmask and the handler's first test is diff --git a/tests/Ta/main.cpp b/tests/Ta/main.cpp index 7b0f583..ed0d1b8 100644 --- a/tests/Ta/main.cpp +++ b/tests/Ta/main.cpp @@ -153,6 +153,23 @@ int main() { Check(CaptureFlagsOff == 0x18 && CaptureDeclaredLen == 0x14, "the flags word sits past the declared length on purpose"); + // ---- The capture payload's two fields + { + std::vector cap(CaptureDeclaredLen); + BuildCapturePayload(cap); + Check(Get32(cap, CaptureFrameCountOff) == 1, "frame count defaults to 1"); + Check(Get32(cap, CaptureSelectorOff) == 1, "selector defaults to 1"); + Check(Get32(cap, 0) == 0, "payload+0 is left for QTEE to patch the region into"); + // An all-zero payload is what -201 looks like on the wire. + std::vector zero(CaptureDeclaredLen, std::byte{0}); + Check(Get32(zero, CaptureSelectorOff) == 0, "selector 0 returns metric 0"); + // The fields must fit inside the declared length. + Check(CaptureSelectorOff + 4 <= CaptureDeclaredLen, "selector fits the payload"); + Check(CaptureFrameCountOff < CaptureSelectorOff, "count precedes selector"); + // ...while the flags word deliberately does not. + Check(CaptureFlagsOff >= CaptureDeclaredLen, "the flags word sits past it"); + } + // ---- SAVE_DATA masks: bit 30 is the whole discriminator Check((SaveMaskTemplate & (1u << 30)) != 0, "template save sets bit 30"); Check((SaveMaskCalibration & (1u << 30)) == 0, "calibration save clears bit 30");