Android Signing Key Rotation – Explained
— Android — 9 min read
This article explains how to safely rotate signing keys for Android apps, without losing trust to
the previous keys. Basic knowledge in JDK and Android SDK command-line tools like adb
, apksigner
,
keytool
is required. Also, it would be nice if you have basic knowledge in command-line shells
like bash
or zsh
. It is recommended to configure the $PATH
variable of your default shell to have
the installation directories of JDK and Android SDK command-line tools, so you can avoid typing the
full path of the tool everytime.
Introduction
From the Android developer documentation:
Android 9 (API level 28) supports APK key rotation, which gives apps the ability to change their signing key as part of an APK update. To make rotation practical, APKs must indicate levels of trust between the new and old signing key. To support key rotation, we updated the APK signature scheme from v2 to v3 to allow the new and old keys to be used. V3 adds information about the supported SDK versions and a proof-of-rotation struct to the APK signing block.
Devices running Android 8.1 (API level 27) or lower don't support changing the signing certificate. If your app's minSdkVersion is 27 or lower, use an old signing certificate to sign your app in addition to the new signature.
This breaks down to these minority use cases:
- You are targetting your app for Android 9 or later.
- Your keystore is going to expire soon. Still you are supporting the project, and have to publish updates.
- The project is handed over to a new company, and they have to use their key to sign the updates.
However, it doesn't mean to solve these situations:
- Loss of keystore.
- Signing apps which targets Android prior to 9 (Pie, API 28).
The introduction of key rotation have minor impact around the developers for now, and most of us don't really care about it. The cryptographic advancements over the APK siging schemes from v1 to v4 is appreciable. The APK signing scheme v2 detects the tampering of zip central directory, and also improve the speed of installation. V3 introduced key rotation and the ability to sign with multiple keystores. The preview V4 scheme produces a new kind of signature in a separate file (apk-name.apk.idsig). This scheme supports ADB incremental APK installation, which speeds up APK install.
Let's get started
For better understanding of how the key rotation works, here is an illustration. The relationship
between the keys and signatures are expressed through lineages, which establishes the cryptographic
trust between the existing key and the new key. If you have n
number of keys, you will need n-1
lineages to maintain trust between keys.
Let's start with an unsigned build of the application app-release-unsigned.apk
.
First of all, let's create a keystore with keytool
tool from JDK.
user@host:~$ keytool -genkeypair \-alias first \-keyalg RSA \-keysize 4096 \-validity 10000 \-keypass ******** \-keystore first.jks \-storepass ******** \-storetype PKCS12
What is your first and last name? [Unknown]: FirstWhat is the name of your organizational unit? [Unknown]: ExampleWhat is the name of your organization? [Unknown]: ExampleWhat is the name of your City or Locality? [Unknown]: ExampleWhat is the name of your State or Province? [Unknown]: ExampleWhat is the two-letter country code for this unit? [Unknown]: INIs CN=First, OU=Example, O=Example, L=Example, ST=Example, C=IN correct? [no]: yes
Next, sign your application with the keystore first.jks
.
user@host:~$ apksigner sign \--ks first.jks \--in app-release-unsigned.apk \--out app-first.apkKeystore password for signer #1: ********
Now, you have your application signed with first.jks
to app-first.jks
. Generate your second keystore second.jks
.
user@host:~$ keytool -genkeypair \-alias second \-keyalg RSA \-keysize 4096 \-validity 10000 \-keypass ******** \-keystore second.jks \-storepass ******** \-storetype PKCS12
This will ask the same questions as asked when you created the first key. After that, sign your app with second key to app-second.apk
.
user@host:~$ apksigner sign \--ks second.jks \--in app-release-unsigned.apk \--out app-second.apkKeystore password for signer #1: ********
All right. Now, verify the both apps are signed with the correct keys.
user@host:~$ apksigner verify -v --print-certs app-first.apk VerifiesVerified using v1 scheme (JAR signing): trueVerified using v2 scheme (APK Signature Scheme v2): trueVerified using v3 scheme (APK Signature Scheme v3): trueNumber of signers: 1Signer #1 certificate DN: CN=First, OU=Example, O=Example, L=Example, ST=Example, C=INSigner #1 certificate SHA-256 digest: b3f51e5541d0952099f14c2e8b0c1cdd7c50d4378f6ed5b29ad10613e86d6bfcSigner #1 certificate SHA-1 digest: 583905f0bea5b7e33a14296d3dfed320706d37b3Signer #1 certificate MD5 digest: fd7c9b3ae1c1b57aa8a4dd9a6e23f56bSigner #1 key algorithm: RSASigner #1 key size (bits): 4096Signer #1 public key SHA-256 digest: fb4a91fdaf390e50b023191c58983de2cf57423cf920d7d6132d16062793081fSigner #1 public key SHA-1 digest: 4810c2293d6cdf901e422c70494c69b3fdad7902Signer #1 public key MD5 digest: 314d879ff989fb8d2c7dbdde23b14e8c
If the command outputs Verifies
, everything is good.
Let's verify the app-second.apk
user@host:~$ apksigner verify -v --print-certs app-second.apkVerifiesVerified using v1 scheme (JAR signing): trueVerified using v2 scheme (APK Signature Scheme v2): trueVerified using v3 scheme (APK Signature Scheme v3): trueNumber of signers: 1Signer #1 certificate DN: CN=Second, OU=Example, O=Example, L=Example, ST=Example, C=INSigner #1 certificate SHA-256 digest: 1c7642f167f13281932c4d6f0df654217117ede891e529dc4cecb590ec92e365Signer #1 certificate SHA-1 digest: 6581fe770a6a676a4ae0a8af464465bc27d44e4aSigner #1 certificate MD5 digest: 8845af2fad3b535727b1b0ed63b00e43Signer #1 key algorithm: RSASigner #1 key size (bits): 4096Signer #1 public key SHA-256 digest: b255b671617884a9ab827a782f70d04491333599af33dfd5449367df154e286aSigner #1 public key SHA-1 digest: 3e1ed768068912ec5cdce22f9c2e3ac25dd2eab2Signer #1 public key MD5 digest: 1af143237d97328108a49e3d84968628.... (output redacted for brevity)
Just like the first one, if the command outputs Verifies
, everything is good.
Let's try to install the signed apps.
user@host:~$ adb install app-first.apk Performing Streamed InstallSuccess
user@host:~$ adb install app-second.apkPerforming Streamed Installadb: failed to install app-second.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.example.app signatures do not match previously installed version; ignoring!]
This error is expected. Both of the apps have same package identifiers and different signatures. Android forbids update if signature mismatches.
Android accepts the APK only if it proves the trust with older signing keys. APK Signature Scheme v3 introduces the facility to do this. With the apksigner
utility, we have to rotate the key to create a signing certificate lineage and sign the APK. It includes the proof that the new key has a relationship to the old key.
user@host:~$ apksigner rotate \--old-signer \--ks first.jks \--new-signer \--ks second.jks \--out lineage-1-2
Keystore password for old signer: ********Keystore password for new signer: ********
Now we sign the APK with the new signer second.jks
keystore, provided the old keystore first.jks
and the signing certificate lineage of the keystores.
user@host:~$ apksigner sign \--ks first.jks \--next-signer --ks second.jks \--lineage lineage-1-2 \--in app-release-unsigned.apk \--out app-rotated-1-2.apk
Keystore password for signer #1: ********Keystore password for signer #2: ********
All right. Let's take a look on the signature of app-rotated-1-2.apk
user@host:~$ apksigner verify -v --print-certs app-rotated-1-2.apkVerifiesVerified using v1 scheme (JAR signing): trueVerified using v2 scheme (APK Signature Scheme v2): trueVerified using v3 scheme (APK Signature Scheme v3): trueNumber of signers: 1Signer #1 certificate DN: CN=Second, OU=Example, O=Example, L=Example, ST=Example, C=INSigner #1 certificate SHA-256 digest: 1c7642f167f13281932c4d6f0df654217117ede891e529dc4cecb590ec92e365Signer #1 certificate SHA-1 digest: 6581fe770a6a676a4ae0a8af464465bc27d44e4aSigner #1 certificate MD5 digest: 8845af2fad3b535727b1b0ed63b00e43Signer #1 key algorithm: RSASigner #1 key size (bits): 4096Signer #1 public key SHA-256 digest: b255b671617884a9ab827a782f70d04491333599af33dfd5449367df154e286aSigner #1 public key SHA-1 digest: 3e1ed768068912ec5cdce22f9c2e3ac25dd2eab2Signer #1 public key MD5 digest: 1af143237d97328108a49e3d84968628.... (output redacted for brevity)
The signature looks exactly like from the app-second.apk
. Let's install over the app-first.apk
.
adb install app-rotated-1-2.apk Performing Streamed InstallSuccess
The rotated APK installs fine for the Androids before 9.0 Pie API 28. For previous Androids, -r
flag is necessary. It tells the package manager to re-install the package. For Android Pie 9.0 and later, it is also possible to install app-second.apk
after app-rotated-1-2.apk
. This is because the app-rotated-1-2.apk
proves the new signature belongs to the same developer, and further installations will requires signing from the new keystore only.
For the earlier versions of Android, the future updates will requires to be signed with the previous keystore and the signing certificate lineage, which invalidates the benefit of key rotation. I hope Google will find a solution for that.
Rotate it again
What will have to do if you want to publish updates with a newer keystore third.jks
? Rotate it again, with older keystores along the previous signing certificate lineage.
user@host:~$ apksigner rotate \--in lineage-1-2 \--out lineage-1-2-3 \--old-signer --ks second.jks \--new-signer --ks third.jks
Keystore password for old signer: ********Keystore password for new signer: ********
This will generate a new signing certificate lineage, which proves the third.jks
keystore belongs to the family of first.jks
and second.jks
. Now sign with the latest keystore third.jks
, previous keystore second.jks
and the latest signing certificate lineage lineage-1-2-3
.
user@host:~$ apksigner sign \--ks first.jks \--next-signer --ks second.jks \--next-signer --ks third.jks \--lineage lineage-1-2-3 \--in app-release-unsigned.apk \--out app-rotated-2-3.apk
Keystore password for signer #1: ********Keystore password for signer #2: ********Keystore password for signer #3: ********
The rotated APK will have signature by third.jks
keystore, but includes the proof that it can be verified over app-first.apk
and app-second.apk
.
user@host:~$ apksigner verify -v --print-certs app-rotated-2-3.apk VerifiesVerified using v1 scheme (JAR signing): trueVerified using v2 scheme (APK Signature Scheme v2): trueVerified using v3 scheme (APK Signature Scheme v3): trueNumber of signers: 1Signer #1 certificate DN: CN=Third, OU=Example, O=Example, L=Example, ST=Example, C=INSigner #1 certificate SHA-256 digest: 046b1a6ae24f12b2d7d2938d7054c06bac308c672981f1bea38d40afb9c23984Signer #1 certificate SHA-1 digest: d3f77987a973960b360777a5c3f534eb57ee52e5Signer #1 certificate MD5 digest: 688f275d697fe3630ad377d7ffa8d6e5Signer #1 key algorithm: RSASigner #1 key size (bits): 4096Signer #1 public key SHA-256 digest: 11f007348e9bc258207c58a819c3ffbde118d99c7f3ecf6fd6ec94db18f768a7Signer #1 public key SHA-1 digest: aa6c269b71a495699aa6cf32aeae15377713754aSigner #1 public key MD5 digest: a1c68cc9d3a02954d2ceeeef06f5b3c1
Finally, install over the app-second.apk
adb install app-rotated-2-3.apk Performing Streamed InstallSuccess
For Android 9.0 Pie or later, the further updates required to be signed with the third.jks
keystore only. But make sure that you have to keep the signing certificate lineage forever, because in case you have to use a new keystore, you will need it.
adb install app-third.apkPerforming Streamed InstallSuccess