"""One-shot: parse APK Signing Block v2/v3 and print SHA-256(hex) of signing cert. Matches what AntiTamperModule.kt computes — SHA-256 of the DER-encoded X.509 cert that the PackageManager would return for the APK. Spec: https://source.android.com/docs/security/features/apksigning/v2 """ from __future__ import annotations import hashlib import struct import sys import zipfile from pathlib import Path APK_SIG_BLOCK_MAGIC = b"APK Sig Block 42" V2_BLOCK_ID = 0x7109871A V3_BLOCK_ID = 0xF05368C0 V3_1_BLOCK_ID = 0x1B93AD61 def _find_eocd(data: bytes) -> int: sig = b"PK\x05\x06" # EOCD must be in last 65557 bytes start = max(0, len(data) - 65557) idx = data.rfind(sig, start) if idx < 0: raise RuntimeError("EOCD not found") return idx def _read_uint32_le(b: bytes, off: int) -> int: return struct.unpack_from(" int: return struct.unpack_from(" bytes: data = path.read_bytes() eocd = _find_eocd(data) cd_offset = _read_uint32_le(data, eocd + 16) # Magic ends at cd_offset; the 8 bytes before magic are block size (excluding self) magic_end = cd_offset magic_start = magic_end - len(APK_SIG_BLOCK_MAGIC) if data[magic_start:magic_end] != APK_SIG_BLOCK_MAGIC: raise RuntimeError("APK Signing Block magic not found before central directory") size_off = magic_start - 8 block_size_excl = _read_uint64_le(data, size_off) # The block layout: size_of_block(8) | pairs | size_of_block(8) | magic(16) block_total = block_size_excl + 8 block_start = magic_end - block_total # Block = leading_size(8) | pairs | trailing_size(8) | magic(16) # pairs region = between leading_size and trailing_size return data[block_start + 8 : magic_start - 8] def iter_pairs(pairs: bytes): i = 0 n = len(pairs) while i < n: length = _read_uint64_le(pairs, i) i += 8 pair_id = _read_uint32_le(pairs, i) value = pairs[i + 4 : i + length] yield pair_id, value i += length def extract_cert_der_v2_or_v3(block_value: bytes) -> bytes: # block_value = "signers" sequence # signers = length-prefixed sequence of signer # signer = signed_data || signatures || public_key (each length-prefixed) # signed_data = digests || certificates || additional_attributes (each length-prefixed) # certificates = sequence of length-prefixed DER X.509 certs def read_lp(buf: bytes, off: int) -> tuple[bytes, int]: length = _read_uint32_le(buf, off) return buf[off + 4 : off + 4 + length], off + 4 + length # outer = signers sequence (already length-prefixed by caller? actually block_value IS the value) # Per spec, the block "value" begins with sequence of length-prefixed signer structures # i.e. no outer length prefix here. off = 0 # The very first uint32 in block_value is the length of the signers sequence signers_seq, _ = read_lp(block_value, 0) off = 0 signer, _ = read_lp(signers_seq, off) # signer = signed_data || min_sdk(4) || max_sdk(4) || signatures || public_key (v3 has sdk fields) # Simplest: signed_data is the first length-prefixed blob in signer. signed_data, _ = read_lp(signer, 0) # signed_data = digests || certificates || ... inner_off = 0 digests, inner_off = read_lp(signed_data, inner_off) certs_seq, inner_off = read_lp(signed_data, inner_off) # certs_seq = sequence of length-prefixed DER certs first_cert, _ = read_lp(certs_seq, 0) return first_cert def main(argv: list[str]) -> int: if len(argv) < 2: print("usage: _extract_apk_sig_hash.py ", file=sys.stderr) return 2 apk = Path(argv[1]) if not apk.is_file(): print(f"not found: {apk}", file=sys.stderr) return 2 pairs = extract_sig_block(apk) found_block: bytes | None = None chosen_id = None for pid, value in iter_pairs(pairs): if pid in (V2_BLOCK_ID, V3_BLOCK_ID, V3_1_BLOCK_ID): # prefer v3 over v2 if both present (matches what PM returns on modern Android) if chosen_id in (None, V2_BLOCK_ID) and pid in (V3_BLOCK_ID, V3_1_BLOCK_ID): found_block = value chosen_id = pid elif chosen_id is None: found_block = value chosen_id = pid if found_block is None: print("no v2/v3 signing block found", file=sys.stderr) return 3 cert_der = extract_cert_der_v2_or_v3(found_block) sha = hashlib.sha256(cert_der).hexdigest() print(sha) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))