diff --git a/implementations/main.cpp b/implementations/main.cpp index e82bdf6..8eacd3b 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -54,10 +54,22 @@ constexpr const char* Version = "0.0.3"; bool g_verbose = false; bool g_listeners = false; bool g_auth = false; +bool g_enrol = false; int g_frames = 40; int g_frameGapMs = 500; std::string g_logDir = "/var/log/fingerprintd"; int g_rescan = -1; // -1 = leave the config's value alone + +// The namespace key the trustlet hashes into the SFS group's directory name. +// Defaults to Android's, because that is where the store this device already +// holds was written and it is what an existing template can be read under. +// +// A DIFFERENT path is a different group directory, i.e. complete isolation +// from the Android groups. That matters for enrolment: SAVE_DATA rewrites the +// group's index container, and an index QTEE later fails to verify takes every +// template listed in it with it. Enrolling into our own namespace cannot +// damage a store we did not write. +std::string g_groupPath{fingerprintd::ta::GroupNamespacePath}; std::uint32_t g_gid = 0; std::string g_taPath = "/lib/firmware/focal64.mbn"; std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; @@ -1037,9 +1049,8 @@ int Probe() { // the chain answers -2 and loads nothing, which reads like a missing // container and is an ordering bug. if (g_listeners) { - auto sag = fingerprintd::ta::BuildSetActiveGroup(g_gid); - std::println("\nSET_ACTIVE_GROUP gid={} path='{}'", g_gid, - fingerprintd::ta::GroupNamespacePath); + auto sag = fingerprintd::ta::BuildSetActiveGroup(g_gid, g_groupPath); + std::println("\nSET_ACTIVE_GROUP gid={} path='{}'", g_gid, g_groupPath); auto g = SendCommand(app, fingerprintd::ta::Cmd::SetActiveGroup, sag); Report(fingerprintd::ta::Cmd::SetActiveGroup, g); @@ -1174,6 +1185,119 @@ int Probe() { std::println(" {}", tally.Identified() ? "FINGER IDENTIFIED" : "no match"); } + // ---- Enrolment + // + // The first thing here that WRITES: template containers through the gpfile + // listener and counter records through RPMB. Both are gated behind + // explicit flags, and RPMB writes cannot be undone. + if (g_enrol) { + namespace ta = fingerprintd::ta; + namespace en = fingerprintd::engine; + + if (g_sfsReadOnly || !g_rpmbWrite) { + std::println(std::cerr, + "enrolment needs --sfs-writable and --rpmb-write; refusing"); + return 1; + } + + // Stock's opening sequence. AUTHENTICATE is what arms the capture + // session; CANCEL and RESET_LOCKOUT bracket it. + std::vector au(ta::AuthPayloadSize); + ta::BuildAuthPayload(au, 1, 0); + std::println("\n=== enrol pre-sequence ==="); + SendCommand(app, ta::Cmd::Cancel, {}); + SendCommand(app, ta::Cmd::ResetLockout, {}); + SendCommand(app, ta::Cmd::Authenticate, au); + SendCommand(app, ta::Cmd::Cancel, {}); + SendCommand(app, ta::Cmd::ResetLockout, {}); + + auto pe = SendCommand(app, ta::Cmd::PreEnroll, {}); + Report(ta::Cmd::PreEnroll, pe); + + SendCommand(app, ta::Cmd::Authenticate, au); + SendCommand(app, ta::Cmd::Cancel, {}); + + // The token is all zero: with trustlet.enable_trusted_enrollment false + // the trustlet skips the version check, the challenge compare and the + // HMAC verify outright, which is why pmOS needs no Gatekeeper. The u32 + // at +69 is the GID this enrolment lands under. + std::vector tok(ta::EnrollPayloadSize); + ta::BuildEnrollPayload(tok, g_gid); + std::println("\n=== ENROLL gid={} ===", g_gid); + auto er = SendCommand(app, ta::Cmd::Enroll, tok); + Report(ta::Cmd::Enroll, er); + if (!er.invoked || er.result != 0 || er.rc != 0) { + std::println(std::cerr, "ENROLL refused; nothing written"); + return 1; + } + + for (int c = 3; c > 0; c--) { + std::println("*** press and LIFT, repeatedly, in {}... ***", c); + std::fflush(stdout); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + std::println("\n*** GO -- press, hold briefly, lift, and move the finger " + "slightly each time ***\n"); + + // Enrolment takes ONE sample per PRESS. Stock sends touch on the + // rising edge and release on the falling one and nothing in between; + // its whole enrolment trace contains no image-ready event. Feeding + // every held frame instead gives the algorithm near-duplicate images + // from a single press. + en::TouchTracker tracker; + en::EnrolSession enrol; + for (int i = 0; i < g_frames && !enrol.Complete(); i++) { + std::vector q(0x10, std::byte{0}); + SendCommand(app, ta::Cmd::QueryEventStatus, q); + + std::vector cap(ta::CaptureDeclaredLen); + ta::BuildCapturePayload(cap); + auto c = SendCommand(app, ta::Cmd::CaptureImage, cap); + bool finger = baseline.IsFinger(c.metric); + + std::string note; + for (ta::Event ev : tracker.Observe(finger, en::Mode::Enrol)) { + std::vector evbuf(ta::EventContextSize); + ta::BuildEventContext(evbuf, { .event = ev }); + auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf); + if (!r.invoked) continue; + // 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); + note += std::format(" ev{} rem={}", static_cast(ev), + r.samplesRemaining); + } + SendCommand(app, ta::Cmd::QueryEventStatus, q); + + std::println(" frame {:2}: metric={:<4}{}{}", i + 1, c.metric, + finger ? " FINGER" : " ", note); + std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); + } + + std::println("\nsamples: {} of {} accepted", enrol.Accepted(), enrol.Total()); + if (!enrol.Complete()) { + std::println(std::cerr, + "enrolment did not complete -- NOT saving a partial template"); + return 1; + } + + auto po = SendCommand(app, ta::Cmd::PostEnroll, {}); + Report(ta::Cmd::PostEnroll, po); + + // Bit 30 set is the template path; clear is calibration. + std::vector sd(0x10, std::byte{0}); + for (std::size_t k = 0; k < 4; k++) + sd[k] = static_cast((ta::SaveMaskTemplate >> (8 * k)) & 0xFF); + std::println("\n=== SAVE_DATA (template) ==="); + auto sv = SendCommand(app, ta::Cmd::SaveData, sd); + Report(ta::Cmd::SaveData, sv); + + auto en2 = SendCommand(app, ta::Cmd::Enumerate, {}); + Report(ta::Cmd::Enumerate, en2); + std::println(" templates now in group {}: {}", g_gid, en2.rc); + } + std::println("\ntrustlet initialised against a powered sensor."); pthread_cancel(th); pthread_join(th, nullptr); @@ -1200,9 +1324,11 @@ int main(int argc, char** argv) { 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 == "--enrol") { g_enrol = true; g_listeners = true; } if (a.starts_with("--frames=")) g_frames = std::stoi(std::string(a.substr(9))); if (a.starts_with("--log-dir=")) g_logDir = a.substr(10); if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9))); + if (a.starts_with("--group-path=")) g_groupPath = a.substr(13); if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11); if (a.starts_with("--gid=")) g_gid = static_cast( std::stoul(std::string(a.substr(6)))); diff --git a/interfaces/Fingerprintd-Ta.cppm b/interfaces/Fingerprintd-Ta.cppm index 628e44e..9ce55c8 100644 --- a/interfaces/Fingerprintd-Ta.cppm +++ b/interfaces/Fingerprintd-Ta.cppm @@ -41,6 +41,7 @@ export namespace fingerprintd::ta { Enroll = 0x2001, PostEnroll = 0x2002, Cancel = 0x2004, + ResetLockout = 0x200a, Enumerate = 0x2005, SetActiveGroup = 0x2007, Authenticate = 0x2008, @@ -208,11 +209,17 @@ export namespace fingerprintd::ta { // ---- ENROLL / AUTHENTICATE payloads ----------------------------------- - // ENROLL takes a 69-byte hw_auth_token, a u32 timeout at +69 and a u8 flag - // at +73 (stub 0xa0c8). + // ENROLL takes a 69-byte hw_auth_token, a u32 at +69 and a u8 flag at +73 + // (stub 0xa0c8). + // + // The u32 at +69 was recorded in this project as a "timeout". It is not: + // the trustlet reports it back as the GROUP ID. Setting it to 60 is where + // `gid = 60` came from, and the whole gid-60 store exists because a + // mislabelled field was filled with a plausible-looking number. Naming it + // honestly is what makes an enrolment able to choose its own group. inline constexpr std::size_t EnrollPayloadSize = 74; inline constexpr std::size_t EnrollTokenSize = 69; - inline constexpr std::size_t EnrollTimeoutOff = 69; + inline constexpr std::size_t EnrollGidOff = 69; // No Gatekeeper is needed. ff_trustlet_enroll reads config // trustlet.enable_trusted_enrollment and, when false, skips the version @@ -220,9 +227,9 @@ export namespace fingerprintd::ta { // outright (0xce34 tbz -> 0xd198), so an all-zero token is accepted. // pmOS has no Gatekeeper to mint one and nothing there verifies auth // tokens anyway. - inline void BuildEnrollPayload(std::span out, std::uint32_t timeoutSeconds) { + inline void BuildEnrollPayload(std::span out, std::uint32_t gid) { std::ranges::fill(out.first(EnrollPayloadSize), std::byte{0}); - detail::StoreU32(out, EnrollTimeoutOff, timeoutSeconds); + detail::StoreU32(out, EnrollGidOff, gid); } // AUTHENTICATE (TA 0xea88 takes these as x0/w1/w2/w3): diff --git a/tests/Ta/main.cpp b/tests/Ta/main.cpp index 65dae76..e3e2487 100644 --- a/tests/Ta/main.cpp +++ b/tests/Ta/main.cpp @@ -195,7 +195,11 @@ int main() { std::vector tok(EnrollPayloadSize); BuildEnrollPayload(tok, 60); Check(EnrollPayloadSize == 74 && EnrollTokenSize == 69, "enroll payload sizes"); - Check(Get32(tok, EnrollTimeoutOff) == 60, "timeout at +69"); + // +69 is the GID, not a timeout. The trustlet reports it back as the + // group, which is the entire provenance of gid 60. + Check(Get32(tok, EnrollGidOff) == 60, "gid at +69"); + BuildEnrollPayload(tok, 1000); + Check(Get32(tok, EnrollGidOff) == 1000, "an enrolment chooses its own group"); bool tokenZero = true; for (std::size_t i = 0; i < EnrollTokenSize; i++) if (tok[i] != std::byte{0}) tokenZero = false;