自 Android 12 起,Android 執行階段 (ART) 模組即為 Mainline 模組。更新模組時,可能需要重建 bootclasspath jar 和系統伺服器的預先編譯 (AOT) 編譯成果。由於這些構件與安全性息息相關,Android 12 採用了「裝置端簽署」功能,以防這些構件遭到竄改。本頁面將介紹裝置端簽署架構,以及該架構與其他 Android 安全性功能的互動方式。
整體設計
裝置端簽署有兩個核心元件:
odrefresh
是 ART Mainline 模組的一部分。負責產生執行階段構件。它會根據已安裝的 ART 模組、bootclasspath jar 和系統伺服器 jar 版本,檢查現有構件,以判斷這些構件是否為最新版本,或是否需要重新產生。如果需要重新產生,odrefresh
會產生並儲存這些檔案。odsign
是 Android 平台的一部分,這個函式會在/data
分區掛載後,在早期啟動期間執行。其主要職責是叫用odrefresh
,以檢查是否需要產生或更新任何構件。對於odrefresh
產生的任何新或更新的構件,odsign
會計算雜湊函式。這種雜湊運算的結果稱為檔案摘要。對於任何現有的構件,odsign
會驗證現有構件的摘要與odsign
先前計算的摘要相符。這麼做可確保構件未遭到竄改。
在錯誤情況下 (例如檔案摘要不相符),odrefresh
和 odsign
會丟棄 /data
上的所有現有構件,並嘗試重新產生這些構件。如果失敗,系統會改回 JIT 模式。
odrefresh
和 odsign
受到 dm-verity
保護,並屬於 Android 的驗證開機程序鏈結。
使用 fs-verity 計算檔案摘要
fs-verity 是 Linux 核心的功能,可根據 Merlkle 樹狀圖驗證檔案資料。在檔案上啟用 fs-verity 後,檔案系統會使用 SHA-256 雜湊,在檔案資料上建立 Merkle 樹狀結構,並將其儲存在檔案旁的隱藏位置,並將檔案標示為唯讀。fs-verity 會根據需求,自動根據 Merkle 樹狀結構驗證檔案資料。fs-verity 會將 Merkle 樹狀結構的根雜湊設為 fs-verity 檔案摘要,並確保從檔案讀取的任何資料都與此檔案摘要一致。
odsign
會在開機時,針對裝置端編譯的構件進行最佳化加密驗證,藉此提升開機效能。產生構件時,odsign
會為其啟用 fs-verity。odsign
驗證構件時,會驗證 fs-verity 檔案摘要,而非完整檔案雜湊。這樣就不必在啟動期間讀取及雜湊建構的完整資料。相反地,當使用者使用時,fs-verity 會根據每個區塊逐一對映射資料進行雜湊運算。
如果裝置的核心不支援 fs-verity,odsign
會改為在使用者空間中計算檔案摘要。odsign
使用與 fs-verity 相同的 Merkle 樹狀結構雜湊演算法,因此兩者產生的摘要都相同。所有搭載 Android 11 以上版本的裝置都需要 fs-verity。
儲存檔案摘要
odsign
會將構件的檔案摘要儲存在名為 odsign.info
的獨立檔案中。為確保 odsign.info
不會遭到竄改,odsign.info
會使用具有重要安全性屬性的簽署金鑰進行簽署。具體來說,金鑰只能在早期啟動期間產生及使用,因為這時系統只會執行可信任的程式碼。詳情請參閱「可信任的簽署金鑰」。
驗證檔案摘要
每次開機時,如果 odrefresh
判斷現有的構件已更新,odsign
會確保檔案自產生以來未遭竄改。odsign
會透過驗證檔案摘要來執行這項操作。首先,它會驗證 odsign.info
的簽名。如果簽名有效,odsign
會驗證每個檔案的摘要是否與 odsign.info
中的對應摘要相符。
信任的簽署金鑰
Android 12 推出了名為「啟動階段金鑰」的新 Keystore 功能,可解決下列安全性疑慮:
- 如何防止攻擊者使用我們的簽署金鑰,為自己的
odsign.info
版本簽署? - 如何防止攻擊者產生自己的簽署金鑰,並使用該金鑰簽署自己的
odsign.info
版本?
啟動階段金鑰可將 Android 的啟動週期分成多個層級,並以加密編譯方式將金鑰的建立和使用作業與特定層級連結。odsign
會在早期層級建立簽署金鑰,此時只有受信任的程式碼正在執行,並透過 dm-verity
受到保護。
啟動階段層級的編號從 0 到魔術數字 1000000000。在 Android 的啟動程序中,您可以透過 init.rc
設定系統屬性,提高啟動層級。例如,下列程式碼會將啟動層級設為 10:
setprop keystore.boot_level 10
Keystore 的用戶端可以建立與特定啟動層級相關聯的金鑰。舉例來說,如果您為啟動層級 10 建立金鑰,則該金鑰只能在裝置處於啟動層級 10 時使用。
odsign
使用開機層級 30,而它建立的簽署金鑰會與該開機層級綁定。使用金鑰簽署構件前,odsign
會驗證金鑰是否與啟動層級 30 相關聯。
這麼做可避免本節前述的兩種攻擊:
- 攻擊者無法使用產生的金鑰,因為在攻擊者有機會執行惡意程式碼時,啟動層級已超過 30,而 KeyStore 會拒絕使用金鑰的作業。
- 攻擊者無法建立新金鑰,因為在攻擊者有機會執行惡意程式碼時,啟動層級已超過 30,而 Keystore 會拒絕建立具有該啟動層級的新金鑰。如果攻擊者建立的新金鑰未與啟動層級 30 相關聯,
odsign
就會拒絕該金鑰。
Keystore 可確保系統正確執行啟動層級。以下各節將進一步說明如何針對不同的 Keymaster 版本執行這項操作。
Keymaster 4.0 實作
不同版本的 Keymaster 會以不同方式處理引導階段金鑰的實作方式。在搭載 Keymaster 4.0 TEE/Strongbox 的裝置上,Keymaster 會以以下方式處理實作項目:
- 在第一次啟動時,Keystore 會建立對稱金鑰 K0,並將
MAX_USES_PER_BOOT
標記設為1
。也就是說,每個開機階段只能使用一次金鑰。 - 在啟動期間,如果啟動層級增加,您可以使用 HKDF 函式
Ki+i=HKDF(Ki, "some_fixed_string")
從 K0 產生該啟動層級的新金鑰。舉例來說,如果您從開機層級 0 移動到開機層級 10,HKDF 就會叫用 10 次,從 K0 衍生出 K10。 開機層級變更時,系統會從記憶體中刪除先前開機層級的金鑰,且與先前開機層級相關聯的金鑰將無法再使用。
鍵 K0 是
MAX_USES_PER_BOOT=1
鍵。這表示在稍後的開機階段也無法使用該鍵,因為至少會發生一次開機層級轉換 (至最終開機層級)。
當 odsign
等 Keystore 用戶端要求在啟動層級 i
中建立金鑰時,其 blob 會使用金鑰 Ki
進行加密。由於 Ki
在啟動階段 i
後就無法使用,因此無法在後續啟動階段建立或解密此金鑰。
Keymaster 4.1 和 KeyMint 1.0 實作
Keymaster 4.1 和 KeyMint 1.0 的實作方式與 Keymaster 4.0 的實作方式大致相同。主要差異在於 K0 不是 MAX_USES_PER_BOOT
金鑰,而是 EARLY_BOOT_ONLY
金鑰,後者是在 Keymaster 4.1 中推出。EARLY_BOOT_ONLY
金鑰只能在沒有不受信任的程式碼執行時,於啟動作業的早期階段使用。這可提供額外的保護層級:在 Keymaster 4.0 實作中,攻擊者若入侵檔案系統和 SELinux,便可修改 Keystore 資料庫,建立專屬的 MAX_USES_PER_BOOT=1
金鑰,用於簽署構件。由於 EARLY_BOOT_ONLY
金鑰只能在早期啟動期間建立,因此 Keymaster 4.1 和 KeyMint 1.0 實作無法執行這類攻擊。
信任簽署金鑰的公開元件
odsign
會從 Keystore 擷取簽署金鑰的公開金鑰元件。不過,Keystore 不會從儲存對應私密金鑰的 TEE/SE 擷取該公開金鑰。而是從自己的磁碟上資料庫中擷取公開金鑰。也就是說,攻擊者若入侵檔案系統,便可修改 KeyStore 資料庫,讓其包含由攻擊者控制的公開/私密金鑰組的公開金鑰。
為避免這種攻擊,odsign
會建立額外的 HMAC 金鑰,其啟動層級與簽署金鑰相同。接著,在建立簽署金鑰時,odsign
會使用這個 HMAC 金鑰建立公開金鑰的簽章,並將其儲存在磁碟上。在後續啟動時,當系統擷取簽署金鑰的公開金鑰時,會使用 HMAC 金鑰驗證磁碟上的簽章是否與擷取的公開金鑰簽章相符。如果兩者相符,則公開金鑰可信任,因為 HMAC 金鑰只能在早期啟動階段使用,因此不可能由攻擊者建立。