Android APK V1 簽名原理

weixin_34050427發表於2017-06-22

對於 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]來總結一下:

300515-14176641027f5262.png
數字簽名原理

二、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 方法。

300515-82b5f9bec4b64f26.png
`main` 函式

run 主要做了 4 件事,前面都是一些準備工作,最後呼叫 signJar 方法開始進行簽名。

300515-0bee54df945f3fdd.png
`run` 方法

signJar 方法中經歷了幾個步驟,詳細可以看如下截圖中的註釋,最重要的是計算 META-INF/MANIFEST.MF 清單檔案、META-INF/.SF* 待簽名檔案、META-INF/.RSA* 簽名結果檔案。

300515-9f28e9bf3ac97d62.png
signJar 簽名流程

下面,將對每個步驟詳細展開。

2.1、計算並寫入 META-INF/MANIFEST.MF 清單檔案

我們先來認識一下 manifest 檔案:來自於 jar 包的檔案清單,在 apk 中我們用來記錄所有非目錄檔案的 資料指紋,如下圖所示。

300515-81d310470c8ce3c8.png
MANIFEST.MF 檔案內容

從檔案開頭到第一個空行之間(圖中的 1-3 行)是 manifest 檔案主屬性,從第 5 行開始就是其所包含的條目(entry)。

條目是由 條目名稱條目屬性 組成,條目名稱就是 Name:之後的值如圖中的res/drawable-xhdpi-v4/img_blank.png,條目屬性是一個name-value格式的map如圖中的{"SHA-256-Digest":"ft47V9YtB/3V9uUqZbN4kTMP+SMJ2D3AK1j7G8lj9l0="}

其條目的生成過程如下:


300515-3891971d0783cf71.png
MANIFEST.MF 計算

針對每個待簽名 zip 包中的檔案(除了 META-INF 下簽名相關的如 .MF SIG- *.SF *.DSA *.RSA *.EC檔案),進行如下判斷:

  • 如果 Manifest 清單中沒有出現,那麼計算 hash 然後增加到 Manifest 中;
  • 如果 Manifest 清單中已經包含,那麼計算 hash 後和 Manifest 中的 hash 進行對比覆蓋。

其中最重要的是獲取 hash 屬性的方法 getDigestAttributes

300515-54f3134400be963d.png
生成 MANIFEST.MF 條目屬性

先呼叫 getDigests 生成指紋,再封裝成 manifest 條目的屬性物件。

300515-e35685d53a78a262.png
計算指紋

總結一下就是,針對每個待簽名 zip 包中的檔案(除了 META-INF 下簽名相關的如 .MF SIG- *.SF *.DSA *.RSA *.EC檔案),計算其資料指紋並寫入 META-INF/MANIFEST.MF 清單檔案中。

至此,.MF 檔案完成計算並寫入。

2.2、計算並寫入 META-INF/*.SF 簽名檔案

這裡的 .SF 檔案實際上也是清單檔案的一種,它是對上述 MANIFEST.MF 檔案的資料指紋,我們來看看它的內容。

300515-9b0abba85d4230d5.png
.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 記錄。

我們來看看其生成過程:

300515-d224d9ca754f0928.png
.SF 檔案計算並寫入

先建立了 SignatureFile 物件,再寫入 ZipOutputStream 中,關鍵在於SignatureFile,我們繼續看其建構函式。

300515-97eb7fe8b7bd2e8a.png
.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]](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] 格式資料:

300515-9b8d7b559b335705.png
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 檢視檔案資料:

300515-c39032f28e5eefcd.png
.RSA 中的 16 進位制加密資料
2.3.2 數字證照

執行 openssl pkcs7 -inform DER -in META-INF/CERT.RSA -noout -print_certs -text 檢視 .RSA 中儲存的證照資訊。截圖中可以看到證照包含了簽名演算法、有效期、證照主體、證照籤發者、公鑰等資訊。

300515-17ded692b631f079.png
.RSA 中的數字證照

如何把加密資料和證照放到 .RSA 檔案中的呢?

2.3.3 .RSA 檔案計算過程

從原始碼中可以看出先是呼叫 sf.generateBlock 生成 Block 靜態內部類的物件,最後寫入 ZipOutputStream

300515-402ded1b9afdeda9.png
.RSA 檔案計算並寫入

核心在於 sf.generateBlock,其中只是執行了return new Block(...),所以關鍵還是 Block 的建構函式:

300515-c375decb6a01a014.png
.RSA 檔案計算

從原始碼中可以看出,主要是根據私鑰演算法得到對應的簽名演算法,再用簽名演算法對待簽名資料(這裡是 .SF 檔案)進行簽名,最後把簽名資料和證照放在一起生成位元組陣列。

此致,.RSA 檔案完成計算並寫入。

2.4、寫入除 .MF .RSA .SF 檔案之外的所有檔案

分為兩步,先寫入 META-INF 目錄內的,再寫入 META-INF 目錄外的。writeEntry方法比較簡單,實際上是完成了檔案從 zipFileZipOutputStream的複製。

300515-2aa52082169d435b.png
寫入簽名相關檔案之外的檔案

三、總結

最後總結一下 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)

相關文章