apksigner 1.0.0

APK signer and verifier CLI — a faithful Rust port of AOSP apksigner
apksigner-1.0.0 is not a library.

tests oracle differential peak RSS rust license

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 — SHA1withDSA with 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 .idsig included, 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-version and --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: and stdin password 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 unsafe in the crypto path, no C crypto. Signing and verification run on RustCrypto primitives. The CLI contains exactly one unsafe block — a single documented libc::signal(SIGPIPE, SIG_DFL) call, so a closed pipe kills the process like any other Unix filter instead of panicking.

Install

git clone https://github.com/rodroidmods/apksigner-rs
cd apksigner-rs
cargo build --release
./target/release/apksigner --version

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.

[dependencies]
apksig-rs = "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:

cargo build --release                       # no KMS engines
cargo build --release -p apksig-rs --features kms-aws
cargo build --release -p apksig-rs --features kms-gcp

Quick start

Sign an APK. By default it is signed in place; --out writes elsewhere.

./target/release/apksigner sign \
  --ks release.jks --ks-pass pass:hunter2 --ks-key-alias release \
  --min-sdk-version 24 --v4-signing-enabled true \
  --out app-signed.apk app.apk

That produces app-signed.apk and app-signed.apk.idsig for the v4 signature.

Verify it, and inspect the signing certificates:

./target/release/apksigner verify --verbose --min-sdk-version 24 app-signed.apk
./target/release/apksigner verify --print-certs app-signed.apk
./target/release/apksigner verify --min-sdk-version 24 \
  --v4-signature-file app-signed.apk.idsig app-signed.apk
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.
./target/release/apksigner rotate --out lineage.bin \
  --old-signer --ks old.jks --ks-pass pass:... --ks-key-alias old \
  --new-signer --ks new.jks --ks-pass pass:... --ks-key-alias new

# 2. Inspect it, including each signer's capabilities.
./target/release/apksigner lineage --in lineage.bin --print-certs

# 3. Sign: the old signer covers v1/v2 (and pre-P v3), the new one covers v3.1.
./target/release/apksigner sign --ks old.jks --ks-pass pass:... --ks-key-alias old \
  --lineage lineage.bin --min-sdk-version 24 \
  --next-signer --ks new.jks --ks-pass pass:... --ks-key-alias new \
  --signer-for-min-sdk-version 28 \
  --out app-signed.apk app.apk

KMS-backed signing — the certificate chain is supplied locally, the key never leaves the KMS:

./target/release/apksigner sign --kms-type AWS --kms-key-alias alias/for/the/key \
  --cert signer.pem --out app-signed.apk app.apk

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 apksig::apk_signer::{ApkSignerBuilder, SignerConfigBuilder};
use apksig::apk_verifier::Builder as ApkVerifierBuilder;
use apksig::key_config::PrivateKey;

let private_key = PrivateKey::Rsa(Box::new(rsa::RsaPrivateKey::from_pkcs8_der(&key_der)?));

let signer_config = SignerConfigBuilder::from_private_key(
    "example", private_key, vec![cert_der], /* deterministic_dsa_signing = */ false,
)?.build();

let mut signer = ApkSignerBuilder::new(vec![signer_config])?
    .set_input_apk_file("app.apk")
    .set_output_apk_file("app-signed.apk")
    .set_min_sdk_version(24)?
    .set_v4_signing_enabled(true)?
    .set_v4_signature_output_file("app-signed.apk.idsig")
    .build()?;
signer.sign()?;

// The v4 signature lives beside the APK, so verification is handed it explicitly.
let result = ApkVerifierBuilder::from_file("app-signed.apk")
    .set_min_checked_platform_version(24)
    .set_v4_signature_file(Some(std::fs::read("app-signed.apk.idsig")?))
    .build()
    .verify()?;
assert!(result.is_verified() && result.is_verified_using_v3_scheme());

This snippet is a compiled, runnable example:

cargo run -p apksig-rs --example sign_and_verify -- key.pk8 cert.pem app.apk app-signed.apk
# 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
cargo run -p apksig-rs --example verify_apk -- --min-sdk 24 app.apk app.apk.idsig

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.

cargo test --workspace                        # 498 tests, 0 failures
cargo test --workspace --all-features         # 513 tests, 0 failures (adds the KMS backends)
scripts/build-reference-oracle.sh             # build the Java oracle
scripts/compare-with-reference-oracle.sh      # diff every fixture (verify)
scripts/compare-signing-with-oracle.sh        # diff signed output (sign)
scripts/compare-with-oracle.sh                # diff against build-tools 34

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.

  • SHA1withDSA with a 2048-bit key below API 21. V1SchemeSigner.getSuggestedSignatureDigestAlgorithm deliberately selects SHA-1 for a DSA key when the APK's minSdkVersion is below 21, and the port follows that rule exactly. A modern JDK refuses to sign the combination — the JCA policy throws InvalidKeyException: The security strength of SHA-1 digest algorithm is not sufficient for this key size — so Google's apksigner aborts 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 name SHA256withDetDSA, which Android's Conscrypt registers and stock OpenJDK does not, so the reference oracle aborts with NoSuchAlgorithmException: 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 how k is 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: flate2 is built with its zlib feature, 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_bytes pins 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:line coordinates 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_SHA256 for 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

cargo build --workspace            # 0 warnings
cargo test --workspace             # 498 tests (513 with --all-features)

Publishing is two crates, in this order (the CLI depends on the library by version):

cargo publish -p apksig-rs     # library; the crates.io name `apksig` is taken by another project
cargo publish -p apksigner     # CLI

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.