A verify nobody answered is not a failure, and the trustlet is not ours to ship
Two things the packaging left behind. A verify that ran its 600-frame budget without the sensor being touched was reported to the client as verify-unknown-error. Nothing had gone wrong: nobody had pressed. It cost three verifications during packaging, each reading as a broken daemon. fprintd's contract is that a verify runs until the client stops it, so the frame cap bounds one trustlet scan session rather than the user's patience, and a window with no press simply runs again. Verified across the rollover: 600 frames untouched, "still waiting", then a press matching on its first contact frame in 44 ms. Presses that happen and never reach a verdict now report verify-retry-scan -- a bad scan, which fprintd has a word for, and not the matcher saying no. The cost is that an unanswered verify polls every ~200 ms for as long as the client holds it. The cure is measured and available -- gpio75 is silent at idle and bursts on contact -- but it would make the IRQ the only way a press is ever noticed, deleting the poll under every rate this daemon has been measured at. Noted where the loop waits, not done. And the trustlet: focal64.mbn is a proprietary OEM-signed blob, so the package ships a fp6-vendor-blobs manifest fragment instead, the same mechanism soc-fairphone-fp6-audio uses for the amp config. It needed a new directive there -- a QTEE image is an ELF header file plus one payload per program header, not one file -- and reassembly on the phone reproduces the image QTEE has accepted since August, byte for byte.
This commit is contained in:
parent
905e261d63
commit
b228287c5b
5 changed files with 67 additions and 5 deletions
|
|
@ -66,7 +66,7 @@ namespace {
|
||||||
|
|
||||||
// Bumping this is what publishes a package: the registry answers 409 for a
|
// Bumping this is what publishes a package: the registry answers 409 for a
|
||||||
// version it already has, which a build treats as a no-op.
|
// version it already has, which a build treats as a no-op.
|
||||||
constexpr const char* Version = "0.1.1";
|
constexpr const char* Version = "0.1.2";
|
||||||
|
|
||||||
bool g_verbose = false;
|
bool g_verbose = false;
|
||||||
// 500 ms was the research harness's pace, chosen so a human could read the
|
// 500 ms was the research harness's pace, chosen so a human could read the
|
||||||
|
|
@ -1623,6 +1623,16 @@ public:
|
||||||
// returns immediately and the loop runs as fast as QTEE allows --
|
// returns immediately and the loop runs as fast as QTEE allows --
|
||||||
// exactly what a press wants. The timeout is the idle fallback, so
|
// exactly what a press wants. The timeout is the idle fallback, so
|
||||||
// a release is still noticed promptly.
|
// a release is still noticed promptly.
|
||||||
|
//
|
||||||
|
// IDLE WAIT, NOT DONE: since a verify now runs until the client
|
||||||
|
// stops it, an unanswered one polls the trustlet every ~200 ms for
|
||||||
|
// as long as the lock screen is up. The measurement above says the
|
||||||
|
// cure is available -- gpio75 is silent at idle and bursts on
|
||||||
|
// contact -- so an idle frame could wait on the edge for seconds
|
||||||
|
// instead of capturing. It is not done here because it would make
|
||||||
|
// the IRQ the only way a press is ever noticed, deleting the poll
|
||||||
|
// that is currently the safety net under every rate this daemon
|
||||||
|
// has been measured at, and that trade needs a finger to settle.
|
||||||
if (g_edgeWake) sensor_.WaitEdges(g_frameGapMs);
|
if (g_edgeWake) sensor_.WaitEdges(g_frameGapMs);
|
||||||
else std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs));
|
else std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs));
|
||||||
}
|
}
|
||||||
|
|
@ -1991,7 +2001,27 @@ private:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Job::Kind::Verify: {
|
case Job::Kind::Verify: {
|
||||||
|
// fprintd's contract is that a verify runs until the client
|
||||||
|
// stops it. The frame cap bounds one trustlet scan session,
|
||||||
|
// not the user's patience, so a window in which the sensor was
|
||||||
|
// never touched is not an outcome -- it is nothing having
|
||||||
|
// happened yet, and the loop simply runs again. Reporting it
|
||||||
|
// instead cost three verifications during packaging, each
|
||||||
|
// ending in verify-unknown-error, which reads as a broken
|
||||||
|
// daemon and meant only that nobody pressed.
|
||||||
|
//
|
||||||
|
// The cost of waiting is a poll: the loop captures every
|
||||||
|
// ~200 ms whether or not a finger is there. It is bounded in
|
||||||
|
// practice by the client -- pam_fprintd stops on its own
|
||||||
|
// timeout, and a claimant that vanishes from the bus has its
|
||||||
|
// claim dropped -- and by VerifyStop, which is what sets
|
||||||
|
// cancel_. See the idle-wait note in Verify().
|
||||||
auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
||||||
|
while (!o.decided && !o.cancelled && o.presses == 0) {
|
||||||
|
std::println("verify: {} frame(s), sensor never touched -- still waiting",
|
||||||
|
o.frames);
|
||||||
|
o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
||||||
|
}
|
||||||
std::println("verify: {} over {} press(es), {} frame(s)",
|
std::println("verify: {} over {} press(es), {} frame(s)",
|
||||||
o.cancelled ? "cancelled" : !o.decided ? "undecided"
|
o.cancelled ? "cancelled" : !o.decided ? "undecided"
|
||||||
: o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames);
|
: o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames);
|
||||||
|
|
@ -1999,7 +2029,11 @@ private:
|
||||||
ev->done = true;
|
ev->done = true;
|
||||||
ev->fid = o.fid;
|
ev->fid = o.fid;
|
||||||
if (o.cancelled) { ev->status = ""; ev->done = false; }
|
if (o.cancelled) { ev->status = ""; ev->done = false; }
|
||||||
else if (!o.decided) ev->status = "verify-unknown-error";
|
// Presses happened and none of them reached a verdict: every
|
||||||
|
// frame was a finger arriving but never settling. That is a
|
||||||
|
// bad scan, which fprintd has a word for, and it is not the
|
||||||
|
// same as the matcher saying no.
|
||||||
|
else if (!o.decided) ev->status = "verify-retry-scan";
|
||||||
else if (o.matched) ev->status = "verify-match";
|
else if (o.matched) ev->status = "verify-match";
|
||||||
else ev->status = "verify-no-match";
|
else ev->status = "verify-no-match";
|
||||||
PostEvent(std::move(ev));
|
PostEvent(std::move(ev));
|
||||||
|
|
|
||||||
12
packaging/20-focal64.manifest
Normal file
12
packaging/20-focal64.manifest
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# FocalTech FT9391 trustlet (proprietary, OEM-signed) - reassembled from the
|
||||||
|
# stock modem partition on-device instead of being shipped (fp6 repo
|
||||||
|
# journal/blobs/, journal/fingerprint/). It is the matcher: every pixel the
|
||||||
|
# sensor produces stays inside it, and no part of the daemon can substitute
|
||||||
|
# for it. QTEE's signature gate is fused OEM root, measured 2026-09-03, so
|
||||||
|
# there is no version of this that is our own code.
|
||||||
|
#
|
||||||
|
# Not one file: image/focal64.mdt plus focal64.b00..b08, placed at each ELF
|
||||||
|
# segment's p_offset. Slot A first, slot B as fallback. The sha256 is of the
|
||||||
|
# reassembled image and is the one QTEE has actually accepted since
|
||||||
|
# 2026-08-24.
|
||||||
|
mbn modem_a,modem_b image focal64 /usr/lib/firmware/focal64.mbn 1930c490d67e6f006ec346d3bee9f73d812bc0b7374bfc112873dc97ebb6cd68
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
# Alpine, so an APKBUILD that compiled from source could not be built by
|
# Alpine, so an APKBUILD that compiled from source could not be built by
|
||||||
# anyone but us either.
|
# anyone but us either.
|
||||||
pkgname=fingerprintd
|
pkgname=fingerprintd
|
||||||
pkgver=0.1.1
|
pkgver=0.1.2
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)"
|
pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)"
|
||||||
url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd"
|
url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd"
|
||||||
|
|
@ -25,7 +25,12 @@ license="GPL-3.0-only"
|
||||||
# (i:fprintd=1.94.5-r1), so the provides below breaks that condition and apk
|
# (i:fprintd=1.94.5-r1), so the provides below breaks that condition and apk
|
||||||
# would purge it as no-longer-needed. Depending on it explicitly is what keeps
|
# would purge it as no-longer-needed. Depending on it explicitly is what keeps
|
||||||
# it. It has no dependency on fprintd itself, so nothing is being forced.
|
# it. It has no dependency on fprintd itself, so nothing is being forced.
|
||||||
depends="dbus glib libc++ fprintd-pam"
|
#
|
||||||
|
# fp6-vendor-blobs runs the manifest fragment below, which reassembles the
|
||||||
|
# trustlet out of the stock modem partition on first boot. Without it there is
|
||||||
|
# no matcher and the unit stays inert on its ConditionPathExists -- so this is
|
||||||
|
# a real dependency, not a nicety. It is an FP6 device package; so is this.
|
||||||
|
depends="dbus glib libc++ fprintd-pam fp6-vendor-blobs"
|
||||||
# The versioned provides both satisfies plasma-workspace's fprintd dependency
|
# The versioned provides both satisfies plasma-workspace's fprintd dependency
|
||||||
# — its Users KCM is the fingerprint enrolment UI and speaks exactly this bus
|
# — its Users KCM is the fingerprint enrolment UI and speaks exactly this bus
|
||||||
# name — and EXCLUDES the real package, which is required rather than tidy:
|
# name — and EXCLUDES the real package, which is required rather than tidy:
|
||||||
|
|
@ -89,6 +94,13 @@ package() {
|
||||||
# two policy keys in it were each forced by a measurement.
|
# two policy keys in it were each forced by a measurement.
|
||||||
install -Dm644 fingerprintd.json \
|
install -Dm644 fingerprintd.json \
|
||||||
"$pkgdir"/usr/lib/firmware/fingerprintd.json
|
"$pkgdir"/usr/lib/firmware/fingerprintd.json
|
||||||
|
|
||||||
|
# the trustlet is NOT in this package and never will be: it is a
|
||||||
|
# proprietary OEM-signed blob. This tells fp6-vendor-blobs how to
|
||||||
|
# reassemble it from the phone's own stock partitions, which is the
|
||||||
|
# same mechanism soc-fairphone-fp6-audio uses for the amp config.
|
||||||
|
install -Dm644 20-focal64.manifest \
|
||||||
|
"$pkgdir"/usr/share/fp6-vendor-blobs/manifest.d/20-focal64.manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
systemd() {
|
systemd() {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@ ConditionPathExists=/dev/tee0
|
||||||
# through the SFS root's persist-data/root3 symlinks.
|
# through the SFS root's persist-data/root3 symlinks.
|
||||||
RequiresMountsFor=/mnt/persist
|
RequiresMountsFor=/mnt/persist
|
||||||
Requires=dbus.service
|
Requires=dbus.service
|
||||||
After=dbus.service
|
# fp6-vendor-blobs reassembles the trustlet from the stock modem
|
||||||
|
# partition; it runs in sysinit so this ordering already holds, and
|
||||||
|
# saying so keeps the ConditionPathExists above from looking arbitrary.
|
||||||
|
After=dbus.service fp6-vendor-blobs.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ cp packaging/fingerprintd.service \
|
||||||
packaging/fingerprintd.tmpfiles.conf \
|
packaging/fingerprintd.tmpfiles.conf \
|
||||||
packaging/fingerprintd.modules-load.conf \
|
packaging/fingerprintd.modules-load.conf \
|
||||||
packaging/fingerprintd.json \
|
packaging/fingerprintd.json \
|
||||||
|
packaging/20-focal64.manifest \
|
||||||
"$stage/fingerprintd-$VER/"
|
"$stage/fingerprintd-$VER/"
|
||||||
tar -C "$stage" -czf "fingerprintd-$VER.tar.gz" "fingerprintd-$VER"
|
tar -C "$stage" -czf "fingerprintd-$VER.tar.gz" "fingerprintd-$VER"
|
||||||
echo "wrote fingerprintd-$VER.tar.gz ($(du -h "fingerprintd-$VER.tar.gz" | cut -f1))"
|
echo "wrote fingerprintd-$VER.tar.gz ($(du -h "fingerprintd-$VER.tar.gz" | cut -f1))"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue