RizTech Academy logo
RizTech Academy
Shipping to Google PlayLesson 2 of 530 min

Signing and the key you must not lose

Every app on an Android phone is signed, and the signature is how Android knows an update came from the same developer as the app already installed. Until now Mandi Bhav has been signed with your debug key, which Android Studio made on your laptop and which no store will accept. A release needs a key you keep for as long as the app exists. This lesson makes one, keeps it and its passwords out of the repository, and explains what Google Play's two-key arrangement means for the day you lose it.

Why a signature, and why the same one

When an update arrives, Android compares its signing certificate with the installed app's. If they match, the update replaces the app and keeps its data. If they do not, Android refuses. A scratch copy of Mandi Bhav was installed on the emulator from a debug build, and then a release build signed with a different key was installed over it:

adb install -r app-release.apk
adb: failed to install app-release.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com.riztechacademy.mandibhav signatures do not match newer version; ignoring!]

That refusal protects Kavita: nobody else can push an "update" to her Mandi Bhav. It also means a key is for life. Android's documentation is plain about the old consequence: if you sign the new version with a different certificate, it must have a different package name, and users install it as a completely new app, with none of their data and none of your ratings.

And an unsigned build cannot be installed at all. Built without a key, the release is app-release-unsigned.apk:

adb: failed to install app-release-unsigned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl2079929598.tmp/base.apk: Attempt to get length of null array]

Two keys: Play App Signing

Google Play splits the job between two keys:

App signing key Upload key
Who holds it Google, in its key management service You
What it signs The APKs Play builds from your bundle and installs on phones The bundle you upload
What phones check This one Never see it
If lost You cannot lose it; Google keeps it Request an upload key reset in the Play Console
If you managed it yourself and lost it No more updates, ever

For apps created since August 2021, Play App Signing is how Play works: you upload a bundle signed with your upload key, Google checks that signature to know it is you, and signs what it delivers with the app signing key. By default Google generates the app signing key for a new app; the Help page says you can supply your own instead, with Change key, before a release rolls out to open testing or production, which you would do if the same app must be signed with the same key in another store.

So the key in this lesson is an upload key. Losing it is serious but recoverable: you make a new one, export its certificate, and ask for an upload key reset, which Google's Help page places under Protected with Play > Play Store protection > Manage Play app signing (an Android developers page calls the same screen Release > Setup > App signing; the console has moved it before, so search for "App signing" if neither matches). A leaked upload key is not enough to publish on its own, because an upload also needs your Play Console account, but reset it the same day anyway. Before Play App Signing became the rule in 2021, many developers held the app signing key themselves, on a laptop, and a lost laptop with no backup meant an app that could never be updated again. Apps created before August 2021 can still work that way.

Making the upload key

keytool comes with the JDK:

keytool -genkeypair -v -keystore ~/keys/mandibhav-upload.jks -alias upload -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Mandi Bhav Upload, O=RizTech Academy, L=Pune, C=IN"

It asks for a keystore password and a key password; type them, do not put them on the command line, where your shell's history would keep them. The run for this lesson, with a throwaway key:

Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
	for: CN=Mandi Bhav Upload, O=RizTech Academy, L=Pune, C=IN
Option Why
-keystore ~/keys/… Outside the project folder, so it cannot be committed by accident
-alias upload The key's name inside the keystore; the build needs it
-keyalg RSA -keysize 2048 Google's Help page asks for an RSA upload key of at least 2,048 bits
-validity 10000 About 27 years. Android's documentation recommends at least 25, and Play wants keys valid beyond 22 October 2033

Android Studio's Build > Generate Signed Bundle/APK wizard does the same through a form. Either way, the result is one file and two passwords, and they are the most important thing in the project that is not in git.

Keeping the secrets out of the repository

The documentation's first way to sign a release puts the passwords in build.gradle.kts, in plain text, and then tells you to move them out. Mandi Bhav's build reads them from somewhere git never sees:

val keystoreProperties = Properties().apply {
    rootProject.file("keystore.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}
fun uploadSigning(property: String, environmentVariable: String): String? =
    keystoreProperties.getProperty(property) ?: providers.environmentVariable(environmentVariable).orNull
val uploadStoreFile = uploadSigning("storeFile", "MANDIBHAV_UPLOAD_STORE_FILE")
signingConfigs {
    if (uploadStoreFile != null) {
        create("upload") {
            storeFile = rootProject.file(uploadStoreFile)
            storePassword = uploadSigning("storePassword", "MANDIBHAV_UPLOAD_STORE_PASSWORD")
            keyAlias = uploadSigning("keyAlias", "MANDIBHAV_UPLOAD_KEY_ALIAS")
            keyPassword = uploadSigning("keyPassword", "MANDIBHAV_UPLOAD_KEY_PASSWORD")
        }
    }
}

with signingConfig = signingConfigs.findByName("upload") in the release block. On your laptop the values come from keystore.properties in the project root:

storeFile=/Users/you/keys/mandibhav-upload.jks
storePassword=…
keyAlias=upload
keyPassword=…

On a build server they come from the four environment variables, set as the server's secrets. And .gitignore gets three lines, so neither the file nor any keystore can be added by accident:

*.jks
*.keystore
keystore.properties
git check-ignore -v keystore.properties upload.jks release.keystore
.gitignore:17:keystore.properties	keystore.properties
.gitignore:15:*.jks	upload.jks
.gitignore:16:*.keystore	release.keystore

With the throwaway keystore set up, ./gradlew :app:assembleRelease :app:bundleRelease signed both. apksigner, in the SDK's build-tools, confirms the APK:

apksigner verify --print-certs app/build/outputs/apk/release/app-release.apk
V2 Signer: certificate DN: CN=Mandi Bhav Upload, O=RizTech Academy, L=Pune, C=IN
V2 Signer: certificate SHA-256 digest: 2bdd7e3bd727c24c49d963c53174872df0d7ccb7c6d76b79dd1b2fcebe2758b3

and keytool -printcert -jarfile app/build/outputs/bundle/release/app-release.aab showed the same certificate on the bundle, valid until 27 March 2054. The same build with the four environment variables set and no keystore.properties signed with the same certificate; with a wrong password it stopped at packaging:

> com.android.ide.common.signing.KeytoolException: Failed to read key upload from store "…/mandibhav-upload.jks": keystore password was incorrect

./gradlew :app:signingReport prints which key signs each variant, with its fingerprints and expiry date, and no passwords. Note what it said with the upload key configured: release, and also benchmarkRelease and nonMinifiedRelease, the Baseline Profile plugin's builds, used the upload config. Those builds are then signed differently from your debug build, so installing one over the other fails as above until you uninstall first.

The trap: a build only you can run

Without any key, Mandi Bhav still builds: debug as always, release as an unsigned APK and bundle. That matters for everyone else who clones the repository, a learner, a teammate, a build server running tests. The documentation's sample reads the file unconditionally:

val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
keystoreProperties.load(FileInputStream(keystorePropertiesFile))

In a scratch copy with no keystore.properties, even ./gradlew :app:assembleDebug failed:

* What went wrong:
…/keystore.properties (No such file or directory)

BUILD FAILED in 1s

The build script runs before any task, so a release-only secret broke the debug build for everyone who does not have it. Hence the takeIf { it.exists() } and findByName: no file means no signing config, not a failed build.

A second trap hid in the first attempt at that experiment: writing java.io.FileInputStream(…) in full inside build.gradle.kts failed with Unresolved reference 'io' on receiver of type 'JavaPluginExtension', because in a Gradle script java means the Java plugin's settings. Import the class instead, as the sample does.

Where the key lives

Put it Not
A password manager, with the keystore file attached, shared only with whoever releases The repository, a chat message, an email
An offline backup: an encrypted drive in a drawer Only on one laptop
The build server's secret store, if releases are built there A plain file on a shared server

Write down, next to the key, the alias and the SHA-256 fingerprint signingReport prints. When something goes wrong in two years, the fingerprint is how you prove which key is which.

Check your work

What did the emulator say when a differently signed release was installed over the debug build? INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com.riztechacademy.mandibhav signatures do not match newer version.

Which key do phones check, under Play App Signing? The app signing key, which Google holds. Your upload key only proves to Google that the upload came from you.

You lose the upload key. What now? Make a new one, export its certificate with keytool -export -rfc, and request an upload key reset in the Play Console. The app's signing key, and so every installed copy, is unaffected.

Why does Mandi Bhav's build use findByName("upload") and not getByName? Without a key there is no upload config; findByName returns null and the release is left unsigned, where getByName would fail the build.

What broke when the build read keystore.properties unconditionally? Every build, debug included, for anyone without the file: keystore.properties (No such file or directory).

Practice

  1. Make your own upload key with keytool, outside the project. Store the keystore and both passwords in a password manager, and write down the alias and the SHA-256 fingerprint.
  2. Create keystore.properties, build bundleRelease, and check the signature with keytool -printcert -jarfile. Then run git status and confirm nothing new appears.
  3. Build the release twice more: once with the four environment variables and no keystore.properties, and once with a wrong password. Read both outputs.
  4. Break it on purpose: in a scratch copy, paste the documentation's unconditional sample, move keystore.properties away, and run assembleDebug. Then fix it the way Mandi Bhav does.
  5. Install your upload-signed release over the debug build on the emulator and read the error. Uninstall, install the release, then try to install the debug build over it.
  6. Harder: export your upload key's certificate with keytool -export -rfc -keystore … -alias upload -file upload_certificate.pem, the file an upload key reset asks for. Then write the steps your team would follow if the laptop with the key were stolen tonight: who does what, in what order, and what you tell Google.

Official documentation

  • Sign your app — Keys and keystores, Play App Signing, removing signing information from build files, and key validity.
  • Use Play App Signing — The two keys, choosing the app signing key, upload key resets and key upgrades.
  • apksigner — Signing and verifying APKs from the command line, and the signature schemes.

Next: the Play Console and your first listing.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship