diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml new file mode 100644 index 0000000..753a61a --- /dev/null +++ b/.forgejo/workflows/package.yml @@ -0,0 +1,66 @@ +name: package + +# Builds the fingerprintd apk for aarch64 from the pushed commit and publishes +# it to the Forgejo Alpine registry — the repo installed phones already point +# at (via catcrafts-fp6-repo), so a release reaches users through plain +# 'apk upgrade' without an fp6-img image run. +# +# Build: crafter-build cross-compiles against an Alpine aarch64 sysroot +# (packaging/build-package.sh — the README's "Cross-compiling" flow), the test +# suite runs natively, and packaging/APKBUILD wraps the result. This needs no +# privileged runner: it runs in an alpine:edge container on the ordinary +# arch-latest runner. +# +# Release gating is the version: pkgver comes from implementations/main.cpp, +# the registry answers 409 for an already-published version, and the publish +# step treats that as "nothing to do" — so pushes only release when the +# Version constant bumps. +# +# Publishing needs PACKAGE_TOKEN (catbot account, package:write scope) — an +# org-level secret on Catcrafts, shared with imsd and fp6-img; without it the +# build still runs and the publish step skips quietly. + +on: + workflow_dispatch: + push: + branches: [main] + +jobs: + package: + runs-on: arch-latest + container: + image: alpine:edge + timeout-minutes: 90 + steps: + # actions/checkout is a Node action; bare alpine has no node + - name: Provision job container + run: apk add -q nodejs git curl + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build and package + run: ./packaging/build-package.sh + + - name: Publish to the apk registry + env: + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + run: | + if [ -z "$PACKAGE_TOKEN" ]; then + echo "no PACKAGE_TOKEN secret configured; skipping package publish" + exit 0 + fi + found=0 + for f in /home/build/.local/share/abuild/*/aarch64/fingerprintd*.apk; do + [ -e "$f" ] || continue + found=1 + code=$(curl -s -o /dev/null -w '%{http_code}' \ + --user "catbot:$PACKAGE_TOKEN" --upload-file "$f" \ + "https://forgejo.catcrafts.net/api/packages/Catcrafts/alpine/edge/fp6") + case "$code" in + 201) echo "published: $(basename "$f")" ;; + 409) echo "already published: $(basename "$f")" ;; + *) echo "FAILED ($code): $(basename "$f")"; exit 1 ;; + esac + done + [ "$found" = 1 ] || { echo "no packages found to publish"; exit 1; } diff --git a/implementations/main.cpp b/implementations/main.cpp index 0cf6063..e3657af 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -66,7 +66,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.1.0"; +constexpr const char* Version = "0.1.1"; bool g_verbose = false; // 500 ms was the research harness's pace, chosen so a human could read the @@ -87,6 +87,9 @@ int g_samples = -1; constexpr int SamplesFallback = 20; // stock's value, if the config lacks the key bool g_samplesForced = false; std::string g_logDir = "/var/log/fingerprintd"; +// Only an explicit --log-dir turns the transcript on for the daemon; see +// StartTranscript. +bool g_logDirExplicit = false; std::string g_stateDir = "/var/lib/fingerprintd"; int g_rescan = -1; // -1 = leave the config's value alone @@ -172,11 +175,16 @@ std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; qcomtee_object* g_root = QCOMTEE_OBJECT_NULL; -// Every run writes its own timestamped transcript. Not a convenience: a run -// whose result nobody recorded is a run that has to be repeated on a human's -// finger. And a SINGLE shared log path is worse than none -- the next run, -// including a quick control, destroys the interesting one, which is how the -// first successful authentication in this project was very nearly lost. +// Every PROBE run writes its own timestamped transcript. Not a convenience: a +// run whose result nobody recorded is a run that has to be repeated on a +// human's finger. And a SINGLE shared log path is worse than none -- the next +// run, including a quick control, destroys the interesting one, which is how +// the first successful authentication in this project was very nearly lost. +// +// The daemon is the exception, and takes one only when --log-dir says so. That +// reasoning is about experiments; a packaged daemon that ran it would leave a +// file per start in an unrotated directory, and record the time of every +// unlock, to say what the journal already has. // // Done at the file-descriptor level rather than by wrapping a stream, because // std::println writes to stdout through C stdio: an ostream wrapper would @@ -2623,6 +2631,12 @@ int RunProbeTaLoad(const std::string& path) { } int main(int argc, char** argv) { + // Under systemd stdout is a pipe, and a pipe means FULL buffering: the + // daemon's lines would sit in the buffer rather than reach the journal, + // which is how a working daemon looks like a hung one. StartTranscript + // used to set this as a side effect of taking over fd 1, and the daemon + // does not run it. + ::setvbuf(stdout, nullptr, _IOLBF, 0); std::span args(argv, static_cast(argc)); bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false; bool doLearnProbe = false; @@ -2652,7 +2666,7 @@ int main(int argc, char** argv) { // The observer thread and the loop would both drain the same fd, so // the diagnostic and the wake are mutually exclusive. if (a == "--edge-wake") g_edgeWake = true; - if (a.starts_with("--log-dir=")) g_logDir = a.substr(10); + if (a.starts_with("--log-dir=")) { g_logDir = a.substr(10); g_logDirExplicit = true; } if (a.starts_with("--state-dir=")) g_stateDir = a.substr(12); if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9))); if (a.starts_with("--reject-budget=")) g_pressRejectBudget = std::stoi(std::string(a.substr(16))); @@ -2664,7 +2678,7 @@ int main(int argc, char** argv) { if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11); if (a.starts_with("--gid=")) gid = static_cast(std::stoul(std::string(a.substr(6)))); } - StartTranscript(g_logDir); + if (!daemon || g_logDirExplicit) StartTranscript(g_logDir); if (!probeTa.empty()) return RunProbeTaLoad(probeTa); if (daemon) return RunDaemon(); if (probe) return RunProbe(doAuth, doEnrol, doCalSave, doLearnProbe, gid, frames); diff --git a/packaging/80-fingerprintd.preset b/packaging/80-fingerprintd.preset new file mode 100644 index 0000000..5915238 --- /dev/null +++ b/packaging/80-fingerprintd.preset @@ -0,0 +1 @@ +enable fingerprintd.service diff --git a/packaging/APKBUILD b/packaging/APKBUILD new file mode 100644 index 0000000..c4be451 --- /dev/null +++ b/packaging/APKBUILD @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# Maintainer: Jorijn van der Graaf +# Binary packaging: wraps a crafter-build binary cross-compiled per the +# README's "Cross-compiling" section into a proper apk — used by this repo's +# package CI (packaging/build-package.sh, which seds pkgver from +# implementations/main.cpp) and runnable by hand. The source tarball is +# produced by packaging/make-bin-tarball.sh. There is deliberately no +# source-building aport: the build driver is crafter-build, which is not in +# Alpine, so an APKBUILD that compiled from source could not be built by +# anyone but us either. +pkgname=fingerprintd +pkgver=0.1.1 +pkgrel=0 +pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)" +url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd" +arch="aarch64" +license="GPL-3.0-only" +# GLib for the D-Bus interface; libc++ because the binary is a clang/libc++ +# C++26 modules build linked dynamically against the phone's own runtime. +# +# fprintd-pam is pam_fprintd, which is the point of the whole daemon: it is +# what turns a matched finger into a login. It is an install_if subpackage +# conditioned on the EXACT version fprintd-pam was built against +# (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 +# it. It has no dependency on fprintd itself, so nothing is being forced. +depends="dbus glib libc++ fprintd-pam" +# 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: +# fprintd is D-Bus-activatable, so a client call would otherwise start the +# real daemon and fight for net.reactivated.Fprint. fprintd-pam is a separate +# package that does not depend on fprintd, so PAM keeps working. +# +# The cost, which is real: the fprintd-enroll/-list/-verify/-delete CLIs go +# away with the package. Enrolment then goes through Plasma's Users KCM. +provides="fprintd=$pkgver-r$pkgrel" +# The unit is the deliverable — a daemon holding QTEE's listener table open for +# the life of the boot is not something to start by hand — but abuild wants +# systemd files in their own package, and install_if puts them back on any +# system that has systemd. No OpenRC service: nothing here has ever been run +# under one, and the unit's conditions (the vendor blob, /dev/tee0) are what +# keep the package inert on a phone that cannot use it. +subpackages="$pkgname-systemd" +# nothing is compiled here, and the aarch64 ELF's NEEDED entries must not be +# traced against an x86_64 build host +options="!check !tracedeps" +source="fingerprintd-$pkgver.tar.gz" + +package() { + cd "$srcdir/fingerprintd-$pkgver" + install -Dm755 fingerprintd "$pkgdir"/usr/bin/fingerprintd + + install -Dm644 fingerprintd.service \ + "$pkgdir"/usr/lib/systemd/system/fingerprintd.service + install -Dm644 mnt-persist.mount \ + "$pkgdir"/usr/lib/systemd/system/mnt-persist.mount + # enabled by preset, and by an explicit .wants link so a fingerprint + # surviving a reboot never depends on a manual systemctl enable. The + # mount needs neither: fingerprintd.service pulls it in with + # RequiresMountsFor. + install -Dm644 80-fingerprintd.preset \ + "$pkgdir"/usr/lib/systemd/system-preset/80-fingerprintd.preset + mkdir -p "$pkgdir"/etc/systemd/system/multi-user.target.wants + ln -s /usr/lib/systemd/system/fingerprintd.service \ + "$pkgdir"/etc/systemd/system/multi-user.target.wants/fingerprintd.service + + # who may own and call the bus name + install -Dm644 net.reactivated.Fprint.conf \ + "$pkgdir"/usr/share/dbus-1/system.d/net.reactivated.Fprint.conf + # replaces fprintd's activation file, which points at /usr/libexec/fprintd + install -Dm644 net.reactivated.Fprint.service \ + "$pkgdir"/usr/share/dbus-1/system-services/net.reactivated.Fprint.service + # the action ids fprintd defined; see the file for what enforces them + install -Dm644 net.reactivated.fprint.device.policy \ + "$pkgdir"/usr/share/polkit-1/actions/net.reactivated.fprint.device.policy + + # the SFS root's directories and its two symlinks into the persist mount + install -Dm644 fingerprintd.tmpfiles.conf \ + "$pkgdir"/usr/lib/tmpfiles.d/fingerprintd.conf + # qcomtee -> /dev/tee0 + install -Dm644 fingerprintd.modules-load.conf \ + "$pkgdir"/usr/lib/modules-load.d/fingerprintd.conf + + # the trustlet's configuration, generated by fp6fpcfg.py from the captured + # stock dump — see packaging/README.config.md. Not a user config file: + # the TA discards a file whose configuration_uuid does not match, and the + # two policy keys in it were each forced by a measurement. + install -Dm644 fingerprintd.json \ + "$pkgdir"/usr/lib/firmware/fingerprintd.json +} + +systemd() { + install_if="$pkgname=$pkgver-r$pkgrel systemd" + + amove usr/lib/systemd + amove etc/systemd +} diff --git a/packaging/README.config.md b/packaging/README.config.md new file mode 100644 index 0000000..409020e --- /dev/null +++ b/packaging/README.config.md @@ -0,0 +1,37 @@ +# `fingerprintd.json` — where it comes from, and why this exact file + +The trustlet gates its config file on `common.configuration_uuid` and silently +falls back to built-in defaults on a mismatch, so the file is not decorative: +`SYNC_CONFIG` returning 0 is what makes the whole init chain run. + +This copy is generated, not hand-written. Its source is `fp6fpcfg.py` in the +fp6 bring-up repo, which builds it from the captured stock configuration dump +(`journal/fingerprint/captures/2026-08-25-focal64-effective-config-from-stock.txt`): + +```sh +utilities/fp6fpcfg.py --daemon --verbose > packaging/fingerprintd.json +``` + +sha256 begins `b205c756914a66f1`. + +## Why the `--verbose` variant + +`--verbose` here is the *trustlet's* own log level, not the daemon's. It is +shipped because it is the file every accuracy number was measured on — 30/30 +held presses, zero false accepts, 36-330 ms to a verdict — and the TA's log +level cannot be raised again by a runtime `SYNC_CONFIG`, so a session that +needs the matcher's own lines has to have started with it. + +`--daemon` alone produces the same file with trustlet logging off (sha256 +`6c4503e628406424`, four `diagnosis.*` keys differ and nothing else). It is +plausibly the better shipping default and it is **untested**: no rate in the +journal was measured on it. Switching is a measurement, not an edit. + +## The two policy keys + +* `common.max_authentication_rescan_times: 0` — at the stock budget a wrong + finger never yields a terminal frame, so a PAM client waits forever for the + `verify-no-match` it needs. +* `trustlet.enable_trusted_enrollment: false` — skips the challenge compare + and the `hw_auth_token` HMAC verify. pmOS has no Gatekeeper to issue a token + and nothing on pmOS verifies one. diff --git a/packaging/build-package.sh b/packaging/build-package.sh new file mode 100755 index 0000000..d08d9de --- /dev/null +++ b/packaging/build-package.sh @@ -0,0 +1,114 @@ +#!/bin/sh -eu +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# CI package build: cross-compile fingerprintd for aarch64 with crafter-build +# (the README's "Cross-compiling" flow), run the test suite natively, and +# package the result via packaging/APKBUILD. Expects an x86_64 Alpine +# environment with root — the workflow runs it in an alpine:edge job +# container on an ordinary runner. Root only installs packages and hands off +# to a scratch user: the sysroot is built with apk.static --usermode (which +# refuses root) and abuild wants a user too. +# +# Built packages land in /home/build/.local/share/abuild/*/aarch64/fingerprintd*.apk; +# the workflow's publish step uploads them to the Forgejo Alpine registry. + +set -eu + +# The musl build of crafter-build (Crafter.Build CI's release-musl job): this +# container is Alpine, and the glibc launcher cannot run on musl. v2 = SSE4.2 +# baseline: the CI box is an Intel N5105 (no AVX). Overridable for local +# rehearsals (file:// works). +CRAFTER_URL=${CRAFTER_URL:-https://forgejo.catcrafts.net/Catcrafts/Crafter.Build/releases/download/latest/crafter-build-linux-x86_64-musl-v2.tar.gz} + +SRC=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) + +# clang cross-targets aarch64 natively and the target's libc++/glib come from +# the sysroot; llvm-runtimes/libc++-dev/glib-dev here serve the NATIVE +# test-suite run. build-base = Alpine's standard build environment (the one +# abuild implies): binutils' ld/ar for clang's default link driver, gcc's +# libgcc_s/crt objects the musl clang driver links against. +if [ "$(id -u)" = 0 ]; then + apk add -q git curl tar clang lld llvm llvm-runtimes libc++-dev llvm-libunwind-dev glib-dev \ + build-base abuild sudo + id build >/dev/null 2>&1 || adduser -D build + addgroup build abuild 2>/dev/null || true + echo 'build ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/build + # abuild in cross mode strips with $CHOST-strip; llvm-strip handles any + # ELF arch, so give it that name + ln -sf "$(command -v llvm-strip)" /usr/local/bin/aarch64-alpine-linux-musl-strip + # the CI checkout arrives root-owned; crafter-build writes bin/ into it + chown -R build "$SRC" + # -l: a login shell, so HOME really is /home/build (abuild keys + output); + # it scrubs the environment, so carry the one knob that matters across + exec su -l build -c "CRAFTER_URL='${CRAFTER_URL:-}' sh -eu '$SRC/packaging/build-package.sh'" +fi + +retry() { # retry + _desc=$1; shift + for _i in 1 2 3; do + "$@" && return 0 + echo "$_desc failed (attempt $_i/3), retrying in 15s..." >&2 + sleep 15 + done + echo "$_desc failed after 3 attempts" >&2 + return 1 +} + +# implementations/main.cpp is the version's single source of truth (same +# derivation as make-bin-tarball.sh) +VER=$(sed -n 's/.*char\* Version = "\(.*\)".*/\1/p' "$SRC/implementations/main.cpp") +[ -n "$VER" ] || { echo "cannot read Version from implementations/main.cpp" >&2; exit 1; } +echo ">> packaging fingerprintd $VER" + +# --- crafter-build: static launcher from the rolling release +mkdir -p "$HOME/crafter-build" +retry "fetch crafter-build" \ + sh -c "curl -fsSL '$CRAFTER_URL' | tar -xz -C '$HOME/crafter-build'" +PATH="$HOME/crafter-build/bin:$PATH" +export CRAFTER_BUILD_HOME="$HOME/crafter-build/share/crafter-build" + +# --- aarch64 Alpine sysroot (unprivileged: apk.static --usermode) +SYSROOT="$HOME/.cache/fingerprintd/sysroot-aarch64-alpine" +retry "make sysroot" "$SRC/packaging/make-sysroot.sh" "$SYSROOT" + +# --- libqcomtee: Qualcomm's BSD-3 QTEE client (quic-teec, pinned commit), +# which is not in Alpine and is not vendored here. crafter-build looks for it +# under ~/.cache/fingerprintd/libqcomtee-, which is this script's +# default output dir. Needs git, installed above. +retry "make libqcomtee" "$SRC/packaging/make-libqcomtee.sh" \ + --target=aarch64-alpine-linux-musl --sysroot="$SYSROOT" --march=armv8-a + +# --- cross-compile the daemon; run the suites natively. The core is portable +# by construction (no GLib, no libqcomtee, no system headers), which is what +# lets the wire formats and state machines be tested on the build host at all. +cd "$SRC" +XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8-a --mtune=generic" +crafter-build -- $XTARGET +crafter-build test + +# --- bundle + package +./packaging/make-bin-tarball.sh "$VER" + +PKG="$HOME/pkg" +rm -rf "$PKG" +mkdir -p "$PKG" +cp "$SRC/packaging/APKBUILD" "$PKG/APKBUILD" +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 +# per-package keys (same situation as fp6-img's pmbootstrap-built packages). +# abuild >= 3.18 keeps keys under ~/.config/abuild and output under +# ~/.local/share/abuild (REPODEST default). +abuild-keygen -a -n >/dev/null 2>&1 +sudo cp "$HOME"/.config/abuild/*.rsa.pub /etc/apk/keys/ +# CHOST puts abuild in cross mode so arch="aarch64" packages on this x86_64 +# host. -d skips dependency handling entirely: nothing compiles under abuild +# (with -r, cross mode would try to install a nonexistent build-base-aarch64 +# plus the runtime depends); !tracedeps in the APKBUILD keeps abuild from +# resolving the aarch64 ELF NEEDED entries against this x86_64 host. +cd "$PKG" +abuild checksum +CHOST=aarch64 abuild -d + +echo "=== built packages ===" +ls -la "$HOME"/.local/share/abuild/*/aarch64/fingerprintd*.apk diff --git a/packaging/deploy-dev.sh b/packaging/deploy-dev.sh index 0c8956e..71dfe33 100755 --- a/packaging/deploy-dev.sh +++ b/packaging/deploy-dev.sh @@ -92,8 +92,11 @@ mount | grep " /mnt/persist " | grep -q "rw," || { echo "persist not rw" >&2; ex echo ">> persist mounted rw" sudo systemctl stop fingerprintd-test 2>/dev/null || true sleep 1 +# --log-dir is explicit because the daemon writes no transcript without it +# (a packaged daemon must not), and fplearn.sh reads the newest one. sudo systemd-run --unit=fingerprintd-test --collect \ /tmp/fingerprintd --daemon --verbose --edge-wake \ + --log-dir=/var/log/fingerprintd \ --sfs-root=/var/lib/fingerprintd/sfs --sfs-writable --rpmb-write $EXTRA >/dev/null [ -z "$EXTRA" ] || echo ">> extra daemon flags: $EXTRA" printf ">> daemon starting" diff --git a/packaging/fingerprintd.json b/packaging/fingerprintd.json new file mode 100644 index 0000000..5dc6bf6 --- /dev/null +++ b/packaging/fingerprintd.json @@ -0,0 +1 @@ +{"algorithm":{"enrolling_overlap_intervals":"0x54A0","min_enrolling_quality_threshold":20,"min_enrolling_coverage_threshold":70,"enroll_overlap_compare_cnts":30,"enroll_overlap_min_area":60,"enroll_overlap_max_area":80,"enroll_overlap_ang":0,"enable_duplicated_finger_checking":true,"max_extral_enroll_low_quality":16,"max_extral_enroll_too_slow":10,"enable_template_learning":true,"template_update_threshold":50,"max_sub_template_num":96},"driver":{"spi_bus_num":2,"spi_c_s_num":0},"device":{"spi_default_bps":4800000,"spi_mode":0,"preferred_device_id":"0x9391"},"trustlet":{"enable_trusted_enrollment":false},"common":{"max_enrolling_fingers":5,"max_enrolling_samples":20,"max_authentication_rescan_times":0},"diagnosis":{"enable_logcat_trustlet":true,"enable_trustlet_native_log":true,"enable_algorithm_log":true,"framework_log_level":6,"firmware_log_level":6,"algorithm_log_level":2}} diff --git a/packaging/fingerprintd.modules-load.conf b/packaging/fingerprintd.modules-load.conf new file mode 100644 index 0000000..32cf854 --- /dev/null +++ b/packaging/fingerprintd.modules-load.conf @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# /dev/tee0, which the daemon opens to reach QTEE. Needs CONFIG_QCOMTEE=m in +# the kernel package; on a kernel without it this line is a no-op and +# fingerprintd.service stays inert on its ConditionPathExists. +qcomtee diff --git a/packaging/fingerprintd.service b/packaging/fingerprintd.service new file mode 100644 index 0000000..d2facd2 --- /dev/null +++ b/packaging/fingerprintd.service @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +[Unit] +Description=Fingerprint daemon (FocalTech FT9391 behind QTEE) +Documentation=https://forgejo.catcrafts.net/Catcrafts/fingerprintd +# The trustlet does the matching and it is a proprietary OEM-signed blob +# extracted from the stock vendor partition, so it is not in this package. +# Without it there is no sensor, and the unit stays out of the way instead of +# restart-looping. +ConditionPathExists=/usr/lib/firmware/focal64.mbn +# /dev/tee0 is the qcomtee driver, which the pmOS kernel does not build yet +# (CONFIG_QCOMTEE). modules-load.d asks for the module; this condition is what +# makes the package harmless on a kernel that has none. +ConditionPathExists=/dev/tee0 +# The templates are QTEE containers on the Android persist partition, reached +# through the SFS root's persist-data/root3 symlinks. +RequiresMountsFor=/mnt/persist +Requires=dbus.service +After=dbus.service + +[Service] +Type=simple +# --edge-wake wait on the sensor IRQ instead of polling for a finger +# --sfs-writable QTEE must be able to WRITE the template store, or an +# enrolment cannot be saved. It can also unlink a container it +# rejects, which is why it is opt-in rather than the default. +# --rpmb-write the anti-rollback counter lives in RPMB; a save that cannot +# write it does not commit. +# No --verbose: it prints the frame-by-frame state machine, which on a phone +# is both journal noise and a record of when its owner unlocked it. +ExecStart=/usr/bin/fingerprintd --daemon --edge-wake \ + --sfs-root=/var/lib/fingerprintd/sfs --sfs-writable --rpmb-write +# Root is required and not reducible: the daemon drives the sensor rails over +# gpiochip, holds /dev/tee0, and serves QTEE's RPMB transactions against the +# raw UFS RPMB device. No sandboxing is declared here rather than declaring +# some that was never tested against those three. +Restart=on-failure +RestartSec=5 +# SIGTERM: the worker finishes the invoke it is inside before the session goes +# down; QTEE's listener table is global to the boot and a half-torn session +# leaves it holding ours. +KillMode=mixed +TimeoutStopSec=15 + +[Install] +WantedBy=multi-user.target diff --git a/packaging/fingerprintd.tmpfiles.conf b/packaging/fingerprintd.tmpfiles.conf new file mode 100644 index 0000000..a6ff4cf --- /dev/null +++ b/packaging/fingerprintd.tmpfiles.conf @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# The SFS root. QTEE asks the normal world for absolute Android paths; the +# daemon maps each prefix onto one name below and refuses anything that +# escapes the root. The names are the mapping and cannot be renamed: +# +# /mnt/vendor/persist/data/ -> persist-data the templates +# /persist/data/ -> root3 the same directory +# /data/vendor/tzstorage/ -> tzstorage +# /data/misc/qsee/ -> misc +# +# persist-data and root3 are the same place on purpose: QTEE reaches the +# template store under both prefixes. +d /var/lib/fingerprintd 0700 root root - +d /var/lib/fingerprintd/sfs 0700 root root - +d /var/lib/fingerprintd/sfs/tzstorage 0700 root root - +d /var/lib/fingerprintd/sfs/misc 0700 root root - +L /var/lib/fingerprintd/sfs/persist-data - - - - /mnt/persist/data +L /var/lib/fingerprintd/sfs/root3 - - - - /mnt/persist/data diff --git a/packaging/make-bin-tarball.sh b/packaging/make-bin-tarball.sh new file mode 100755 index 0000000..1f675dc --- /dev/null +++ b/packaging/make-bin-tarball.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# make-bin-tarball.sh — bundle the cross-compiled fingerprintd binary and its +# runtime files into the source tarball APKBUILD consumes. Run from the repo +# root after a cross build; output lands in the current directory. +set -eu + +VER="${1:-$(sed -n 's/.*char\* Version = "\(.*\)".*/\1/p' implementations/main.cpp)}" +[ -n "$VER" ] || { echo "could not determine version — pass it as \$1" >&2; exit 1; } +BIN=$(ls -t bin/fingerprintd-aarch64-*/fingerprintd 2>/dev/null | head -n1) +[ -n "$BIN" ] || { echo "no aarch64 fingerprintd build found — cross-compile first" >&2; exit 1; } + +stage=$(mktemp -d) +trap 'rm -rf "$stage"' EXIT +mkdir "$stage/fingerprintd-$VER" +cp "$BIN" "$stage/fingerprintd-$VER/fingerprintd" +cp packaging/fingerprintd.service \ + packaging/mnt-persist.mount \ + packaging/80-fingerprintd.preset \ + packaging/net.reactivated.Fprint.conf \ + packaging/net.reactivated.Fprint.service \ + packaging/net.reactivated.fprint.device.policy \ + packaging/fingerprintd.tmpfiles.conf \ + packaging/fingerprintd.modules-load.conf \ + packaging/fingerprintd.json \ + "$stage/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))" diff --git a/packaging/mnt-persist.mount b/packaging/mnt-persist.mount new file mode 100644 index 0000000..d9fee6c --- /dev/null +++ b/packaging/mnt-persist.mount @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# The Android persist partition. QTEE's own storage paths land here through +# the SFS root's persist-data/root3 symlinks, and the enrolled templates are +# the containers under data/ — the same place stock Android keeps them, which +# is not a choice: every container is sealed to a hardware anti-rollback +# counter, so a template cannot be relocated, copied or restored. +[Unit] +Description=Android persist partition +Documentation=https://forgejo.catcrafts.net/Catcrafts/fingerprintd +ConditionPathExists=/dev/disk/by-partlabel/persist + +[Mount] +What=/dev/disk/by-partlabel/persist +Where=/mnt/persist +Type=ext4 +Options=rw,nosuid,nodev,noexec diff --git a/packaging/net.reactivated.Fprint.service b/packaging/net.reactivated.Fprint.service new file mode 100644 index 0000000..9f5ba1a --- /dev/null +++ b/packaging/net.reactivated.Fprint.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=net.reactivated.Fprint +Exec=/usr/bin/fingerprintd --daemon +User=root +SystemdService=fingerprintd.service diff --git a/packaging/net.reactivated.fprint.device.policy b/packaging/net.reactivated.fprint.device.policy new file mode 100644 index 0000000..a08dd9f --- /dev/null +++ b/packaging/net.reactivated.fprint.device.policy @@ -0,0 +1,53 @@ + + + + + Catcrafts + https://forgejo.catcrafts.net/Catcrafts/fingerprintd + fingerprint + + + Verify a fingerprint + Privileges are required to verify fingerprints. + + no + no + yes + + + + + Enroll new fingerprints + Privileges are required to enroll new fingerprints. + + no + no + auth_self_keep + + + + + Select a user to enroll + Privileges are required to select a user to enroll. + + no + no + auth_admin_keep + + +