对于 Android 开发者而言, APK 签名的重要性不言而喻。Android 7.0 后 APK 签名已经从基于 Jar 签名的 V1 版本升级到了 V2 版本,为了能更好的理解,我们将从 V1、V2、签名验证三个方面进行详细、深入介绍,但是鉴于篇幅原因,本文先介绍 V1 版签名原理。
一、重要概念
1、散列算法
Wiki 定义[1]:
A hash function is any function that can be used to map data of arbitrary size to data of fixed size. The values returned by a hash function are called hash values, hash codes, digests, or simply hashes.
也就是说散列函数(通常也叫散列算法)可以把任意长度的数据映射成固定长度的数据,映射出来的数据称为散列值、哈希值、摘要。因为输入数据不同,得到的散列值不同(很大概率),所以可当做输入数据的指纹。常见的散列算法[2]有 MD5、SHA-1、SHA-256,以下要介绍的 APK 签名会用到 SHA-265[3] 散列算法。
2、加密
Wiki 定义[4]:
In cryptography, encryption is the process of encoding a message or information in such a way that only authorized parties can access it.
加密就是把可读的明文数据通过加密算法转换成不可读的密文数据,只有通过相应的解密算法才能把密文数据转换成明文数据,常见的加密算法分为对称加密和非对称加密。
对称加密就是加密和解密都用同一把“钥匙”。
举个栗子,小明有一把普通锁,这把锁能且只能用同一把钥匙(暂且称为 K )上锁和开锁:假如小明用钥匙 K 上了锁,他的朋友小红要打开这个锁,那么只能事先让小明配一把一样的钥匙 K' 给她。
非对称加密就是加密和解密用的是两把不同的“钥匙”。
举个栗子,小明有一把神奇锁,和普通锁不同的是,这把神奇锁必须要借助两把不同的钥匙(暂且称为钥匙 A 和 B)才能完成上锁和开锁:假如小明用钥匙 A 上了锁,那么用钥匙 A 已经不能开锁了,能且只能用与之相对应的钥匙 B 开锁,反过来也一样,而且钥匙 A 和钥匙 B 是一一对应关系。
实际应用中,小明自己留着钥匙 A 而且保密,然后把钥匙 B 挂在上了锁的箱子外面一起寄送出去,收到箱子的人就可以用钥匙 B 来打开。因为只有通过钥匙 A 上锁的箱子才能被钥匙 B 打开,这就保证了箱子确实是用钥匙 A 上锁后寄过来的。
非对称加密应用非常广泛,有 SSL、SSH 以及非常火的比特币。
以下要介绍的 APK 签名会用到使用最广泛的非对称加密算法 —— RSA。
3、数字签名
Wiki 定义[5]:
A digital signature is a mathematical scheme for demonstrating the authenticity of digital messages or documents.
数字签名就是证明数据真实性的一种方式。
上面讲非对称加密时举例用的是箱子,如果把箱子换成一段数据的指纹(SHA-256),那么对数据指纹加密的结果实际上就是其数字签名。
数字签名只能通过钥匙 B(公钥)解密,那么如何保证和小明手上的钥匙 A(私钥)成对呢?也就是如何保证这份签名来自小明?这个时候就需要公钥证书出场了。
4、公钥证书(数字证书)
还是 Wiki 定义[6]:
In cryptography, a public key certificate, also known as a digital certificate or identity certificate, is an electronic document used to prove the ownership of a public key.
公钥证书就是证明公钥的所有者,证书包括了公钥信息、公钥所有者信息、证书签发者信息等;而公钥证书的真实性由证书颁发机构 —— CA 来保证(CA 证书一般内置在各类操作系统中)。
继续上面的例子,小明把钥匙 B 不是直接挂在箱子外面而是加密后放入公钥证书中,再把证书挂在箱子外一起寄送出去,接收者通过系统内置 CA 证书的公钥解密得到钥匙 B(公钥),再去开锁。这样就保证了钥匙 B 确实是小明的,箱子也确实是小明用钥匙 A 上锁的,而且箱子没有被人动过手脚。
APK 签名原理和上述 4 个概念息息相关,一份经过签名的数据,包含原始数据、数字签名、公钥证书三个部分。用一张图[7]来总结一下:
二、APK V1 签名原理(前方高能警告,将出现大量 jarsigner 源码细节)
1、签名工具
APK 签名可以用 jarsigner 或者 signapk 两个工具,Android Studio 默认用的是 signapk,二者主要的区别在于证书和秘钥存储的格式不同,前者是通过 Java KeyStore(.jks 文件或者 .keystore 文件) 格式,后者分别用 .pem 和 .pk8 格式来存储证书和密钥。
Java KeyStore 生成方式:
【生成】证书库
keytool -genkey -v -keystore strange.keystore -alias strange -keyalg RSA -keysize 2048 -validity 10000
【查看】证书库
keytool -list -v -keystore {path2jks} -storepass “pass"
.pem .pk8 生成方式:
【生成】密钥
openssl genrsa -out key.pem 2048
【生成】证书请求
openssl req -new -key key.pem -out request.pem
【生成】 pem格式的 x.509 证书
openssl x509 -req -days 10000 -in request.pem -signkey key.pem -out certificate.pem -sha256
【生成】 pk8 格式密钥
openssl pkcs8 -topk8 -outform DER -in key.pem -inform PEM -out key.pk8 -nocrypt
【查看】pem证书
openssl x509 -in publicKey.x509.pem -text -noout
无论是用的哪种签名方式,最终都是在 META-INF 目录下生成三个文件:MANIFEST.MF、CERT.SF、CERT.RSA(如果是 jarsigner 签名 .SF 和 .RSA 文件名会根据 alias 来定),这三个文件各司其职,最终构成了 APK 签名信息。
2、原理分析
我们来分析一下 jarsigner 源码[8](signapk.jar 与其类似),看看这三个文件是如何生成的。
首先看 main
函数,只做了一件事情:调用 run
方法。
run
主要做了 4 件事,前面都是一些准备工作,最后调用 signJar
方法开始进行签名。
signJar
方法中经历了几个步骤,详细可以看如下截图中的注释,最重要的是计算 META-INF/MANIFEST.MF 清单文件、META-INF/.SF* 待签名文件、META-INF/.RSA* 签名结果文件。
下面,将对每个步骤详细展开。
2.1、计算并写入 META-INF/MANIFEST.MF 清单文件
我们先来认识一下 manifest 文件:来自于 jar 包的文件清单,在 apk 中我们用来记录所有非目录文件的 数据指纹,如下图所示。
从文件开头到第一个空行之间(图中的 1-3 行)是 manifest 文件主属性,从第 5 行开始就是其所包含的条目(entry)。
条目是由 条目名称 和 条目属性 组成,条目名称就是 Name:
之后的值如图中的res/drawable-xhdpi-v4/img_blank.png
,条目属性是一个name-value
格式的map
如图中的{"SHA-256-Digest":"ft47V9YtB/3V9uUqZbN4kTMP+SMJ2D3AK1j7G8lj9l0="}
。
其条目的生成过程如下:
针对每个待签名 zip 包中的文件(除了 META-INF 下签名相关的如 .MF SIG- *.SF *.DSA *.RSA *.EC文件),进行如下判断:
- 如果 Manifest 清单中没有出现,那么计算 hash 然后增加到 Manifest 中;
- 如果 Manifest 清单中已经包含,那么计算 hash 后和 Manifest 中的 hash 进行对比覆盖。
其中最重要的是获取 hash 属性的方法 getDigestAttributes
。
先调用 getDigests
生成指纹,再封装成 manifest 条目的属性对象。
总结一下就是,针对每个待签名 zip 包中的文件(除了 META-INF 下签名相关的如 .MF SIG- *.SF *.DSA *.RSA *.EC文件),计算其数据指纹并写入 META-INF/MANIFEST.MF 清单文件中。
至此,.MF 文件完成计算并写入。
2.2、计算并写入 META-INF/*.SF 签名文件
这里的 .SF 文件实际上也是清单文件的一种,它是对上述 MANIFEST.MF 文件的数据指纹,我们来看看它的内容。
从上面对 MANIFEST.MF 文件内容的分析,我们可以看出来 SF 包含了 文件属性 和一系列 条目 。
a. Signature-Version
是签名版本。
b. SHA-256-Digest-Manifest-Main-Attributes
是 MANIFEST.MF 文件属性 的数据指纹 Base64 值。
c. SHA-256-Digest-Manifest
是整个 MANIFEST.MF 的数据指纹 Base64 值。
d. Created-By
指明文件生成工具。
e. 第 7 行开始的各个条目,就是对 MANIFEST.MF 各个条目的数据指纹 Base64 值。
如果把 MANIFEST.MF 当做是对 APK 中各个文件的 hash 记录,那么 *.SF 就是 MANIFEST.MF 及其各个条目的 hash 记录。
我们来看看其生成过程:
先创建了 SignatureFile
对象,再写入 ZipOutputStream
中,关键在于SignatureFile
,我们继续看其构造函数。
其实就两步,一是写属性,二是写条目。而指纹的计算都是通过 Java 的 ManifestDigester
及 ManifestDigester.Entry
对象来完成。
至此, .SF 文件完成计算并写入。
2.3、计算并写入 META-INF/*.RSA 签名结果文件
.RSA 是 PKCS#7[9] 标准格式的文件,我们只关心它所保存的以下两种数据:
a. 用
私钥
对 .SF 文件指纹
进行非对称加密
后得到的 加密数据
b. 携带公钥
以及各种身份信息的 数字证书
来看看上述两种数据:
2.3.1 加密数据
通过 openssl asn1parse 格式化查看加密后的数据及其偏移量,从下图中可以看出加密后数据处在 PKCS#7 的最后。
![PKCS#7 简要数据结构[10]](http://upload-images.jianshu.io/upload_images/300515-6fe37c6e1e0dfa08.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
执行 openssl asn1parse -i -inform der -in STRANGEW.RSA
得到如下 ASN1[11] 格式数据:
最后这一段就是加密后的 16 进制数据了,我们来分析一下。
1115 是字节偏移量(十进制)
d=5 表示所处 PKCS#7 数据结构的层级是第 5 层
hl=4 表示头所占字节数为 4 个字节
l=256 表示数据字节数为 256(对应 SHA-256 指纹算法)
执行 dd if=STRANGEW.RSA of=signed-sha256.bin bs=1 skip=$[ 1115 + 4 ] count=256
把加密数据导出来到 signed-sha256.bin 文件。
执行 hexdump -C signed-sha256.bin
查看文件数据:
2.3.2 数字证书
执行 openssl pkcs7 -inform DER -in META-INF/CERT.RSA -noout -print_certs -text
查看 .RSA 中保存的证书信息。截图中可以看到证书包含了签名算法、有效期、证书主体、证书签发者、公钥等信息。
如何把加密数据和证书放到 .RSA 文件中的呢?
2.3.3 .RSA 文件计算过程
从源码中可以看出先是调用 sf.generateBlock
生成 Block
静态内部类的对象,最后写入 ZipOutputStream
。
核心在于 sf.generateBlock
,其中只是执行了return new Block(...)
,所以关键还是 Block
的构造函数:
从源码中可以看出,主要是根据私钥算法得到对应的签名算法,再用签名算法对待签名数据(这里是 .SF 文件)进行签名,最后把签名数据和证书放在一起生成字节数组。
此致,.RSA 文件完成计算并写入。
2.4、写入除 .MF .RSA .SF 文件之外的所有文件
分为两步,先写入 META-INF 目录内的,再写入 META-INF 目录外的。writeEntry
方法比较简单,实际上是完成了文件从 zipFile
到 ZipOutputStream
的复制。
三、总结
最后总结一下 MANIFEST.MF、CERT.SF、CERT.RSA 如何各司其职构成了 APK 的签名:
a. 解析出 CERT.RSA 文件中的证书、公钥,解密 CERT.RSA 中的加密数据
b. 解密结果和 CERT.SF 的指纹进行对比,保证 CERT.SF 没有被篡改
c. 而 CERT.SF 中的内容再和 MANIFEST.MF 指纹对比,保证 MANIFEST.MF 文件没有被篡改
d. MANIFEST.MF 中的内容和 APK 所有文件指纹逐一对比,保证 APK 没有被篡改