fdroidserver.apksigcopier module¶
Copy/extract/patch android apk signatures & compare apks.
apksigcopier is a tool for copying android APK signatures from a signed APK to an unsigned one (in order to verify reproducible builds).
It can also be used to compare two APKs with different signatures; this requires apksigner.
CLI¶
$ apksigcopier extract [OPTIONS] SIGNED_APK OUTPUT_DIR $ apksigcopier patch [OPTIONS] METADATA_DIR UNSIGNED_APK OUTPUT_APK $ apksigcopier copy [OPTIONS] SIGNED_APK UNSIGNED_APK OUTPUT_APK $ apksigcopier compare [OPTIONS] FIRST_APK SECOND_APK
The following environment variables can be set to 1, yes, or true to override the default behaviour:
set APKSIGCOPIER_EXCLUDE_ALL_META=1 to exclude all metadata files
set APKSIGCOPIER_COPY_EXTRA_BYTES=1 to copy extra bytes after data (e.g. a v2 sig)
set APKSIGCOPIER_SKIP_REALIGNMENT=1 to skip realignment of ZIP entries
API¶
>> from apksigcopier import do_extract, do_patch, do_copy, do_compare >> do_extract(signed_apk, output_dir, v1_only=NO) >> do_patch(metadata_dir, unsigned_apk, output_apk, v1_only=NO) >> do_copy(signed_apk, unsigned_apk, output_apk, v1_only=NO) >> do_compare(first_apk, second_apk, unsigned=False)
You can use False, None, and True instead of NO, AUTO, and YES respectively.
The following global variables (which default to False), can be set to override the default behaviour:
set exclude_all_meta=True to exclude all metadata files
set copy_extra_bytes=True to copy extra bytes after data (e.g. a v2 sig)
set skip_realignment=True to skip realignment of ZIP entries
- exception fdroidserver.apksigcopier.APKSigningBlockError¶
Bases:
APKSigCopierError
Something wrong with the APK Signing Block.
- class fdroidserver.apksigcopier.APKZipInfo(zinfo: ZipInfo, **override: Any)¶
Bases:
ReproducibleZipInfo
Reproducible ZipInfo for APK files.
- Attributes:
- CRC
- comment
- compress_size
- compress_type
- create_system
- create_version
- date_time
- external_attr
- extra
- extract_version
- file_size
- filename
- flag_bits
- header_offset
- internal_attr
- orig_filename
- reserved
- volume
Methods
FileHeader
([zip64])Return the per-file header as a bytes object.
from_file
(filename[, arcname, strict_timestamps])Construct an appropriate ZipInfo for a file on the filesystem.
is_dir
()Return True if this archive member is a directory.
- COMPRESSLEVEL = 9¶
- CRC¶
- comment¶
- compress_size¶
- compress_type¶
- create_system¶
- create_version¶
- date_time¶
- external_attr¶
- extra¶
- extract_version¶
- file_size¶
- filename¶
- flag_bits¶
- header_offset¶
- internal_attr¶
- orig_filename¶
- reserved¶
- volume¶
- exception fdroidserver.apksigcopier.NoAPKSigningBlock¶
Bases:
APKSigningBlockError
APK Signing Block Missing.
- class fdroidserver.apksigcopier.ReproducibleZipInfo(zinfo: ZipInfo, **override: Any)¶
Bases:
ZipInfo
Reproducible ZipInfo hack.
- Attributes:
- CRC
- comment
- compress_size
- compress_type
- create_system
- create_version
- date_time
- external_attr
- extra
- extract_version
- file_size
- filename
- flag_bits
- header_offset
- internal_attr
- orig_filename
- reserved
- volume
Methods
FileHeader
([zip64])Return the per-file header as a bytes object.
from_file
(filename[, arcname, strict_timestamps])Construct an appropriate ZipInfo for a file on the filesystem.
is_dir
()Return True if this archive member is a directory.
- CRC¶
- comment¶
- compress_size¶
- compress_type¶
- create_system¶
- create_version¶
- date_time¶
- external_attr¶
- extra¶
- extract_version¶
- file_size¶
- filename¶
- flag_bits¶
- header_offset¶
- internal_attr¶
- orig_filename¶
- reserved¶
- volume¶
- class fdroidserver.apksigcopier.ZipData(cd_offset, eocd_offset, cd_and_eocd)¶
Bases:
tuple
- Attributes:
- cd_and_eocd
Alias for field number 2
- cd_offset
Alias for field number 0
- eocd_offset
Alias for field number 1
Methods
count
(value, /)Return number of occurrences of value.
index
(value[, start, stop])Return first index of value.
- cd_and_eocd¶
Alias for field number 2
- cd_offset¶
Alias for field number 0
- eocd_offset¶
Alias for field number 1
- exception fdroidserver.apksigcopier.ZipError¶
Bases:
APKSigCopierError
Something wrong with ZIP file.
- fdroidserver.apksigcopier.copy_apk(unsigned_apk: str, output_apk: str, *, copy_extra: Optional[bool] = None, exclude: Optional[Callable[[str], bool]] = None, realign: Optional[bool] = None, zfe_size: Optional[int] = None) Tuple[int, int, int, int, int, int] ¶
Copy APK like apksigner would, excluding files matched by exclude_from_copying().
Adds a zipflinger virtual entry of zfe_size bytes if one is not already present and zfe_size is not None.
Returns max date_time.
The following global variables (which default to False), can be set to override the default behaviour:
set exclude_all_meta=True to exclude all metadata files
set copy_extra_bytes=True to copy extra bytes after data (e.g. a v2 sig)
set skip_realignment=True to skip realignment of ZIP entries
The default behaviour can also be changed using the keyword-only arguments exclude, copy_extra, and realign; these take precedence over the global variables when not None. NB: exclude is a callable, not a bool; realign is the inverse of skip_realignment.
>>> import apksigcopier, os, zipfile >>> apk = "test/apks/apks/golden-aligned-in.apk" >>> with zipfile.ZipFile(apk, "r") as zf: ... infos_in = zf.infolist() >>> with tempfile.TemporaryDirectory() as tmpdir: ... out = os.path.join(tmpdir, "out.apk") ... apksigcopier.copy_apk(apk, out) ... with zipfile.ZipFile(out, "r") as zf: ... infos_out = zf.infolist() (2017, 5, 15, 11, 28, 40) >>> for i in infos_in: ... print(i.filename) META-INF/ META-INF/MANIFEST.MF AndroidManifest.xml classes.dex temp.txt lib/armeabi/fake.so resources.arsc temp2.txt >>> for i in infos_out: ... print(i.filename) AndroidManifest.xml classes.dex temp.txt lib/armeabi/fake.so resources.arsc temp2.txt >>> infos_in[2] <ZipInfo filename='AndroidManifest.xml' compress_type=deflate file_size=1672 compress_size=630> >>> infos_out[0] <ZipInfo filename='AndroidManifest.xml' compress_type=deflate file_size=1672 compress_size=630> >>> repr(infos_in[2:]) == repr(infos_out) True
- fdroidserver.apksigcopier.detect_zfe(apkfile: str) Optional[int] ¶
Detect zipflinger virtual entry.
Returns the size of the virtual entry if found, None otherwise.
Raises ZipError if the size is less than 30 or greater than 4096, or the data isn’t all zeroes.
- fdroidserver.apksigcopier.do_copy(signed_apk: str, unsigned_apk: str, output_apk: str, v1_only: Optional[Union[Literal['no', 'auto', 'yes'], bool]] = 'no', *, exclude: Optional[Callable[[str], bool]] = None, ignore_differences: bool = False) None ¶
Copy signatures from signed_apk onto unsigned_apk and save as output_apk.
The v1_only parameter controls whether the absence of a v1 signature is considered an error or not: * use v1_only=NO (or v1_only=False) to only accept (v1+)v2/v3 signatures; * use v1_only=AUTO (or v1_only=None) to automatically detect v2/v3 signatures; * use v1_only=YES (or v1_only=True) to ignore any v2/v3 signatures.
- fdroidserver.apksigcopier.do_extract(signed_apk: str, output_dir: str, v1_only: Optional[Union[Literal['no', 'auto', 'yes'], bool]] = 'no', *, ignore_differences: bool = False) None ¶
Extract signatures from signed_apk and save in output_dir.
The v1_only parameter controls whether the absence of a v1 signature is considered an error or not: * use v1_only=NO (or v1_only=False) to only accept (v1+)v2/v3 signatures; * use v1_only=AUTO (or v1_only=None) to automatically detect v2/v3 signatures; * use v1_only=YES (or v1_only=True) to ignore any v2/v3 signatures.
- fdroidserver.apksigcopier.do_patch(metadata_dir: str, unsigned_apk: str, output_apk: str, v1_only: Optional[Union[Literal['no', 'auto', 'yes'], bool]] = 'no', *, exclude: Optional[Callable[[str], bool]] = None, ignore_differences: bool = False) None ¶
Patch signatures from metadata_dir onto unsigned_apk and save as output_apk.
The v1_only parameter controls whether the absence of a v1 signature is considered an error or not: * use v1_only=NO (or v1_only=False) to only accept (v1+)v2/v3 signatures; * use v1_only=AUTO (or v1_only=None) to automatically detect v2/v3 signatures; * use v1_only=YES (or v1_only=True) to ignore any v2/v3 signatures.
- fdroidserver.apksigcopier.exclude_default(filename: str) bool ¶
Like exclude_from_copying().
Excludes directories and filenames in COPY_EXCLUDE (i.e. MANIFEST.MF).
- fdroidserver.apksigcopier.exclude_from_copying(filename: str) bool ¶
Check whether to exclude a file during copy_apk().
Excludes filenames in COPY_EXCLUDE (i.e. MANIFEST.MF) by default; when exclude_all_meta is set to True instead, excludes all metadata files as matched by is_meta().
Directories are always excluded.
>>> import apksigcopier >>> from apksigcopier import exclude_from_copying >>> exclude_from_copying("classes.dex") False >>> exclude_from_copying("foo/") True >>> exclude_from_copying("META-INF/") True >>> exclude_from_copying("META-INF/MANIFEST.MF") True >>> exclude_from_copying("META-INF/CERT.SF") False >>> exclude_from_copying("META-INF/OOPS") False
>>> apksigcopier.exclude_all_meta = True >>> exclude_from_copying("classes.dex") False >>> exclude_from_copying("META-INF/") True >>> exclude_from_copying("META-INF/MANIFEST.MF") True >>> exclude_from_copying("META-INF/CERT.SF") True >>> exclude_from_copying("META-INF/OOPS") False
- fdroidserver.apksigcopier.exclude_meta(filename: str) bool ¶
Like exclude_from_copying(); excludes directories and all metadata files.
- fdroidserver.apksigcopier.extract_differences(signed_apk: str, extracted_meta: Iterable[Tuple[ZipInfo, bytes]]) Optional[Dict[str, Any]] ¶
Extract ZIP metadata differences from signed APK.
>>> import apksigcopier as asc, pprint >>> apk = "test/apks/apks/debuggable-boolean.apk" >>> meta = tuple(asc.extract_meta(apk)) >>> [ x.filename for x, _ in meta ] ['META-INF/CERT.SF', 'META-INF/CERT.RSA', 'META-INF/MANIFEST.MF'] >>> diff = asc.extract_differences(apk, meta) >>> pprint.pprint(diff) {'files': {'META-INF/CERT.RSA': {'flag_bits': 2056}, 'META-INF/CERT.SF': {'flag_bits': 2056}, 'META-INF/MANIFEST.MF': {'flag_bits': 2056}}}
>>> meta[2][0].extract_version = 42 >>> try: ... asc.extract_differences(apk, meta) ... except asc.ZipError as e: ... print(e) Unsupported extract_version
>>> asc.validate_differences(diff) is None True >>> diff["files"]["META-INF/OOPS"] = {} >>> asc.validate_differences(diff) ".files key 'META-INF/OOPS' is not a metadata file" >>> del diff["files"]["META-INF/OOPS"] >>> diff["files"]["META-INF/CERT.RSA"]["compresslevel"] = 42 >>> asc.validate_differences(diff) ".files['META-INF/CERT.RSA'].compresslevel has an unexpected value" >>> diff["oops"] = 42 >>> asc.validate_differences(diff) 'contains unknown key(s)'
- fdroidserver.apksigcopier.extract_meta(signed_apk: str) Iterator[Tuple[ZipInfo, bytes]] ¶
Extract v1 signature metadata files from signed APK.
Yields (ZipInfo, data) pairs.
>>> from apksigcopier import extract_meta >>> apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" >>> meta = tuple(extract_meta(apk)) >>> [ x.filename for x, _ in meta ] ['META-INF/RSA-2048.SF', 'META-INF/RSA-2048.RSA', 'META-INF/MANIFEST.MF'] >>> for line in meta[0][1].splitlines()[:4]: ... print(line.decode()) Signature-Version: 1.0 Created-By: 1.0 (Android) SHA-256-Digest-Manifest: hz7AxDJU9Namxoou/kc4Z2GVRS9anCGI+M52tbCsXT0= X-Android-APK-Signed: 2, 3 >>> for line in meta[2][1].splitlines()[:2]: ... print(line.decode()) Manifest-Version: 1.0 Created-By: 1.8.0_45-internal (Oracle Corporation)
- fdroidserver.apksigcopier.extract_v2_sig(apkfile: str, expected: bool = True) Optional[Tuple[int, bytes]] ¶
Extract APK Signing Block and offset from APK.
When successful, returns (sb_offset, sig_block); otherwise raises NoAPKSigningBlock when expected is True, else returns None.
>>> import apksigcopier as asc >>> apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" >>> sb_offset, sig_block = asc.extract_v2_sig(apk) >>> sb_offset 8192 >>> len(sig_block) 4096
>>> apk = "test/apks/apks/golden-aligned-in.apk" >>> try: ... asc.extract_v2_sig(apk) ... except asc.NoAPKSigningBlock as e: ... print(e) No APK Signing Block
- fdroidserver.apksigcopier.is_directory(filename: str) bool ¶
ZIP entries with filenames that end with a ‘/’ are directories.
- fdroidserver.apksigcopier.is_meta(filename: str) bool ¶
Check whether filename is a JAR metadata file.
Returns whether filename is a v1 (JAR) signature file (.SF), signature block file (.RSA, .DSA, or .EC), or manifest (MANIFEST.MF).
See https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html
>>> from apksigcopier import is_meta >>> is_meta("classes.dex") False >>> is_meta("META-INF/CERT.SF") True >>> is_meta("META-INF/CERT.RSA") True >>> is_meta("META-INF/MANIFEST.MF") True >>> is_meta("META-INF/OOPS") False
- fdroidserver.apksigcopier.noautoyes(value: Optional[Union[Literal['no', 'auto', 'yes'], bool]]) Literal['no', 'auto', 'yes'] ¶
Turn False into NO, None into AUTO, and True into YES.
>>> from apksigcopier import noautoyes, NO, AUTO, YES >>> noautoyes(False) == NO == noautoyes(NO) True >>> noautoyes(None) == AUTO == noautoyes(AUTO) True >>> noautoyes(True) == YES == noautoyes(YES) True
- fdroidserver.apksigcopier.patch_apk(extracted_meta: Iterable[Tuple[ZipInfo, bytes]], extracted_v2_sig: Optional[Tuple[int, bytes]], unsigned_apk: str, output_apk: str, *, differences: Optional[Dict[str, Any]] = None, exclude: Optional[Callable[[str], bool]] = None) None ¶
Patch extracted_meta + extracted_v2_sig (if not None) onto unsigned_apk and save as output_apk.
- fdroidserver.apksigcopier.patch_meta(extracted_meta: Iterable[Tuple[ZipInfo, bytes]], output_apk: str, date_time: Tuple[int, int, int, int, int, int] = (1980, 0, 0, 0, 0, 0), *, differences: Optional[Dict[str, Any]] = None) None ¶
Add v1 signature metadata to APK (removes v2 sig block, if any).
>>> import apksigcopier as asc >>> unsigned_apk = "test/apks/apks/golden-aligned-in.apk" >>> signed_apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" >>> meta = tuple(asc.extract_meta(signed_apk)) >>> [ x.filename for x, _ in meta ] ['META-INF/RSA-2048.SF', 'META-INF/RSA-2048.RSA', 'META-INF/MANIFEST.MF'] >>> with zipfile.ZipFile(unsigned_apk, "r") as zf: ... infos_in = zf.infolist() >>> with tempfile.TemporaryDirectory() as tmpdir: ... out = os.path.join(tmpdir, "out.apk") ... asc.copy_apk(unsigned_apk, out) ... asc.patch_meta(meta, out) ... with zipfile.ZipFile(out, "r") as zf: ... infos_out = zf.infolist() (2017, 5, 15, 11, 28, 40) >>> for i in infos_in: ... print(i.filename) META-INF/ META-INF/MANIFEST.MF AndroidManifest.xml classes.dex temp.txt lib/armeabi/fake.so resources.arsc temp2.txt >>> for i in infos_out: ... print(i.filename) AndroidManifest.xml classes.dex temp.txt lib/armeabi/fake.so resources.arsc temp2.txt META-INF/RSA-2048.SF META-INF/RSA-2048.RSA META-INF/MANIFEST.MF
- fdroidserver.apksigcopier.patch_v2_sig(extracted_v2_sig: Tuple[int, bytes], output_apk: str) None ¶
Implant extracted v2/v3 signature into APK.
>>> import apksigcopier as asc >>> unsigned_apk = "test/apks/apks/golden-aligned-in.apk" >>> signed_apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" >>> meta = tuple(asc.extract_meta(signed_apk)) >>> v2_sig = asc.extract_v2_sig(signed_apk) >>> with tempfile.TemporaryDirectory() as tmpdir: ... out = os.path.join(tmpdir, "out.apk") ... date_time = asc.copy_apk(unsigned_apk, out) ... asc.patch_meta(meta, out, date_time=date_time) ... asc.extract_v2_sig(out, expected=False) is None ... asc.patch_v2_sig(v2_sig, out) ... asc.extract_v2_sig(out) == v2_sig ... with open(signed_apk, "rb") as a, open(out, "rb") as b: ... a.read() == b.read() True True True
- fdroidserver.apksigcopier.validate_differences(differences: Dict[str, Any]) Optional[str] ¶
Validate differences dict.
Returns None if valid, error otherwise.
- fdroidserver.apksigcopier.zip_data(apkfile: str, count: int = 1024) ZipData ¶
Extract central directory, EOCD, and offsets from ZIP.
Returns ZipData.
>>> import apksigcopier >>> apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" >>> data = apksigcopier.zip_data(apk) >>> data.cd_offset, data.eocd_offset (12288, 12843) >>> len(data.cd_and_eocd) 577