Some checks failed
image / image (push) Failing after 2h3m26s
Run 49 died in section 3b: pmbootstrap's abuild-keygen runs inside the chroot as its own user (uid 12345), so the key in config_abuild/ is 0600 to that uid and the build user cannot read it; openssl dgst -sign exited 1 and apk-resign.py swallowed its stderr. Take a private copy via sudo for the duration of the re-sign, and make the script name an unreadable key and let openssl's stderr through instead of hiding it.
84 lines
3.1 KiB
Python
Executable file
84 lines
3.1 KiB
Python
Executable file
#!/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 os
|
|
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}")
|
|
if not os.access(privkey, os.R_OK):
|
|
sys.exit(f"{privkey}: not readable by uid {os.getuid()} (pmbootstrap's "
|
|
"abuild-keygen runs as the chroot user, uid 12345, and leaves "
|
|
"the key 0600 to it - sign from a readable copy)")
|
|
try:
|
|
sig = subprocess.run(
|
|
["openssl", "dgst", f"-{DIGEST[kind]}", "-sign", privkey],
|
|
input=control, stdout=subprocess.PIPE, check=True,
|
|
).stdout
|
|
except subprocess.CalledProcessError as e:
|
|
sys.exit(f"openssl dgst -sign exited {e.returncode} signing {path}")
|
|
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:])
|