imsd: install the published apk instead of building it
The imsd repo's package CI is now the only producer of the imsd apk. The image installs the exact registry package users get via 'apk upgrade' (pinned version + sha256 of the registry files), so the two can no longer diverge and the payload-parity rule between two packagings is gone. pmbootstrap has no knob for a third-party repository, and after the main 'apk add' it re-adds every package in its local packages dir by file path, which makes apk verify the package's own signature. Registry packages are signed with per-run keys nobody keeps (phones trust the registry-signed index), so apk-resign.py replaces the signature stream with one from this run's abuild key; control and data streams stay byte-identical and the identity checksum equals the registry's. Verified on the host with apk 3.0.8: originals UNTRUSTED, re-signed OK, checksums equal. The publish step skips imsd-*: those files came from the registry.
This commit is contained in:
parent
bd8971dc89
commit
8117b30384
3 changed files with 130 additions and 36 deletions
|
|
@ -44,12 +44,14 @@ jobs:
|
||||||
path: dist/*
|
path: dist/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
# Ship every locally built apk (kernel, modemmanager, libqmi, imsd,
|
# Ship every locally built apk (kernel, modemmanager, libqmi,
|
||||||
# callaudioshim, audio files, ...) to the Forgejo Alpine registry, so
|
# callaudioshim, audio files, ...) to the Forgejo Alpine registry, so
|
||||||
# installed systems get updates via 'apk upgrade' instead of losing
|
# installed systems get updates via 'apk upgrade' instead of losing
|
||||||
# the FP6 patches to the next upstream version bump. Requires the
|
# the FP6 patches to the next upstream version bump. Requires the
|
||||||
# PACKAGE_TOKEN repo secret (catbot account, package:write scope);
|
# PACKAGE_TOKEN repo secret (catbot account, package:write scope);
|
||||||
# skips quietly until it exists. 409 = same version already published.
|
# 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.
|
||||||
- name: Publish packages to the apk registry
|
- name: Publish packages to the apk registry
|
||||||
env:
|
env:
|
||||||
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
|
@ -62,6 +64,9 @@ jobs:
|
||||||
found=0
|
found=0
|
||||||
for f in /home/build/.local/var/pmbootstrap/packages/*/aarch64/*.apk; do
|
for f in /home/build/.local/var/pmbootstrap/packages/*/aarch64/*.apk; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
|
case "$(basename "$f")" in
|
||||||
|
imsd-*) echo "registry-sourced, not republished: $(basename "$f")"; continue ;;
|
||||||
|
esac
|
||||||
found=1
|
found=1
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
--user "catbot:$PACKAGE_TOKEN" --upload-file "$f" \
|
--user "catbot:$PACKAGE_TOKEN" --upload-file "$f" \
|
||||||
|
|
|
||||||
76
apk-resign.py
Executable file
76
apk-resign.py
Executable file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-sign an apk (v2 format) with another RSA key, in place.
|
||||||
|
|
||||||
|
apk-resign.py <pkg.apk> <private key .rsa> <public key file name>
|
||||||
|
|
||||||
|
An apk is three concatenated gzip streams: the signature tar, the control
|
||||||
|
tar (.PKGINFO) and the data tar. The signature covers the control stream
|
||||||
|
only, so replacing the first stream re-signs the package while its identity
|
||||||
|
checksum (over the control stream) and its contents stay byte-identical.
|
||||||
|
This is what abuild-sign does to the control segment when abuild packages.
|
||||||
|
"""
|
||||||
|
import gzip
|
||||||
|
import io
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
DIGEST = {"RSA": "sha1", "RSA256": "sha256", "RSA512": "sha512"}
|
||||||
|
|
||||||
|
|
||||||
|
def gzip_members(data):
|
||||||
|
off = 0
|
||||||
|
while off < len(data):
|
||||||
|
d = zlib.decompressobj(31)
|
||||||
|
d.decompress(data[off:])
|
||||||
|
end = len(data) - len(d.unused_data)
|
||||||
|
if end <= off:
|
||||||
|
raise ValueError("gzip stream did not advance")
|
||||||
|
yield data[off:end]
|
||||||
|
off = end
|
||||||
|
|
||||||
|
|
||||||
|
def first_entry_name(tar):
|
||||||
|
"""Name of the first regular file in a tar stream, skipping pax headers."""
|
||||||
|
off = 0
|
||||||
|
while off + 512 <= len(tar):
|
||||||
|
hdr = tar[off:off + 512]
|
||||||
|
size = int(hdr[124:136].split(b"\0")[0].strip() or b"0", 8)
|
||||||
|
if hdr[156:157] not in (b"x", b"g"): # not a pax extended/global header
|
||||||
|
return hdr[:100].rstrip(b"\0").decode()
|
||||||
|
off += 512 + (size + 511) // 512 * 512
|
||||||
|
raise ValueError("no file entry in signature tar")
|
||||||
|
|
||||||
|
|
||||||
|
def main(path, privkey, pubname):
|
||||||
|
members = list(gzip_members(open(path, "rb").read()))
|
||||||
|
if len(members) != 3:
|
||||||
|
sys.exit(f"{path}: expected 3 gzip streams, found {len(members)}")
|
||||||
|
old_sig, control, payload = members
|
||||||
|
# keep the original digest type: the entry is .SIGN.<RSA|RSA256|RSA512>.<key>
|
||||||
|
name = first_entry_name(gzip.decompress(old_sig))
|
||||||
|
kind = name.split(".")[2] if name.startswith(".SIGN.") else ""
|
||||||
|
if kind not in DIGEST:
|
||||||
|
sys.exit(f"{path}: unexpected signature entry {name!r}")
|
||||||
|
sig = subprocess.run(
|
||||||
|
["openssl", "dgst", f"-{DIGEST[kind]}", "-sign", privkey],
|
||||||
|
input=control, capture_output=True, check=True,
|
||||||
|
).stdout
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=buf, mode="w", format=tarfile.USTAR_FORMAT) as tar:
|
||||||
|
info = tarfile.TarInfo(f".SIGN.{kind}.{pubname}")
|
||||||
|
info.size = len(sig)
|
||||||
|
info.mode = 0o644
|
||||||
|
tar.addfile(info, io.BytesIO(sig))
|
||||||
|
# like abuild-tar --cut: header + data blocks, no end-of-archive marker
|
||||||
|
cut = 512 + (len(sig) + 511) // 512 * 512
|
||||||
|
new_sig = gzip.compress(buf.getvalue()[:cut], mtime=0)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(new_sig + control + payload)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
main(*sys.argv[1:])
|
||||||
83
build.sh
83
build.sh
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh -eu
|
#!/bin/sh -eu
|
||||||
# fp6-img pipeline: build a flashable postmarketOS image for the Fairphone 6
|
# fp6-img pipeline: build a flashable postmarketOS image for the Fairphone 6
|
||||||
# with the Catcrafts kernel (milos-linux combined-stable) and, once its tag
|
# with the Catcrafts kernel (milos-linux combined-stable) and imsd (VoLTE)
|
||||||
# is published, imsd (VoLTE).
|
# installed from the Catcrafts apk registry.
|
||||||
#
|
#
|
||||||
# Runs in CI inside an Alpine container on the privileged "pmos" runner
|
# Runs in CI inside an Alpine container on the privileged "pmos" runner
|
||||||
# (pmbootstrap needs loop devices; the aarch64 chroots need the qemu-user
|
# (pmbootstrap needs loop devices; the aarch64 chroots need the qemu-user
|
||||||
|
|
@ -17,13 +17,19 @@ set -eu
|
||||||
|
|
||||||
KERNEL_REPO=https://forgejo.catcrafts.net/Catcrafts/milos-linux.git
|
KERNEL_REPO=https://forgejo.catcrafts.net/Catcrafts/milos-linux.git
|
||||||
KERNEL_BRANCH=combined-stable
|
KERNEL_BRANCH=combined-stable
|
||||||
IMSD_REPO=https://forgejo.catcrafts.net/Catcrafts/imsd.git
|
# imsd is not built here: the image installs the apk the imsd repo's package
|
||||||
# imsd 0.3.1: 0.3.0 + the ims-pdn-up hardening (mmcli errors logged
|
# CI publishes to the registry (section 3b), so image and 'apk upgrade' carry
|
||||||
# verbatim, registration gate, configurable ip-type) + README carrier
|
# the same binary. Pinned by version AND by the sha256 of the registry files;
|
||||||
# updates. The aport now lives IN the imsd repo (packaging/aport/,
|
# a bump is these lines (sha256sum the two apks under $IMSD_REGISTRY/aarch64/).
|
||||||
# transferred 2026-09-01) and is copied out of this checkout below.
|
|
||||||
# Bump deliberately, not via tip-chasing.
|
# Bump deliberately, not via tip-chasing.
|
||||||
IMSD_COMMIT=17e0f6b
|
# 0.3.1: 0.3.0 + the ims-pdn-up hardening (mmcli errors logged verbatim,
|
||||||
|
# registration gate, configurable ip-type) + README carrier updates.
|
||||||
|
IMSD_REGISTRY=https://forgejo.catcrafts.net/api/packages/Catcrafts/alpine/edge/fp6
|
||||||
|
IMSD_VERSION=0.3.1-r0
|
||||||
|
IMSD_SHA256="
|
||||||
|
f1c317d7ff9448c05df068d683d31da08e4bfc074704d96cf52cb8acbdee6304 imsd-0.3.1-r0.apk
|
||||||
|
a78ef31fc2943ac02e120c46df26353d515ac9840d959cf5193b2afe1c665fa6 imsd-systemd-0.3.1-r0.apk
|
||||||
|
"
|
||||||
PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git
|
PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
@ -38,7 +44,7 @@ if [ "$(id -u)" = 0 ]; then
|
||||||
# multipath-tools: kpartx; util-linux: losetup with --json support
|
# multipath-tools: kpartx; util-linux: losetup with --json support
|
||||||
# (pmbootstrap's host-tool checks + image mounting need both)
|
# (pmbootstrap's host-tool checks + image mounting need both)
|
||||||
apk add -q git sudo openssl python3 py3-pip multipath-tools util-linux \
|
apk add -q git sudo openssl python3 py3-pip multipath-tools util-linux \
|
||||||
tar xz
|
tar xz curl
|
||||||
# the pmOS gitlab hiccups under crawler load and truncates clones
|
# the pmOS gitlab hiccups under crawler load and truncates clones
|
||||||
# ("early EOF"); that should cost a retry, not the run — same reasoning
|
# ("early EOF"); that should cost a retry, not the run — same reasoning
|
||||||
# as clone_retry below, which isn't defined yet in this root branch
|
# as clone_retry below, which isn't defined yet in this root branch
|
||||||
|
|
@ -138,8 +144,7 @@ cp -r aports/device/fp6-device-tweaks "$WORK/pmaports/device/"
|
||||||
cp -r aports/device/fp6-charging-mode "$WORK/pmaports/device/"
|
cp -r aports/device/fp6-charging-mode "$WORK/pmaports/device/"
|
||||||
cp -r aports/device/catcrafts-fp6-repo "$WORK/pmaports/device/"
|
cp -r aports/device/catcrafts-fp6-repo "$WORK/pmaports/device/"
|
||||||
cp -r aports/main/postmarketos-config-nftables "$WORK/pmaports/main/"
|
cp -r aports/main/postmarketos-config-nftables "$WORK/pmaports/main/"
|
||||||
# imsd's aport is NOT carried here: the imsd repo owns it (packaging/aport/)
|
# imsd has no aport here at all: section 3b installs the published apk.
|
||||||
# and section 2 copies it out of the pinned checkout.
|
|
||||||
# Alpine forks carrying the GNSS patches (libqmi !470 unreleased; MM !1463
|
# Alpine forks carrying the GNSS patches (libqmi !470 unreleased; MM !1463
|
||||||
# draft) - deps of modemmanager/imsd, built from aports because r100 > repo.
|
# draft) - deps of modemmanager/imsd, built from aports because r100 > repo.
|
||||||
cp -r aports/temp/libqmi "$WORK/pmaports/temp/"
|
cp -r aports/temp/libqmi "$WORK/pmaports/temp/"
|
||||||
|
|
@ -165,22 +170,7 @@ sed -i "s/^_commit=.*/_commit=\"$COMMIT\"/" "$KAPORT/APKBUILD"
|
||||||
KDATE=$(git -C "$WORK/milos-src" log -1 --format=%cd --date=format:%Y%m%d)
|
KDATE=$(git -C "$WORK/milos-src" log -1 --format=%cd --date=format:%Y%m%d)
|
||||||
sed -i "s/^pkgver=\([0-9.]*\)\$/pkgver=\1_git$KDATE/" "$KAPORT/APKBUILD"
|
sed -i "s/^pkgver=\([0-9.]*\)\$/pkgver=\1_git$KDATE/" "$KAPORT/APKBUILD"
|
||||||
|
|
||||||
# Same dance for imsd, pinned to a reviewed commit rather than branch tip.
|
# Same dance for the gitlab.freedesktop.org packages (libqmi, modemmanager,
|
||||||
# The aport itself comes from the same pinned checkout (packaging/aport/,
|
|
||||||
# owned by the imsd repo since 2026-09-01), so daemon and packaging can
|
|
||||||
# never skew.
|
|
||||||
|
|
||||||
clone_retry "$WORK/imsd-src" -q "$IMSD_REPO"
|
|
||||||
git -C "$WORK/imsd-src" checkout -q "$IMSD_COMMIT"
|
|
||||||
IAPORT="$WORK/pmaports/modem/imsd"
|
|
||||||
mkdir -p "$WORK/pmaports/modem"
|
|
||||||
rm -rf "$IAPORT"
|
|
||||||
cp -r "$WORK/imsd-src/packaging/aport" "$IAPORT"
|
|
||||||
git -C "$WORK/imsd-src" archive --prefix=imsd/ \
|
|
||||||
-o "$IAPORT/imsd-$IMSD_COMMIT.tar.gz" HEAD
|
|
||||||
sed -i "s/^_commit=.*/_commit=\"$IMSD_COMMIT\"/" "$IAPORT/APKBUILD"
|
|
||||||
|
|
||||||
# Same again for the gitlab.freedesktop.org packages (libqmi, modemmanager,
|
|
||||||
# libcamera): their pinned tarballs came from fd.o's on-demand archive
|
# libcamera): their pinned tarballs came from fd.o's on-demand archive
|
||||||
# endpoint, which 503/504s for hours at a stretch — runs #25, #27, #30 and
|
# endpoint, which 503/504s for hours at a stretch — runs #25, #27, #30 and
|
||||||
# #31 all died there, outlasting any in-run retry. git clone is served from
|
# #31 all died there, outlasting any in-run retry. git clone is served from
|
||||||
|
|
@ -228,12 +218,11 @@ 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,fp6-device-tweaks,fp6-charging-mode,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# All five source tarballs are generated locally above, so every checksum
|
# All four source tarballs are generated locally above, so every checksum
|
||||||
# step is offline. libcamera needs a checksum step now too: its committed
|
# step is offline. libcamera needs a checksum step now too: its committed
|
||||||
# sums were for the fd.o-served tarball, and git-archive output is not
|
# sums were for the fd.o-served tarball, and git-archive output is not
|
||||||
# byte-identical to it (verified: sha512 differs).
|
# byte-identical to it (verified: sha512 differs).
|
||||||
pmbootstrap checksum linux-postmarketos-qcom-milos
|
pmbootstrap checksum linux-postmarketos-qcom-milos
|
||||||
pmbootstrap checksum imsd
|
|
||||||
pmbootstrap checksum libqmi
|
pmbootstrap checksum libqmi
|
||||||
pmbootstrap checksum modemmanager
|
pmbootstrap checksum modemmanager
|
||||||
pmbootstrap checksum libcamera
|
pmbootstrap checksum libcamera
|
||||||
|
|
@ -255,12 +244,36 @@ retry "build modemmanager" pmbootstrap $NOCROSS build --arch aarch64 modemmanage
|
||||||
# patched -r2 exists for the publish step even if the install set resolves
|
# patched -r2 exists for the publish step even if the install set resolves
|
||||||
# it before the overlay is considered.
|
# it before the overlay is considered.
|
||||||
retry "build libcamera" pmbootstrap $NOCROSS build --arch aarch64 libcamera
|
retry "build libcamera" pmbootstrap $NOCROSS build --arch aarch64 libcamera
|
||||||
# imsd is the one crossdirect build left to the install phase, which has no
|
# --- 3b. imsd: the published apk, not a local build --------------------------
|
||||||
# per-package flag — pre-build it here in no-crossdirect mode so install
|
# The imsd repo's package CI is the only producer of the imsd apk; the image
|
||||||
# finds it current.
|
# installs the exact registry package users later get via 'apk upgrade'.
|
||||||
if [ -n "$NOCROSS" ]; then
|
# pmbootstrap has no knob for a third-party repository, and after the main
|
||||||
retry "build imsd" pmbootstrap $NOCROSS build --arch aarch64 imsd
|
# '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
|
||||||
|
# packages are signed with per-run keys nobody keeps (phones trust the
|
||||||
|
# registry-signed index instead). So: fetch, check against the sha256 pin,
|
||||||
|
# re-sign the envelope with this run's abuild key (control and data streams
|
||||||
|
# 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"
|
||||||
|
done
|
||||||
|
(cd "$IMSD_DL" && printf '%s\n' "$IMSD_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
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
for _f in "$IMSD_DL"/*.apk; do
|
||||||
|
python3 ./apk-resign.py "$_f" "$ABUILD_KEY" "$(basename "$ABUILD_KEY").pub"
|
||||||
|
done
|
||||||
|
mkdir -p "$WORKDIR/packages/edge/aarch64"
|
||||||
|
mv "$IMSD_DL"/*.apk "$WORKDIR/packages/edge/aarch64/"
|
||||||
|
pmbootstrap index
|
||||||
|
|
||||||
# --- 4. build the image -------------------------------------------------------
|
# --- 4. build the image -------------------------------------------------------
|
||||||
# Same default credentials as the official postmarketOS images.
|
# Same default credentials as the official postmarketOS images.
|
||||||
|
|
@ -291,7 +304,7 @@ cp README.md install.sh "$STAGE/fp6-img/"
|
||||||
echo "kernel: $KERNEL_REPO $KERNEL_BRANCH @ $COMMIT"
|
echo "kernel: $KERNEL_REPO $KERNEL_BRANCH @ $COMMIT"
|
||||||
echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
echo "default login: user / 147147 (same as official postmarketOS images)"
|
echo "default login: user / 147147 (same as official postmarketOS images)"
|
||||||
echo "imsd: $IMSD_REPO @ $IMSD_COMMIT ($(apkbuild_var "$IAPORT" pkgver))"
|
echo "imsd: $IMSD_REGISTRY imsd-$IMSD_VERSION (registry package, sha256-pinned)"
|
||||||
} > "$STAGE/fp6-img/build-info.txt"
|
} > "$STAGE/fp6-img/build-info.txt"
|
||||||
# sums of the extracted contents
|
# sums of the extracted contents
|
||||||
(cd "$STAGE/fp6-img" && sha256sum -- * > sha256sums.txt)
|
(cd "$STAGE/fp6-img" && sha256sum -- * > sha256sums.txt)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue