diff --git a/implementations/main.cpp b/implementations/main.cpp index 8eacd3b..7409f20 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -1264,7 +1264,10 @@ int Probe() { // Samples remaining rides in the response on the common path, // whether or not the sample was accepted -- which matters // because the trustlet's log starves exactly when one is. - enrol.Observe(r.samplesRemaining); + // Only the event that runs the enrol path reports a real + // count. A release leaves the field at 0, which reads exactly + // like "finished". + enrol.Observe(r.samplesRemaining, ev == ta::Event::FingerTouched); note += std::format(" ev{} rem={}", static_cast(ev), r.samplesRemaining); } diff --git a/interfaces/Fingerprintd-Engine.cppm b/interfaces/Fingerprintd-Engine.cppm index 2f1d087..46f5684 100644 --- a/interfaces/Fingerprintd-Engine.cppm +++ b/interfaces/Fingerprintd-Engine.cppm @@ -119,11 +119,32 @@ export namespace fingerprintd::engine { // remaining into the response payload on the common path whether or not // the sample was accepted, and the log starves exactly when a frame is // accepted. `rem` counting down is the only reliable progress signal. + // + // "The common path" means do_enroll's common path -- and a finger-RELEASE + // event never enters do_enroll at all. Its response leaves the field + // untouched, so it reads 0, which is indistinguishable from "no samples + // remaining, you are finished". Taking that at face value ends an + // enrolment after one press and then calls SAVE_DATA on an algorithm + // holding no template, which answers -1. + // + // So a reading is only meaningful when it came from the event that runs + // the enrol path. The caller has to say so; there is no way to tell from + // the value. class EnrolSession { public: - void Observe(std::int32_t remaining) { - if (remaining < 0) return; // not populated by this command - if (!started_) { total_ = remaining; started_ = true; } + void Observe(std::int32_t remaining, bool fromEnrolPath) { + if (!fromEnrolPath) return; // a release reports nothing + if (remaining < 0) return; // not populated at all + if (!started_) { + // A first reading of 0 is an unpopulated field, not a finished + // enrolment: the count starts at max_enrolling_samples. + if (remaining == 0) return; + total_ = remaining; + remaining_ = remaining; + started_ = true; + return; + } + if (remaining > remaining_) return; // the count only ever falls remaining_ = remaining; } bool Started() const { return started_; } @@ -139,7 +160,7 @@ export namespace fingerprintd::engine { private: bool started_ = false; std::int32_t total_ = 0; - std::int32_t remaining_ = 0; + std::int32_t remaining_ = -1; }; // ---- Authentication accounting ---------------------------------------- diff --git a/tests/Engine/main.cpp b/tests/Engine/main.cpp index 2ba0743..cb158d3 100644 --- a/tests/Engine/main.cpp +++ b/tests/Engine/main.cpp @@ -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");