From 9575e5517d7bb9741c82ff9d13abd833d13c3482 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 5 Sep 2026 03:56:13 +0200 Subject: [PATCH 1/3] fp6-vendor-blobs: reassemble a Qualcomm trustlet, not just copy a file The fingerprint matcher is a proprietary OEM-signed trustlet, and it is not shipped as one file: QTEE images live in the modem partition's image/ as an ELF header+hashes file plus one payload per program header, and the loader wants each payload written back at its segment's p_offset. So the existing file directive cannot reach it. An mbn directive does, with the same guarantees file has: the sha256 is of the reassembled image, a mismatch tries the next partition, and an unverified image is never installed. Reassembly is not a concatenation -- segments are page aligned but not contiguous, gaps stay zero, and two pairs of focal64's nine segments share an offset, so they are written in index order and the later one wins, exactly as the bring-up repo's reassemble.py does. Verified on the dev phone against the hash QTEE has actually accepted since August: 3600472 bytes, sha256 1930c490..., reassembled from the phone's own modem_a. The retry path was verified too, with a deliberately wrong first partition -- which is how the variable clobber got caught: POSIX sh has no locals, and reassemble() taking rdir= rewrote its caller's copy to the mount path, so the second partition would have been searched at $MNT/$MNT/... The fast path needed teaching as well: mbn's dest is the fifth field, and a first boot would otherwise have exited early and extracted nothing. Not pushed. The consumer fragment lives in the fingerprintd package. --- aports/device/fp6-vendor-blobs/APKBUILD | 2 +- .../fp6-vendor-blobs/fp6-vendor-blobs-extract | 110 +++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/aports/device/fp6-vendor-blobs/APKBUILD b/aports/device/fp6-vendor-blobs/APKBUILD index 6cd43ae..8dc7a5b 100644 --- a/aports/device/fp6-vendor-blobs/APKBUILD +++ b/aports/device/fp6-vendor-blobs/APKBUILD @@ -14,7 +14,7 @@ maintainer="Jorijn van der Graaf " pkgname=fp6-vendor-blobs pkgver=1 -pkgrel=1 +pkgrel=2 pkgdesc="On-device extraction of vendor blobs from the stock Android partitions" url="https://forgejo.catcrafts.net/Catcrafts/fp6-img" arch="noarch" diff --git a/aports/device/fp6-vendor-blobs/fp6-vendor-blobs-extract b/aports/device/fp6-vendor-blobs/fp6-vendor-blobs-extract index 4239697..1289b21 100644 --- a/aports/device/fp6-vendor-blobs/fp6-vendor-blobs-extract +++ b/aports/device/fp6-vendor-blobs/fp6-vendor-blobs-extract @@ -10,6 +10,7 @@ # processed in sorted order; '#' comments and blank lines ignored: # # file +# mbn # rebind # # file: mount the first available listed partition READ-ONLY (ext4 also @@ -20,6 +21,16 @@ # unverified blob is never installed and a missing one never silently # skipped. Dests that already exist are left alone (no hashing: a # deliberately replaced file stays). +# mbn: the same, for a Qualcomm trustlet, which is not shipped as one file. +# QTEE images live in the modem partition's image/ as an ELF header+hashes +# file (.mdt) plus one payload per program header (.b00, .b01, +# ...), and the loader wants them written back at each segment's p_offset. +# Reassembly is therefore not a concatenation: segments are page aligned +# but not contiguous, gaps stay zero, and two segments may share an offset +# (focal64 has two such pairs), so they are written in index order and the +# later one wins. Same guarantees as file: the sha256 is of the reassembled +# image, a mismatch tries the next partition, and an unverified image is +# never installed. # rebind: if this fragment's run extracted at least one file, unbind and # re-probe on so the consuming driver picks the file up # in the same boot. Unconditional on purpose: a still-bound consumer may @@ -103,6 +114,90 @@ mount_part() { MNT_PART=$1 } +# Little-endian scalars out of an ELF header. aarch64 is little endian and so +# is the image, so od's host order is the right one. +u64() { od -An -tu8 -j "$2" -N 8 "$1" | tr -d ' '; } +u16() { od -An -tu2 -j "$2" -N 2 "$1" | tr -d ' '; } + +# Reassemble /.mdt + .b0N into a flat image at . Mirrors +# utilities/ta-analysis/reassemble.py in the fp6 bring-up repo, which is where +# the format was worked out and where the known-good hash comes from. +# POSIX sh has no locals, so these names are deliberately distinct from +# extract_mbn's: reassemble() taking rdir= would rewrite its CALLER's copy to +# the mount path, and the next partition in the retry loop would then be +# searched at $MNT/$MNT/... +reassemble() { # + mdir=$1 mname=$2 mout=$3 + mdt="$mdir/$mname.mdt" + [ -f "$mdt" ] || return 1 + phoff=$(u64 "$mdt" 32) phentsize=$(u16 "$mdt" 54) phnum=$(u16 "$mdt" 56) + [ -n "$phoff" ] && [ -n "$phentsize" ] && [ -n "$phnum" ] || return 1 + [ "$phnum" -gt 0 ] 2>/dev/null || return 1 + + # The image is as long as the furthest segment reaches; everything no + # segment covers stays zero. + total=0 i=0 + while [ "$i" -lt "$phnum" ]; do + o=$((phoff + i * phentsize)) + pfsz=$(u64 "$mdt" $((o + 32))) + if [ "$pfsz" -gt 0 ]; then + poff=$(u64 "$mdt" $((o + 8))) + [ $((poff + pfsz)) -gt "$total" ] && total=$((poff + pfsz)) + fi + i=$((i + 1)) + done + [ "$total" -gt 0 ] || return 1 + : > "$mout" || return 1 + truncate -s "$total" "$mout" || return 1 + + i=0 + while [ "$i" -lt "$phnum" ]; do + o=$((phoff + i * phentsize)) + pfsz=$(u64 "$mdt" $((o + 32))) + if [ "$pfsz" -gt 0 ]; then + poff=$(u64 "$mdt" $((o + 8))) + seg=$(printf '%s/%s.b%02d' "$mdir" "$mname" "$i") + [ -f "$seg" ] || { log "$mname: segment $i missing"; return 1; } + # dd seeks in whole blocks, which is only correct because + # every p_offset in these images is page aligned. Refuse + # rather than silently misplace a segment if that changes. + [ $((poff % 4096)) -eq 0 ] || { + log "$mname: segment $i offset $poff is not page aligned" + return 1 + } + dd if="$seg" of="$mout" bs=4096 seek=$((poff / 4096)) \ + conv=notrunc 2>/dev/null || return 1 + fi + i=$((i + 1)) + done + return 0 +} + +extract_mbn() { # + parts=$1 rdir=$2 rname=$3 dest=$4 want=$5 + for part in $(echo "$parts" | tr ',' ' '); do + mount_part "$part" || { log "$part: not mountable, trying next"; continue; } + [ -f "$MNT/$rdir/$rname.mdt" ] || { log "$part: no $rdir/$rname.mdt, trying next"; continue; } + tmp="$dest.fp6-extract.$$" + mkdir -p "${dest%/*}" || fail "cannot create ${dest%/*}" + if ! reassemble "$MNT/$rdir" "$rname" "$tmp"; then + rm -f "$tmp" + log "$part: reassembling $rname failed, trying next" + continue + fi + got=$(sha256sum "$tmp" | awk '{print $1}') + if [ "$got" != "$want" ]; then + rm -f "$tmp" + log "$part:$rdir/$rname sha256 $got != expected, trying next" + continue + fi + chmod 644 "$tmp" && mv "$tmp" "$dest" || { rm -f "$tmp"; fail "installing $dest failed"; } + log "reassembled $part:$rdir/$rname.{mdt,b0N} -> $dest" + return 0 + done + fail "no listed partition ($parts) yields $rname with sha256 $want - $dest NOT installed" +} + extract() { # parts=$1 src=$2 dest=$3 want=$4 for part in $(echo "$parts" | tr ',' ' '); do @@ -155,8 +250,11 @@ fi missing= for f in "$MANIFEST_DIR"/*.manifest; do [ -e "$f" ] || continue - while read -r kind _ _ dest _; do - [ "$kind" = file ] && [ ! -e "$dest" ] && missing=1 + while read -r kind a b c d e; do + case "$kind" in + file) [ -e "$c" ] || missing=1 ;; + mbn) [ -e "$d" ] || missing=1 ;; + esac done < "$f" done [ -z "$missing" ] && exit 0 @@ -165,7 +263,7 @@ for f in "$MANIFEST_DIR"/*.manifest; do [ -e "$f" ] || continue extracted= rebinds= - while read -r kind a b c d; do + while read -r kind a b c d e; do case "$kind" in ''|'#'*) ;; file) @@ -174,6 +272,12 @@ for f in "$MANIFEST_DIR"/*.manifest; do extract "$a" "$b" "$c" "$d" Date: Sat, 5 Sep 2026 04:25:51 +0200 Subject: [PATCH 2/3] kernel: build qcomtee, which is one line and not the coupled change we recorded The fingerprint stack needs /dev/tee0, and the pmOS config has never built the driver -- so every session so far has loaded an out-of-tree module. That has been recorded since 2 September as the release blocker, on the reading that drivers/tee/qcomtee/Kconfig's `select QCOM_TZMEM_MODE_SHMBRIDGE` would flip tzmem out of Generic mode and drag a boot+selftest round with it. It does not. QCOM_TZMEM_MODE_SHMBRIDGE is a member of a choice block, and Kconfig cannot select a choice member -- the statement is inert, and silently so. Setting CONFIG_QCOMTEE=m and running olddefconfig against the tree produces exactly one line of difference and leaves CONFIG_QCOM_TZMEM_MODE_ GENERIC=y, which is the mode every fingerprint measurement was taken on. The driver is already in the base tree; the only reason it was out-of-tree here was that nothing enabled it. Verified on the dev phone: the module built from this config carries ZERO module parameters against the research build's 95, matching vermagic, and the phone boots on it with modules-load.d bringing it up, /dev/tee0 present, fingerprintd ready and the enrolled template loading. So the shipped module is upstream code on the tested tzmem mode. Not pushed. Whether to ship QCOMTEE with tzmem in Generic mode is a separate question with a real answer -- journal/tee warns about that mode -- and it is now sequenceable on its own instead of forced by a select that does nothing. --- aports/device/linux-postmarketos-qcom-milos/APKBUILD | 2 +- .../config-postmarketos-qcom-milos.aarch64 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aports/device/linux-postmarketos-qcom-milos/APKBUILD b/aports/device/linux-postmarketos-qcom-milos/APKBUILD index 94fde64..c7c3224 100644 --- a/aports/device/linux-postmarketos-qcom-milos/APKBUILD +++ b/aports/device/linux-postmarketos-qcom-milos/APKBUILD @@ -8,7 +8,7 @@ _flavor="postmarketos-qcom-milos" pkgname=linux-$_flavor pkgver=7.2.0 # always sorts above the upstream aport (r0..r99) -pkgrel=100 +pkgrel=101 pkgdesc="Milos mainline kernel + Catcrafts FP6 bring-up carries (combined-stable)" arch="aarch64" _carch="arm64" diff --git a/aports/device/linux-postmarketos-qcom-milos/config-postmarketos-qcom-milos.aarch64 b/aports/device/linux-postmarketos-qcom-milos/config-postmarketos-qcom-milos.aarch64 index 9c26c1c..e0aedd3 100644 --- a/aports/device/linux-postmarketos-qcom-milos/config-postmarketos-qcom-milos.aarch64 +++ b/aports/device/linux-postmarketos-qcom-milos/config-postmarketos-qcom-milos.aarch64 @@ -7224,7 +7224,7 @@ CONFIG_TEE_DMABUF_HEAPS=y CONFIG_OPTEE=y # CONFIG_OPTEE_INSECURE_LOAD_IMAGE is not set CONFIG_OPTEE_STATIC_PROTMEM_POOL=y -# CONFIG_QCOMTEE is not set +CONFIG_QCOMTEE=m # CONFIG_MUX_CORE is not set CONFIG_PM_OPP=y # CONFIG_SIOX is not set From 2a4427919e592ec0d0f552baaea132174de84b73 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 5 Sep 2026 20:31:37 +0200 Subject: [PATCH 3/3] Install fingerprintd from the registry, so the image unlocks with a finger The daemon's own package CI publishes it to the registry the same way imsd's does, so the image takes it from there: the exact apk a user later gets via apk upgrade, sha256-pinned, re-signed for the chroot. Section 3b now fetches both sets, and every fetched file must have a pin -- the check used to be `grep . | sha256sum -c`, which an empty pin list would have sailed through with nothing checked. Three apks: the daemon, its systemd units, and the session agent, which does nothing until a user writes ~/.config/fingerprintd/fingers.conf. The daemon needs the kernel aport's CONFIG_QCOMTEE=m (pkgrel 101) and fp6-vendor-blobs 1-r2's mbn directive to reassemble the trustlet, both built in this run; 0.2.3 says >=1-r2 so a mismatched pair is refused rather than installed. The CI publish step skips fingerprintd-* like imsd-*: registry-sourced, not ours to republish. README: fingerprint in the list, and the two things a user will otherwise report as a dead sensor -- the lock screen listens for 60 seconds after it appears, and a held press is what the matcher was measured on -- plus the untested question of stock Android's own fingerprints after using this. Verified on the dev phone (fp6 repo journal/fingerprint/, 2026-09-05): the registry 0.2.2 package enrols through Plasma's Users page and unlocks the lock screen; 0.2.3 differs by the dependency and a post-upgrade restart. The image build itself, with the fprintd purge inside the chroot, runs first in CI. --- .forgejo/workflows/build.yml | 6 ++-- README.md | 28 +++++++++++++++++-- build.sh | 53 ++++++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index fa00071..ea5f6b0 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -50,8 +50,8 @@ jobs: # the FP6 patches to the next upstream version bump. Requires the # PACKAGE_TOKEN repo secret (catbot account, package:write scope); # skips quietly until it exists. 409 = same version already published. - # imsd is skipped: build.sh 3b took it FROM the registry (re-signed - # for the chroot), so it is not ours to publish. + # imsd and fingerprintd are skipped: build.sh 3b took them FROM the + # registry (re-signed for the chroot), so they are not ours to publish. - name: Publish packages to the apk registry env: PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} @@ -65,7 +65,7 @@ jobs: for f in /home/build/.local/var/pmbootstrap/packages/*/aarch64/*.apk; do [ -e "$f" ] || continue case "$(basename "$f")" in - imsd-*) echo "registry-sourced, not republished: $(basename "$f")"; continue ;; + imsd-*|fingerprintd-*) echo "registry-sourced, not republished: $(basename "$f")"; continue ;; esac found=1 code=$(curl -s -o /dev/null -w '%{http_code}' \ diff --git a/README.md b/README.md index fdd08a3..e94b37f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ Maintained by [Jorijn van der Graaf](https://catcrafts.net/about) Everything on the `combined-stable` branch: display, touch, wifi, cellular data, NFC (reader), speaker audio, microphone, IMU, magnetometer, barometer, -ambient light/proximity plus VoLTE calls (both directions) through imsd. +ambient light/proximity, fingerprint unlock through +[fingerprintd](https://forgejo.catcrafts.net/Catcrafts/fingerprintd), plus +VoLTE calls (both directions) through imsd. ## Flashing @@ -49,4 +51,26 @@ Find it in a stock-firmware capture or your carrier's IMS documentation, then `systemctl restart imsd` (the service is enabled at boot and waits for this file to exist). See the [imsd README](https://forgejo.catcrafts.net/Catcrafts/imsd) for the full -variable reference and carrier assumptions. \ No newline at end of file +variable reference and carrier assumptions. + +## Fingerprint + +Enrol under **Settings → Users** (Plasma's own fingerprint page; the `fprintd` +command-line tools are not installed, fingerprintd replaces that package). +**Hold** the finger on the sensor for each of the ~20 presses rather than +tapping it; a held press is what the matcher was measured on. + +Two things to know: + +- The lock screen listens for a finger for **60 seconds after it appears**. + A press after that reaches nothing and looks like a dead sensor. Lock and + unlock again to re-arm it. +- The matcher is the phone's own proprietary trustlet, reassembled from the + stock modem partition on first boot and never shipped by us. Templates are + stored on the Android `persist` partition, sealed to the same hardware + anti-rollback counter stock Android uses. Whether fingerprints enrolled under + stock Android survive a return to it after using this has **not** been + tested. + +A finger can also run something in your session on a match +(`~/.config/fingerprintd/fingers.conf`, see the fingerprintd README). diff --git a/build.sh b/build.sh index 64bd9f7..805c6a3 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,7 @@ #!/bin/sh -eu # fp6-img pipeline: build a flashable postmarketOS image for the Fairphone 6 -# with the Catcrafts kernel (milos-linux combined-stable) and imsd (VoLTE) +# with the Catcrafts kernel (milos-linux combined-stable), imsd (VoLTE) and +# fingerprintd (fingerprint unlock) # installed from the Catcrafts apk registry. # # Runs in CI inside an Alpine container on the privileged "pmos" runner @@ -30,6 +31,19 @@ IMSD_SHA256=" f1c317d7ff9448c05df068d683d31da08e4bfc074704d96cf52cb8acbdee6304 imsd-0.3.1-r0.apk a78ef31fc2943ac02e120c46df26353d515ac9840d959cf5193b2afe1c665fa6 imsd-systemd-0.3.1-r0.apk " +# fingerprintd (fingerprint unlock) comes from the same registry the same way: +# its repo's package CI is the only producer, and the same pinning rule holds. +# Three apks: the daemon, its systemd units, and the session agent (inert +# until a user writes ~/.config/fingerprintd/fingers.conf). Needs the kernel +# aport's CONFIG_QCOMTEE=m (pkgrel 101) and fp6-vendor-blobs >= 1-r2, both +# built in this run. 0.2.3: 0.2.2 (enrol, unlock, agent, actions) + the +# versioned blobs dependency + a post-upgrade daemon restart. +FPD_VERSION=0.2.3-r0 +FPD_SHA256=" +3e28f0c1a9a844592ab6878b2dfc0d8f91674549e44bdc1652e7d7d029de1765 fingerprintd-0.2.3-r0.apk +0cc46eba5c6c77d5bb54cd9f0e2902f7644720f9c98153062ffa33e19ca36889 fingerprintd-systemd-0.2.3-r0.apk +6dfdbc6f971ba4b8f811f828e5868869c7d71fea6c7045e2bffd51bf2736c040 fingerprintd-agent-0.2.3-r0.apk +" PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git cd "$(dirname "$0")" @@ -215,7 +229,7 @@ aports = $WORK/pmaports device = fairphone-fp6 ui = plasma-mobile systemd = always -extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd,fp6-device-tweaks,fp6-charging-mode,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel +extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd,fingerprintd,fingerprintd-systemd,fingerprintd-agent,fp6-device-tweaks,fp6-charging-mode,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel EOF # All four source tarballs are generated locally above, so every checksum @@ -244,9 +258,9 @@ retry "build modemmanager" pmbootstrap $NOCROSS build --arch aarch64 modemmanage # patched -r2 exists for the publish step even if the install set resolves # it before the overlay is considered. retry "build libcamera" pmbootstrap $NOCROSS build --arch aarch64 libcamera -# --- 3b. imsd: the published apk, not a local build -------------------------- -# The imsd repo's package CI is the only producer of the imsd apk; the image -# installs the exact registry package users later get via 'apk upgrade'. +# --- 3b. imsd + fingerprintd: the published apks, not local builds ----------- +# Each repo's package CI is the only producer of its apk; the image installs +# the exact registry package users later get via 'apk upgrade'. # pmbootstrap has no knob for a third-party repository, and after the main # 'apk add' it re-adds every package found in its local packages dir BY FILE # PATH — which makes apk verify the package's own signature, and registry @@ -256,13 +270,21 @@ retry "build libcamera" pmbootstrap $NOCROSS build --arch aarch64 libcamera # stay byte-identical, so the identity checksum equals the registry's), drop # into the local packages dir, re-index. The abuild key exists because the # builds above initialized the buildroot. -IMSD_DL="$WORK/imsd-apk" -rm -rf "$IMSD_DL" -mkdir -p "$IMSD_DL" -for _f in "imsd-$IMSD_VERSION.apk" "imsd-systemd-$IMSD_VERSION.apk"; do - retry "fetch $_f" curl -fsSL -o "$IMSD_DL/$_f" "$IMSD_REGISTRY/aarch64/$_f" +REG_DL="$WORK/registry-apks" +rm -rf "$REG_DL" +mkdir -p "$REG_DL" +for _f in "imsd-$IMSD_VERSION.apk" "imsd-systemd-$IMSD_VERSION.apk" \ + "fingerprintd-$FPD_VERSION.apk" "fingerprintd-systemd-$FPD_VERSION.apk" \ + "fingerprintd-agent-$FPD_VERSION.apk"; do + # every fetched file must have a pin: 'grep .' below drops empty lines, + # so an empty pin list would otherwise pass the check with nothing checked + printf '%s\n' "$IMSD_SHA256" "$FPD_SHA256" | grep -q " $_f\$" || { + echo "no sha256 pin for $_f - add it to IMSD_SHA256/FPD_SHA256" >&2 + exit 1 + } + retry "fetch $_f" curl -fsSL -o "$REG_DL/$_f" "$IMSD_REGISTRY/aarch64/$_f" done -(cd "$IMSD_DL" && printf '%s\n' "$IMSD_SHA256" | grep . | sha256sum -c -) +(cd "$REG_DL" && printf '%s\n' "$IMSD_SHA256" "$FPD_SHA256" | grep . | sha256sum -c -) ABUILD_KEY=$(echo "$WORKDIR"/config_abuild/*.rsa) if [ ! -f "$ABUILD_KEY" ]; then echo "expected exactly one abuild key in $WORKDIR/config_abuild" >&2 @@ -271,9 +293,9 @@ fi # abuild-keygen ran inside the chroot as pmbootstrap's user (uid 12345), so # the key is 0600 to that uid and unreadable here (run 49 died on exactly # this); sign from a private copy taken via sudo, then drop it. -KEYCOPY="$IMSD_DL/abuild-key.rsa" +KEYCOPY="$REG_DL/abuild-key.rsa" sudo install -m 0600 -o "$(id -un)" "$ABUILD_KEY" "$KEYCOPY" -for _f in "$IMSD_DL"/*.apk; do +for _f in "$REG_DL"/*.apk; do python3 ./apk-resign.py "$_f" "$KEYCOPY" "$(basename "$ABUILD_KEY").pub" done rm -f "$KEYCOPY" @@ -286,11 +308,11 @@ if [ ! -d "$PKGDIR" ]; then echo "$PKGDIR missing - the package builds above should have created it" >&2 exit 1 fi -for _f in "$IMSD_DL"/*.apk; do +for _f in "$REG_DL"/*.apk; do sudo install -m 0644 -o "$(stat -c %u "$PKGDIR")" -g "$(stat -c %g "$PKGDIR")" \ "$_f" "$PKGDIR/$(basename "$_f")" done -rm -f "$IMSD_DL"/*.apk +rm -f "$REG_DL"/*.apk pmbootstrap index # --- 4. build the image ------------------------------------------------------- @@ -323,6 +345,7 @@ cp README.md install.sh "$STAGE/fp6-img/" echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "default login: user / 147147 (same as official postmarketOS images)" echo "imsd: $IMSD_REGISTRY imsd-$IMSD_VERSION (registry package, sha256-pinned)" + echo "fingerprintd: $IMSD_REGISTRY fingerprintd-$FPD_VERSION (registry package, sha256-pinned)" } > "$STAGE/fp6-img/build-info.txt" # sums of the extracted contents (cd "$STAGE/fp6-img" && sha256sum -- * > sha256sums.txt)