裝置端簽署架構

自 Android 12 起,Android 執行階段 (ART) 模組已成為 Mainline 模組。更新模組可能需要重建開機類路徑 JAR 和系統伺服器的預先 (AOT) 編譯構件。由於這些構件涉及安全性,Android 12 採用名為「裝置端簽署」的功能,防止這些構件遭到竄改。本頁面說明裝置端簽署架構,以及該架構與其他 Android 安全性功能的互動。

整體設計

裝置端簽署包含兩個核心元件:

  • odrefresh 是 ART Mainline 模組的一部分。負責產生執行階段構件。這項工具會根據已安裝的 ART 模組版本、bootclasspath JAR 和系統伺服器 JAR,檢查現有構件是否為最新版本,或是否需要重新產生。如果需要重新產生,odrefresh 會產生並儲存這些代碼。

  • odsign 是 Android 平台的一部分,這個指令會在早期啟動期間執行,也就是掛接 /data 分區後。主要職責是叫用 odrefresh,檢查是否需要產生或更新任何構件。對於 odrefresh 生成的任何新或更新構件,odsign 會計算雜湊函式。這類雜湊運算的結果稱為「檔案摘要」。對於任何現有構件,odsign 會驗證現有構件的摘要是否與 odsign 先前計算的摘要相符。確保構件未遭到竄改。

如果發生錯誤 (例如檔案的摘要不符),odrefreshodsign 會捨棄 /data 上的所有現有構件,並嘗試重新產生。如果失敗,系統會改回 JIT 模式。

odrefresh」和「odsign」受到「dm-verity」保護,且屬於 Android 驗證開機程序鏈結的一部分。

使用 fs-verity 計算檔案摘要

fs-verity 是 Linux 核心的一項功能,可根據 Merkle 樹狀結構驗證檔案資料。在檔案上啟用 fs-verity 後,檔案系統會使用 SHA-256 雜湊,在檔案資料上建構 Merkle 樹狀結構,並將其儲存在檔案旁的隱藏位置,然後將檔案標示為唯讀。fs-verity 會在讀取檔案時,自動根據 Merkle 樹狀結構驗證檔案資料。fs-verity 會將 Merkle 樹狀結構的根雜湊做為值 (稱為 fs-verity 檔案摘要),並確保從檔案讀取的任何資料都與這個檔案摘要一致。

odsign 會使用 fs-verity,在啟動時最佳化裝置上編譯構件的加密驗證,進而提升啟動效能。生成構件時,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,而金鑰儲存區會拒絕使用該金鑰的作業。
  • 攻擊者無法建立新金鑰,因為當攻擊者有機會執行惡意程式碼時,啟動層級已超過 30,而 Keystore 會拒絕以該啟動層級建立新金鑰。如果攻擊者建立的新金鑰未與啟動層級 30 繫結,odsign 會拒絕該金鑰。

金鑰儲存區可確保系統正確強制執行啟動層級。以下各節會進一步詳細說明如何針對不同 KeyMint (先前為 Keymaster) 版本執行這項操作。

導入 Keymaster 4.0

不同版本的 Keymaster 處理開機階段金鑰的實作方式不同。在搭載 Keymaster 4.0 TEE/StrongBox 的裝置上,Keymaster 會依下列方式處理實作項目:

  1. 首次啟動時,金鑰儲存區會建立對稱金鑰 K0,並將 MAX_USES_PER_BOOT 標記設為 1。也就是說,每個啟動程序只能使用一次金鑰。
  2. 在啟動期間,如果啟動層級提高,可以使用 HKDF 函式 Ki+i=HKDF(Ki, "some_fixed_string"),從 K0 為該啟動層級產生新金鑰。舉例來說,如果從開機層級 0 移至開機層級 10,系統會叫用 HKDF 10 次,從 K0 衍生 K10。
  3. 開機層級變更時,系統會從記憶體中清除先前開機層級的金鑰,且與先前開機層級相關聯的金鑰將無法再使用。

    金鑰 K0 是 MAX_USES_PER_BOOT=1 金鑰。也就是說,之後也無法在開機時使用該金鑰,因為至少會發生一次開機層級轉換 (轉換至最終開機層級)。

當 Keystore 用戶端 (例如 odsign) 要求在開機層級 i 中建立金鑰時,系統會使用金鑰 Ki 加密其 Blob。由於 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 金鑰只能在早期啟動層級使用,因此不可能由攻擊者建立。