Android APK V1 签名原理

作者: Cavabiao | 来源:发表于2017-06-22 15:13 被阅读4677次

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

    2、原理分析

    我们来分析一下 jarsigner 源码[8](signapk.jar 与其类似),看看这三个文件是如何生成的。

    首先看 main 函数,只做了一件事情:调用 run 方法。

    `main` 函数

    run 主要做了 4 件事,前面都是一些准备工作,最后调用 signJar 方法开始进行签名。

    `run` 方法

    signJar 方法中经历了几个步骤,详细可以看如下截图中的注释,最重要的是计算 META-INF/MANIFEST.MF 清单文件、META-INF/.SF* 待签名文件、META-INF/.RSA* 签名结果文件。

    signJar 签名流程

    下面,将对每个步骤详细展开。

    2.1、计算并写入 META-INF/MANIFEST.MF 清单文件

    我们先来认识一下 manifest 文件:来自于 jar 包的文件清单,在 apk 中我们用来记录所有非目录文件的 数据指纹,如下图所示。

    MANIFEST.MF 文件内容
    从文件开头到第一个空行之间(图中的 1-3 行)是 manifest 文件主属性,从第 5 行开始就是其所包含的条目(entry)。

    条目是由 条目名称条目属性 组成,条目名称就是 Name:之后的值如图中的res/drawable-xhdpi-v4/img_blank.png,条目属性是一个name-value格式的map如图中的{"SHA-256-Digest":"ft47V9YtB/3V9uUqZbN4kTMP+SMJ2D3AK1j7G8lj9l0="}

    其条目的生成过程如下:


    MANIFEST.MF 计算

    针对每个待签名 zip 包中的文件(除了 META-INF 下签名相关的如 .MF SIG- *.SF *.DSA *.RSA *.EC文件),进行如下判断:

    • 如果 Manifest 清单中没有出现,那么计算 hash 然后增加到 Manifest 中;
    • 如果 Manifest 清单中已经包含,那么计算 hash 后和 Manifest 中的 hash 进行对比覆盖。

    其中最重要的是获取 hash 属性的方法 getDigestAttributes

    生成 MANIFEST.MF 条目属性

    先调用 getDigests 生成指纹,再封装成 manifest 条目的属性对象。

    计算指纹

    总结一下就是,针对每个待签名 zip 包中的文件(除了 META-INF 下签名相关的如 .MF SIG- *.SF *.DSA *.RSA *.EC文件),计算其数据指纹并写入 META-INF/MANIFEST.MF 清单文件中。

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

    2.2、计算并写入 META-INF/*.SF 签名文件

    这里的 .SF 文件实际上也是清单文件的一种,它是对上述 MANIFEST.MF 文件的数据指纹,我们来看看它的内容。

    .SF 文件实际上也是清单文件

    从上面对 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 记录。

    我们来看看其生成过程:

    .SF 文件计算并写入

    先创建了 SignatureFile 对象,再写入 ZipOutputStream 中,关键在于SignatureFile,我们继续看其构造函数。

    .SF 文件计算

    其实就两步,一是写属性,二是写条目。而指纹的计算都是通过 Java 的 ManifestDigesterManifestDigester.Entry 对象来完成。

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

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

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

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

    来看看上述两种数据:

    2.3.1 加密数据

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

    ![PKCS#7 简要数据结构[10]](https://img.haomeiwen.com/i300515/6fe37c6e1e0dfa08.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    执行 openssl asn1parse -i -inform der -in STRANGEW.RSA 得到如下 ASN1[11] 格式数据:

    ASN1 格式数据

    最后这一段就是加密后的 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 查看文件数据:

    .RSA 中的 16 进制加密数据
    2.3.2 数字证书

    执行 openssl pkcs7 -inform DER -in META-INF/CERT.RSA -noout -print_certs -text 查看 .RSA 中保存的证书信息。截图中可以看到证书包含了签名算法、有效期、证书主体、证书签发者、公钥等信息。

    .RSA 中的数字证书

    如何把加密数据和证书放到 .RSA 文件中的呢?

    2.3.3 .RSA 文件计算过程

    从源码中可以看出先是调用 sf.generateBlock 生成 Block 静态内部类的对象,最后写入 ZipOutputStream

    .RSA 文件计算并写入

    核心在于 sf.generateBlock,其中只是执行了return new Block(...),所以关键还是 Block 的构造函数:

    .RSA 文件计算

    从源码中可以看出,主要是根据私钥算法得到对应的签名算法,再用签名算法对待签名数据(这里是 .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 没有被篡改


    1. https://en.wikipedia.org/wiki/Hash_function

    2. https://en.wikipedia.org/wiki/List_of_hash_functions

    3. https://en.wikipedia.org/wiki/SHA-2

    4. https://en.wikipedia.org/wiki/Encryption

    5. https://en.wikipedia.org/wiki/Digital_signature

    6. https://en.wikipedia.org/wiki/Public_key_certificate

    7. https://zh.wikipedia.org/wiki/%E6%95%B8%E4%BD%8D%E7%B0%BD%E7%AB%A0

    8. http://hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/classes/sun/security/tools/jarsigner

    9. https://en.wikipedia.org/wiki/PKCS

    10. http://qistoph.blogspot.hk/2012/01/manual-verify-pkcs7-signed-data-with.html

    11. https://wiki.openssl.org/index.php/Manual:Asn1parse(1)

    相关文章

      网友评论

        本文标题:Android APK V1 签名原理

        本文链接:https://www.haomeiwen.com/subject/kjefkttx.html