Android 应用签名是应用打包过程的重要步骤之一,Google 要求所有的应用必须被签名才可以安装到 Android 操作系统中。Android 的签名机制也为开发者识别和更新自己应用提供了方便。本文尝试对 Android 应用签名机制进行简单分析,个人理解有限,难免有纰漏之处,请多多拍砖。
背景知识
想要搞清楚安卓应用签名到底是什么东西,首先需要了解一些背景知识。
数字摘要 Digital Digest
数字摘要主要作用是将任意长度的消息使用单向 HASH 算法摘要成一串固定长度的密文。常用的 HASH 算法包括 SHA-1, SHA-256, MD5 等等,MD5 中的 MD 就是 Message Digest 的缩写。数字摘要有时也被称为数字指纹,消息摘要等等,其实表达的都是一个意思。它有以下三个特点:

Keystore 文件
Android 签名需要用到一种后缀名为 keystore 的文件。在打 Debug 包的时候,如果没有在 build.gradle 文件中指定的话,Gradle 就会自动为我们生成一个 keystore文件,保存在系统用户根目录 .android 文件夹内,名称为 debug.keystore. 我们以它为例看看 keystore 文件包含了什么内容。
通过 keytool 工具来查看,默认的密码是 android.$ keytool -list -v -keystore debug.keystore -storepass android结果如下:
密钥库类型: jks
密钥库提供方: SUN
您的密钥库包含 1 个条目
别名: androiddebugkey
创建日期: 2013-4-26
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=Android Debug, O=Android, C=US
发布者: CN=Android Debug, O=Android, C=US
序列号: 517a38f2
有效期为 Fri Apr 26 15:46:58 CST 2013 至 Sun Apr 19 15:46:58 CST 2043
证书指纹:
MD5: 37:09:10:A9:F1:AE:9C:E4:C0:85:B9:35:D9:93:93:52
SHA1: F1:60:3F:72:2A:F2:3A:BC:BE:1C:DB:F6:F4:5B:FD:5E:34:8C:01:A9
SHA256: 86:C7:CB:D1:56:E7:D8:B8:AD:67:A7:A1:8F:C0:F6:E6:FC:E1:3D:45:AE:BC:F5:DF:B4:A9:F9:9A:F7:89:F7:0D
签名算法名称: SHA1withRSA
主体公共密钥算法: 1024 位 RSA 密钥
版本: 3
可以看到,其实 keystore 类似一个钥匙仓库,里面有证书的所有者和发布者信息,包含了私钥和公钥信息,并设置了密码进行保护。那什么是公钥和私钥呢。
公共密钥系统 RSA
公钥和私钥都是公共密钥系统里的概念。最初所有的加密算法都属于对称加密,也就是说加密和解密使用的相同的密码,通信双方如何安全沟通和保存密码,是这种加密方法的主要难题。难保没有猪队友。
而在公共密钥系统中,加密和解密使用的是不同的密钥,分别称为公钥和私钥,公钥意思就是所有人都可以知道,私钥则只有所有者才持有,单从公钥无法在现有的计算能力下推导出私钥。这样一来就不存在沟通过程中泄露密钥的问题,只要私钥不泄露,通信就一直是安全的。
公共密钥系统可以说是现在最最最重要密钥系统,是互联网的基石之一。
公共密钥系统可以用来加密,也可以用来签名。加密方案中,是不希望别人知道我的消息,所以公钥用于加密信息,私钥用于解密信息;而签名方案中,是不希望别人冒充我发消息,只有我才能发布这个签名,所以需要用私钥进行签名,公钥用于验证签名。
什么是应用签名
回到正题,我们先来看看 Android 应用签名发生在构建的哪一步。

在编译过程中,编译器首先会将源代码和资源进行编译,生成 DEX 文件和一些编译后的资源文件,然后 APK Manager 会根据配置使用 keystore 文件进行签名,签名后才会将所有资源压缩到一个 ZIP 包里,这个 ZIP 包其实我们安装的时候用的 APK 文件。可以看到签名是在构建基本完成的时候发生的。
签名过程
那 APK Manager 是如何使用 keystore 进行签名的呢?我们一步一步看到底发生了什么。
首先对编译后生成的所有的文件进行扫描,每个文件生成一个数字摘要,保存在 META-INF/MANIFEST.MF 文件中
Name: res/drawable-xhdpi-v4/im_ic_keyboard_pressed.png
SHA-256-Digest: cqjOi3gUv9O0IBfgLOlIJUZTBwyCPcWbXIs/o6TMfTc
Name: classes.dex
SHA-256-Digest: FJCwLV1TyZuL1qxkDsJ6bXTmaSkK+JkKt5zmpDBc8Tg=
我们看一下 im_ic_keyboard_pressed.png 这个文件的数字摘要到底是如何计算出来的。
$ shasum -a 256 im_ic_keyboard_pressed.png
72a8ce8b7814bfd3b42017e02ce948254653070c823dc59b5c8b3fa3a4cc7d37 im_ic_keyboard_pressed.png
$ echo "72a8ce8b7814bfd3b42017e02ce948254653070c823dc59b5c8b3fa3a4cc7d37" | xxd -r -p | base64
cqjOi3gUv9O0IBfgLOlIJUZTBwyCPcWbXIs/o6TMfTc=
可以看到跟我们在 MANIFEST.MF 中看到的值是能够对上的。2. 对 MANIFEST.MF 文件生成数字摘要,并写入 CERT.SF,这里有一个细节,除了对整个文件做 HASH 外,还会将文件分成多段计算 HASH 同样保存在 CERT.SF 文件中【为什么,还没搞清楚】3. 计算 CERT.SF 的数字摘要,并使用 RSA 私钥进行加密,生成签名4. 将签名、公钥、哈希算法信息写入 CERT.RSA 文文件,并将这些文件添加到 APK 压缩包 META-INF 目录中
目前应用签名不需要申请可信的证书机构 (Certificate Authority) 签发的证书,开发者可以通过 keytool 来创建自签名的证书。
为什么要签名
应用签名不能保证 APK 不被篡改,只是为了能够校验出 APK 是否被篡改。在系统安装过程中,如果发现 APK 被篡改,安装就会失败。那系统是如何校验的呢?
签名校验过程系统取得已安装 APK 中保存的公钥,用它对新 APK 中的 CERT.RSA 保存的签名信息进行解密;对 CERT.SF 文件计算摘要,与上一步解密出来的信息进行比对,如果不一致说明 CERT.SF 被篡改,拒绝安装对 MANIFEST.SF 文件计算摘要,与 CERT.SF 文件中的信息进行对比,如果不一致,则说明 MANIFEST.SF 文件被篡改,拒绝安装对 APK 内所有其他文件计算数字摘要,如果文件没有出现在 MANIFEST.SF 或者摘要与 MANIFEST.SF 中包含的不相同,说明加入了新的文件或者文件被篡改,拒绝安装
整个校验过程中,环环相扣,从 CERT.RSA -> CERT.SF -> MANIFEST.SF -> All Other Files,只要有一环失败,系统就会终止 APK 的安装.
如果给新版应用分配了新的签名文件,那就必须更改包名,这样系统才会认为是不同的应用。否则安装就会失败,提示签名不一致。INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES
签名的其他用途
除了用于安装时校验应用,签名还有一些别的用途。
应用模块化。Android 允许相同签名的两个应用使用同样的 Linux UserId,这样一来两个应用就可以共享数据存储了。同时如果应用申请的话,两个应用还可以在同一个进程中运行。通过这种方式可以模块化部署应用,每个模块也能独立的进行升级。猜想很多主题资源包可能就是通过这种方式来安装的。
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="com.meng.sharedappid"
package="meng.mainuicomponents">
共享代码数据。Android 提供了可以在使用同样签名的应用间共享代码的功能。另外一个前提也是两个应用设置了使用同样的 shareUserId。可以使用包名拿到兄弟 APP 的 Context,然后拿到 ClassLoader 加载兄弟 APP 的类,并可以实例化并通过反射来调用具体的方法。
val friendContext = this.createPackageContext("packageName", Context.CONTEXT_INCLUDE_CODE)
val friendClass = friendContext.classLoader.loadClass("packageName.className")
val noparams = arrayOf>() //say the function (functionName) required no inputs
friendClass.getMethod("functionName", *noparams).invoke(friendClass.newInstance(), null)










