{"id":17160208,"url":"https://github.com/obfusk/apksigcopier","last_synced_at":"2025-05-15T18:03:34.817Z","repository":{"id":49559971,"uuid":"351271708","full_name":"obfusk/apksigcopier","owner":"obfusk","description":"apksigcopier - copy/extract/patch android apk signatures \u0026 compare apks","archived":false,"fork":false,"pushed_at":"2025-01-11T02:50:15.000Z","size":1116,"stargazers_count":219,"open_issues_count":25,"forks_count":32,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-01T03:24:23.325Z","etag":null,"topics":["android","apk","compare","f-droid","reproducible","reproducible-builds","signing"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/obfusk.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.txt","contributing":null,"funding":null,"license":"LICENSE.GPLv3","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-25T01:20:33.000Z","updated_at":"2025-03-28T21:17:03.000Z","dependencies_parsed_at":"2023-12-16T19:21:51.155Z","dependency_job_id":"f9a08e1c-e769-4f28-9e56-f8c9f33d02b2","html_url":"https://github.com/obfusk/apksigcopier","commit_stats":{"total_commits":146,"total_committers":2,"mean_commits":73.0,"dds":0.006849315068493178,"last_synced_commit":"bc08f220f8c4b5e429b99f3263a56c68f1cda4e9"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obfusk%2Fapksigcopier","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obfusk%2Fapksigcopier/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obfusk%2Fapksigcopier/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obfusk%2Fapksigcopier/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/obfusk","download_url":"https://codeload.github.com/obfusk/apksigcopier/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247773711,"owners_count":20993634,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","apk","compare","f-droid","reproducible","reproducible-builds","signing"],"created_at":"2024-10-14T22:24:03.338Z","updated_at":"2025-04-08T04:09:58.655Z","avatar_url":"https://github.com/obfusk.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!-- SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman \u003cflx@obfusk.net\u003e --\u003e\n\u003c!-- SPDX-License-Identifier: GPL-3.0-or-later --\u003e\n\n[![GitHub Release](https://img.shields.io/github/release/obfusk/apksigcopier.svg?logo=github)](https://github.com/obfusk/apksigcopier/releases)\n[![PyPI Version](https://img.shields.io/pypi/v/apksigcopier.svg)](https://pypi.python.org/pypi/apksigcopier)\n[![Python Versions](https://img.shields.io/pypi/pyversions/apksigcopier.svg)](https://pypi.python.org/pypi/apksigcopier)\n[![CI](https://github.com/obfusk/apksigcopier/workflows/CI/badge.svg)](https://github.com/obfusk/apksigcopier/actions?query=workflow%3ACI)\n[![GPLv3+](https://img.shields.io/badge/license-GPLv3+-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.html)\n\n\u003ca href=\"https://repology.org/project/apksigcopier/versions\"\u003e\n  \u003cimg src=\"https://repology.org/badge/vertical-allrepos/apksigcopier.svg?header=\"\n    alt=\"Packaging status\" align=\"right\" /\u003e\n\u003c/a\u003e\n\n\u003ca href=\"https://repology.org/project/python:apksigcopier/versions\"\u003e\n  \u003cimg src=\"https://repology.org/badge/vertical-allrepos/python:apksigcopier.svg?header=\"\n    alt=\"Packaging status\" align=\"right\" /\u003e\n\u003c/a\u003e\n\n# apksigcopier\n\n## copy/extract/patch android apk signatures \u0026 compare apks\n\n`apksigcopier` is a tool that enables using an [android APK\nsignature](https://source.android.com/docs/security/features/apksigning) as a\n[build input](https://reproducible-builds.org/docs/embedded-signatures/) (by\ncopying it from a signed APK to an unsigned one), making it possible to create a\n([bit-by-bit identical](https://reproducible-builds.org/docs/definition/))\n[reproducible build](https://reproducible-builds.org/) from the source code\nwithout having access to the private key used to create the signature.  It can\nalso be used to verify that two APKs with different signatures are otherwise\nidentical.  Its command-line tool offers four operations:\n\n* copy signatures directly from a signed to an unsigned APK\n* extract signatures from a signed APK to a directory\n* patch previously extracted signatures onto an unsigned APK\n* compare two APKs with different signatures\n\n### Extract\n\n```bash\n$ mkdir meta\n$ apksigcopier extract signed.apk meta\n$ ls -1 meta\n8BEA2A77.RSA\n8BEA2A77.SF\nAPKSigningBlock\nAPKSigningBlockOffset\nMANIFEST.MF\n```\n\n### Patch\n\n```bash\n$ apksigcopier patch meta unsigned.apk out.apk\n```\n\n### Copy (Extract \u0026 Patch)\n\n```bash\n$ apksigcopier copy signed.apk unsigned.apk out.apk\n```\n\n### Compare (Copy \u0026 Verify)\n\nCompare two APKs by copying the signature from the first to a copy of the second\nand checking if the resulting APK verifies.\n\nThis command requires `apksigner`.\n\n```bash\n$ apksigcopier compare foo-from-fdroid.apk foo-built-locally.apk\n$ apksigcopier compare --unsigned foo.apk foo-unsigned.apk\n```\n\nNB: copying from an APK v1-signed with `signflinger` to an APK signed with\n`apksigner` works, whereas the reverse fails; see the [FAQ](#faq).\n\n### Help\n\n```bash\n$ apksigcopier --help\n$ apksigcopier copy --help      # extract --help, patch --help, etc.\n\n$ man apksigcopier              # requires the man page to be installed\n```\n\n### Environment Variables\n\nThe following environment variables can be set to `1`, `yes`, or\n`true` to override the default behaviour:\n\n* set `APKSIGCOPIER_EXCLUDE_ALL_META=1` to exclude all metadata files\n* set `APKSIGCOPIER_COPY_EXTRA_BYTES=1` to copy extra bytes after data (e.g. a v2 sig)\n* set `APKSIGCOPIER_SKIP_REALIGNMENT=1` to skip realignment of ZIP entries\n\n## Python API\n\n```python\n\u003e\u003e\u003e from apksigcopier import do_extract, do_patch, do_copy, do_compare\n\u003e\u003e\u003e do_extract(signed_apk, output_dir, v1_only=NO)\n\u003e\u003e\u003e do_patch(metadata_dir, unsigned_apk, output_apk, v1_only=NO)\n\u003e\u003e\u003e do_copy(signed_apk, unsigned_apk, output_apk, v1_only=NO)\n\u003e\u003e\u003e do_compare(first_apk, second_apk, unsigned=False)\n```\n\nYou can use `False`, `None`, and `True` instead of `NO`, `AUTO`, and\n`YES` respectively.\n\nThe following global variables (which default to `False`), can be set\nto override the default behaviour:\n\n* set `exclude_all_meta=True` to exclude all metadata files\n* set `copy_extra_bytes=True` to copy extra bytes after data (e.g. a v2 sig)\n* set `skip_realignment=True` to skip realignment of ZIP entries\n\n## FAQ\n\n### What is the purpose of this tool?\n\nThis is a tool for [*reproducible builds*](https://reproducible-builds.org/)\nonly.  Its purpose is to allow verifying that *different builds* from the same\nsource code produce identical results, to prove that two APKs -- one built and\nsigned by the upstream developer, another one built by you (or some trusted\nthird party) from the published source code -- are *identical*.  Since you\ncannot create an identical signature without the private key, you need to copy\nit (and nothing else) as part of the build process instead to be able to create\na bit-by-bit identical APK.\n\n\u003e The motivation behind the Reproducible Builds project is [...] to allow\n\u003e verification that no vulnerabilities or backdoors have been introduced during\n\u003e this compilation process. By promising identical results are always generated\n\u003e from a given source, this allows multiple third parties to come to a consensus\n\u003e on a “correct” result, highlighting any deviations as suspect and worthy of\n\u003e scrutiny.\n\n#### Modified APKs\n\nCopying a signature to a modified APK will not work (i.e. it cannot possibly be\nvalid even if the copying itself seems to work) and this is not a tool for doing\nanything of the sort.\n\nCopying a signature will succeed even if the signature is not valid for the\ntarget APK -- as long as the target APK is unsigned and not larger than the\nsource APK it can be inserted successfully.  But a signature that is not valid\nfor the target APK will never verify.\n\n### What kind of signatures does apksigcopier support?\n\nIt currently supports v1 + v2 + v3 (which is a variant of v2).\n\nIt should also support v4, since these are stored in a separate file\n(and require a complementary v2/v3 signature).\n\nWhen using the `extract` command, the v2/v3 signature is saved as\n`APKSigningBlock` + `APKSigningBlockOffset`.\n\n### How does patching work?\n\nFirst it copies the APK exactly like `apksigner` would when signing it,\nincluding re-aligning ZIP entries and skipping existing v1 signature files.\n\nThen it adds the extracted v1 signature files (`.SF`, `.RSA`/`.DSA`/`.EC`,\n`MANIFEST.MF`) to the APK, using the correct ZIP metadata (either the same\nmetadata as `apksigner` would, or from `differences.json`).\n\nAnd lastly it inserts the extracted APK Signing Block at the correct offset\n(adding zero padding if needed) and updates the central directory (CD) offset in\nthe end of central directory (EOCD) record.\n\nFor more information about the ZIP file format, see e.g. [the Wikipedia\narticle](https://en.wikipedia.org/wiki/ZIP_%28file_format%29).\n\n### What does the \"APK Signing Block offset \u003c central directory offset\" error mean?\n\nIt means that `apksigcopier` can't insert the APK Signing Block at the required\nlocation, since that offset is in the middle of the ZIP data (instead of right\nafter the data, before the central directory).\n\nIn other words: the APK you are trying to copy the signature to is larger than\nthe one the signature was copied from.  Thus the signature cannot be copied (and\ncould never have been valid for the APK you are trying to copy it to).\n\nIn the context of verifying [reproducible builds](https://reproducible-builds.org),\ngetting this error almost certainly means the build was not reproducible.\n\n### What does the \"Unexpected metadata\" error mean?\n\nIt almost always means the target APK was signed; you can only copy a signature\nto an unsigned APK.\n\n### What about signatures made by apksigner from build-tools \u003e= 35.0.0-rc1?\n\nSince `build-tools` \u003e= 35.0.0-rc1, backwards-incompatible changes to `apksigner`\nbreak `apksigcopier` as it now by default forcibly replaces existing alignment\npadding and changed the default page alignment from 4k to 16k (same as Android\nGradle Plugin \u003e= 8.3, so the latter is only an issue when using older AGP).\n\nUnlike `zipalign` and Android Gradle Plugin, which use zero padding, `apksigner`\nuses a `0xd935` \"Android ZIP Alignment Extra Field\" which stores the alignment\nitself plus zero padding and is thus always at least 6 bytes.\n\nIt now forcibly replaces existing padding even when the file is already aligned\nas it should be, except when `--alignment-preserved` is specified, in which case\nit will keep existing (non)alignment and padding.\n\nThis means it will replace existing zero padding with different padding for each\nand every non-compressed file.  This padding will not only be different but also\nlonger for regular files aligned to 4 bytes with zero padding, but often the\nsame size for `.so` shared objects aligned to 16k (unless they happened to\nrequire less than 6 bytes of zero padding before).\n\nUnfortunately, supporting this change in `apksigcopier` without breaking\ncompatibility with the signatures currently supported would require rather\nsignificant changes.  Luckily, there are 3 workarounds available:\n\nFirst: use `apksigner` from `build-tools` \u003c= 34.0.0 (clearly not ideal).\n\nSecond: use `apksigner sign` from `build-tools` \u003e= 35.0.0-rc1 with the\n`--alignment-preserved` option.\n\nThird: use [`zipalign.py --page-size 16 --pad-like-apksigner\n--replace`](https://github.com/obfusk/reproducible-apk-tools#zipalignpy) on the\nunsigned APK to replace the padding the same way `apksigner` now does before\nusing `apksigcopier`.\n\n### What about APKs signed by gradle/zipflinger/signflinger instead of apksigner?\n\nCompared to APKs signed by `apksigner`, APKs signed with a v1 signature by\n`zipflinger`/`signflinger` (e.g. using `gradle`) have different ZIP metadata --\n`create_system`, `create_version`, `external_attr`, `extract_version`,\n`flag_bits` -- and `compresslevel` for the v1 signature files (`.SF`,\n`.RSA`/`.DSA`/`.EC`, `MANIFEST.MF`); they also usually have a 132-byte virtual\nentry at the start as well.\n\nRecent versions of `apksigcopier` will detect these ZIP metadata differences and\nthe virtual entry (if any); `extract` will save them in a `differences.json`\nfile (if they exist), which `patch` will read (if it exists); `copy` and\n`compare` simply pass the same information along internally.\n\n#### CAVEAT for compare\n\nNB: because `compare` copies from the first APK to the second, it will fail when\nonly the second APK is v1-signed with `zipflinger`/`signflinger`; e.g.\n\n```bash\n$ compare foo-signflinger.apk foo-apksigner.apk   # copies virtual entry; works\n$ compare foo-apksigner.apk foo-signflinger.apk   # only 2nd APK has virtual entry\nDOES NOT VERIFY\n[...]\nError: failed to verify /tmp/.../output.apk.\n```\n\n### What are these virtual entries?\n\nA virtual entry is a ZIP entry with an empty filename, an extra field filled\nwith zero bytes, and no corresponding central directory entry (so it should be\neffectively invisible to most ZIP tools).\n\nWhen `zipflinger` deletes an entry it leaves a \"hole\" in the archive when there\nremain non-deleted entries after it.  It later fills these \"holes\" with virtual\nentries.\n\nThere is usually a 132-byte virtual entry at the start of an APK signed with a\nv1 signature by `signflinger`/`zipflinger`; almost certainly this is a default\nmanifest ZIP entry created at initialisation, deleted (from the central\ndirectory but not from the file) during v1 signing, and eventually replaced by a\nvirtual entry.\n\nDepending on what value of `Created-By` and `Built-By` were used for the default\nmanifest, this virtual entry may be a different size; `apksigcopier` supports\nany size between 30 and 4096 bytes.\n\n\u003c!--\n## Tab Completion\n\nNB: the syntax for the environment variable changed in click \u003e= 8.0,\nuse e.g. `source_bash` instead of `bash_source` for older versions.\n\nFor Bash, add this to `~/.bashrc`:\n\n```bash\neval \"$(_APKSIGCOPIER_COMPLETE=bash_source apksigcopier)\"\n```\n\nFor Zsh, add this to `~/.zshrc`:\n\n```zsh\neval \"$(_APKSIGCOPIER_COMPLETE=zsh_source apksigcopier)\"\n```\n\nFor Fish, add this to `~/.config/fish/completions/apksigcopier.fish`:\n\n```fish\neval (env _APKSIGCOPIER_COMPLETE=fish_source apksigcopier)\n```\n--\u003e\n\n## Installing\n\n### Debian\n\nOfficial packages are available in\n[Debian](https://packages.debian.org/apksigcopier) and\n[Ubuntu](https://packages.ubuntu.com/apksigcopier).\n\n```bash\n$ apt install apksigcopier\n```\n\nYou can also manually build a Debian package using the `debian/sid`\nbranch, or download a pre-built `.deb` via GitHub releases.\n\n### NixOS \u0026 Arch Linux\n\nOfficial packages are also available in\n[nixpkgs](https://search.nixos.org/packages?query=apksigcopier) and\n[Arch Linux](https://archlinux.org/packages/community/any/apksigcopier/)\n(and derivatives).\n\n### Using pip\n\n```bash\n$ pip install apksigcopier\n```\n\nNB: depending on your system you may need to use e.g. `pip3 --user`\ninstead of just `pip`.\n\n### From git\n\nNB: this installs the latest development version, not the latest\nrelease.\n\n```bash\n$ git clone https://github.com/obfusk/apksigcopier.git\n$ cd apksigcopier\n$ pip install -e .\n```\n\nNB: you may need to add e.g. `~/.local/bin` to your `$PATH` in order\nto run `apksigcopier`.\n\nTo update to the latest development version:\n\n```bash\n$ cd apksigcopier\n$ git pull --rebase\n```\n\n## Dependencies\n\n* Python \u003e= 3.7 + click.\n* The `compare` command also requires `apksigner`.\n\n### Debian/Ubuntu\n\n```bash\n$ apt install python3-click\n$ apt install apksigner         # only needed for the compare command\n```\n\n## License\n\n[![GPLv3+](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.html)\n\n\u003c!-- vim: set tw=70 sw=2 sts=2 et fdm=marker : --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobfusk%2Fapksigcopier","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fobfusk%2Fapksigcopier","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobfusk%2Fapksigcopier/lists"}