diff --git a/implementations/main.cpp b/implementations/main.cpp index 56df2c8..693085f 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -68,7 +68,7 @@ namespace { // 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. -constexpr const char* Version = "0.2.3"; +constexpr const char* Version = "0.2.4"; bool g_verbose = false; // 500 ms was the research harness's pace, chosen so a human could read the @@ -952,6 +952,24 @@ void UnloadStale(qcomtee_object* loader) { qcomtee_object_refs_dec(p[2].object); } +// The loader's verdict on the image itself (IQSEEComCompatAppLoader.h in +// Qualcomm's securemsm-kernel), for the codes a phone in the field has produced +// or plausibly will. Named because the first field report was a bare 12: QTEE +// refusing an image whose signature or per-segment hashes do not check out, +// which on this device means a trustlet that is not the active slot's own. +const char* LoaderVerdict(qcomtee_result_t result) { + switch (result) { + case 11: return " ERROR_PIL_ROLLBACK_FAILURE: the image is older than the anti-rollback state allows"; + case 12: return " ERROR_ELF_SIGNATURE_ERROR: QTEE refused the image's signature or segment hashes;" + " the trustlet is damaged or not this slot's build. Re-derive it from the" + " active slot: /usr/lib/fp6-vendor-blobs/extract --refresh"; + case 13: return " ERROR_METADATA_INVALID: not a QTEE trustlet image"; + case 16: return " ERROR_ALREADY_LOADED"; + case 28: return " ERROR_ELF_LOADING"; + default: return ""; + } +} + qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) { UnloadStale(loader); @@ -981,8 +999,8 @@ qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) { p[3].attr = QCOMTEE_OBJREF_OUTPUT; qcomtee_result_t result = 0; if (qcomtee_object_invoke(loader, 1, p, 4, &result) || result) { - std::println(std::cerr, "loadFromBuffer failed, result={}", - static_cast(result)); + std::println(std::cerr, "loadFromBuffer failed, result={}{}", + static_cast(result), LoaderVerdict(result)); return QCOMTEE_OBJECT_NULL; } std::println("trustlet loaded from {} ({} bytes), distName='{}'", path, diff --git a/packaging/20-focal64.manifest b/packaging/20-focal64.manifest index b27cadd..75fb90e 100644 --- a/packaging/20-focal64.manifest +++ b/packaging/20-focal64.manifest @@ -6,7 +6,15 @@ # 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 +# segment's p_offset. The extractor tries the ACTIVE slot first (its TZ is +# the one running) and the other slot as fallback. +# +# No sha256 pin ('-'): Fairphone re-signs this trustlet every Android +# release, so a whole-image hash matches exactly one build - six builds, six +# hashes, one trustlet - and the pin 0.2.3 shipped (16.82.0's) left every +# unit on another build without a sensor (two of two field reports, +# 2026-09-11). The extractor verifies the structure; QTEE verifies the +# signature and refuses a damaged or foreign image (one flipped byte -> +# ERROR_ELF_SIGNATURE_ERROR, measured 2026-09-03), and the daemon names that +# verdict in its log. +mbn modem_a,modem_b image focal64 /usr/lib/firmware/focal64.mbn - diff --git a/packaging/APKBUILD b/packaging/APKBUILD index 458a0aa..1320d3c 100644 --- a/packaging/APKBUILD +++ b/packaging/APKBUILD @@ -10,7 +10,7 @@ # Alpine, so an APKBUILD that compiled from source could not be built by # anyone but us either. pkgname=fingerprintd -pkgver=0.2.3 +pkgver=0.2.4 pkgrel=0 pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)" url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd" @@ -30,10 +30,12 @@ license="GPL-3.0-only" # 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. -# The mbn directive the fragment uses arrived in 1-r2; the extractor before it -# exits on an unknown directive, which on a first boot would fail the blobs -# unit after the audio fragment had run and before this one could. -depends="dbus glib libc++ fprintd-pam fp6-vendor-blobs>=1-r2" +# The mbn directive the fragment uses arrived in 1-r2; the unpinned form +# ('-' as the sha256, active slot first) and --refresh arrived in 1-r3. An +# older extractor would read '-' as a hash and refuse every image, which on a +# first boot fails the blobs unit after the audio fragment ran and before +# this one could. +depends="dbus glib libc++ fprintd-pam fp6-vendor-blobs>=1-r3" # The versioned provides both satisfies plasma-workspace's fprintd dependency # — 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: @@ -55,9 +57,11 @@ subpackages="$pkgname-agent $pkgname-systemd" # traced against an x86_64 build host options="!check !tracedeps" source="fingerprintd-$pkgver.tar.gz" -# apk swaps the binary on disk and leaves the running daemon alone; the -# script restarts it so an upgrade does not leave the old code answering. -install="$pkgname.post-upgrade" +# post-install: reassemble the trustlet now, so a fresh 'apk add' has a sensor +# without waiting for a boot. post-upgrade: re-derive it from the active slot +# (heals a hand-placed or wrongly pinned file), then restart the daemon -- +# apk swaps the binary on disk and leaves the running one alone. +install="$pkgname.post-install $pkgname.post-upgrade" package() { cd "$srcdir/fingerprintd-$pkgver" diff --git a/packaging/build-package.sh b/packaging/build-package.sh index d93d8ca..3d22785 100755 --- a/packaging/build-package.sh +++ b/packaging/build-package.sh @@ -100,7 +100,7 @@ rm -rf "$PKG" mkdir -p "$PKG" cp "$SRC/packaging/APKBUILD" "$PKG/APKBUILD" # install= scripts are read from the aport dir, not from source= -cp "$SRC/packaging/fingerprintd.post-upgrade" "$PKG/" +cp "$SRC/packaging/fingerprintd.post-install" "$SRC/packaging/fingerprintd.post-upgrade" "$PKG/" mv "fingerprintd-$VER.tar.gz" "$PKG/" sed -i "s/^pkgver=.*/pkgver=$VER/" "$PKG/APKBUILD" # a throwaway signing key: phones trust the registry-signed APKINDEX, not diff --git a/packaging/fingerprintd.post-install b/packaging/fingerprintd.post-install new file mode 100755 index 0000000..4e50c81 --- /dev/null +++ b/packaging/fingerprintd.post-install @@ -0,0 +1,16 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# +# The trustlet is not in this package: fp6-vendor-blobs reassembles it from +# the phone's own modem partition (packaging/20-focal64.manifest). On an +# image's first boot its unit does that before udev; on a phone that is +# already running, do it now, so a fresh 'apk add fingerprintd' does not wait +# for a boot to have a sensor. --if-device makes this a quiet no-op inside +# build/CI chroots; --refresh re-derives the file from the active slot even if +# one is present, and never removes a file it cannot replace. +# +# The daemon itself starts at the next boot: its unit, module load and +# tmpfiles arrive with the -systemd subpackage, after this script has run. +/usr/lib/fp6-vendor-blobs/extract --if-device --refresh || : +exit 0 diff --git a/packaging/fingerprintd.post-upgrade b/packaging/fingerprintd.post-upgrade old mode 100644 new mode 100755 index 8dd66b5..8b5a1d4 --- a/packaging/fingerprintd.post-upgrade +++ b/packaging/fingerprintd.post-upgrade @@ -2,14 +2,20 @@ # SPDX-License-Identifier: GPL-3.0-only # SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® # -# apk replaces /usr/bin/fingerprintd on disk and leaves the running daemon -# alone. The phone is then a working matcher running the OLD code: the journal -# says MATCH and whatever the upgrade added is dead until a reboot, which is -# how 0.2.2's FingerMatched signal first presented (fp6 journal, fingerprint -# lane, 2026-09-05). Restart it if it is running. +# Two things an upgrade must do on a running phone. First re-derive the +# trustlet from the active slot: 0.2.3 pinned one Android build's hash, so a +# phone on any other build has no trustlet, or a hand-placed one, and +# --refresh replaces it with the phone's own (fp6 journal, fingerprint lane, +# 2026-09-11). --if-device keeps this a no-op inside build/CI chroots. +/usr/lib/fp6-vendor-blobs/extract --if-device --refresh || : # -# try-restart: a daemon that is not running -- no /dev/tee0, no trustlet -- -# stays not running. Inside a build or CI chroot there is no systemd to ask. +# Then restart the daemon: apk replaces /usr/bin/fingerprintd on disk and +# leaves the running one alone, so the phone would keep matching with the OLD +# code until a reboot (how 0.2.2's FingerMatched signal first presented, +# 2026-09-05). A plain restart rather than try-restart: a daemon that exited +# on a refused trustlet is not running and must come up on the re-derived +# file, and the unit's ConditionPathExists lines keep this inert on a phone +# with no trustlet or no /dev/tee0. No systemd inside a chroot. [ -d /run/systemd/system ] || exit 0 -systemctl try-restart fingerprintd.service 2>/dev/null || : +systemctl restart fingerprintd.service 2>/dev/null || : exit 0