Android 應用簽名是應用打包過程的重要步驟之一,Google 要求所有的應用必須被簽名才可以安裝到 Android 作業系統中。Android 的簽名機制也為開發者識別和更新自己應用提供了方便。
數字摘要 Digital Digest
數字摘要主要作用是將任意長度的訊息使用單向 HASH 演算法摘要成一串固定長度的密文。常用的 HASH 演算法包括 SHA-1, SHA-256, MD5 等等,MD5 中的 MD 就是 Message Digest 的縮寫。數字摘要有時也被稱為數字指紋,訊息摘要等等,其實表達的都是一個意思。它有以下三個特點:
- 輸出長度固定 例如 MD5 演算法摘要資訊有 128 位元,而 SHA-1 有 160 位元
- 不考慮碰撞的情況下,只要輸入原始資料不同,摘要也不會相同。即使稍微改變輸出,摘要就會變得完全不同。相同的輸入也會產生相同的輸出
- 單向不可逆。從摘要無法恢復原始訊息

Keystore 檔案
Android 簽名需要用到一種字尾名為 keystore 的檔案。在打 Debug 包的時候,如果沒有在 build.gradle 檔案中指定的話,Gradle 就會自動為我們生成一個 keystore檔案,儲存在系統使用者根目錄 .android 資料夾內,名稱為 debug.keystore. 我們以它為例看看 keystore 檔案包含了什麼內容。 通過 keytool 工具來檢視,預設的密碼是 android. $ keytool -list -v -keystore debug.keystore -storepass android 結果如下:
金鑰庫型別: jks
金鑰庫提供方: SUN
您的金鑰庫包含 1 個條目
別名: androiddebugkey
建立日期: 2013-4-26
條目型別: PrivateKeyEntry
證書鏈長度: 1
證書[1]:
所有者: CN=Android Debug, O=Android, C=US
釋出者: CN=Android Debug, O=Android, C=US
序列號: 517a38f2
有效期為 Fri Apr 26 15:46:58 CST 2013 至 Sun Apr 19 15:46:58 CST 2043
證書指紋:
MD5: 37:09:10:A9:F1:AE:9C:E4:C0:85:B9:35:D9:93:93:52
SHA1: F1:60:3F:72:2A:F2:3A:BC:BE:1C:DB:F6:F4:5B:FD:5E:34:8C:01:A9
SHA256: 86:C7:CB:D1:56:E7:D8:B8:AD:67:A7:A1:8F:C0:F6:E6:FC:E1:3D:45:AE:BC:F5:DF:B4:A9:F9:9A:F7:89:F7:0D
簽名演算法名稱: SHA1withRSA
主體公共金鑰演算法: 1024 位 RSA 金鑰
版本: 3
複製程式碼
其實 keystore 類似一個鑰匙倉庫,裡面有證書的所有者和釋出者資訊,包含了私鑰和公鑰資訊,並設定了密碼進行保護。
公共金鑰系統 RSA
公鑰和私鑰都是公共金鑰系統裡的概念。最初所有的加密演算法都屬於對稱加密,也就是說加密和解密使用的相同的密碼,通訊雙方如何安全溝通和儲存密碼,是這種加密方法的主要難題。難保沒有豬隊友。 而在公共金鑰系統中,加密和解密使用的是不同的金鑰,分別稱為公鑰和私鑰,公鑰意思就是所有人都可以知道,私鑰則只有所有者才持有,單從公鑰無法在現有的計算能力下推匯出私鑰。這樣一來就不存在溝通過程中洩露金鑰的問題,只要私鑰不洩露,通訊就一直是安全的。 公共金鑰系統可以說是現在最最最重要金鑰系統,是網際網路的基石之一。 公共金鑰系統可以用來加密,也可以用來簽名。加密方案中,是不希望別人知道我的訊息,所以公鑰用於加密資訊,私鑰用於解密資訊;而簽名方案中,是不希望別人冒充我發訊息,只有我才能釋出這個簽名,所以需要用私鑰進行簽名,公鑰用於驗證簽名。
應用簽名
我們先來看看 Android 應用簽名發生在構建的哪一步。

在編譯過程中,編譯器首先會將原始碼和資源進行編譯,生成 DEX 檔案和一些編譯後的資原始檔,然後 APK Manager 會根據配置使用 keystore 檔案進行簽名,簽名後才會將所有資源壓縮到一個 ZIP 包裡,這個 ZIP 包其實我們安裝的時候用的 APK 檔案。可以看到簽名是在構建基本完成的時候發生的。
簽名過程
那 APK Manager 是如何使用 keystore 進行簽名的呢?我們一步一步看到底發生了什麼。
- 首先對編譯後生成的所有的檔案進行掃描,每個檔案生成一個數字摘要,儲存在 META-INF/MANIFEST.MF 檔案中
Name: res/drawable-xhdpi-v4/im_ic_keyboard_pressed.png
SHA-256-Digest: cqjOi3gUv9O0IBfgLOlIJUZTBwyCPcWbXIs/o6TMfTc
Name: classes.dex
SHA-256-Digest: FJCwLV1TyZuL1qxkDsJ6bXTmaSkK+JkKt5zmpDBc8Tg=
複製程式碼
我們看一下 im_ic_keyboard_pressed.png 這個檔案的數字摘要到底是如何計算出來的。
- 第一步對檔案進行 SHA-256 雜湊,得到一串 16 進位制的雜湊值。
$ shasum -a 256 im_ic_keyboard_pressed.png
72a8ce8b7814bfd3b42017e02ce948254653070c823dc59b5c8b3fa3a4cc7d37 im_ic_keyboard_pressed.png
複製程式碼
- 第二步我們將其轉換為二進位制格式並進行 base64 編碼
$ echo "72a8ce8b7814bfd3b42017e02ce948254653070c823dc59b5c8b3fa3a4cc7d37" | xxd -r -p | base64
cqjOi3gUv9O0IBfgLOlIJUZTBwyCPcWbXIs/o6TMfTc=
複製程式碼
可以看到跟我們在 MANIFEST.MF 中看到的值是能夠對上的。
-
對 MANIFEST.MF 檔案生成數字摘要,並寫入 CERT.SF,這裡有一個細節,除了對整個檔案做 HASH 外,還會將檔案分成多段計算 HASH 同樣儲存在 CERT.SF 檔案中
-
計算 CERT.SF 的數字摘要,並使用 RSA 私鑰進行加密,生成簽名
-
將簽名、公鑰、雜湊演算法資訊寫入 CERT.RSA 文檔案,並將這些檔案新增到 APK 壓縮包 META-INF 目錄中
目前應用簽名不需要申請可信的證書機構 (Certificate Authority) 簽發的證書,開發者可以通過 keytool 來建立自簽名的證書。
為什麼要簽名
應用簽名不能保證 APK 不被篡改,只是為了能夠校驗出 APK 是否被篡改。在系統安裝過程中,如果發現 APK 被篡改,安裝就會失敗。那系統是如何校驗的呢?
- 系統取得已安裝 APK 中儲存的公鑰,用它對新 APK 中的 CERT.RSA 儲存的簽名資訊進行解密;對 CERT.SF 檔案計算摘要,與上一步解密出來的資訊進行比對,如果不一致說明 CERT.SF 被篡改,拒絕安裝
- 對 MANIFEST.SF 檔案計算摘要,與 CERT.SF 檔案中的資訊進行對比,如果不一致,則說明 MANIFEST.SF 檔案被篡改,拒絕安裝
- 對 APK 內所有其他檔案計算數字摘要,如果檔案沒有出現在 MANIFEST.SF 或者摘要與 MANIFEST.SF 中包含的不相同,說明加入了新的檔案或者檔案被篡改,拒絕安裝
整個校驗過程中,環環相扣,從 CERT.RSA -> CERT.SF -> MANIFEST.SF -> All Other Files,只要有一環失敗,系統就會終止 APK 的安裝.
如果給新版應用分配了新的簽名檔案,那就必須更改包名,這樣系統才會認為是不同的應用。否則安裝就會失敗,提示簽名不一致。 INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES
簽名的其他用途
除了用於安裝時校驗應用,簽名還有一些別的用途。
- 應用模組化。Android 允許相同簽名的兩個應用使用同樣的 Linux UserId,這樣一來兩個應用就可以共享資料儲存了。同時如果應用申請的話,兩個應用還可以在同一個程式中執行。通過這種方式可以模組化部署應用,每個模組也能獨立的進行升級。猜想很多主題資源包可能就是通過這種方式來安裝的。
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="com.meng.sharedappid"
package="meng.mainuicomponents">
複製程式碼
- 共享程式碼資料。Android 提供了可以在使用同樣簽名的應用間共享程式碼的功能。另外一個前提也是兩個應用設定了使用同樣的 shareUserId。可以使用包名拿到兄弟 APP 的 Context,然後拿到 ClassLoader 載入兄弟 APP 的類,並可以例項化並通過反射來呼叫具體的方法。
val friendContext = this.createPackageContext("packageName", Context.CONTEXT_INCLUDE_CODE)
val friendClass = friendContext.classLoader.loadClass("packageName.className")
val noparams = arrayOf<Class<*>>() //say the function (functionName) required no inputs
friendClass.getMethod("functionName", *noparams).invoke(friendClass.newInstance(), null)
複製程式碼
- 用來宣告安全級別。比如隸屬同一個公司的多個應用實現共享登陸功能,可以各自實現自己的 ContentProvider,向外提供訪問本應用資料的介面,但這個介面需要限制不能被其他第三方的應用讀取,通過限制安全級別為簽名級別,系統就能保證只有相同簽名的應用才可以訪問到這個 ContentProvider。以下是在 AndroidManifest.xml 檔案中的使用示例。
<!-- 宣告許可權供兄弟 APP 使用 -->
<permission
android:name="com.xxx.permission.SHARED_ACCOUNT"
android:protectionLevel="signature" />
<!-- 申請獲取兄弟 APP 的宣告的許可權 -->
<uses-permission android:name="com.xxx.broapp1.permission.SHARED_ACCOUNT" />
<uses-permission android:name="com.xxx.broapp2.permission.SHARED_ACCOUNT" />
<!-- 定義 ContentProvider 來供兄弟 APP 獲取共享賬戶資訊,指定度許可權為簽名宣告的許可權 -->
<provider
android:name="com.xxx.XXXXSharedAccountProvider"
android:authorities="com.xxx.shared_account"
android:exported="true"
android:readPermission="com.xxx.permission.SHARED_ACCOUNT" />
複製程式碼
簽名機制的演進
V1
第一代是基於 JAR 檔案簽名,它主要的缺陷是隻保護了一部分檔案,而不是對整個 APK 檔案做保護。這是因為所有檔案都不可能包含了自身的簽名,因為它不可能為自己簽名後再把簽名資訊儲存到自己內部,這是一個雞生蛋蛋生雞的問題,因為這個問題的存在,第一代簽名機制會忽略所有以 .SF/.DSA/.RSA 的檔案以及 META-INFO 目錄下的所有檔案。 所以攻擊者就可以解壓縮後在 APK/META-INF 目錄新增一個含有惡意程式碼的檔案,然後再壓縮成 APK,同樣是可以覆蓋安裝正版應用的,這樣一來好好的應用就會被防毒軟體標記為惡意軟體,從而達到攻擊應用的目的。 除了容易被攻擊外,應用安裝起來也比較慢,因為安裝器在校驗時需要解壓計算所有檔案的數字摘要,確認沒有被惡意修改。
美團打渠道包的方法本質上就利用了這個第一代簽名的漏洞,在 META-INF 目錄下新建了一個包含 vendor 名稱的檔案,從而不需要重新編譯,只需要解壓縮 APK,新增檔案,重新壓縮就完成了一個渠道包的生成,速度非常快。
V2
Android 7.0 引入了第二代簽名,避免了第一代簽名模式的問題,主要改進在於它在驗證過程中,將整個 APK 檔案當作一個整體,只校驗 APK 檔案的簽名就可以了,從而一方面更嚴苛的避免了 APK 被篡改,另外一方面也不用加壓縮後對所有檔案進行校驗,從而極大提升了安裝速度。第二代簽名向後相容,使用新簽名的 APK 可以安裝到 <7.0 的系統上,但要求 APK 同時也進行 v1 的簽名。 具體來說,第二代簽名將整個 APK 檔案進行簽名,並將簽名資訊儲存在了 APK 檔案的的尾部 Central Directory 的前邊。它可以對第一三四,以及第二塊除了簽名部分的其他區域提供一致性保護。

在計算簽名的時候,會將這些部分的資料切割成 1MB 大小的 CHUNK,分別計算簽名,然後彙總後再計算一個總簽名,這麼做的主要目的是為了平行計算,加快速度。

為了避免攻擊者在 7.0 以上系統中繞過 v2 簽名機制(比如刪除 APK Signing Block?),v2 簽名要求如果 APK 同時提供了 V1 簽名的話,需要在 META-INF/*.SF 檔案中增加一行 X-Android-APK-Signed 屬性,這樣一來,支援 V2 簽名的系統在回滾到 V1 簽名的時候就會校驗是否存在這個屬性,如果存在,就會拒絕安裝 APK,這一切都是建立在 *.SF 檔案被 V1 簽名保護的基礎之上。
V3
Android 9.0 引入了第三代簽名機制,主要增加一個功能叫 APK key rotation. 意思是允許開發者在更新 APK 的時候更換籤名。簽名的主要機制跟 V2 其實是一樣的,只是重新設計了 APK Signing Block 的儲存結構,以支援更換籤名。這裡就不再細說,可以參考官方的 文件 下圖是安裝一個 APK 時,系統對三代簽名校驗的流程示意。
