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.APKSigCopierError

Bases: Exception

Base class for errors.

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
fdroidserver.apksigcopier.zipflinger_virtual_entry(size: int) bytes

Create zipflinger virtual entry.