From 30bb987c27e0325f8e05035d9a3ed2d36c6f6ee9 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 5 Sep 2026 01:44:15 +0200 Subject: [PATCH] A template restore destroys the template; refuse it, and stop caching a failed load The paired test I built cannot exist on this hardware, and finding that out cost Jorijn his enrolled finger. Restoring an earlier container byte for byte made the trustlet read its header, UNLINK it, look for a .bak and advance the RPMB counter -- for both containers. QTEE binds every SFS object to a hardware anti-rollback counter, so an earlier version is not stale to it, it is tampered, and the response to tampering is deletion. No copy can undo that. The journal has warned about this counter since August in the context of the group index; it applies to the template itself just as hard, and I did not check before building a feature on the assumption that it did not. So restore refuses and explains, with the measurement in the message. Snapshots stay: they are still forensic evidence of what a template looked like, they just cannot be put back. Changing a template means enrolling again. The daemon compounded it. The group-selection cache stored "0 templates loaded" as a valid answer, so once the trustlet had unlinked everything, every later claim answered "skipped (already active, 0 template(s))" and never tried again. It now caches only a positive result, so a failed load is retried rather than made permanent. --- implementations/main.cpp | 7 ++++++- packaging/fplearn.sh | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index 1346d43..e3b735d 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -1230,7 +1230,12 @@ public: // store: an enrolment, or a template removed underneath us. int SetActiveGroup(std::uint32_t gid, bool force = false) { namespace ta = fingerprintd::ta; - if (!force && gid == gid_ && templatesLoaded_ >= 0) { + // > 0, never >= 0: caching a ZERO turns a failed load into a + // permanent one. It did exactly that on 2026-09-05 -- the trustlet + // unlinked a restored template, reported 0, and every later claim + // answered "skipped (already active, 0 template(s))" without ever + // trying again. + if (!force && gid == gid_ && templatesLoaded_ > 0) { std::println("SET_ACTIVE_GROUP gid={} skipped (already active, {} template(s))", gid, templatesLoaded_); return templatesLoaded_; diff --git a/packaging/fplearn.sh b/packaging/fplearn.sh index a7e2adc..376b68f 100755 --- a/packaging/fplearn.sh +++ b/packaging/fplearn.sh @@ -2,8 +2,10 @@ # fplearn.sh -- the template-learning measurement, as a protocol rather than a # pile of remembered commands. # -# fplearn.sh snapshot [name] save the current template aside -# fplearn.sh restore put a saved template back and reload it +# fplearn.sh snapshot [name] save the current template aside (forensics +# only -- see restore) +# fplearn.sh restore REFUSES: a restore DESTROYS the template on +# this device, see the note in the command # fplearn.sh wipe remove every stored template (backup first) # fplearn.sh enrol [finger] enrol at the config's sample count # fplearn.sh base [n] [w] trial with learning OFF (the baseline) @@ -113,6 +115,34 @@ snapshot) echo "restore with: fplearn.sh restore $NAME" ;; restore) + # DISABLED, and it must stay disabled: restoring a template DESTROYS it. + # + # Measured 2026-09-05. A byte-exact restore of an earlier container made the + # trustlet read its header, UNLINK it, look for a .bak, and bump the RPMB + # counter -- for both containers. The enrolled finger was gone, and no copy + # can bring it back, because QTEE binds every SFS object to a hardware + # anti-rollback counter that has since advanced. An older container is not + # stale to QTEE, it is TAMPERED, and the response to tampering is deletion. + # + # So the paired within-session comparison this was built for is IMPOSSIBLE + # on this hardware. A template can be measured, and it can be replaced by + # enrolling again, but it can never be put back. + cat <<'WARN' +REFUSING: restoring a template DESTROYS it on this device. + +QTEE binds every stored object to a hardware anti-rollback counter. An earlier +container is not stale to it, it is tampered, and it responds by UNLINKING the +file. Measured 2026-09-05: a byte-exact restore of the previous template made +the trustlet unlink BOTH containers and advance the RPMB counter. The enrolled +finger was destroyed and no copy could bring it back. + +Snapshots are still worth taking -- they are forensic evidence of what a +template looked like -- but they cannot be put back. To change the template, +enrol again. +WARN + exit 1 ;; + +restore-force-DESTRUCTIVE) NAME=${2:-} D=$SNAPDIR/$NAME [ -n "$NAME" ] && [ -d "$D" ] || { echo "usage: fplearn.sh restore "; echo "available:"; ls "$SNAPDIR" 2>/dev/null | sed 's/^/ /'; exit 1; }