fs-verity: 只讀的基於檔案的真實性保護¶
簡介¶
fs-verity (fs/verity/) 是一個支援層,檔案系統可以連線到它,以支援只讀檔案的透明完整性和真實性保護。目前,它受 ext4、f2fs 和 btrfs 檔案系統支援。與 fscrypt 類似,支援 fs-verity 不需要太多檔案系統特定的程式碼。
fs-verity 類似於 dm-verity,但它作用於檔案而不是塊裝置。在支援 fs-verity 的檔案系統上的常規檔案上,使用者空間可以執行一個 ioctl 呼叫,使檔案系統為檔案構建一個 Merkle 樹,並將其持久化到與檔案關聯的檔案系統特定位置。
此後,檔案被設定為只讀,並且所有對檔案的讀取都會自動根據檔案的 Merkle 樹進行驗證。讀取任何損壞的資料,包括 mmap 讀取,都將失敗。
使用者空間可以使用另一個 ioctl 呼叫來檢索 fs-verity 正在為檔案強制執行的根雜湊(實際上是“fs-verity 檔案摘要”,它是一個包含 Merkle 樹根雜湊的雜湊值)。無論檔案大小如何,此 ioctl 都以恆定時間執行。
fs-verity 本質上是一種以恆定時間雜湊檔案的方式,但有一個注意事項:執行時,如果讀取會違反雜湊,則讀取將失敗。
用例¶
fs-verity 本身只提供完整性保護,即檢測意外(非惡意)損壞。
然而,由於 fs-verity 使檔案雜湊的檢索效率極高,因此它主要用作支援認證(檢測惡意修改)或審計(在使用前記錄檔案雜湊)的工具。
可以使用標準檔案雜湊而不是 fs-verity。但是,如果檔案很大且只訪問一小部分,則效率低下。例如,Android 應用程式包 (APK) 檔案經常出現這種情況。這些檔案通常包含許多翻譯、類和其他資源,這些資源在特定裝置上很少或甚至從不訪問。在啟動應用程式之前讀取並雜湊整個檔案將是緩慢且浪費的。
與提前雜湊不同,fs-verity 還會每次分頁資料時重新驗證資料。這確保了惡意磁碟韌體無法在執行時不被檢測地更改檔案內容。
fs-verity 不會取代或廢棄 dm-verity。dm-verity 仍應在只讀檔案系統上使用。fs-verity 適用於必須存在於讀寫檔案系統上的檔案,因為這些檔案是獨立更新的且可能是使用者安裝的,因此無法使用 dm-verity。
fs-verity 不強制要求其檔案雜湊的特定認證方案。(類似地,dm-verity 也不強制要求其塊裝置根雜湊的特定認證方案。)認證 fs-verity 檔案雜湊的選項包括:
受信任的使用者空間程式碼。通常,訪問檔案的使用者空間程式碼可以被信任來認證它們。例如,一個應用程式希望在使用資料檔案之前認證它們,或者一個作為作業系統一部分(已經透過其他方式認證,例如從使用 dm-verity 的只讀分割槽載入)的應用程式載入器希望在載入應用程式之前認證它們。在這些情況下,此受信任的使用者空間程式碼可以透過使用 FS_IOC_MEASURE_VERITY 檢索其 fs-verity 摘要,然後使用任何支援數字簽名的使用者空間加密庫來驗證其簽名,從而認證檔案的內容。
完整性測量架構 (IMA)。IMA 支援 fs-verity 檔案摘要作為其傳統完整檔案摘要的替代方案。“IMA 評估”強制要求檔案在其“security.ima”擴充套件屬性中包含有效且匹配的簽名,這由 IMA 策略控制。有關更多資訊,請參閱 IMA 文件。
完整性策略強制執行 (IPE)。IPE 支援根據檔案的不可變安全屬性(包括受 fs-verity 內建簽名保護的屬性)強制執行訪問控制決策。“IPE 策略”專門允許使用屬性
fsverity_digest(用於透過其 verity 摘要識別檔案)和fsverity_signature(用於授權具有已驗證 fs-verity 內建簽名的檔案)來授權 fs-verity 檔案。有關配置 IPE 策略和理解其操作模式的詳細資訊,請參閱 IPE 管理指南。受信任的使用者空間程式碼與 內建簽名驗證 結合使用。此方法應非常謹慎地使用。
使用者 API¶
FS_IOC_ENABLE_VERITY¶
FS_IOC_ENABLE_VERITY ioctl 在檔案上啟用 fs-verity。它接受一個指向 struct fsverity_enable_arg 結構體的指標,定義如下:
struct fsverity_enable_arg {
__u32 version;
__u32 hash_algorithm;
__u32 block_size;
__u32 salt_size;
__u64 salt_ptr;
__u32 sig_size;
__u32 __reserved1;
__u64 sig_ptr;
__u64 __reserved2[11];
};
此結構體包含為檔案構建 Merkle 樹的引數。它必須按如下方式初始化:
version必須為 1。hash_algorithm必須是用於 Merkle 樹的雜湊演算法識別符號,例如 FS_VERITY_HASH_ALG_SHA256。有關可能值的列表,請參閱include/uapi/linux/fsverity.h。block_size是 Merkle 樹的塊大小,以位元組為單位。在 Linux v6.3 及更高版本中,這可以是 1024 到系統頁面大小和檔案系統塊大小的最小值之間(包含)的任何 2 的冪。在早期版本中,只允許頁面大小。salt_size是鹽值的位元組大小,如果未提供鹽值則為 0。鹽值是一個預先新增到每個雜湊塊的值;它可用於為特定檔案或裝置個性化雜湊。目前,最大鹽值大小為 32 位元組。salt_ptr是指向鹽值的指標,如果未提供鹽值則為 NULL。sig_size是內建簽名的位元組大小,如果未提供內建簽名則為 0。目前,內建簽名(有點隨意地)限制為 16128 位元組。sig_ptr是指向內建簽名的指標,如果未提供內建簽名則為 NULL。只有在使用 內建簽名驗證 功能時才需要內建簽名。IMA 評估不需要它,如果檔案簽名完全在使用者空間中處理,也不需要它。所有保留欄位必須清零。
FS_IOC_ENABLE_VERITY 使檔案系統為檔案構建一個 Merkle 樹,並將其持久化到與檔案關聯的檔案系統特定位置,然後將檔案標記為 verity 檔案。此 ioctl 在大檔案上可能需要很長時間才能執行,並且可被致命訊號中斷。
FS_IOC_ENABLE_VERITY 檢查 inode 的寫入許可權。但是,它必須在 O_RDONLY 檔案描述符上執行,並且不能有任何程序開啟檔案進行寫入。在執行此 ioctl 期間嘗試開啟檔案進行寫入將失敗並返回 ETXTBSY。(這對於保證在啟用 verity 後不會存在可寫檔案描述符,以及保證在構建 Merkle 樹期間檔案內容穩定是必要的。)
成功時,FS_IOC_ENABLE_VERITY 返回 0,並且檔案成為 verity 檔案。失敗時(包括被致命訊號中斷的情況),檔案不會發生任何更改。
FS_IOC_ENABLE_VERITY 可能因以下錯誤而失敗:
EACCES:程序沒有檔案的寫入許可權EBADMSG:內建簽名格式不正確EBUSY:此 ioctl 已在該檔案上執行EEXIST:檔案已啟用 verityEFAULT:呼叫者提供了不可訪問的記憶體EFBIG:檔案太大,無法啟用 verityEINTR:操作被致命訊號中斷EINVAL:不支援的版本、雜湊演算法或塊大小;或設定了保留位;或檔案描述符既不是常規檔案也不是目錄。EISDIR:檔案描述符指向一個目錄EKEYREJECTED:內建簽名與檔案不匹配EMSGSIZE:鹽值或內建簽名太長ENOKEY:“.fs-verity”金鑰環不包含驗證內建簽名所需的證書ENOPKG:fs-verity 識別雜湊演算法,但它在當前配置的核心加密 API 中不可用(例如,對於 SHA-512,缺少 CONFIG_CRYPTO_SHA512)。ENOTTY:此型別的檔案系統未實現 fs-verityEOPNOTSUPP:核心未配置 fs-verity 支援;或檔案系統超級塊未啟用“verity”特性;或檔案系統不支援此檔案上的 fs-verity。(請參閱 檔案系統支援。)EPERM:檔案是隻追加的;或者,需要內建簽名但未提供。EROFS:檔案系統是隻讀的ETXTBSY:有人開啟檔案進行寫入。這可以是呼叫者的檔案描述符,另一個開啟的檔案描述符,或者可寫記憶體對映持有的檔案引用。
FS_IOC_MEASURE_VERITY¶
FS_IOC_MEASURE_VERITY ioctl 檢索 verity 檔案的摘要。fs-verity 檔案摘要是一個加密摘要,用於識別在讀取時強制執行的檔案內容;它透過 Merkle 樹計算,與傳統的完整檔案摘要不同。
此 ioctl 接受一個指向可變長度結構體的指標:
struct fsverity_digest {
__u16 digest_algorithm;
__u16 digest_size; /* input/output */
__u8 digest[];
};
digest_size 是一個輸入/輸出欄位。在輸入時,它必須初始化為為可變長度 digest 欄位分配的位元組數。
成功時,返回 0,核心按如下方式填充結構體:
digest_algorithm將是用於檔案摘要的雜湊演算法。它將與fsverity_enable_arg::hash_algorithm匹配。digest_size將是摘要的位元組大小,例如 SHA-256 為 32。(這可能與digest_algorithm重複。)digest將是摘要的實際位元組。
FS_IOC_MEASURE_VERITY 保證以恆定時間執行,無論檔案大小如何。
FS_IOC_MEASURE_VERITY 可能因以下錯誤而失敗:
EFAULT:呼叫者提供了不可訪問的記憶體ENODATA:檔案不是 verity 檔案ENOTTY:此型別的檔案系統未實現 fs-verityEOPNOTSUPP:核心未配置 fs-verity 支援,或檔案系統超級塊未啟用“verity”特性。(請參閱 檔案系統支援。)EOVERFLOW:摘要長於指定的digest_size位元組。嘗試提供更大的緩衝區。
FS_IOC_READ_VERITY_METADATA¶
FS_IOC_READ_VERITY_METADATA ioctl 從 verity 檔案讀取 verity 元資料。此 ioctl 自 Linux v5.12 起可用。
此 ioctl 對於需要在當前執行的核心之外執行 verity 驗證的情況很有用。
一個例子是伺服器程式接收 verity 檔案並將其提供給客戶端程式,以便客戶端可以自行對檔案進行 fs-verity 相容的驗證。這隻有在客戶端不信任伺服器且伺服器需要為客戶端提供儲存時才有意義。
另一個例子是在使用者空間中建立檔案系統映象時(例如使用 mkfs.ext4 -d)複製 verity 元資料。
這是一個相當特殊的用例,大多數 fs-verity 使用者不需要此 ioctl。
此 ioctl 接受一個指向以下結構體的指標:
#define FS_VERITY_METADATA_TYPE_MERKLE_TREE 1
#define FS_VERITY_METADATA_TYPE_DESCRIPTOR 2
#define FS_VERITY_METADATA_TYPE_SIGNATURE 3
struct fsverity_read_metadata_arg {
__u64 metadata_type;
__u64 offset;
__u64 length;
__u64 buf_ptr;
__u64 __reserved;
};
metadata_type 指定要讀取的元資料型別
FS_VERITY_METADATA_TYPE_MERKLE_TREE讀取 Merkle 樹的塊。塊按從根級別到葉級別的順序返回。在每個級別內,塊按其雜湊本身進行雜湊的相同順序返回。有關更多資訊,請參閱 Merkle 樹。FS_VERITY_METADATA_TYPE_DESCRIPTOR讀取 fs-verity 描述符。請參閱 fs-verity 描述符。FS_VERITY_METADATA_TYPE_SIGNATURE讀取傳遞給 FS_IOC_ENABLE_VERITY 的內建簽名(如果有)。請參閱 內建簽名驗證。
語義類似於 pread()。 offset 指定要從元資料項讀取的偏移量(以位元組為單位),length 指定要從元資料項讀取的最大位元組數。buf_ptr 是指向要讀取到的緩衝區的指標,轉換為 64 位整數。__reserved 必須為 0。成功時,返回讀取的位元組數。在元資料項結束時返回 0。返回的長度可能小於 length,例如當 ioctl 被中斷時。
FS_IOC_READ_VERITY_METADATA 返回的元資料不保證根據 FS_IOC_MEASURE_VERITY 將返回的檔案摘要進行認證,因為元資料預期無論如何都用於實現 fs-verity 相容驗證(儘管在沒有惡意磁碟的情況下,元資料確實會匹配)。例如,為了實現此 ioctl,檔案系統可以只從磁碟讀取 Merkle 樹塊,而無需實際驗證到根節點的路徑。
FS_IOC_READ_VERITY_METADATA 可能因以下錯誤而失敗:
EFAULT:呼叫者提供了不可訪問的記憶體EINTR:在讀取任何資料之前 ioctl 被中斷EINVAL:設定了保留欄位,或offset + length溢位ENODATA:檔案不是 verity 檔案,或者請求了 FS_VERITY_METADATA_TYPE_SIGNATURE 但檔案沒有內建簽名ENOTTY:此型別的檔案系統未實現 fs-verity,或此 ioctl 尚未實現EOPNOTSUPP:核心未配置 fs-verity 支援,或檔案系統超級塊未啟用“verity”特性。(請參閱 檔案系統支援。)
FS_IOC_GETFLAGS¶
現有的 ioctl FS_IOC_GETFLAGS(並非 fs-verity 專用)也可用於檢查檔案是否啟用了 fs-verity。為此,請檢查返回標誌中的 FS_VERITY_FL (0x00100000)。
verity 標誌不能透過 FS_IOC_SETFLAGS 設定。您必須使用 FS_IOC_ENABLE_VERITY 代替,因為必須提供引數。
statx¶
自 Linux v5.5 起,如果檔案啟用了 fs-verity,則 statx() 系統呼叫會設定 STATX_ATTR_VERITY。這比 FS_IOC_GETFLAGS 和 FS_IOC_MEASURE_VERITY 效能更好,因為它不需要開啟檔案,而開啟 verity 檔案可能很昂貴。
訪問 verity 檔案¶
應用程式可以像訪問非 verity 檔案一樣透明地訪問 verity 檔案,但有以下例外:
Verity 檔案是隻讀的。它們不能以寫入模式開啟或被 truncate(),即使檔案模式位允許。嘗試執行這些操作之一將失敗並返回 EPERM。但是,對所有者、模式、時間戳和 xattr 等元資料的更改仍然允許,因為 fs-verity 不會測量這些。Verity 檔案也可以被重新命名、刪除和連結。
verity 檔案不支援直接 I/O。嘗試在此類檔案上使用直接 I/O 將回退到緩衝 I/O。
verity 檔案不支援 DAX(直接訪問),因為這會規避資料驗證。
讀取與 verity Merkle 樹不匹配的資料將失敗並返回 EIO (對於 read()) 或 SIGBUS (對於 mmap() 讀取)。
如果 sysctl “fs.verity.require_signatures” 設定為 1,並且檔案未由“.fs-verity”金鑰環中的金鑰簽名,則開啟檔案將失敗。請參閱 內建簽名驗證。
不支援直接訪問 Merkle 樹。因此,如果複製 verity 檔案,或備份和恢復,它將失去其“verity”特性。fs-verity 主要用於可執行檔案等由包管理器管理的檔案。
檔案摘要計算¶
本節描述 fs-verity 如何使用 Merkle 樹雜湊檔案內容以生成加密識別檔案內容的摘要。此演算法對於所有支援 fs-verity 的檔案系統都相同。
只有當用戶空間需要自行計算 fs-verity 檔案摘要(例如為了簽署檔案)時,才需要了解此演算法。
Merkle 樹¶
檔案內容被劃分為塊,其中塊大小是可配置的,但通常為 4096 位元組。如果需要,最後一個塊的末尾會用零填充。然後,每個塊都會被雜湊,生成第一級雜湊。接著,此第一級中的雜湊會被分組為“塊大小”位元組的塊(如果需要,末尾用零填充),然後這些塊被雜湊,生成第二級雜湊。這個過程一直向上進行,直到只剩下一個塊。此塊的雜湊即為“Merkle 樹根雜湊”。
如果檔案適合一個塊且不為空,則“Merkle 樹根雜湊”簡單地是單個數據塊的雜湊。如果檔案為空,則“Merkle 樹根雜湊”全部為零。
這裡的“塊”不一定與“檔案系統塊”相同。
如果指定了鹽值,則它會被零填充到雜湊演算法壓縮函式的輸入大小的最近倍數,例如 SHA-256 為 64 位元組,或 SHA-512 為 128 位元組。填充後的鹽值會預先新增到每個被雜湊的資料或 Merkle 樹塊。
塊填充的目的是使每個雜湊都在相同數量的資料上進行,這簡化了實現併為硬體加速提供了更多可能性。鹽值填充的目的是在預計算加鹽雜湊狀態後(然後為每個雜湊匯入)使加鹽“免費”。
示例:在 SHA-256 和 4K 塊的推薦配置中,每個塊可容納 128 個雜湊值。因此,Merkle 樹的每個級別大約比前一個級別小 128 倍,對於大檔案,Merkle 樹的大小大約收斂到原始檔案大小的 1/127。但是,對於小檔案,填充很重要,使得空間開銷按比例更大。
fs-verity 描述符¶
Merkle 樹根雜湊本身是模糊的。例如,它無法區分一個大檔案和一個小型第二檔案,該小型檔案的資料恰好是第一個檔案的頂層雜湊塊。填充到下一個塊邊界的約定也會導致歧義。
為了解決這個問題,fs-verity 檔案摘要實際上是計算以下結構體的雜湊,該結構體包含 Merkle 樹根雜湊以及檔案大小等其他欄位:
struct fsverity_descriptor {
__u8 version; /* must be 1 */
__u8 hash_algorithm; /* Merkle tree hash algorithm */
__u8 log_blocksize; /* log2 of size of data and tree blocks */
__u8 salt_size; /* size of salt in bytes; 0 if none */
__le32 __reserved_0x04; /* must be 0 */
__le64 data_size; /* size of file the Merkle tree is built over */
__u8 root_hash[64]; /* Merkle tree root hash */
__u8 salt[32]; /* salt prepended to each hashed block */
__u8 __reserved[144]; /* must be 0's */
};
內建簽名驗證¶
CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y 增加了對核心中 fs-verity 內建簽名驗證的支援。
重要!在使用此功能之前,請務必非常小心。這不是使用 fs-verity 進行簽名的唯一方法,替代方案(例如使用者空間簽名驗證和 IMA 評估)可能要好得多。也很容易陷入認為此功能解決了比實際更多問題的陷阱。
啟用此選項將增加以下功能:
在引導時,核心建立一個名為“.fs-verity”的金鑰環。root 使用者可以使用 add_key() 系統呼叫將受信任的 X.509 證書新增到此金鑰環中。
FS_IOC_ENABLE_VERITY 接受指向以 PKCS#7 格式的 DER 格式的檔案 fs-verity 摘要的分離簽名的指標。成功時,ioctl 將簽名與 Merkle 樹一起持久化。然後,無論 sysctl 變數“fs.verity.require_signatures”(在下一項中描述)的狀態如何,只要檔案簽名存在,核心就會使用“.fs-verity”金鑰環中的證書驗證檔案的實際摘要與此簽名是否匹配。IPE LSM 依賴此行為來識別並標記包含已驗證內建 fsverity 簽名的 fsverity 檔案。
一個新的 sysctl “fs.verity.require_signatures” 可用。當設定為 1 時,核心要求所有 verity 檔案都具有如 (2) 中所述的正確簽名的摘要。
(2) 中描述的簽名所必須簽名的資料是以下格式的 fs-verity 檔案摘要:
struct fsverity_formatted_digest {
char magic[8]; /* must be "FSVerity" */
__le16 digest_algorithm;
__le16 digest_size;
__u8 digest[];
};
就是這樣。需要再次強調的是,fs-verity 內建簽名不是使用 fs-verity 進行簽名的唯一方法。有關 fs-verity 使用方式的概述,請參閱 用例。fs-verity 內建簽名存在一些主要限制,在使用它們之前應仔細考慮:
內建簽名驗證不會強制核心強制任何檔案實際啟用 fs-verity。因此,它不是一個完整的身份驗證策略。目前,如果使用它,完成身份驗證策略的一種方法是讓受信任的使用者空間程式碼在訪問檔案之前明確檢查檔案是否啟用了帶有簽名的 fs-verity。(在 fs.verity.require_signatures=1 的情況下,只需檢查是否啟用了 fs-verity 就足夠了。)但是,在這種情況下,受信任的使用者空間程式碼可以直接將簽名與檔案一起儲存,並使用加密庫自行驗證,而不是使用此功能。
另一種方法是將 fs-verity 內建簽名驗證與 IPE LSM 結合使用,IPE LSM 支援定義一個由核心強制執行的系統範圍身份驗證策略,該策略只允許具有已驗證 fs-verity 內建簽名的檔案執行某些操作,例如執行。請注意,IPE 不要求 fs.verity.require_signatures=1。有關更多詳細資訊,請參閱 IPE 管理指南。
檔案的內建簽名只能在檔案啟用 fs-verity 的同時設定。以後更改或刪除內建簽名需要重新建立檔案。
內建簽名驗證對系統上所有啟用 fs-verity 的檔案使用同一組公鑰。不能為不同檔案信任不同的金鑰;每個金鑰都是全有或全無的。
sysctl fs.verity.require_signatures 適用於系統範圍。將其設定為 1 只有在系統上所有 fs-verity 使用者都同意將其設定為 1 時才有效。此限制可能會阻止 fs-verity 在其會有幫助的情況下使用。
內建簽名驗證只能使用核心支援的簽名演算法。例如,核心尚不支援 Ed25519,儘管這通常是新加密設計推薦的簽名演算法。
fs-verity 內建簽名採用 PKCS#7 格式,公鑰採用 X.509 格式。這些格式被廣泛使用,包括被一些其他核心功能(這就是 fs-verity 內建簽名使用它們的原因)使用,並且功能非常豐富。不幸的是,歷史表明,解析和處理這些格式(它們來自 1990 年代,基於 ASN.1)的程式碼通常由於其複雜性而存在漏洞。這種複雜性並非加密本身固有的。
不需要 X.509 和 PKCS#7 高階功能的 fs-verity 使用者應強烈考慮使用更簡單的格式,例如純 Ed25519 金鑰和簽名,並在使用者空間中驗證簽名。
選擇使用 X.509 和 PKCS#7 的 fs-verity 使用者仍應考慮在使用者空間中驗證這些簽名更靈活(出於本文前面提到的其他原因),並消除了啟用 CONFIG_FS_VERITY_BUILTIN_SIGNATURES 及其相關核心攻擊面增加的需要。在某些情況下,甚至可能是必要的,因為高階 X.509 和 PKCS#7 功能並非總是按預期與核心一起工作。例如,核心不檢查 X.509 證書的有效期。
注意:支援 fs-verity 的 IMA 評估不使用 PKCS#7 進行簽名,因此它部分避免了此處討論的問題。IMA 評估確實使用 X.509。
檔案系統支援¶
fs-verity 受以下幾個檔案系統支援。必須啟用 CONFIG_FS_VERITY kconfig 選項才能在這些檔案系統上使用 fs-verity。
include/linux/fsverity.h 聲明瞭 fs/verity/ 支援層和檔案系統之間的介面。簡而言之,檔案系統必須提供一個 fsverity_operations 結構體,該結構體提供讀寫 verity 元資料到檔案系統特定位置的方法,包括 Merkle 樹塊和 fsverity_descriptor。檔案系統還必須在特定時間呼叫 fs/verity/ 中的函式,例如當檔案開啟或頁面已讀入頁快取時。(請參閱 驗證資料。)
ext4¶
ext4 自 Linux v5.4 和 e2fsprogs v1.45.2 起支援 fs-verity。
要在 ext4 檔案系統上建立 verity 檔案,檔案系統必須已使用 -O verity 格式化或已在其上執行 tune2fs -O verity。“verity”是一個 RO_COMPAT 檔案系統特性,因此一旦設定,舊核心將只能以只讀方式掛載檔案系統,舊版本的 e2fsck 將無法檢查檔案系統。
最初,帶有“verity”特性的 ext4 檔案系統只能在其塊大小等於系統頁面大小(通常為 4096 位元組)時掛載。在 Linux v6.3 中,此限制已移除。
ext4 在 verity 檔案上設定 EXT4_VERITY_FL 磁碟 inode 標誌。它只能由 FS_IOC_ENABLE_VERITY 設定,並且不能清除。
ext4 還支援加密,可以與 fs-verity 同時使用。在這種情況下,驗證的是明文資料而不是密文。這是使 fs-verity 檔案摘要有意義所必需的,因為每個檔案都以不同的方式加密。
ext4 將 verity 元資料(Merkle 樹和 fsverity_descriptor)儲存在檔案末尾之後,從 i_size 之後的第一個 64K 邊界開始。這種方法之所以有效,是因為 (a) verity 檔案是隻讀的,並且 (b) 完全超出 i_size 的頁面對使用者空間不可見,但 ext4 可以在內部讀/寫它們,只需對 ext4 進行一些相對較小的更改。這種方法避免了依賴 EA_INODE 特性以及重新設計 ext4 的 xattr 支援以支援將多吉位元組的 xattr 分頁到記憶體中,以及支援加密 xattr。請注意,當檔案被加密時,verity 元資料必須被加密,因為它包含明文資料的雜湊值。
ext4 只允許對基於 extent 的檔案進行 verity。
f2fs¶
f2fs 自 Linux v5.4 和 f2fs-tools v1.11.0 起支援 fs-verity。
要在 f2fs 檔案系統上建立 verity 檔案,檔案系統必須已使用 -O verity 格式化。
f2fs 在 verity 檔案上設定 FADVISE_VERITY_BIT 磁碟 inode 標誌。它只能由 FS_IOC_ENABLE_VERITY 設定,並且不能清除。
與 ext4 類似,f2fs 將 verity 元資料(Merkle 樹和 fsverity_descriptor)儲存在檔案末尾之後,從 i_size 之後的第一個 64K 邊界開始。請參閱上面 ext4 的解釋。此外,f2fs 每個 inode 最多支援 4096 位元組的 xattr 條目,這通常甚至不足以容納單個 Merkle 樹塊。
f2fs 不支援對當前有原子或易失性寫入待處理的檔案啟用 verity。
btrfs¶
btrfs 自 Linux v5.15 起支援 fs-verity。啟用 verity 的 inode 會被標記為 RO_COMPAT inode 標誌,並且 verity 元資料儲存在單獨的 btree 項中。
實現細節¶
驗證資料¶
fs-verity 確保對 verity 檔案的所有資料讀取都經過驗證,無論使用哪個系統呼叫進行讀取(例如 mmap()、read()、pread()),也無論它是第一次讀取還是後續讀取(除非後續讀取可以返回已驗證的快取資料)。下面,我們描述檔案系統如何實現這一點。
頁快取¶
對於使用 Linux 頁快取的檔案系統,必須修改 ->read_folio() 和 ->readahead() 方法以在 folio 被標記為 Uptodate 之前驗證它們。僅僅掛接 ->read_iter() 是不夠的,因為 ->read_iter() 不用於記憶體對映。
因此,fs/verity/ 提供了函式 fsverity_verify_blocks(),用於驗證已讀入 verity inode 頁快取的資料。包含的 folio 仍必須被鎖定且未 Uptodate,因此使用者空間尚無法讀取。根據需要進行驗證,fsverity_verify_blocks() 將透過 fsverity_operations::read_merkle_tree_page() 回撥到檔案系統以讀取雜湊塊。
如果驗證失敗,fsverity_verify_blocks() 返回 false;在這種情況下,檔案系統不得設定 folio Uptodate。在此之後,根據通常的 Linux 頁快取行為,使用者空間嘗試從包含 folio 的檔案部分 read() 將失敗並返回 EIO,而對記憶體對映中 folio 的訪問將引發 SIGBUS。
原則上,驗證資料塊需要驗證 Merkle 樹中從資料塊到根雜湊的整個路徑。然而,為了效率,檔案系統可以快取雜湊塊。因此,fsverity_verify_blocks() 只向上遍歷樹讀取雜湊塊,直到看到一個已經驗證的雜湊塊。然後它驗證到該塊的路徑。
這種最佳化也被 dm-verity 使用,可以實現出色的順序讀取效能。這是因為通常(例如,對於 4K 塊和 SHA-256,128 次中有 127 次)來自樹底層的雜湊塊將已快取並從讀取前一個數據塊中檢查過。然而,隨機讀取的效能較差。
基於塊裝置的檔案系統¶
Linux 中基於塊裝置的檔案系統(例如 ext4 和 f2fs)也使用頁快取,因此上述小節也適用。然而,它們通常也一次從檔案中讀取許多資料塊,這些塊被分組到一個名為“bio”的結構中。為了使這些型別的檔案系統更容易支援 fs-verity,fs/verity/ 還提供了一個函式 fsverity_verify_bio(),用於驗證 bio 中的所有資料塊。
ext4 和 f2fs 也支援加密。如果一個 verity 檔案同時也被加密,則資料必須在驗證之前解密。為了支援這一點,這些檔案系統為每個 bio 分配一個“讀取後上下文”並將其儲存在 ->bi_private 中:
struct bio_post_read_ctx {
struct bio *bio;
struct work_struct work;
unsigned int cur_step;
unsigned int enabled_steps;
};
enabled_steps 是一個位掩碼,指定是否啟用瞭解密、verity 或兩者。在 bio 完成後,對於每個所需的後處理步驟,檔案系統將 bio_post_read_ctx 入隊到一個工作佇列,然後工作佇列執行解密或驗證。最後,沒有發生解密或 verity 錯誤的 folio 被標記為 Uptodate,並且 folio 被解鎖。
在許多檔案系統上,檔案可以包含空洞。通常,->readahead() 只是將空洞塊清零並認為相應的資料是最新的;不會發出 bio。為了防止這種情況繞過 fs-verity,檔案系統使用 fsverity_verify_blocks() 來驗證空洞塊。
檔案系統還停用 verity 檔案上的直接 I/O,因為否則直接 I/O 將繞過 fs-verity。
使用者空間工具¶
本文件側重於核心,但 fs-verity 的使用者空間工具可以在以下位置找到:
有關詳細資訊,包括設定 fs-verity 保護檔案的示例,請參閱 fsverity-utils 原始碼樹中的 README.md 檔案。
測試¶
要測試 fs-verity,請使用 xfstests。例如,使用 kvm-xfstests:
kvm-xfstests -c ext4,f2fs,btrfs -g verity
常見問題¶
本節回答了本檔案中未直接回答的有關 fs-verity 的常見問題。
- 問:
為什麼 fs-verity 不是 IMA 的一部分?
- 答:
fs-verity 和 IMA(完整性測量架構)關注點不同。fs-verity 是一種檔案系統級別的機制,用於使用 Merkle 樹雜湊單個檔案。相比之下,IMA 指定了一個系統範圍的策略,該策略規定了哪些檔案被雜湊以及如何處理這些雜湊,例如記錄它們、認證它們或將它們新增到測量列表中。
IMA 支援 fs-verity 雜湊機制作為完整檔案雜湊的替代方案,適用於那些希望獲得基於 Merkle 樹的雜湊的效能和安全優勢的使用者。然而,強制所有 fs-verity 的使用都透過 IMA 是沒有意義的。即使作為獨立的 檔案系統特性,fs-verity 也已經滿足了許多使用者的需求,並且可以像其他檔案系統特性一樣進行測試,例如使用 xfstests。
- 問:
fs-verity 不是沒用嗎,因為攻擊者可以修改儲存在磁碟上的 Merkle 樹中的雜湊?
- 答:
為了驗證 fs-verity 檔案的真實性,您必須驗證“fs-verity 檔案摘要”的真實性,其中包含 Merkle 樹的根雜湊。請參閱 用例。
- 問:
fs-verity 不是沒用嗎,因為攻擊者可以把 verity 檔案替換成非 verity 檔案?
- 答:
請參閱 用例。在最初的用例中,真正認證檔案的是受信任的使用者空間程式碼;fs-verity 只是一個工具,可以高效安全地完成這項工作。受信任的使用者空間程式碼會將非 verity 檔案視為非真實檔案。
- 問:
為什麼 Merkle 樹需要儲存在磁碟上?難道不能只儲存根雜湊嗎?
- 答:
如果 Merkle 樹不儲存在磁碟上,那麼即使只讀取一個位元組,也必須在檔案首次訪問時計算整個樹。這是 Merkle 樹雜湊工作方式的基本結果。要驗證葉節點,您需要驗證到根雜湊的整個路徑,包括根節點(根雜湊是其雜湊的物件)。但如果根節點不儲存在磁碟上,您必須透過雜湊其子節點來計算它,依此類推,直到實際雜湊了整個檔案。
這使得基於 Merkle 樹的雜湊的大部分意義都喪失了,因為如果您無論如何都必須提前雜湊整個檔案,那麼您可以簡單地進行 sha256(檔案) 代替。那會簡單得多,而且速度也稍快。
誠然,記憶體中的 Merkle 樹仍然可以提供每次讀取時進行驗證的優勢,而不僅僅是第一次讀取。然而,它會效率低下,因為每次雜湊頁被逐出時(您無法將整個 Merkle 樹固定在記憶體中,因為它可能非常大),為了恢復它,您需要再次雜湊樹中其下方的一切。這再次使得基於 Merkle 樹的雜湊的大部分意義都喪失了,因為單個塊讀取可能觸發重新雜湊數千兆位元組的資料。
- 問:
但是不能只儲存葉節點並計算其餘的嗎?
- 答:
請參閱上一個答案;這實際上只是上移了一個級別,因為可以替代地將資料塊解釋為 Merkle 樹的葉節點。誠然,如果儲存葉級別而不是隻儲存資料,樹的計算速度會快得多,但這只是因為每個級別的大小都小於下一級大小的 1%(假設推薦的 SHA-256 和 4K 塊設定)。出於完全相同的原因,透過儲存“僅葉節點”,您已經儲存了樹的 99% 以上,所以您不妨簡單地儲存整個樹。
- 問:
Merkle 樹可以提前構建嗎,例如作為安裝到多臺計算機的軟體包的一部分進行分發?
- 答:
目前不支援。這是原始設計的一部分,但為了簡化核心 UAPI 和因為它不是一個關鍵用例而被移除。檔案通常只安裝一次並使用多次,並且加密雜湊在大多數現代處理器上速度相對較快。
- 問:
為什麼 fs-verity 不支援寫入?
- 答:
寫入支援將非常困難,並且需要完全不同的設計,因此它完全超出了 fs-verity 的範圍。寫入支援將需要:
一種維護資料和雜湊之間一致性的方法,包括所有級別的雜湊,因為崩潰後(特別是可能影響整個檔案!)的損壞是不可接受的。解決此問題的主要選項是資料日誌、寫時複製和日誌結構卷。但要改造現有檔案系統以適應新的永續性機制非常困難。資料日誌在 ext4 上可用,但非常慢。
每次寫入後重建 Merkle 樹,這將極其低效。或者,可以使用不同的認證字典結構,例如“認證跳錶”。然而,這將複雜得多。
將其與 dm-verity 與 dm-integrity 進行比較。dm-verity 非常簡單:核心只驗證只讀資料與只讀 Merkle 樹。相比之下,dm-integrity 支援寫入但速度慢,複雜得多,並且實際上不支援全裝置認證,因為它獨立認證每個扇區,即沒有“根雜湊”。讓相同的裝置對映器目標支援這兩種非常不同的情況並沒有多大意義;同樣適用於 fs-verity。
- 問:
既然 verity 檔案是不可變的,為什麼不設定不可變位?
- 答:
現有的“不可變”位 (FS_IMMUTABLE_FL) 已經有一套特定的語義,它不僅使檔案內容只讀,還阻止檔案被刪除、重新命名、連結或更改其所有者或模式。fs-verity 不需要這些額外的屬性,因此重用不可變位不合適。
- 問:
為什麼 API 使用 ioctl 呼叫而不是 setxattr() 和 getxattr()?
- 答:
濫用 xattr 介面進行基本任意的系統呼叫受到大多數 Linux 檔案系統開發者的強烈反對。xattr 應該真正只是磁碟上的 xattr,而不是一個神奇地觸發 Merkle 樹構建的 API。
- 問:
fs-verity 支援遠端檔案系統嗎?
- 答:
到目前為止,所有已實現 fs-verity 支援的檔案系統都是本地檔案系統,但原則上任何能夠儲存每檔案 verity 元資料的檔案系統都可以支援 fs-verity,無論它是本地還是遠端。某些檔案系統可能儲存 verity 元資料的選項較少;一種可能性是將其儲存在檔案末尾之後,並透過操作 i_size 從使用者空間“隱藏”它。fs/verity/ 提供的 資料驗證函式 還假設檔案系統使用 Linux 頁快取,但本地和遠端檔案系統通常都這樣做。
- 問:
為什麼還有任何檔案系統特定的內容?fs-verity 不應該完全在 VFS 層實現嗎?
- 答:
有很多原因導致這不可能或會非常困難,包括:
為了防止繞過驗證,在 folio 被驗證之前不得將其標記為 Uptodate。目前,每個檔案系統都負責透過
->readahead()將 folio 標記為 Uptodate。因此,目前 VFS 無法自行進行驗證。更改此項將需要對 VFS 和所有檔案系統進行重大更改。它將需要定義一種與檔案系統無關的方式來儲存 verity 元資料。擴充套件屬性不適用於此,因為 (a) Merkle 樹可能達到千兆位元組,但許多檔案系統假設所有 xattr 都適合單個 4K 檔案系統塊,並且 (b) ext4 和 f2fs 加密不加密 xattr,但當檔案內容加密時,Merkle 樹必須加密,因為它儲存明文檔案內容的雜湊。
因此,verity 元資料必須儲存在實際檔案中。使用單獨的檔案會非常難看,因為元資料從根本上是受保護檔案的一部分,並且可能導致使用者刪除實際檔案但沒有元資料檔案,反之亦然。另一方面,如果它在同一個檔案中,則會破壞應用程式,除非檔案系統對 i_size 的概念與 VFS 脫鉤,這將很複雜並需要更改所有檔案系統。
理想情況下,FS_IOC_ENABLE_VERITY 使用檔案系統的事務機制,以便檔案要麼最終啟用 verity,要麼沒有進行任何更改。允許在崩潰後出現中間狀態可能會導致問題。