Package the daemon, so a fingerprint survives a reflash
An aport, the units, and everything a phone needs to come up with a working sensor without a single command being run by hand. Verified on the dev phone across two reboots: modules-load.d loads qcomtee, tmpfiles builds the SFS root, the mount unit brings up persist, and the daemon is ready 51 seconds into the boot, owning net.reactivated.Fprint with the enrolled finger visible. The packaging shape is the one imsd uses for 81voltd. A versioned provides="fprintd=..." satisfies plasma-workspace -- its Users KCM is the enrolment UI and speaks exactly this bus name -- and excludes the real fprintd, which is not tidiness: fprintd is D-Bus-activatable, so a client call would otherwise start it and fight us for the name. The cost is the fprintd-* CLIs, which go with the package. fprintd-pam is an install_if subpackage pinned to the exact fprintd version, so the provides breaks its condition and apk purges it -- taking pam_fprintd, which is the entire point of the daemon, with it. Depending on it explicitly is what keeps it, and it has no dependency on fprintd itself. Two things the packaging exposed in the daemon: The transcript is for experiments, not for a shipped daemon. A file per start in an unrotated directory, recording the time of every unlock, to say what the journal already has. It is now opt-in behind --log-dir, which is what deploy-dev.sh passes since fplearn.sh reads it. Taking it off the daemon path also took away the setvbuf it was doing as a side effect of dup2'ing fd 1, and under systemd stdout is a pipe, which means full buffering: the daemon started, worked, answered D-Bus calls, and printed nothing. A working daemon that looks hung. stdout is now line-buffered from the first line of main. The config ships as generated by fp6fpcfg.py --daemon --verbose, sha256 b205c756914a66f1, because that is the file every accuracy number was measured on. The quieter variant is untested and switching is a measurement. The trustlet is not here and never will be: focal64.mbn is a proprietary OEM-signed blob, and the unit's ConditionPathExists is what keeps the package inert without it -- as it does on a kernel with no CONFIG_QCOMTEE.
This commit is contained in:
parent
ba882ee47b
commit
905e261d63
15 changed files with 518 additions and 8 deletions
66
.forgejo/workflows/package.yml
Normal file
66
.forgejo/workflows/package.yml
Normal file
|
|
@ -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; }
|
||||
|
|
@ -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<char*> args(argv, static_cast<std::size_t>(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::uint32_t>(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);
|
||||
|
|
|
|||
1
packaging/80-fingerprintd.preset
Normal file
1
packaging/80-fingerprintd.preset
Normal file
|
|
@ -0,0 +1 @@
|
|||
enable fingerprintd.service
|
||||
99
packaging/APKBUILD
Normal file
99
packaging/APKBUILD
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
# Maintainer: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
|
||||
# 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
|
||||
}
|
||||
37
packaging/README.config.md
Normal file
37
packaging/README.config.md
Normal file
|
|
@ -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.
|
||||
114
packaging/build-package.sh
Executable file
114
packaging/build-package.sh
Executable file
|
|
@ -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 <description> <cmd...>
|
||||
_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-<target>, 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
1
packaging/fingerprintd.json
Normal file
1
packaging/fingerprintd.json
Normal file
|
|
@ -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}}
|
||||
6
packaging/fingerprintd.modules-load.conf
Normal file
6
packaging/fingerprintd.modules-load.conf
Normal file
|
|
@ -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
|
||||
46
packaging/fingerprintd.service
Normal file
46
packaging/fingerprintd.service
Normal file
|
|
@ -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
|
||||
19
packaging/fingerprintd.tmpfiles.conf
Normal file
19
packaging/fingerprintd.tmpfiles.conf
Normal file
|
|
@ -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
|
||||
29
packaging/make-bin-tarball.sh
Executable file
29
packaging/make-bin-tarball.sh
Executable file
|
|
@ -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))"
|
||||
17
packaging/mnt-persist.mount
Normal file
17
packaging/mnt-persist.mount
Normal file
|
|
@ -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
|
||||
5
packaging/net.reactivated.Fprint.service
Normal file
5
packaging/net.reactivated.Fprint.service
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[D-BUS Service]
|
||||
Name=net.reactivated.Fprint
|
||||
Exec=/usr/bin/fingerprintd --daemon
|
||||
User=root
|
||||
SystemdService=fingerprintd.service
|
||||
53
packaging/net.reactivated.fprint.device.policy
Normal file
53
packaging/net.reactivated.fprint.device.policy
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-only
|
||||
SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
fingerprintd owns net.reactivated.Fprint, which excludes the fprintd
|
||||
package and the actions file it ships. These are the same three action
|
||||
ids with the same defaults, so a client that consults them — or a site
|
||||
that has already written rules against them — sees no change.
|
||||
|
||||
NOT YET ENFORCED. The daemon currently authorizes by caller uid: a
|
||||
client may act on its own prints and no other user's. That is stricter
|
||||
than allow_active=yes for verify and LOOSER than auth_self_keep for
|
||||
enroll, which asks the user to re-authenticate first. Enforcing these
|
||||
needs a polkit agent to be reachable, and the FP6 does not have a
|
||||
working session UI yet, so it would today block enrolment on the one
|
||||
device that can test it. Tracked in the fp6 journal's fingerprint lane.
|
||||
Install: /usr/share/polkit-1/actions/ -->
|
||||
<policyconfig>
|
||||
<vendor>Catcrafts</vendor>
|
||||
<vendor_url>https://forgejo.catcrafts.net/Catcrafts/fingerprintd</vendor_url>
|
||||
<icon_name>fingerprint</icon_name>
|
||||
|
||||
<action id="net.reactivated.fprint.device.verify">
|
||||
<description>Verify a fingerprint</description>
|
||||
<message>Privileges are required to verify fingerprints.</message>
|
||||
<defaults>
|
||||
<allow_any>no</allow_any>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>yes</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
|
||||
<action id="net.reactivated.fprint.device.enroll">
|
||||
<description>Enroll new fingerprints</description>
|
||||
<message>Privileges are required to enroll new fingerprints.</message>
|
||||
<defaults>
|
||||
<allow_any>no</allow_any>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_self_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
|
||||
<action id="net.reactivated.fprint.device.setusername">
|
||||
<description>Select a user to enroll</description>
|
||||
<message>Privileges are required to select a user to enroll.</message>
|
||||
<defaults>
|
||||
<allow_any>no</allow_any>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
</policyconfig>
|
||||
Loading…
Reference in a new issue