From faf0dd9ba1e7a326eed5c2b58d67c1e3957e11da Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Thu, 3 Sep 2026 01:42:55 +0200 Subject: [PATCH] A verify answers only for the finger that was asked about Jorijn asked whether re-enrolling a finger overwrites its template. It does not. The trustlet stores a new one and keeps the old, because FF_CMD_TA_REMOVE is not implemented -- the group on this device now holds six templates, including one from the research harness and a pair from an enrolment two configs ago. The trustlet identifies against every template loaded in the group, and this daemon reported verify-match for whatever fid came back. So a request naming one finger was answered by any of them, including templates no name maps to any more. That is wrong by fprintd's contract, and it also means the last measurement was taken against the union of every template ever enrolled here rather than against the one under test. VerifyStart now computes which fids may answer -- the named finger's, or every named finger's for "any" -- and a match on anything else is not a match for that request. A fid that no name maps to answers for nothing at all. This does not reclaim the slots. Five templates per group is the configured limit and stale ones still occupy it; removing them needs FF_CMD_TA_REMOVE, whose payload is not reverse-engineered. --- implementations/main.cpp | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index 1a09b32..e983094 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -1297,7 +1297,15 @@ public: int frames = 0; }; - VerifyOutcome Verify(std::uint32_t gid, std::atomic& cancel, int maxFrames) { + // `accept` is the set of fids that count as a match for THIS request. The + // trustlet identifies against every template loaded in the group, and a + // group accumulates them: re-enrolling a finger does NOT replace its + // template, it adds one, because FF_CMD_TA_REMOVE is not implemented. So + // without this filter a verify answers for fingers the caller did not ask + // about, and for stale templates no longer named by anything -- which is + // both wrong by fprintd's contract and quietly ruins any measurement. + VerifyOutcome Verify(std::uint32_t gid, std::atomic& cancel, int maxFrames, + const std::vector& accept) { namespace ta = fingerprintd::ta; namespace en = fingerprintd::engine; VerifyOutcome out; @@ -1363,6 +1371,13 @@ public: auto r = SendCommand(app_, ta::Cmd::ReportEvent, evbuf); if (!r.invoked) continue; ta::Verdict v = ta::Classify(r.rc, r.fid); + // A match against a template the caller did not ask for is not + // a match for this request. + if (v == ta::Verdict::Match && !accept.empty() + && std::ranges::find(accept, r.fid) == accept.end()) { + std::println(" fid {} matched but is not the requested finger", r.fid); + v = ta::Verdict::Rejected; + } switch (v) { case ta::Verdict::Match: pressMatched = true; pressFid = r.fid; note += " MATCH"; break; case ta::Verdict::Rejected: pressRejected = true; note += " rej"; break; @@ -1512,6 +1527,7 @@ struct Job { enum class Kind { Claim, Enroll, Verify } kind; std::uint32_t uid = 0; std::string finger; + std::vector acceptFids; // Verify: which fids count GDBusMethodInvocation* invocation = nullptr; // Claim replies asynchronously }; @@ -1629,7 +1645,7 @@ private: break; } case Job::Kind::Verify: { - auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600); + auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids); std::println("verify: {} over {} press(es), {} frame(s)", o.cancelled ? "cancelled" : !o.decided ? "undecided" : o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames); @@ -2037,8 +2053,22 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant* ? std::string(store::NameOf(g_claim.fingers.Entries().front().finger)) : finger; EmitDevice("VerifyFingerSelected", g_variant_new("(s)", sel.c_str())); } + // Which templates may answer this request: the named finger's fid, or + // every named finger's for "any". A fid that no name maps to -- a + // template left behind by an earlier enrolment -- answers for nothing. + std::vector accept; + if (!enroll) { + for (const auto& e : g_claim.fingers.Entries()) + if (finger == store::AnyFinger || store::NameOf(e.finger) == finger) + accept.push_back(e.fid); + if (accept.empty()) { + EmitDevice("VerifyStatus", g_variant_new("(sb)", "verify-no-match", TRUE)); + return; + } + } g_worker->Post(Job{ .kind = enroll ? Job::Kind::Enroll : Job::Kind::Verify, - .uid = g_claim.uid, .finger = finger }); + .uid = g_claim.uid, .finger = finger, + .acceptFids = std::move(accept) }); return; } if (method == "EnrollStop" || method == "VerifyStop") { @@ -2161,7 +2191,7 @@ int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, std::uint32_t gid, int f } if (doAuth) { for (int c = 3; c > 0; c--) { std::println("*** press your finger in {}... ***", c); std::this_thread::sleep_for(std::chrono::seconds(1)); } - auto o = s.Verify(gid, cancel, frames); + auto o = s.Verify(gid, cancel, frames, {}); // probe: any template counts std::println("verify: decided={} matched={} fid={}", o.decided, o.matched, o.fid); } s.Stop();