A 30-minute release build ends with a file. You upload it to Play Console and, a few minutes later, you read something like this:

Your Android App Bundle is signed with the wrong key.

That sentence is how Play users report the error (the full message differs between threads, and Google's help pages don't quote it). Other things Play can reject or warn about after the upload: a target API level that's too low, native libraries that don't support 16 KB memory pages, a versionCode you already used. Each of them can be checked on your PC in seconds, before you upload. Here is what to check, how to do it by hand on Windows, and what WinBuild's verify does with the same checks.

What to check, and why

  • Target SDK. For 2026, Google Play requires new apps and app updates to target Android 16 (API level 36) or higher, with a deadline of 31 August 2026.
  • 16 KB memory pages. All apps targeting Android 15 (API level 35) and higher must support 16 KB memory page sizes on 64-bit devices. From 1 February 2027, updates that don't support it can't be released. For a React Native app the risk is in the native libraries (.so files) inside the bundle.
  • The signing certificate. An AAB is signed with your upload key, the key you use before uploading for Play App Signing. Google's page says the SHA-256 fingerprints of the upload and app signing certificates are on the app signing page of Play Console. A debug certificate or the wrong keystore can't match.
  • A bundle that isn't tampered with or half-signed. Not a Play rule, but it's what makes the checks above trustworthy.

The manual recipe

You need a JDK 17 (keytool, jarsigner), bundletool and, for the 16 KB check, llvm-objdump from the Android NDK. Examples use a file called app-release.aab.

1. Target SDK and version code

java -jar bundletool.jar dump manifest --bundle=app-release.aab --xpath=/manifest/uses-sdk/@android:targetSdkVersion
java -jar bundletool.jar dump manifest --bundle=app-release.aab --xpath=/manifest/@android:versionCode

The dump manifest command prints the bundle's manifest, and --xpath limits the output to one part of it. The target must be 36 or higher for a 2026 upload; the versionCode must be higher than any you've uploaded before for the same package. Play Console's release history shows the highest one.

2. 16 KB alignment of the native libraries

An AAB is a ZIP file. Copy it to app.zip, extract it, and look at the 64-bit libraries under base\lib\arm64-v8a and base\lib\x86_64:

Copy-Item app-release.aab app.zip
Expand-Archive app.zip -DestinationPath aab-contents
Get-ChildItem aab-contents\base\lib\arm64-v8a\*.so | ForEach-Object {
  Write-Host $_.Name
  & llvm-objdump.exe -p $_.FullName | Select-String LOAD
}

In the output, every LOAD line must show align 2**14 or higher. Google's page says to look for segments with 2**13, 2**12 or lower: those libraries need repackaging and a rebuild. Repeat for x86_64. A line that passes looks like this:

LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**14

Google also ships a check_elf_alignment.sh script, but it's for Linux and macOS.

3. The certificate

keytool -printcert -jarfile app-release.aab

keytool -printcert can read the certificate from a signed JAR file with -jarfile, and an AAB is a signed JAR. Check three things in the output:

  • the owner isn't CN=Android Debug;
  • the SHA-256 fingerprint is the one of your upload key;
  • that fingerprint is the one you expect in Play Console. If you've never looked it up, compute it from the keystore with keytool -list -v -keystore upload.jks -alias upload and compare.

4. The signature covers every file

jarsigner -verify -verbose -certs app-release.aab

A good run ends with jar verified.. But don't stop at that line. In each entry line, s means the signature was verified and m means the entry is listed in the manifest. On 20 October 2026 we took a signed jar with the JBR from Android Studio, added a file with jar uf, and jarsigner still printed jar verified. with exit code 0. The added entry was marked ?, and the output ended with a warning that the jar contains unsigned entries that haven't been integrity-checked. Oracle's documentation lists that warning too. So scroll the list: any entry without s and m, and any warning about unsigned entries, means the file isn't what was signed.

What this doesn't cover

Everything above is local. None of it tells you that Play will accept the bundle. Whether the upload key matches the one registered in Play Console, whether the versionCode is free and whether the app and track exist are things only Play Console decides, and you check them there. The 16 KB check reads the ELF headers; it doesn't prove the app runs correctly on a 16 KB device.

How WinBuild handles it

winbuild verify runs these checks on the AAB that bundle produced, in seconds:

  • Artifact checks. The SHA-256 matches the one recorded by bundle; the structure is a real bundle (BundleConfig.pb, manifest, dex); the application ID and versionCode are readable; the app isn't debuggable; every entry outside META-INF/ is listed in the jar manifest, with no duplicate names; and jarsigner -verify -verbose -certs shows every entry with s and m, with none of the warnings above.
  • Play checks, run locally. PAGE_SIZE_16K reads the ELF LOAD segments of the arm64-v8a and x86_64 libraries (alignment of 16 KB or more); TARGET_SDK_PLAY requires a target of 36 or higher; UPLOAD_CERTIFICATE reads the certificate with keytool -printcert -jarfile, rejects CN=Android Debug and, for a build signed by WinBuild, compares it with the fingerprint of the keystore checked before the build.
  • An honest name. The result is localPlayChecksPassed, true only if every Play check that ran locally passed. Checks that weren't run (BUNDLETOOL_APKS, DEVICE_INSTALL) are listed in notChecked. The report always includes a playConsole block saying the Play Console checks were not performed: the upload key fingerprint, the versionCode not already used, the app and track.
  • Fail closed. If the output of jarsigner isn't in the expected format, verify fails instead of passing.

Two real results from 20 October 2026, on an AAB built by Gradle for a second test app (Expo 57, React Native 0.86):

  • The good AAB: verify exits 0, localPlayChecksPassed: true, 1,273 entries, 50 64-bit libraries aligned to 16 KB, target SDK 36, certificate equal to the keystore. verify took 4.5 s.
  • A copy with one file added after signing (assets/injected.txt): exit 1, localPlayChecksPassed: false, rejected by both JAR_MANIFEST_ENTRIES (AAB_ENTRIES_NOT_IN_MANIFEST: 1 voci) and JAR_SIGNATURE (JAR_UNSIGNED_ENTRIES: 1 voci).

One limit: the target SDK requirement is a constant in WinBuild. When Google changes the rule, it has to be updated.

Sources

External sources consulted on 20 October 2026. Figures about WinBuild come from our own build records.

Private alpha

Building Expo Android releases on Windows?

WinBuild is in private alpha. Testers get the package directly and help validate different machines and Expo projects.

Request alpha access →No checkout yet · direct founder feedback

← All articles