對于 Android 開發(fā)者而言, APK 簽名的重要性不言而喻。Android 7.0 后 APK 簽名已經(jīng)從基于 Jar 簽名的 V1 版本升級到了 V2 版本,為了能更好的理解,我們將從 V1、V2、簽名驗(yàn)證三個方面進(jìn)行詳細(xì)、深入介紹,但是鑒于篇幅原因,本文先介紹 V1 版簽名原理。
一、重要概念
1、散列算法
Wiki 定義[1]:
Ahash functionis 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 calledhash values,hash codes,digests, or simplyhashes.
也就是說散列函數(shù)(通常也叫散列算法)可以把任意長度的數(shù)據(jù)映射成固定長度的數(shù)據(jù),映射出來的數(shù)據(jù)稱為散列值、哈希值、摘要。因?yàn)檩斎霐?shù)據(jù)不同,得到的散列值不同(很大概率),所以可當(dāng)做輸入數(shù)據(jù)的指紋。常見的散列算法[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.
加密就是把可讀的明文數(shù)據(jù)通過加密算法轉(zhuǎn)換成不可讀的密文數(shù)據(jù),只有通過相應(yīng)的解密算法才能把密文數(shù)據(jù)轉(zhuǎn)換成明文數(shù)據(jù),常見的加密算法分為對稱加密和非對稱加密。
對稱加密就是加密和解密都用同一把“鑰匙”。
舉個栗子,小明有一把普通鎖,這把鎖能且只能用同一把鑰匙(暫且稱為 K )上鎖和開鎖:假如小明用鑰匙 K 上了鎖,他的朋友小紅要打開這個鎖,那么只能事先讓小明配一把一樣的鑰匙 K' 給她。
非對稱加密就是加密和解密用的是兩把不同的“鑰匙”。
舉個栗子,小明有一把神奇鎖,和普通鎖不同的是,這把神奇鎖必須要借助兩把不同的鑰匙(暫且稱為鑰匙 A 和 B)才能完成上鎖和開鎖:假如小明用鑰匙 A 上了鎖,那么用鑰匙 A 已經(jīng)不能開鎖了,能且只能用與之相對應(yīng)的鑰匙 B 開鎖,反過來也一樣,而且鑰匙 A 和鑰匙 B 是一一對應(yīng)關(guān)系。
實(shí)際應(yīng)用中,小明自己留著鑰匙 A 而且保密,然后把鑰匙 B 掛在上了鎖的箱子外面一起寄送出去,收到箱子的人就可以用鑰匙 B 來打開。因?yàn)?b>只有通過鑰匙 A 上鎖的箱子才能被鑰匙 B 打開,這就保證了箱子確實(shí)是用鑰匙 A 上鎖后寄過來的。
非對稱加密應(yīng)用非常廣泛,有 SSL、SSH 以及非常火的比特幣。
以下要介紹的 APK 簽名會用到使用最廣泛的非對稱加密算法 ——RSA。
3、數(shù)字簽名
Wiki 定義[5]:
A digital signature is a mathematical scheme for demonstrating the authenticity of digital messages or documents.
數(shù)字簽名就是證明數(shù)據(jù)真實(shí)性的一種方式。
上面講非對稱加密時舉例用的是箱子,如果把箱子換成一段數(shù)據(jù)的指紋(SHA-256),那么對數(shù)據(jù)指紋加密的結(jié)果實(shí)際上就是其數(shù)字簽名。
數(shù)字簽名只能通過鑰匙 B(公鑰)解密,那么如何保證和小明手上的鑰匙 A(私鑰)成對呢?也就是如何保證這份簽名來自小明?這個時候就需要公鑰證書出場了。
4、公鑰證書(數(shù)字證書)
還是 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.
公鑰證書就是證明公鑰的所有者,證書包括了公鑰信息、公鑰所有者信息、證書簽發(fā)者信息等;而公鑰證書的真實(shí)性由證書頒發(fā)機(jī)構(gòu) —— CA 來保證(CA 證書一般內(nèi)置在各類操作系統(tǒng)中)。
繼續(xù)上面的例子,小明把鑰匙 B 不是直接掛在箱子外面而是加密后放入公鑰證書中,再把證書掛在箱子外一起寄送出去,接收者通過系統(tǒng)內(nèi)置 CA 證書的公鑰解密得到鑰匙 B(公鑰),再去開鎖。這樣就保證了鑰匙 B 確實(shí)是小明的,箱子也確實(shí)是小明用鑰匙 A 上鎖的,而且箱子沒有被人動過手腳。
APK 簽名原理和上述 4 個概念息息相關(guān),一份經(jīng)過簽名的數(shù)據(jù),包含原始數(shù)據(jù)、數(shù)字簽名、公鑰證書三個部分。用一張圖[7]來總結(jié)一下:

數(shù)字簽名原理
二、APK V1 簽名原理(前方高能警告,將出現(xiàn)大量 jarsigner 源碼細(xì)節(jié))
1、簽名工具
APK 簽名可以用 jarsigner 或者 signapk 兩個工具,Android Studio 默認(rèn)用的是 signapk,二者主要的區(qū)別在于證書和秘鑰存儲的格式不同,前者是通過 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 文件名會根據(jù) alias 來定),這三個文件各司其職,最終構(gòu)成了 APK 簽名信息。
2、原理分析
我們來分析一下 jarsigner 源碼[8](signapk.jar 與其類似),看看這三個文件是如何生成的。
首先看main函數(shù),只做了一件事情:調(diào)用run方法。

`main` 函數(shù)
run主要做了 4 件事,前面都是一些準(zhǔn)備工作,最后調(diào)用signJar方法開始進(jìn)行簽名。

`run` 方法
signJar方法中經(jīng)歷了幾個步驟,詳細(xì)可以看如下截圖中的注釋,最重要的是計算META-INF/MANIFEST.MF清單文件、META-INF/.SF* 待簽名文件、META-INF/.RSA* 簽名結(jié)果文件。

signJar 簽名流程
下面,將對每個步驟詳細(xì)展開。
2.1、計算并寫入 META-INF/MANIFEST.MF 清單文件
我們先來認(rèn)識一下manifest文件:來自于 jar 包的文件清單,在 apk 中我們用來記錄所有非目錄文件的數(shù)據(jù)指紋,如下圖所示。

MANIFEST.MF 文件內(nèi)容
從文件開頭到第一個空行之間(圖中的 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 下簽名相關(guān)的如.MF SIG-*.SF *.DSA? *.RSA *.EC文件),進(jìn)行如下判斷:
如果 Manifest 清單中沒有出現(xiàn),那么計算 hash 然后增加到 Manifest 中;
如果 Manifest 清單中已經(jīng)包含,那么計算 hash 后和 Manifest 中的 hash 進(jìn)行對比覆蓋。
其中最重要的是獲取 hash 屬性的方法getDigestAttributes。

生成 MANIFEST.MF 條目屬性
先調(diào)用getDigests生成指紋,再封裝成 manifest 條目的屬性對象。

計算指紋
總結(jié)一下就是,針對每個待簽名 zip 包中的文件(除了META-INF 下簽名相關(guān)的如.MF SIG-*.SF *.DSA? *.RSA *.EC文件),計算其數(shù)據(jù)指紋并寫入 META-INF/MANIFEST.MF 清單文件中。
至此,.MF 文件完成計算并寫入。
2.2、計算并寫入 META-INF/*.SF 簽名文件
這里的 .SF 文件實(shí)際上也是清單文件的一種,它是對上述 MANIFEST.MF 文件的數(shù)據(jù)指紋,我們來看看它的內(nèi)容。

.SF 文件實(shí)際上也是清單文件
從上面對 MANIFEST.MF 文件內(nèi)容的分析,我們可以看出來 SF 包含了文件屬性和一系列條目。
a.Signature-Version是簽名版本。
b.SHA-256-Digest-Manifest-Main-Attributes是 MANIFEST.MF文件屬性的數(shù)據(jù)指紋 Base64 值。
c.SHA-256-Digest-Manifest是整個 MANIFEST.MF 的數(shù)據(jù)指紋 Base64 值。
d.Created-By指明文件生成工具。
e. 第 7 行開始的各個條目,就是對 MANIFEST.MF 各個條目的數(shù)據(jù)指紋 Base64 值。
如果把 MANIFEST.MF 當(dāng)做是對 APK 中各個文件的 hash 記錄,那么 *.SF 就是 MANIFEST.MF 及其各個條目的 hash 記錄。
我們來看看其生成過程:

.SF 文件計算并寫入
先創(chuàng)建了SignatureFile對象,再寫入ZipOutputStream中,關(guān)鍵在于SignatureFile,我們繼續(xù)看其構(gòu)造函數(shù)。

.SF 文件計算
其實(shí)就兩步,一是寫屬性,二是寫條目。而指紋的計算都是通過 Java 的ManifestDigester及ManifestDigester.Entry對象來完成。
至此, .SF 文件完成計算并寫入。
2.3、計算并寫入 META-INF/*.RSA 簽名結(jié)果文件
.RSA 是 PKCS#7[9]標(biāo)準(zhǔn)格式的文件,我們只關(guān)心它所保存的以下兩種數(shù)據(jù):
a. 用私鑰對 .SF 文件指紋進(jìn)行非對稱加密后得到的加密數(shù)據(jù)
b. 攜帶公鑰以及各種身份信息的數(shù)字證書
來看看上述兩種數(shù)據(jù):
2.3.1 加密數(shù)據(jù)
通過 openssl asn1parse 格式化查看加密后的數(shù)據(jù)及其偏移量,從下圖中可以看出加密后數(shù)據(jù)處在 PKCS#7 的最后。
![PKCS#7 簡要數(shù)據(jù)結(jié)構(gòu)[10]](http://upload-images.jianshu.io/upload_images/300515-6fe37c6e1e0dfa08.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
執(zhí)行openssl asn1parse -i -inform der -in STRANGEW.RSA得到如下 ASN1[11]格式數(shù)據(jù):

ASN1 格式數(shù)據(jù)
最后這一段就是加密后的 16 進(jìn)制數(shù)據(jù)了,我們來分析一下。
1115是字節(jié)偏移量(十進(jìn)制)
d=5表示所處 PKCS#7 數(shù)據(jù)結(jié)構(gòu)的層級是第 5 層
hl=4表示頭所占字節(jié)數(shù)為 4 個字節(jié)
l=256表示數(shù)據(jù)字節(jié)數(shù)為 256(對應(yīng) SHA-256 指紋算法)
執(zhí)行dd if=STRANGEW.RSA of=signed-sha256.bin bs=1 skip=$[ 1115 + 4 ] count=256把加密數(shù)據(jù)導(dǎo)出來到signed-sha256.bin文件。
執(zhí)行hexdump -C signed-sha256.bin查看文件數(shù)據(jù):

.RSA 中的 16 進(jìn)制加密數(shù)據(jù)
2.3.2 數(shù)字證書
執(zhí)行openssl pkcs7 -inform DER -in META-INF/CERT.RSA -noout -print_certs -text查看 .RSA 中保存的證書信息。截圖中可以看到證書包含了簽名算法、有效期、證書主體、證書簽發(fā)者、公鑰等信息。

.RSA 中的數(shù)字證書
如何把加密數(shù)據(jù)和證書放到 .RSA 文件中的呢?
2.3.3 .RSA 文件計算過程
從源碼中可以看出先是調(diào)用sf.generateBlock生成Block靜態(tài)內(nèi)部類的對象,最后寫入ZipOutputStream。

.RSA 文件計算并寫入
核心在于sf.generateBlock,其中只是執(zhí)行了return new Block(...),所以關(guān)鍵還是Block的構(gòu)造函數(shù):

.RSA 文件計算
從源碼中可以看出,主要是根據(jù)私鑰算法得到對應(yīng)的簽名算法,再用簽名算法對待簽名數(shù)據(jù)(這里是 .SF 文件)進(jìn)行簽名,最后把簽名數(shù)據(jù)和證書放在一起生成字節(jié)數(shù)組。
此致,.RSA 文件完成計算并寫入。
2.4、寫入除 .MF .RSA .SF 文件之外的所有文件
分為兩步,先寫入 META-INF 目錄內(nèi)的,再寫入 META-INF 目錄外的。writeEntry方法比較簡單,實(shí)際上是完成了文件從zipFile到ZipOutputStream的復(fù)制。

寫入簽名相關(guān)文件之外的文件
三、總結(jié)
最后總結(jié)一下 MANIFEST.MF、CERT.SF、CERT.RSA 如何各司其職構(gòu)成了 APK 的簽名:
a. 解析出 CERT.RSA 文件中的證書、公鑰,解密 CERT.RSA 中的加密數(shù)據(jù)
b. 解密結(jié)果和 CERT.SF 的指紋進(jìn)行對比,保證 CERT.SF 沒有被篡改
c. 而 CERT.SF 中的內(nèi)容再和 MANIFEST.MF 指紋對比,保證 MANIFEST.MF 文件沒有被篡改
d. MANIFEST.MF 中的內(nèi)容和 APK 所有文件指紋逐一對比,保證 APK 沒有被篡改
作者:Cavabiao
鏈接:http://www.itdecent.cn/p/a27783a713f2
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。