Android签名机制
什么是Android签名
了解 HTTPS 通信的同学都知道,在消息通信时,必须至少解决两个问题:一是确保消息来源的真实性,二是确保消息不会被第三方篡改。
同理,在安装 apk 时,同样也需要确保 apk 来源的真实性,以及 apk 没有被第三方篡改。为了解决这一问题,Android官方要求开发者对 apk 进行签名,而签名就是对apk进行加密的过程。要了解如何实现签名,需要了解两个基本概念:消息摘要、数字签名和数字证书。
消息摘要
消息摘要(Message Digest),又称数字摘要(Digital Digest)或数字指纹(Finger Print)。简单来说,消息摘要就是在消息数据上,执行一个单向的 Hash 函数,生成一个固定长度的Hash值,这个Hash值即是消息摘要。
上面提到的的加密 Hash 函数就是消息摘要算法。它有以下特征:
无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。 例如:应用 MD5 算法摘要的消息有128个比特位,用 SHA-1 算法摘要的消息最终有 160 比特位的输出,SHA-1 的变体可以产生 192 比特位和 256 比特位的消息摘要。一般认为,摘要的最终输出越长,该摘要算法就越安全。
消息摘要看起来是「随机的」。 这些比特看上去是胡乱的杂凑在一起的。可以用大量的输入来检验其输出是否相同,一般,不同的输入会有不同的输出,而且输出的摘要消息可以通过随机性检验。但是,一个摘要并不是真正随机的,因为用相同的算法对相同的消息求两次摘要,其结果必然相同;而若是真正随机的,则无论如何都是无法重现的。因此消息摘要是「伪随机的」。
消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息。 当然,可以采用强力攻击的方法,即尝试每一个可能的信息,计算其摘要,看看是否与已有的摘要相同,如果这样做,最终肯定会恢复出摘要的消息。但实际上,要得到的信息可能是无穷个消息之一,所以这种强力攻击几乎是无效的。
好的摘要算法,没有人能从中找到「碰撞」。或者说,无法找到两条消息,使它们的摘要相同。 虽然「碰撞」是肯定存在的(由于长明文生成短摘要的 Hash 必然会产生碰撞)。即对于给定的一个摘要,不可能找到一条信息使其摘要正好是给定的。
正是由于以上特点,消息摘要算法被广泛应用在「数字签名」领域,作为对明文的摘要算法。著名的消息摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的变体。SHA-256 是 SHA-1 的升级版,现在 Android 签名使用的默认算法都已经升级到 SHA-256 了。
正是因为消息摘要具有这种特性,很适合来验证数据的完整性。比如:在网络传输过程中下载一个大文件 BigFile,我们会同时从网络下载 BigFile 和 BigFile.md5,BigFile.md5 保存 BigFile 的摘要,我们在本地生成 BigFile 的消息摘要和 BigFile.md5 比较,如果内容相同,则表示下载过程正确。
数字签名
数字签名的作用就是保证信息传输的完整性、发送者的身份认证、防止交易中的抵赖发生。数字签名技术是将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。
如 RSA 作为数字签名方案使用时,它的使用流程如下:这种签名实际上就是用信源的私钥加密消息,加密后的消息即成了签体;而用对应的公钥进行验证,若公钥解密后的消息与原来的消息相同,则消息是完整的,否则消息不完整。
RSA正好和公钥密码用于消息保密是相反的过程。因为只有信源才拥有自己地私钥,别人无法重新加密源消息,所以即使有人截获且更改了源消息,也无法重新生成签体,因为只有用信源的私钥才能形成正确地签体。
同样信宿只要验证用信源的公钥解密的消息是否与明文消息相同,就可以知道消息是否被更改过,而且可以认证消息是否是确实来自意定的信源,还可以使信源不能否认曾经发送的消息。所以 这样可以完成数字签名的功能。
但这种方案过于单纯,它仅可以保证消息的完整性,而无法确保消息的保密性。而且这种方案要对所有的消息进行加密操作,这在消息的长度比较大时,效率是非常低的,主要原因在于公钥体制的加解密过程的低效性。所以这种方案一般不可取。
几乎所有的数字签名方案都要和快速高效的摘要算法(Hash 函数)一起使用,当公钥算法与摘要算法结合起来使用时,便构成了一种有效地数字签名方案。
签名证书
通过数字签名技术,确实可以解决可靠通信的问题。一旦验签通过,接收者就能确信该消息是期望的发送者发送的,而发送者也不能否认曾经发送过该消息。
大家有没有注意到,前面讲的数字签名方法,有一个前提,就是消息的接收者必须事先得到正确的公钥。如果一开始公钥就被别人篡改了,那坏人就会被你当成好人,而真正的消息发送者给你发的消息会被你视作无效的。而且,很多时候根本就不具备事先沟通公钥的信息通道。
那么如何保证公钥的安全可信呢?这就要靠数字证书来解决了。
数字证书是一个经证书授权(Certificate Authentication)中心数字签名的包含公钥拥有者信息以及公钥的文件。数字证书的格式普遍采用的是 X.509 V3 国际标准,一个标准的 X.509 数字证书通常包含以下内容:
证书的发布机构(Issuer):该证书是由哪个机构(CA 中心)颁发的。
证书的有效期(Validity):证书的有效期,或者说使用期限。过了该日期,证书就失效了。
证书所有人的公钥(Public-Key):该证书所有人想要公布出去的公钥。
证书所有人的名称(Subject):这个证书是发给谁的,或者说证书的所有者,一般是某个人或者某个公司名称、机构的名称、公司网站的网址等。
证书所使用的签名算法(Signature algorithm):这个数字证书的数字签名所使用的加密算法,这样就可以使用证书发布机构的证书里面的公钥,根据这个算法对指纹进行解密。
证书发行者对证书的数字签名(Thumbprint):数字证书的hash 值(指纹),用于保证数字证书的完整性,确保证书没有被修改过。
数字证书的原理就是在证书发布时,CA 机构会根据签名算法(Signature algorithm)对整个证书计算其 hash 值(指纹)并和证书放在一起,使用者打开证书时,自己也根据签名算法计算一下证书的 hash 值(指纹),如果和证书中记录的指纹对的上,就说明证书没有被修改过。
可以看出,数字证书本身也用到了数字签名技术,只不过签名的内容是整个证书(里面包含了证书所有者的公钥以及其他一些内容)。与普通数字签名不同的是,数字证书的签名者不是随随便便一个普通机构,而是 CA 机构。
总结一下,数字签名和签名验证的大体流程如下图所示:
Android打包流程
整个Android的打包流程如下图所示:
编译打包步骤:
1,打包资源文件,生成R.java文件 打包资源的工具是aapt(The Android Asset Packaing Tool)(E:DocumentsAndroidsdkbuild-tools25.0.0aapt.exe)。
在这个过程中,项目中的AndroidManifest.xml文件和布局文件XML都会编译,然后生成相应的R.java,另外AndroidManifest.xml会被aapt编译成二进制。
存放在APP的res目录下的资源,该类资源在APP打包前大多会被编译,变成二进制文件,并会为每个该类文件赋予一个resource id。对于该类资源的访问,应用层代码则是通过resource id进行访问的。Android应用在编译过程中aapt工具会对资源文件进行编译,并生成一个resource.arsc文件,resource.arsc文件相当于一个文件索引表,记录了很多跟资源相关的信息。
2. 处理aidl文件,生成相应的Java文件 这一过程中使用到的工具是aidl(Android Interface Definition Language),即Android接口描述语言(E:DocumentsAndroidsdkbuild-tools25.0.0aidl.exe)。
aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用。如果在项目没有使用到aidl文件,则可以跳过这一步。
3. 编译项目源代码,生成class文件 项目中所有的Java代码,包括R.java和.aidl文件,都会变Java编译器(javac)编译成.class文件,生成的class文件位于工程中的bin/classes目录下。
4. 转换所有的class文件,生成classes.dex文件 dx工具生成可供Android系统Dalvik虚拟机执行的classes.dex文件,该工具位于(E:DocumentsAndroidsdkbuild-tools25.0.0dx.bat)。
任何第三方的libraries和.class文件都会被转换成.dex文件。dx工具的主要工作是将Java字节码转成成Dalvik字节码、压缩常量池、消除冗余信息等。
5. 打包生成APK文件 所有没有编译的资源,如images、assets目录下资源(该类文件是一些原始文件,APP打包时并不会对其进行编译,而是直接打包到APP中,对于这一类资源文件的访问,应用层代码需要通过文件名对其进行访问);编译过的资源和.dex文件都会被apkbuilder工具打包到最终的.apk文件中。
打包的工具apkbuilder位于 android-sdk/tools目录下。apkbuilder为一个脚本文件,实际调用的是(E:DocumentsAndroidsdktoolslib)文件中的com.android.sdklib.build.ApkbuilderMain类。
6. 对APK文件进行签名 一旦APK文件生成,它必须被签名才能被安装在设备上。
在开发过程中,主要用到的就是两种签名的keystore。一种是用于调试的debug.keystore,它主要用于调试,在Eclipse或者Android Studio中直接run以后跑在手机上的就是使用的debug.keystore。
另一种就是用于发布正式版本的keystore。
7. 对签名后的APK文件进行对齐处理 如果你发布的apk是正式版的话,就必须对APK进行对齐处理,用到的工具是zipalign(E:DocumentsAndroidsdkbuild-tools25.0.0zipalign.exe)
对齐的主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快。对齐的作用就是减少运行时内存的使用。
从上图可以看到,签名发生在打包过程中的倒数第二步,而且签名针对的是已经存在的apk包,并不会影响我们写的代码。事实也确实是如此,Android的签名,大致的签名原理就是对未签名的apk里面的所有文件计算hash,然后保存起来(MANIFEST.MF),然后在对这些hash计算hash保存起来(CERT.SF),然后在计算hash,然后再通过我们上面生成的keystore里面的私钥进行加密并保存(CERT.RSA)。
Android签名方案
Android 系统从诞生到现在的1.0版本,一共经历了三代应用签名方案,分别是v1、v2和v3方案。
- v1 方案:基于 JAR 签名。
- v2 方案:APK 签名方案 v2,在 Android 7.0 引入。
- v3 方案:APK 签名方案v3,在 Android 9.0 引入。
其中,v1 到 v2 是颠覆性的,主要是为了解决 JAR 签名方案的安全性问题,而到了 v3 方案,其实结构上并没有太大的调整,可以理解为 v2 签名方案的升级版。
v1 到 v2 方案的升级,对开发者影响是最大的,就是渠道签署的问题。v2的签名也是为了让不同渠道、市场的安装包有所区别,携带渠道的唯一标识,也即是我们俗称的渠道包。好在各大厂都开源了自己的签渠道方案,例如:Walle(美团)、VasDolly(腾讯)都是非常优秀的方案。
V1签名
签名工具
Android 应用的签名工具有两种:jarsigner 和 apksigner。它们的签名算法没什么区别,主要是签名使用的文件不同。它们的区别如下:
jarsigner:jdk 自带的签名工具,可以对 jar 进行签名。使用 keystore 文件进行签名。生成的签名文件默认使用 keystore 的别名命名。
apksigner:Android sdk 提供的专门用于 Android 应用的签名工具。使用 pk8、x509.pem 文件进行签名。其中 pk8 是私钥文件,x509.pem 是含有公钥的文件。生成的签名文件统一使用“CERT”命名。
既然这两个工具都是给 APK 签名的,那么 keystore 文件和 pk8,x509.pem 他们之间是不是有什么联系呢?答案是肯定的,他们之间是可以转化的,这里就不再分析它们是如何进行转化,网上的例子很多。
还有一个需要注意的知识点,如果我们查看一个keystore 文件的内容,会发现里面包含有一个 MD5 和 SHA1 摘要,这个就是 keystore 文件中私钥的数据摘要,这个信息也是我们在申请很多开发平台账号时需要填入的信息。
签名过程
首先,我们任意选取一个签名后的 APK(Sample-release.APK)进行解压,会得到如下图所示的文件。
可以发现,在 META-INF 文件夹下有三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。它们就是签名过程中生成的文件,它们的作用如下。
MANIFEST.MF
该文件中保存的其实就是逐一遍历 APK 中的所有条目,如果是目录就跳过,如果是一个文件,就用 SHA1(或者 SHA256)消息摘要算法提取出该文件的摘要然后进行 BASE64 编码后,作为「SHA1-Digest」属性的值写入到 MANIFEST.MF 文件中的一个块中。该块有一个「Name」属性, 其值就是该文件在 APK 包中的路径。
打开MANIFEST.MF文件,文件内容如下图:
CERT.SF
打开CERT.SF文件,文件的内容如下:
- SHA1-Digest-Manifest-Main-Attributes:对 MANIFEST.MF 头部的块做SHA1(或者SHA256)后再用 Base64 编码。
- SHA1-Digest-Manifest:对整个 MANIFEST.MF 文件做 SHA1(或者 SHA256)后再用 Base64 编码。
- SHA1-Digest:对 MANIFEST.MF 的各个条目做 SHA1(或者 SHA256)后再用 Base64 编码。
CERT.RSA
把之前生成的 CERT.SF 文件用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。需要注意的是,Android APK 中的 CERT.RSA 证书是自签名的,并不需要第三方权威机构发布或者认证的证书,用户可以在本地机器自行生成这个自签名证书。Android 目前不对应用证书进行 CA 认证。
打开CERT.RSA文件,内容如下图:
这里看到的都是二进制文件,因为 使用RSA 加密了,所以我们需要用 openssl 命令才能查看其内容,如下所示。
代码语言:javascript复制$ openssl pkcs7 -inform DER -in /<文件存放路径 /Sample-release_new/original/META-INF/CERT.RSA -text -noout -print_certs
执行上面的命令后,得到的内容如下图:
综上可以看出,一个完整的签名过程如下图:
签名校验
签名验证是发生在 APK 的安装过程中,一共分为三步:
- 检查 APK 中包含的所有文件,对应的摘要值与 MANIFEST.MF 文件中记录的值一致。
- 使用证书文件(RSA 文件)检验签名文件(SF 文件)没有被修改过。
- 使用签名文件(SF 文件)检验 MF 文件没有被修改过。
综上所述,一个完整的签名验证过程如下所示:
V2签名
APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。通过前面的分析,可以发现 v1 签名有两个地方可以改进:
签名校验速度慢 校验过程中需要对apk中所有文件进行摘要计算,在 APK 资源很多、性能较差的机器上签名校验会花费较长时间,导致安装速度慢。
完整性保障不够 META-INF 目录用来存放签名,自然此目录本身是不计入签名校验过程的,可以随意在这个目录中添加文件,比如一些快速批量打包方案就选择在这个目录中添加渠道文件。
为了解决这两个问题,在 Android 7.0 版本 中引入了全新的 APK Signature Scheme v2。
V2的改进
由于在 v1 仅针对单个 ZIP 条目进行验证,因此,在 APK 签署后可进行许多修改 — 可以移动甚至重新压缩文件。事实上,编译过程中要用到的 ZIPalign 工具就是这么做的,它用于根据正确的字节限制调整 ZIP 条目,以改进运行时性能。而且我们也可以利用这个东西,在打包之后修改 META-INF 目录下面的内容,或者修改 ZIP 的注释来实现多渠道的打包,在 v1 签名中都可以校验通过。
v2 签名将验证归档中的所有字节,而不是单个 ZIP 条目,因此,在签署后无法再运行 ZIPalign(必须在签名之前执行)。正因如此,现在,在编译过程中,Google 将压缩、调整和签署合并成一步完成。
v2 签名
v2 签名会在原先 APK 块中增加了一个新的块(签名块),新的块存储了签名、摘要、签名算法、证书链和额外属性等信息,这个块有特定的格式。最终的签名APK其实就有四块:头文件区、V2签名块、中央目录、尾部。下图是V1签名和V2签名的组成。
整个签名块的格式如下:
- size of block,以字节数(不含此字段)计 (uint64)
- 带 uint64 长度前缀的“ID-值”对序列:
- size of block,以字节数计 – 与第一个字段相同 (uint64)
- magic“APK 签名分块 42”(16 个字节)
在多个“ID-值”对中,APK签名信息的 ID 为 0x7109871a,包含的内容如下: 带长度前缀的 signer:
- 带长度前缀的 signed data,包含digests序列,X.509 certificates 序列,additional attributes序列
- 带长度前缀的 signatures(带长度前缀)序列
- 带长度前缀的 public key(SubjectPublicKeyInfo,ASN.1 DER 形式)
- value可能会包含多个 signer,因为Android允许多个签名。
总结一下:一个签名块,可以包含多个ID-VALUE,APK的签名信息会存放在 ID 为 0x7109871a的键值对里。他的内容可以包含多个签名者的签名信息,每个签名信息下包含signed data、signatures、public key,其中,signed data主要存放摘要序列、证书链、额外属性,signatures包含多个签名算法计算出来的签名值,public key表示签名者公钥,用于校验的时候验证签名的。
签名过程
首先,说一下 APK 摘要计算规则,对于每个摘要算法,计算结果如下:
- 将 APK 中文件 ZIP 条目的内容、ZIP 中央目录、ZIP 中央目录结尾按照 1MB 大小分割成一些小块。
- 计算每个小块的数据摘要,数据内容是 0xa5 块字节长度 块内容。
- 计算整体的数据摘要,数据内容是 0x5a 数据块的数量 每个数据块的摘要内容
总之,就是把 APK 按照 1M 大小分割,分别计算这些分段的摘要,最后把这些分段的摘要在进行计算得到最终的摘要也就是 APK 的摘要。然后将 APK 的摘要 数字证书 其他属性生成签名数据写入到 APK Signing Block 区块。
签名校验过程
接下来我们来看一下v2签名的校验过程,整体大概流程如下图所示。
其中, v2 签名机制是在 Android 7.0 以及以上版本才支持的。因此对于 Android 7.0 以及以上版本,在安装过程中,如果发现有 v2 签名块,则必须走 v2 签名机制,不能绕过。否则降级走 v1 签名机制。v1 和 v2 签名机制是可以同时存在的,其中对于 v1 和 v2 版本同时存在的时候,v1 版本的 META_INF 的 .SF 文件属性当中有一个 X-Android-APK-Signed 属性。
代码语言:javascript复制X-Android-APK-Signed: 2
之前的渠道包生成方案是通过在 META-INF 目录下添加空文件,用空文件的名称来作为渠道的唯一标识。但在新的应用签名方案下 META-INF 已经被列入了保护区了,向 META-INF 添加空文件的方案会对区块 1、3、4 都会有影响。对于这个问题,可以参考美团多渠道打包总结。
V3签名
新版v3签名在v2的基础上,仍然采用检查整个压缩包的校验方式。不同的是在签名部分增可以添加新的证书(Attr块)。在这个新块中,会记录我们之前的签名信息以及新的签名信息,以密钥转轮的方案,来做签名的替换和升级。这意味着,只要旧签名证书在手,我们就可以通过它在新的 APK 文件中,更改签名。
v3 签名新增的新块(attr)存储了所有的签名信息,由更小的 Level 块,以链表的形式存储。
其中每个节点都包含用于为之前版本的应用签名的签名证书,最旧的签名证书对应根节点,系统会让每个节点中的证书为列表中下一个证书签名,从而为每个新密钥提供证据来证明它应该像旧密钥一样可信。
签名校验过程
Android 的签名方案,无论怎么升级,都是要确保向下兼容。因此,在引入 v3 方案后,Android 9.0 及更高版本中,可以根据 APK 签名方案,v3 – v2 – v1 依次尝试验证 APK。而较旧的平台会忽略 v3 签名并尝试 v2 签名,最后才去验证 v1 签名。 整个验证的过程,如下图:
需要注意的是,对于覆盖安装的情况,签名校验只支持升级,而不支持降级。也就是说设备上安装了一个使用 v1 签名的 APK,可以使用 v2 签名的 APK 进行覆盖安装,反之则不允许。
以上就是本文的全部内容,希望对大家的学习有所帮助。