imsd: install the published apk instead of building it
Some checks failed
image / image (push) Failing after 48m0s
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:
parent
6d32d10a8a
commit
e7a1825b50
3 changed files with 130 additions and 36 deletions
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:])
|
||||
Loading…
Reference in a new issue