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.
This commit is contained in:
Jorijn van der Graaf 2026-09-03 01:42:55 +02:00
commit faf0dd9ba1

View file

@ -1297,7 +1297,15 @@ public:
int frames = 0;
};
VerifyOutcome Verify(std::uint32_t gid, std::atomic<bool>& 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<bool>& cancel, int maxFrames,
const std::vector<std::uint32_t>& 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<std::uint32_t> 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<std::uint32_t> 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();