imsd: install the published apk instead of building it
Some checks failed
image / image (push) Failing after 48m0s

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:
Jorijn van der Graaf 2026-09-02 16:11:50 +02:00
commit e7a1825b50
3 changed files with 130 additions and 36 deletions

76
apk-resign.py Executable file
View 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:])