Sign and verify Android APKs — APK Signature Scheme v1, v2, v3, v3.1, v4 and source stamps —
in pure Rust, with the behaviour of Google's apksigner.
A one-to-one port of AOSP platform/tools/apksig, verified against it, not merely inspired by it.
Built and maintained by Rodroid Mods · Telegram channel
Why
apksigner is the reference implementation of Android's APK signing schemes, and it is a
Java tool built on the JCA: it needs a JVM, it needs a keystore provider, and it will not
run in a Rust build pipeline. This is that tool, rewritten in Rust and held to its exact
observable behaviour.
The goal is not "an APK signer that works". It is the same signer: the same options, the same exit codes, the same stdout and stderr, the same signed bytes, and the same failures. Where the port and the reference disagree, it is a bug in the port — and it is measured, corpus-wide, in Verification.
Highlights
- Every signature scheme. v1 (JAR signing), v2, v3, v3.1 (targeted key rotation),
v4 (detached
.idsig), and source stamps — signing and verification. - Every signing algorithm. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA on P-256, P-384 and
P-521, and DSA — including RFC 6979 deterministic DSA (
--deterministic-dsa-signing). - No JVM policy in the way. Two operations Google's tool cannot complete —
SHA1withDSAwith a 2048-bit key below API 21, and--deterministic-dsa-signing— both work here, and the reference verifier accepts the result. See Where the port does more than the reference. - Byte-identical output. Signing with a deterministic key — RSA PKCS#1 v1.5 — produces
exactly the bytes the reference tool produces, the detached
.idsigincluded, because the deflate stream apksig compresses its added v1 entries with is zlib's, and this port's is too. - Key rotation.
rotate,lineage,--lineage,--signer-for-min-sdk-versionand--rotation-min-sdk-version, with v3.1 targeted signing. - Real key sources. JKS and PKCS#12 keystores, encrypted PKCS#8, and PEM/DER key
files — including
pass:,env:,file:andstdinpassword sources. - KMS-backed signing. AWS KMS and Google Cloud KMS signer engines behind cargo
features, plugged in through the same provider registry Java fills with
ServiceLoader. - Streaming. Peak RSS stays around 20 MB while signing a 312 MB APK — no whole-APK buffers.
- No
unsafein the crypto path, no C crypto. Signing and verification run on RustCrypto primitives. The CLI contains exactly oneunsafeblock — a single documentedlibc::signal(SIGPIPE, SIG_DFL)call, so a closed pipe kills the process like any other Unix filter instead of panicking.
Install
The workspace uses the 2024 edition with resolver 3 and tracks the latest stable Rust. The
library is the apksig crate; the CLI is the apksigner crate.
The KMS engines are optional cargo features (kms-aws, kms-gcp), mirroring the
vendor SDK being present on a Java classpath. The default build has neither.
[]
= "1.0" # package name; the import path stays `apksig::…`
To build with a vendor SDK — which reproduces a Java classpath that carries it — enable the corresponding feature:
Quick start
Sign an APK. By default it is signed in place; --out writes elsewhere.
That produces app-signed.apk and app-signed.apk.idsig for the v4 signature.
Verify it, and inspect the signing certificates:
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v4 scheme (APK Signature Scheme v4): true
Rotate a signing key and sign with the resulting lineage:
# 1. Record the rotation: the old key certifies the new one.
# 2. Inspect it, including each signer's capabilities.
# 3. Sign: the old signer covers v1/v2 (and pre-P v3), the new one covers v3.1.
KMS-backed signing — the certificate chain is supplied locally, the key never leaves the KMS:
Supported algorithms
| Key type | v1 | v2 / v3 / v4 | Notes |
|---|---|---|---|
| RSA | ✅ | ✅ | PKCS#1 v1.5 and RSASSA-PSS; digest chosen by modulus size |
| EC P-256 | ✅ | ✅ | ECDSA_WITH_SHA256 |
| EC P-384 | ✅ | ✅ | ECDSA_WITH_SHA512 |
| EC P-521 | ✅ | ✅ | ECDSA_WITH_SHA512 |
| DSA | ✅ | ✅ | SHA256withDSA; SHA-1 below API 21; --deterministic-dsa-signing uses RFC 6979 |
| AWS KMS / Cloud KMS | ✅ | ✅ | v1 reaches the engine through SignerEngineFactory, as Java's does |
Architecture
The Rust module tree mirrors the Java package tree one-to-one, so every file can be traced back to the reference source it ports:
| Rust | Java |
|---|---|
apksig/src/util/ |
com.android.apksig.util — DataSource / DataSink |
apksig/src/zip/, internal/zip/ |
...zip, ...internal.zip |
apksig/src/apk/ |
com.android.apksig.apk |
apksig/src/internal/asn1/, x509/, pkcs7/ |
...internal.asn1, x509, pkcs7 |
apksig/src/internal/apk/{v1,v2,v3,v4,stamp}/ |
...internal.apk.{v1,v2,v3,v4,stamp} |
apksig/src/kms/ |
com.android.apksig.kms |
apksig/src/{apk_signer,apk_verifier,key_config,...}.rs |
the top-level com.android.apksig classes |
The AOSP reference sources are not vendored; scripts/fetch-reference.sh fetches them into
target/apksig-reference (gitignored), and the port is checked against
them by scripts/compare-with-reference-oracle.sh.
Library
The CLI is a thin wrapper over the library. The package is apksig-rs (the crates.io name
apksig belongs to an unrelated project); its library target is apksig, so the import
paths below are unchanged.
use ;
use Builder as ApkVerifierBuilder;
use PrivateKey;
let private_key = Rsa;
let signer_config = from_private_key?.build;
let mut signer = new?
.set_input_apk_file
.set_output_apk_file
.set_min_sdk_version?
.set_v4_signing_enabled?
.set_v4_signature_output_file
.build?;
signer.sign?;
// The v4 signature lives beside the APK, so verification is handed it explicitly.
let result = from_file
.set_min_checked_platform_version
.set_v4_signature_file
.build
.verify?;
assert!;
This snippet is a compiled, runnable example:
# verified: v1=false v2=true v3=true v4=true
More runnable examples live in apksig/examples/:
| Example | Shows |
|---|---|
sign_and_verify |
Sign with v1 + v2 + v3 + v4, then verify the APK and its detached .idsig |
verify_apk |
Verify an APK and print schemes, signer certificates and issues |
sign_in_memory |
Sign to a Vec<u8> with explicit scheme switches, then verify the bytes |
sign_with_keystore |
Load a JKS or PKCS #12 keystore and sign with its key |
key_rotation |
Build a signing lineage and sign with the old and the new key |
Performance
Signing streams. Nothing in the pipeline materialises the APK, so peak RSS is flat in the
size of the input — Java reaches the same property with DataSource.slice views, and the
port now does too.
Peak RSS while signing a 312 MB APK, VmHWM from /proc/<pid>/status for a release build
(Before is the pre-slice build it replaced):
| Schemes enabled | Before | Now |
|---|---|---|
| v1 | 313 MB | 16.0 MB |
| v2 + v3 | 903 MB | 18.1 MB |
| v1 + v2 + v3 + v4 | 908 MB | 21.2 MB |
| v1 + v2 + v3 + verity | 613 MB | 18.9 MB |
Verification
The port is not "tested"; it is differentially compared against two oracles on every change.
| Oracle | Compared | Identical or agreed | Exit-status differences |
|---|---|---|---|
| Built from the vendored reference sources | 376 APK fixtures, verify |
368 | 0 |
Same, verify --verbose |
376 fixtures | 368 | 0 |
Same, detached .idsig |
2 fixtures | 2 | 0 |
Same, sign — RSA, deterministic |
32 signed APKs, .idsig included |
32 | 0 |
Same, sign — EC / DSA, random k |
64 signed APKs, cross-verified | 64 | 0 |
Shipped apksigner (build-tools 34) |
364 comparable fixtures | 361 | 0 |
The signing differential is 6 keys × 4 scheme sets (v1, v1+v2+v3, v1+v2+v3+v4,
v2+v3+v4) × 4 inputs, run through both tools with the same options. RSA PKCS#1 v1.5 is
deterministic, so those 32 cases must be — and are — byte-identical, .idsig and all. ECDSA
and DSA draw a fresh nonce per signature, so no two runs agree byte for byte, in either
implementation; those 64 cases are cross-verified instead: each tool's APK, and each .idsig,
must verify under the other tool, with the same verdict text.
The 8 remaining text differences are the fixtures where the reference tool prints a JVM
stack trace for an uncaught exception. The port reproduces the trace, frame for frame;
only the File.java:line line numbers differ, because the reference numbers them per
build and the port's table follows build-tools 34. No single set of line numbers can match
two different builds, so this difference is recorded rather than papered over.
Signed output is compared case by case against the oracle (see the table above) and
cross-checked in both directions on a real 11.9 MB APK: this port signs it and the Java
oracle verifies the APK and its .idsig; the oracle signs it and this port verifies that.
P-384 and P-521 v4 signatures verify under the oracle, and P-521 v1 signatures verify under
jarsigner.
DSA is cross-checked the same way: the oracle verifies APKs this port signs with a
2048-bit DSA key (v1+v2+v3, the v1 SHA-256 path, and v4 with its .idsig), and this port
verifies APKs the oracle signs. verify piped into a reader that exits first dies on
SIGPIPE with empty stderr instead of panicking.
Full results, per-fixture notes and the behaviour that had to be discovered empirically are in PORT_STATUS.md.
Where the port does more than the reference
Two operations fail in Google's tool because of the JVM it runs on, not because of apksig's logic. The port implements the same logic but performs the cryptography itself, so both complete — and the reference verifier accepts the output in each case.
SHA1withDSAwith a 2048-bit key below API 21.V1SchemeSigner.getSuggestedSignatureDigestAlgorithmdeliberately selects SHA-1 for a DSA key when the APK'sminSdkVersionis below 21, and the port follows that rule exactly. A modern JDK refuses to sign the combination — the JCA policy throwsInvalidKeyException: The security strength of SHA-1 digest algorithm is not sufficient for this key size— so Google'sapksigneraborts and the developer cannot sign at all, even though Android's package manager would accept the result. The port has no such policy layer: it performs the signature and emits an APK that the reference verifier accepts (dsa_signs_v1_with_sha1_below_api_21). That is a capability the reference tool does not have, not a divergence from its behaviour.- Deterministic DSA (
--deterministic-dsa-signing true). The flag asks for the JCA nameSHA256withDetDSA, which Android's Conscrypt registers and stock OpenJDK does not, so the reference oracle aborts withNoSuchAlgorithmException: SHA256withDetDSA Signature not available. The port implements RFC 6979 itself (HMAC-SHA-256, no provider lookup), so the flag works on every host; the reference verifier accepts the result, because determinism changes only howkis derived.
Known limitations
- The KMS engines are not exercised end to end here: that needs real cloud credentials. The registry, the providers and the CLI plumbing are in place, the lookup is reached, and both v1 and v2/v3/v4 signing with a KMS key are covered by tests against a registered stub provider; the network calls to AWS KMS and Cloud KMS are not.
Implementation notes
Worth knowing, but not constraints on what the port can do:
- Byte-identical signing depends on zlib — deliberately. apksig compresses the entries it
adds while signing with the JDK's
new Deflater(9, true)— raw deflate at level 9 — and the JDK's Deflater is zlib. This workspace links the same library:flate2is built with itszlibfeature, i.e.libz-sys, which uses the system zlib when there is one and its bundled copy otherwise. Matching zlib is what keeps the compressed bytes — and therefore the digests, offsets and signatures — identical;deflate_produces_zlib_bytespins it. Because that is the host's zlib, the compressed size can in principle vary between zlib versions; 1.2.x and 1.3.x produce the same level-9 output for these inputs. File.java:linecoordinates in reconstructed stack traces follow build-tools 34; see Verification.- A source stamp with a DSA stamp key fails in both tools, with different wording.
apksig hard-codes
RSA_PKCS1_V1_5_WITH_SHA256for that scheme; the verdict matches, the diagnostic text does not.
Project layout
apksig/ the library (mirrors com.android.apksig)
src/kms/ AWS and Google Cloud KMS signer engines
examples/ runnable library examples (see examples/README.md)
apksigner/ the CLI (mirrors com.android.apksigner)
scripts/fetch-reference.sh fetches the AOSP sources the differentials compare against
scripts/ the oracle build and the differential runs (verify and sign)
docs/branding/ the SVG assets used by this README
PORT_STATUS.md verification results, deviations, empirical findings
Developing
Publishing is two crates, in this order (the CLI depends on the library by version):
The reference sources are the specification. When changing behaviour, run both differentials
against the oracle built from them: compare-with-reference-oracle.sh for verify — the only
check that covers RSA-PSS and the v4 .idsig path, which the shipped build-tools 34 jar cannot
verify — and compare-signing-with-oracle.sh for sign.
Credits
Made by Rodroid Mods — updates, builds and support: t.me/+WmudnO0-xoNhMDQ8.
The signature schemes, their edge cases and the reference behaviour are Google's; see NOTICE for the AOSP attribution.
License
Apache License 2.0, the same license as the AOSP sources this ports.