Android APK V1 签名原理

阿凡达2018-07-06 12:58

对于 Android 开发者而言, APK 签名的重要性不言而喻。Android 7.0 后 APK 签名已经从基于 Jar 签名的 V1 版本升级到了 V2 版本,为了能更好的理解,我们将从 V1、V2、签名验证三个方面进行详细、深入介绍,但是鉴于篇幅原因,本文先介绍 V1 版签名原理。

一、重要概念

1、散列算法

Wiki 定义^ref

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.

也就是说散列函数(通常也叫散列算法)可以把任意长度的数据映射成固定长度的数据,映射出来的数据称为散列值哈希值摘要。因为输入数据不同,得到的散列值不同(很大概率),所以可当做输入数据的指纹。常见的散列算法^ref2有 MD5、SHA-1、SHA-256,以下要介绍的 APK 签名会用到 SHA-265^ref3 散列算法。

2、加密

Wiki 定义^ref4

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 定义^ref5

A digital signature is a mathematical scheme for demonstrating the authenticity of digital messages or documents.

数字签名就是证明数据真实性的一种方式。

上面讲非对称加密时举例用的是箱子,如果把箱子换成一段数据的指纹(SHA-256),那么对数据指纹加密的结果实际上就是其数字签名。

数字签名只能通过钥匙 B(公钥)解密,那么如何保证和小明手上的钥匙 A(私钥)成对呢?也就是如何保证这份签名来自小明?这个时候就需要公钥证书出场了。

4、公钥证书(数字证书)

还是 Wiki 定义^ref6

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 个概念息息相关,一份经过签名的数据,包含原始数据、数字签名、公钥证书三个部分。用一张图^ref7来总结一下:



二、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.MFCERT.SFCERT.RSA(如果是 jarsigner 签名 .SF 和 .RSA 文件名会根据 alias 来定),这三个文件各司其职,最终构成了 APK 签名信息。

2、原理分析

我们来分析一下 jarsigner 源码^ref8(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 的 ManifestDigesterManifestDigester.Entry 对象来完成。

至此, .SF 文件完成计算并写入。

2.3、计算并写入 META-INF/*.RSA 签名结果文件

.RSA 是 PKCS#7^ref9 标准格式的文件,我们只关心它所保存的以下两种数据:

a. 用私钥对 .SF 文件指纹进行非对称加密后得到的 加密数据 b. 携带公钥以及各种身份信息的 数字证书

来看看上述两种数据:

2.3.1 加密数据

通过 openssl asn1parse 格式化查看加密后的数据及其偏移量,从下图中可以看出加密后数据处在 PKCS#7 的最后。

[^ref10]" src="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^ref11 格式数据:

最后这一段就是加密后的 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方法比较简单,实际上是完成了文件从 zipFileZipOutputStream的复制。


三、总结

最后总结一下 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 没有被篡改

本文来自网易实践者社区,经作者黄仕彪授权发布。