From c33076ca9beadf2aa44b6d8583713bd912a1fd18 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 18:48:44 +0200 Subject: [PATCH] Add the authentication loop Arms a scan session and drives the frame loop: capture, decide finger from the calibrated floor, report the touch edges, classify the verdict. It needs no writes of any kind -- no SAVE_DATA, no RPMB write, no SFS write -- so it runs safely against an existing template with the store read-only. That is what makes it the right thing to try before enrolment rather than after. Verified armed on the phone: the template loads, the floor calibrates, and AUTHENTICATE returns rc=0, which also proves the gid agrees with the one SET_ACTIVE_GROUP used (a mismatch answers -200). With no finger present the loop correctly reports nothing: no touch edge, no event, no terminal frame. The fid field is poisoned before every REPORT_EVENT, because a zero-initialised buffer cannot distinguish "the matcher never ran" from "the matcher ran and rejected" -- the failure path writes zero there too. The tally reports terminal frames as the denominator and presses separately, so a run cannot be read as having rejections it did not have. --- implementations/main.cpp | 105 +++++++++++++++++++++++++++++++- interfaces/Fingerprintd-Ta.cppm | 1 + 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index ccaf859..0b72bff 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -53,6 +53,9 @@ constexpr const char* Version = "0.0.3"; bool g_verbose = false; bool g_listeners = false; +bool g_auth = false; +int g_frames = 40; +int g_frameGapMs = 500; std::uint32_t g_gid = 0; std::string g_taPath = "/lib/firmware/focal64.mbn"; std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; @@ -741,7 +744,17 @@ qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) { // 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; }; +struct CommandResult { + bool invoked = false; + qcomtee_result_t result = 0; + std::int32_t rc = 0; + std::int32_t metric = 0; + // Only meaningful for REPORT_EVENT: the matcher's verdict rides in the + // returned request's payload. + std::uint32_t gid = 0; + std::uint32_t fid = 0; + std::int32_t samplesRemaining = -1; +}; CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, std::span payload) { @@ -816,6 +829,11 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, out.invoked = true; out.rc = ta::ResultCode(reqOut); out.metric = ta::CaptureMetric(reqOut); + if (cmd == ta::Cmd::ReportEvent) { + out.gid = ta::MatchedGid(reqOut); + out.fid = ta::MatchedFid(reqOut); + out.samplesRemaining = ta::SamplesRemaining(reqOut); + } if (g_verbose && cmd == ta::Cmd::CaptureImage) { std::string hex; for (std::size_t i = 0; i < 0x30; i++) @@ -1008,6 +1026,89 @@ int Probe() { std::println("idle floor = {}, finger threshold = {}", baseline.Floor(), baseline.Threshold()); + // ---- Authentication + // + // Needs no writes of any kind: no SAVE_DATA, no RPMB write, no SFS write. + // So it runs safely against an existing template with the store read-only, + // which is what makes it the right thing to try before enrolment. + if (g_auth) { + namespace ta = fingerprintd::ta; + namespace en = fingerprintd::engine; + + // AUTHENTICATE arms the scan session. Its gid must match the one + // SET_ACTIVE_GROUP used or the trustlet answers -200. + std::vector au(ta::AuthPayloadSize); + ta::BuildAuthPayload(au, 1, g_gid); + std::println("\nAUTHENTICATE gid={}", g_gid); + auto a = SendCommand(app, ta::Cmd::Authenticate, au); + Report(ta::Cmd::Authenticate, a); + if (!a.invoked || a.result != 0 || a.rc != 0) { + std::println(std::cerr, "could not arm authentication"); + return 1; + } + + for (int c = 3; c > 0; c--) { + std::println("*** press and lift your finger in {}... ***", c); + std::fflush(stdout); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + std::println("\n*** GO -- {} frames, about {} seconds ***\n", g_frames, + (g_frames * g_frameGapMs) / 1000); + en::TouchTracker tracker; + en::AuthTally tally; + + for (int i = 0; i < g_frames; i++) { + std::vector q(0x10, std::byte{0}); + SendCommand(app, ta::Cmd::QueryEventStatus, q); + + std::vector cap(ta::CaptureDeclaredLen); + ta::BuildCapturePayload(cap); + auto c = SendCommand(app, ta::Cmd::CaptureImage, cap); + bool finger = baseline.IsFinger(c.metric); + + auto events = tracker.Observe(finger, en::Mode::Authenticate); + std::string verdicts; + for (ta::Event ev : events) { + std::vector evbuf(ta::EventContextSize); + ta::BuildEventContext(evbuf, { .event = ev }); + // Poison the fid field before the call. A zero-initialised + // buffer cannot tell "the matcher never ran" from "the matcher + // ran and rejected the finger" -- the failure path writes zero + // there too, so zero is ambiguous and 0xAAAAAAAA is not. + ta::PoisonFid(std::span(evbuf).subspan(ta::ResponsePayloadOff)); + + auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf); + if (!r.invoked) continue; + + ta::Verdict v = ta::Classify(r.rc, r.fid); + tally.Observe(v, finger); + verdicts += std::format(" {}", [&] { + switch (v) { + case ta::Verdict::Match: + return std::format("*** MATCH *** gid={} fid={}", r.gid, r.fid); + case ta::Verdict::Rejected: return std::string("REJECTED"); + case ta::Verdict::NotIdentifiedYet: return std::string("not identified yet"); + case ta::Verdict::MatcherNeverRan: return std::string("released"); + } + return std::string("?"); + }()); + } + std::println(" frame {:2}/{}: metric={:<4}{}{}", i + 1, g_frames, c.metric, + finger ? " FINGER" : " ", verdicts); + std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); + } + // Only a terminal verdict is an attempt. Counting rescan frames as + // rejections invents failures that never happened. + std::println("\n=== {} MATCH / {} REJECTED over {} terminal frames ===", + tally.Matches(), tally.Rejections(), tally.TerminalFrames()); + std::println(" ({} answered 'not identified yet', {} never reached the matcher)", + tally.NotIdentifiedYet(), tally.NeverRan()); + if (tally.Presses() > 0) + std::println(" presses: {} total, {} reached a verdict, {} matched", + tally.Presses(), tally.PressesDecided(), tally.PressesMatched()); + std::println(" {}", tally.Identified() ? "FINGER IDENTIFIED" : "no match"); + } + std::println("\ntrustlet initialised against a powered sensor."); pthread_cancel(th); pthread_join(th, nullptr); @@ -1033,6 +1134,8 @@ int main(int argc, char** argv) { // which destroys an enrolled template. Opt in explicitly. if (a == "--sfs-writable") g_sfsReadOnly = false; if (a == "--rpmb-write") g_rpmbWrite = true; + if (a == "--auth") { g_auth = true; g_listeners = true; } + if (a.starts_with("--frames=")) g_frames = std::stoi(std::string(a.substr(9))); if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11); if (a.starts_with("--gid=")) g_gid = static_cast( std::stoul(std::string(a.substr(6)))); diff --git a/interfaces/Fingerprintd-Ta.cppm b/interfaces/Fingerprintd-Ta.cppm index a90f834..628e44e 100644 --- a/interfaces/Fingerprintd-Ta.cppm +++ b/interfaces/Fingerprintd-Ta.cppm @@ -32,6 +32,7 @@ export namespace fingerprintd::ta { SyncStatistics = 0x100e, StartScanning = 0x1012, CaptureImage = 0x1013, + QueryEventStatus = 0x101d, SaveData = 0x1014, ReportEvent = 0x1018, WorkMode = 0x1020,