An enrolment cannot be ended by a finger release

A three-tap enrolment declared itself complete. The transcript says why:

    frame 2: metric=308  FINGER  ev5 rem=10
    frame 3: metric=187          ev6 rem=0
    samples: 10 of 10 accepted

The release event never enters do_enroll, so its response leaves
samples-remaining untouched at 0 -- which is indistinguishable from "none
remaining, you are finished". The session believed it, stopped after one press,
and called SAVE_DATA on an algorithm holding no template. That answered -1 and
wrote nothing, so the store was undamaged, but only by luck: the guard meant to
prevent a partial save was itself satisfied by the bogus count.

A reading is only meaningful when it came from the event that runs the enrol
path, and nothing about the value says so -- the caller has to. Observe now
takes that as an argument. Two further guards: a FIRST reading of 0 is an
unpopulated field rather than a finished enrolment, and the count only ever
falls, so an increase is noise.

Verified by mutation: trusting the release event's count, and accepting a
leading zero, each fail the suite.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 20:40:16 +02:00
commit c785aad653
3 changed files with 70 additions and 10 deletions

View file

@ -140,19 +140,55 @@ int main() {
{
EnrolSession e;
Check(!e.Started(), "not started");
e.Observe(-1);
e.Observe(-1, true);
Check(!e.Started(), "an unpopulated field does not start the session");
e.Observe(10);
e.Observe(10, true);
Check(e.Started() && e.Total() == 10 && e.Stages() == 10, "first response sets the total");
Check(e.Accepted() == 0 && !e.Complete(), "nothing accepted yet");
e.Observe(9);
e.Observe(9, true);
Check(e.Accepted() == 1, "rem 10 -> 9 is one accepted sample");
for (std::int32_t r : {8, 7, 6, 5, 4, 3, 2, 1}) e.Observe(r);
for (std::int32_t r : {8, 7, 6, 5, 4, 3, 2, 1}) e.Observe(r, true);
Check(!e.Complete() && e.Remaining() == 1, "not complete at one remaining");
e.Observe(0);
e.Observe(0, true);
Check(e.Complete() && e.Accepted() == 10, "complete at zero");
}
// ---- A release event must not end the enrolment
//
// This shipped: the release event never enters do_enroll, so its response
// leaves samples-remaining at 0, and a session that trusts it declares
// itself finished after ONE press. SAVE_DATA is then called on an
// algorithm holding no template and answers -1.
{
EnrolSession e;
e.Observe(10, true); // touch: ten to go
e.Observe(0, false); // release: reads zero, means nothing
Check(!e.Complete(), "a release reading of 0 does NOT complete the enrolment");
Check(e.Remaining() == 10, "and does not move the count");
Check(e.Accepted() == 0, "nothing was accepted");
}
// A first reading of 0 is an unpopulated field, not a finished enrolment.
{
EnrolSession e;
e.Observe(0, true);
Check(!e.Started(), "a leading zero does not start a session");
Check(!e.Complete(), "and certainly does not finish one");
e.Observe(10, true);
Check(e.Started() && e.Total() == 10, "a real count still starts it");
}
// The count only falls; a jump back up is noise, not progress.
{
EnrolSession e;
e.Observe(10, true);
e.Observe(9, true);
e.Observe(7, true);
Check(e.Remaining() == 7, "decreases are taken");
e.Observe(9, true);
Check(e.Remaining() == 7, "an increase is ignored");
}
// ---- The three recorded runs, replayed in order
{
auto enrolled = LoadRun("auth-enrolled-finger.txt");