Fast Channel Package Generation for Android APK Signature Scheme v2
By exploiting the unprotected portion of Android’s APK Signature Scheme v2 signing block, the article introduces a fast, secure channel‑package generation technique—implemented in the open‑source Walle tool—that embeds custom IDs without re‑signing, enabling thousands of channels to be added in milliseconds per APK.
When Android 7.0 (Nougat) introduced the new APK Signature Scheme v2, the previously used fast channel‑package generation method (the Meituan "Android Automation – Generating Channel Packages" article) stopped working. This article provides a detailed analysis of the new signing scheme and proposes a new, efficient way to generate channel packages that is compatible with v2.
APK Signature Scheme v2 signs the whole APK file, offering faster installation and stronger protection against tampering. By default, the Android Gradle plugin 2.2.0 and later uses both v2 and the traditional signing scheme.
To disable v2 signing (for testing), you can set v2SigningEnabled false in build.gradle:
android {
...
defaultConfig { ... }
signingConfigs {
release {
storeFile file("myreleasekey.keystore")
storePassword "password"
keyAlias "MyReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}
}The v2 scheme divides an APK (ZIP format) into four blocks: the contents of ZIP entries, the APK Signing Block, the ZIP Central Directory, and the ZIP End of Central Directory. The Signing Block (block 2) is the only part not protected by the signature verification, making it a potential place to store custom data.
Each ID‑value in the Signing Block consists of an 8‑byte length, a 4‑byte ID, and the payload. The official v2 signature uses the ID 0x7109871a. Because the verification code only looks for this specific ID, other ID‑values are ignored, allowing us to embed custom information (e.g., channel identifiers) without breaking the signature.
The relevant verification code in Android’s source is:
public static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock, Result result) throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock); // FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes pairs
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException("Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
throw new SignatureNotFoundException("APK Signing Block entry #" + entryCount + " size out of range: " + lenLong);
}
int len = (int) lenLong;
int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) {
throw new SignatureNotFoundException("APK Signing Block entry #" + entryCount + " size out of range: " + len + ", available: " + pairs.remaining());
}
int id = pairs.getInt();
if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
return getByteBuffer(pairs, len - 4);
}
result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
pairs.position(nextEntryPos);
}
throw new SignatureNotFoundException("No APK Signature Scheme v2 block in APK Signing Block");
}Based on this observation, we can add a custom ID‑value to the Signing Block. The following code demonstrates how to write such a block without re‑signing the APK:
public void writeApkSigningBlock(DataOutput dataOutput) {
long length = 24;
for (int index = 0; index < payloads.size(); ++index) {
ApkSigningPayload payload = payloads.get(index);
byte[] bytes = payload.getByteBuffer();
length += 12 + bytes.length;
}
ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.putLong(length);
dataOutput.write(byteBuffer.array());
for (int index = 0; index < payloads.size(); ++index) {
ApkSigningPayload payload = payloads.get(index);
byte[] bytes = payload.getByteBuffer();
byteBuffer = ByteBuffer.allocate(Integer.BYTES);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.putInt(payload.getId());
dataOutput.write(byteBuffer.array());
dataOutput.write(bytes);
}
...
}Using this technique, the open‑source tool Walle (https://github.com/Meituan-Dianping/walle) was created. Walle provides:
A Java library to write custom ID‑value information into an APK.
A Gradle plugin that integrates the process into the Android build pipeline.
A Java library to read the ID‑value at runtime.
An AAR that exposes an API for com.android.application modules to retrieve channel information.
Because the custom data resides in the unprotected Signing Block, adding a channel ID takes only a file copy plus a small binary write – about 100 ms for a 30 MB APK. Retrieving the channel at runtime takes only a few milliseconds.
The article concludes that the new channel‑package generation method satisfies the security requirements of APK Signature Scheme v2 while dramatically reducing packaging time, making it suitable for large‑scale channel deployments (e.g., 900 channels in under three hours).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Meituan Technology Team
Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
