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.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 18:48:44 +02:00
commit c33076ca9b
2 changed files with 105 additions and 1 deletions

View file

@ -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<const std::byte> 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<std::byte> 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<std::byte> q(0x10, std::byte{0});
SendCommand(app, ta::Cmd::QueryEventStatus, q);
std::vector<std::byte> 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<std::byte> 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::uint32_t>(
std::stoul(std::string(a.substr(6))));