android
Play App Signing: Upload Key vs App Signing Key, and Lost Keystores
How the Play upload key and app signing key differ, generating a keystore with keytool, why Google Sign-In breaks on Play installs, and resetting a lost key.
By Kyle Hurst ·
Every Android app on Google Play is signed twice. You sign it with an upload key, Google strips that signature and re-signs it with an app signing key you never touch. Most of the confusion around Play App Signing comes from not knowing which key is which: Google Sign-In that works in your release build but fails from the Play Store, a “wrong key” upload error, or the panic of a missing .jks file. Once you know who holds which key, every one of those has a clear fix.
Why you can’t avoid Play App Signing
Since August 2021, new apps on Google Play must be published as Android App Bundles (.aab), not APKs. An AAB isn’t installable by itself. Google builds optimized APKs from it per device (screen density, CPU architecture, language), and those generated APKs need a signature. Google can only sign them if it holds the signing key, so in Google’s own words, “Play App Signing is required for new apps so that they can use AABs”.
Apps created before August 2021 can still self-manage their key and ship APKs, but they give up App Bundles, Play Feature Delivery and Play Asset Delivery (Android developers). For anything new, Play App Signing is the only path.
The two keys, and who holds them
Google defines them like this (source):
- App signing key: “The key that is used to sign APKs that are installed on a user’s device.” It never changes during the app’s lifetime.
- Upload key: “The key you use to sign the app bundle or APK before you upload it for app signing with Google Play.”
| Upload key | App signing key | |
|---|---|---|
| Who holds the private key | You (a keystore on your machine or CI) | |
| What it signs | The .aab you upload | The APKs users actually install |
| Lost or leaked? | Resettable through Play Console | Can’t be lost by you; can be upgraded |
| Default type | RSA 2048+ in a Java keystore | Google-generated RSA 4096 |
| Fingerprint matters for | Play verifying your uploads | Firebase, Google Sign-In, Maps, OAuth, Facebook Login |
The flow looks like this:
your machine / CI Google Play user's phone
───────────────── ─────────── ────────────
app.aab ──sign with UPLOAD key──▶ verify upload signature
build per-device APKs
sign with APP SIGNING key ──▶ installs APK, checks
app signing cert on
every future update
The upload key only proves to Google that the bundle came from you. The device never sees it. Per Google’s Play Console help, the upload key is RSA 2048 bits or more in a Java keystore, and Google-generated app signing keys are RSA 4096. Google also recommends the two be different keys, which is the whole point: if your laptop is stolen, the key that matters to installed users isn’t on it.
New apps in 2026 are automatically enrolled in what Google calls “quantum-ready, hybrid signing with Google-generated keys”, which pairs a classical RSA 4096 key with a post-quantum ML-DSA-65 key, plus a separate classical key for devices on Android 16 and below. You don’t manage any of that. You can choose your own app signing key instead, but only before the first release goes to the open testing or production track. After that, it’s fixed.
Generating an upload keystore with keytool
Android Studio can do this under Build > Generate Signed Bundle/APK, but the command line is easier to script and document. This is the command from Flutter’s deployment docs, and it works for any Android project:
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA \
-storetype JKS -keysize 2048 -validity 10000 -alias upload
What the flags do:
| Flag | Meaning |
|---|---|
-genkey -v | Generate a key pair and self-signed certificate, verbose output |
-keystore ~/upload-keystore.jks | Where the keystore file goes (outside your repo) |
-keyalg RSA -keysize 2048 | RSA 2048, the minimum Play accepts for an upload key |
-storetype JKS | Java keystore format, which Play’s docs describe |
-validity 10000 | Days, about 27 years |
-alias upload | The name you’ll reference in Gradle |
keytool prompts for a keystore password, a key password, and certificate name fields. The validity matters: Google recommends at least 25 years, and Play requires that your key’s validity end after 22 October 2033. 10000 days clears both.
To see the fingerprints of the key you just made:
keytool -list -v -alias upload -keystore ~/upload-keystore.jks
Gradle signingConfig without committing secrets
Google’s own sample uses a keystore.properties file in the project root and warns that signing info in build files is plain text. That’s fine locally if the file is in .gitignore. In CI, environment variables are cleaner. A Kotlin DSL version that reads either:
// app/build.gradle.kts
import java.util.Properties
val keystoreProps = Properties().apply {
val f = rootProject.file("keystore.properties")
if (f.exists()) f.inputStream().use { load(it) }
}
fun secret(name: String, envName: String): String? =
System.getenv(envName) ?: keystoreProps.getProperty(name)
android {
signingConfigs {
create("release") {
secret("storeFile", "UPLOAD_KEYSTORE_PATH")?.let { storeFile = file(it) }
storePassword = secret("storePassword", "UPLOAD_KEYSTORE_PASSWORD")
keyAlias = secret("keyAlias", "UPLOAD_KEY_ALIAS")
keyPassword = secret("keyPassword", "UPLOAD_KEY_PASSWORD")
}
}
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("release")
}
}
}
And in .gitignore:
keystore.properties
*.jks
*.keystore
Most CI systems only store text secrets, so the usual trick is to Base64-encode the keystore, store that string as a secret, and decode it to a temp file during the build (base64 -d > "$RUNNER_TEMP/upload.jks"). Base64 is encoding, not encryption, so that secret is exactly as sensitive as the raw file. Our Base64 encoder explains the format, and Base64 vs Base32 vs Base58 covers why it’s the default for binary-in-text.
One easy mistake: a release build type left on signingConfigs.getByName("debug"). The build succeeds, Play rejects the upload because it’s signed with the wrong key, and any fingerprint you registered for “release” doesn’t match.
The SHA-1 gotcha: Firebase, Google Sign-In and Maps
This is the most common Play App Signing bug. You add your release keystore’s SHA-1 to Firebase, Google Sign-In works in your locally built release APK, you ship to Play, and sign-in fails for every user, often as ApiException: 10.
The reason is in the flow diagram. Users install APKs signed with Google’s app signing key, not your upload key, so the certificate fingerprint Google’s APIs see on a Play install is one you never registered. Google says it plainly: “you must register the Google-held app signing key fingerprint with your API providers, not just your local upload key”, naming Google Maps, OAuth and Facebook Login as examples.
The fix is to register every fingerprint that will ever sign an install you care about:
| Fingerprint | Where to get it | Used by |
|---|---|---|
| Debug key | ./gradlew signingReport or keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore | Local debug builds |
| Upload key | keytool -list -v on your keystore, or the Play app signing page | Sideloaded release builds |
| App signing key | Play Console app signing page only | Everything installed from Play |
Where it is in Play Console: Google moves this page around. The current help article puts it at Protected with Play > Play Store distribution > Go to Play app signing, then the App signing key section (source). Older docs still say Release > Setup > App signing or App integrity. Wherever it lives, that page shows the MD5, SHA-1 and SHA-256 fingerprints for both the upload and app signing certificates (Android developers).
In Firebase, add both SHA-1 and SHA-256 under Project settings > Your apps > Add fingerprint, then re-download google-services.json. If you later do a key upgrade (below), you have new fingerprints to register too.
A certificate fingerprint is just a hash (SHA-1 or SHA-256) of the certificate’s DER bytes, which is why it’s a fixed-length hex string and why one changed byte gives a completely different value. Our hash generator shows that behavior on any input, and the X.509 decoder will open a PEM certificate you’ve downloaded from Play Console. For what’s inside a certificate, see how to read an X.509 certificate.
Lost or leaked upload key: request a reset
This is the good news of Play App Signing. If you lose your upload key, or it leaks, “you are not locked out of your app”. Google still has the app signing key, so you only need to tell it to trust a new upload key. The steps from Google’s help page:
-
Create a new upload key, with the keytool command above or Android Studio.
-
Export its certificate as PEM:
keytool -export -rfc -keystore upload-keystore.jks \ -alias upload -file upload_certificate.pemThis exports only the public certificate. Never send anyone the
.jks. -
Request the reset in Play Console: Protected with Play > Play Store protection > Manage Play app signing, then in the Upload key certificate section click Request upload key reset.
-
Pick a reason, upload the PEM, and submit.
Resetting the upload key doesn’t affect the app signing key, so installed users notice nothing.
How long it takes: Google’s help page doesn’t give a time. Expo’s credentials docs say Google sets the new upload certificate’s validity to start 72 hours in the future, so your first upload after the reset has to wait at least that long. Plan for a few days without uploads, which hurts if you lose the key the night before a hotfix.
Lost app signing key: only a legacy problem
If your app uses Play App Signing, you can’t lose the app signing key. Google holds it, you can’t download the private key, and Google says it may keep a backup for disaster recovery.
The real disaster is a legacy app that never enrolled and self-manages its key. Google’s docs are blunt: if you lose that key, “you will not be able to release new versions of your app to users as updates to the original app,” and the Play help page adds that it “cannot be reset if you manage it yourself.” The only way out is a new app with a new package name, which means new reviews, new installs, and no update path for existing users.
If you maintain a legacy app and still have its key, enroll now. Go to Protected with Play > Play Store distribution > Go to Play app signing, accept the terms, and use Google’s PEPK tool to send a copy of your existing key. Google recommends generating a fresh upload key at the same time. From then on, losing a keystore is a reset request instead of the end of the app.
Key upgrade: changing the app signing key itself
Key upgrade is for when the app signing key is compromised or you want a stronger one (Android developers). Updates still have to install over old versions, so Google signs per Android version:
| Android version | Key enforced |
|---|---|
| Android 17 (API 37) and up | The upgraded quantum-ready hybrid key |
| Android 13 to 16 | The latest classical key |
| Android 7 to 12 | Not enforced; Play Protect checks the latest classical key |
Google’s help page says you can request an upgrade annually. Each upgrade means registering new fingerprints (a classical one and a post-quantum one) with Firebase and any other API provider, or you recreate the sign-in bug above for users on newer Android.
Backing up keystores
The upload key is resettable, but a reset still costs days of not shipping. Treat it like any production secret:
- Keep at least two copies in different places, for example a password manager attachment and an encrypted offline backup. A single CI secret isn’t a backup.
- Store the passwords and the alias with the file. A
.jksyou can’t unlock is the same as a lost one. - Write down the SHA-256 fingerprint, so you can tell which file is the real upload key months later.
- Never commit the keystore or
keystore.properties, even to a private repo. Leaked repos are the most common way upload keys get compromised.
Expo and Flutter notes
- Flutter: the
keytoolcommand above is Flutter’s own. Flutter reads aandroid/key.propertiesfile withstorePassword,keyPassword,keyAliasandstoreFile, and warns not to commit it or the keystore (docs). The double-signing story is identical. - Expo / EAS: EAS Build can generate and hold the keystore for you, and with Play App Signing that keystore acts as your upload key. Run
eas credentials, choose Android, and use thecredentials.jsonoption to download a local copy (docs). Do this once and back it up like any other keystore. You still need the app signing fingerprint from Play Console for Firebase.
Checklist
- Generate an upload key, RSA 2048+, valid 25+ years, stored outside the repo.
- Wire Gradle to env vars or an ignored properties file.
- Let Google generate the app signing key unless you have a reason not to.
- Copy the app signing key’s SHA-1 and SHA-256 from Play Console into Firebase, Google Cloud, Maps and any OAuth providers.
- Back up the keystore, passwords and alias in two places.
- If the upload key is lost, export a new PEM and request a reset. Expect a few days.
Signing is one of a dozen things that trip up a first Play release. Our Google Play publishing checklist walks through the rest, from the closed-testing requirement to target API levels and store listing assets. The target API 36 and 16 KB page size guide covers the build-side requirements in detail.