"""v1 (JAR) APK signer in the format Android's own SignApk used in the 2.x era, plus zipalign. Why not jarsigner: modern JDKs write "SHA-1-Digest" entries (Android <= 4.2 only looks for "SHA1-Digest"/"SHA-Digest") and put sha1WithRSAEncryption in the PKCS#7 SignerInfo, which old JarUtils can't map. Either makes Android 2.x treat the APK as unsigned and refuse to install it. Usage: signapk.py """ import base64 import hashlib import struct import sys import zipfile from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.serialization import pkcs12 ALIGN = 4 FIXED_TIME = (2008, 1, 1, 0, 0, 0) # ---------------------------------------------------------------- minimal DER def der_len(n): if n < 0x80: return bytes([n]) b = n.to_bytes((n.bit_length() + 7) // 8, "big") return bytes([0x80 | len(b)]) + b def tlv(tag, body): return bytes([tag]) + der_len(len(body)) + body def seq(*parts): return tlv(0x30, b"".join(parts)) def set_of(*parts): return tlv(0x31, b"".join(sorted(parts))) def oid(dotted): nums = [int(x) for x in dotted.split(".")] body = bytes([nums[0] * 40 + nums[1]]) for n in nums[2:]: chunk = [n & 0x7F] n >>= 7 while n: chunk.append(0x80 | (n & 0x7F)) n >>= 7 body += bytes(reversed(chunk)) return tlv(0x06, body) def integer(n): b = n.to_bytes((n.bit_length() + 8) // 8 or 1, "big", signed=True) return tlv(0x02, b) NULL = b"\x05\x00" OID_SHA1 = "1.3.14.3.2.26" OID_RSA = "1.2.840.113549.1.1.1" OID_DATA = "1.2.840.113549.1.7.1" OID_SIGNED_DATA = "1.2.840.113549.1.7.2" def pkcs7_detached(sf_bytes, key, cert): signature = key.sign(sf_bytes, padding.PKCS1v15(), hashes.SHA1()) cert_der = cert.public_bytes(serialization.Encoding.DER) sha1_alg = seq(oid(OID_SHA1), NULL) signer_info = seq( integer(1), seq(cert.issuer.public_bytes(), integer(cert.serial_number)), sha1_alg, seq(oid(OID_RSA), NULL), # rsaEncryption, as old JarUtils expects tlv(0x04, signature), ) signed_data = seq( integer(1), set_of(sha1_alg), seq(oid(OID_DATA)), # detached: content is META-INF/CERT.SF tlv(0xA0, cert_der), # [0] IMPLICIT certificates set_of(signer_info), ) return seq(oid(OID_SIGNED_DATA), tlv(0xA0, signed_data)) # ---------------------------------------------------------------- manifest text def b64sha1(data): return base64.b64encode(hashlib.sha1(data).digest()).decode() def wrap(line): """JAR manifest lines are at most 72 bytes; continuation lines start with a space.""" raw = line.encode("utf-8") out = [raw[:70]] raw = raw[70:] while raw: out.append(b" " + raw[:69]) raw = raw[69:] return b"\r\n".join(out) + b"\r\n" def sign(p12_path, password, src, dst): key, cert, _ = pkcs12.load_key_and_certificates(open(p12_path, "rb").read(), password.encode()) zin = zipfile.ZipFile(src) entries = [i for i in zin.infolist() if not i.filename.startswith("META-INF/") and not i.is_dir()] data = {i.filename: zin.read(i.filename) for i in entries} manifest = b"Manifest-Version: 1.0\r\nCreated-By: 1.0 (Android SignApk)\r\n\r\n" sf_entries = b"" for i in entries: section = wrap("Name: " + i.filename) + wrap("SHA1-Digest: " + b64sha1(data[i.filename])) + b"\r\n" manifest += section sf_entries += wrap("Name: " + i.filename) + wrap("SHA1-Digest: " + b64sha1(section)) + b"\r\n" sf = (b"Signature-Version: 1.0\r\nCreated-By: 1.0 (Android SignApk)\r\n" + wrap("SHA1-Digest-Manifest: " + b64sha1(manifest)) + b"\r\n" + sf_entries) rsa = pkcs7_detached(sf, key, cert) with zipfile.ZipFile(dst, "w") as zout: def put(name, payload, compress): zi = zipfile.ZipInfo(name, date_time=FIXED_TIME) zi.compress_type = compress zi.external_attr = 0o644 << 16 zi.create_system = 0 extra = b"" if compress == zipfile.ZIP_STORED: base = zout.fp.tell() + 30 + len(name.encode("utf-8")) + 6 pad = (ALIGN - base % ALIGN) % ALIGN extra = struct.pack("