Linux内核中的dm-verity完整性验证深度实战:从哈希树构建到链式可信计算
一、为什么我们需要块设备完整性验证
在容器化部署和边缘计算场景中,一个棘手的问题是:如何确保磁盘上的数据没有被篡改?传统的文件系统(ext4, xfs)只保证数据格式正确,无法检测以下攻击场景:
- 物理磁盘被替换或写入恶意固件
- 宿主机 root 权限被攻破后静默篡改容器镜像层
- 攻击者修改只读层的特定字节以改变程序行为
dm-verity 正是为了解决这类问题而生。它将整个块设备的每一个数据块都纳入到一棵 Merkle 哈希树的保护下,任何单比特的修改都会导致根哈希验证失败,立即触发 I/O 错误。这种机制是 Google Android Verified Boot、Chromebook 安全模型以及容器镜像签名链的核心支柱。
本文将从内核实现机制、哈希树数据结构、用户空间工具链四个层面剖析 dm-verity,并提供完整的 C 语言实战代码。
二、哈希树数据结构:dm-verity 的磁盘布局
dm-verity 将块设备严格地划分为两部分:数据区(data device) 和 哈希树(hash device)。数据区存储原始内容,哈希区存储对应的哈希元数据。
磁盘布局自上而下:
┌──────────────────────────────────────┐
│ Superblock (4096 bytes) │ ← 根哈希 + 算法 + 种子
├──────────────────────────────────────┤
│ Hash Level 2 (Top hashes) │ ← 中间层哈希
├──────────────────────────────────────┤
│ Hash Level 1 (Bottom hashes) │ ← 叶子层哈希(最末级)
└──────────────────────────────────────┘
↑ Hash Device
┌──────────────────────────────────────┐
│ Data Blocks (4KB each) │ ← 原始数据区
└──────────────────────────────────────┘
↑ Data Device
每个数据块大小为 4096 字节(典型配置),使用 SHA-256 计算其哈希值。然后这些哈希值本身也被分组成 4KB 块,递归地计算上一层哈希,直到最终得到一个唯一的根哈希。
假设块设备大小为 1GB,共有 262144 个 4KB 数据块:
- Level 1:262144 个哈希值,占用 262144 × 32 = 8MB,即 2048 个哈希块
- Level 2:2048 个哈希值,占用 2048 × 32 = 64KB,即 16 个哈希树块
- Level 3(根):16 个哈希值,占用 512 字节,归入 superblock
哈希树的层级高度计算公式为:levels = ceil(log_4096(data_blocks)) = ceil(log(data_blocks)/12),其中 4096/32 = 128 为每个哈希块中包含的哈希数。
三、内核验证机制:每次读操作的交易
当内核从数据区读取一个 4KB 块时,dm-verity 的处理流程如下:
- 从数据区读取原始块数据
- 对数据块计算 SHA-256 哈希,得到 leaf_hash
- 从哈希区读取对应叶子位置的数据块,验证 leaf_hash 是否匹配
- 若不匹配,立即触发 `EIO` 错误并标记设备为损坏状态
- 若匹配,逐级向上验证直到根哈希
关键代码路径位于 drivers/md/dm-verity.c(以 dm-verity target 类型实现)。核心验证函数 verity_work 在每次 I/O 完成后被调用:
// 内核验证伪代码(简化自 dm-verity-verify.c)
static int verify_block(struct dm_verity *v, struct dm_verity_io *io,
sector_t block)
{
// 1. 计算数据块的 SHA-256
sha256_hash(v, data_page, data_size, calculated_hash);
// 2. 读取哈希树中对应的预期哈希值
expected_hash = read_hash_block(v, io->block);
// 3. 比较哈希值
if (memcmp(calculated_hash, expected_hash, v->digest_size) != 0) {
// 验证失败!
DMERR("verification failed at block %llu", block);
return -EIO;
}
// 4. 逐级向上验证
for (level = 0; level < v->levels; level++) {
parent_hash = calculate_parent_hash(v, level, ...);
if (!verify_parent_hash(v, level + 1, parent_hash))
return -EIO;
}
return 0; // 全部验证通过
}
值得注意的是,dm-verity 仅在读取时验证,写入操作会完全失败(除非在创建时一次性写入)。这种"写一次读多次"(WORM)模型是只读完整性保护的核心假设。
四、用户空间工具:veritysetup 的使用
cryptsetup 工具包中的 veritysetup 提供了创建和管理 verity 设备的接口。
创建哈希树
# 创建 100MB 的测试数据
dd if=/dev/urandom of=/tmp/data.img bs=1M count=100
# 创建哈希树设备
veritysetup format /tmp/data.img /tmp/hash.img
# 输出包含:Root Hash: aabbccdd1122...
通过 device-mapper 加载
# 获取 root hash(从 format 阶段的输出)
ROOT_HASH="aabbccdd11223344..."
# 创建 verity device-mapper target
echo "0 $(blockdev --getsize /tmp/data.img) verity 1 \
/tmp/data.img /tmp/hash.img \
4096 4096 262144 1 sha256 \
$ROOT_HASH" | dmsetup create veritydev
# 现在 /dev/mapper/veritydev 是一个完整性验证的块设备
mkfs.ext4 /dev/mapper/veritydev
mount /dev/mapper/veritydev /mnt/verity
五、实际代码:手动构建哈希树
以下 C 代码展示了如何手动构建一个 verity 哈希树,模拟 veritysetup 的核心逻辑:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>
#include <stdint.h>
#define BLOCK_SIZE 4096
#define DIGEST_SIZE 32 // SHA-256
#define HASHES_PER_BLOCK (BLOCK_SIZE / DIGEST_SIZE) // 128
struct verity_params {
uint64_t data_blocks;
uint8_t root_hash[DIGEST_SIZE];
uint8_t salt[32];
size_t salt_size;
uint32_t hash_block_size;
};
// 计算单个数据块的哈希
static void hash_block(const uint8_t *data, size_t len,
const uint8_t *salt, size_t salt_len,
uint8_t *out)
{
SHA256_CTX ctx;
SHA256_Init(&ctx);
if (salt && salt_len > 0)
SHA256_Update(&ctx, salt, salt_len);
SHA256_Update(&ctx, data, len);
SHA256_Final(out, &ctx);
}
// 递归构建哈希树
static int build_hash_tree(struct verity_params *params,
const uint8_t *hash_device, // 哈希设备映射
uint64_t level_offset, // 当前层在哈希设备中的起始块
uint64_t block_count) // 当前层块数
{
uint8_t parent_block[BLOCK_SIZE];
uint8_t child_hash[DIGEST_SIZE];
if (block_count == 1) {
// 到达顶层,这是根哈希
uint8_t root_block[BLOCK_SIZE];
memcpy(root_block, hash_device + level_offset * BLOCK_SIZE, BLOCK_SIZE);
memcpy(params->root_hash, root_block, DIGEST_SIZE);
printf("Root hash computed: ");
for (int i = 0; i < DIGEST_SIZE; i++)
printf("%02x", params->root_hash[i]);
printf("\n");
return 0;
}
// 计算上一层的每个块的哈希
uint64_t parent_blocks = (block_count + HASHES_PER_BLOCK - 1) / HASHES_PER_BLOCK;
for (uint64_t i = 0; i < parent_blocks; i++) {
memset(parent_block, 0, BLOCK_SIZE);
for (uint64_t j = 0; j < HASHES_PER_BLOCK; j++) {
uint64_t child_idx = i * HASHES_PER_BLOCK + j;
if (child_idx >= block_count)
break;
// 读取当前层子块,计算其哈希
const uint8_t *child_block_data =
hash_device + (level_offset + child_idx) * BLOCK_SIZE;
SHA256(child_block_data, BLOCK_SIZE, child_hash);
memcpy(parent_block + j * DIGEST_SIZE, child_hash, DIGEST_SIZE);
}
// 将父块写入哈希设备
uint64_t parent_offset = level_offset - parent_blocks + i;
// memcpy(hash_device + parent_offset * BLOCK_SIZE, parent_block, BLOCK_SIZE);
printf("Level block %lu/%lu: ", i, parent_blocks);
for (int k = 0; k < 8; k++)
printf("%02x", parent_block[k]);
printf("...\n");
}
// 递归构建上层
return build_hash_tree(params, hash_device,
level_offset - parent_blocks, parent_blocks);
}
// 完整性校验函数
static int verify_data_block(const uint8_t *data_device,
const uint8_t *hash_device,
const struct verity_params *params,
uint64_t block_num)
{
uint8_t calculated[DIGEST_SIZE];
uint8_t expected[DIGEST_SIZE];
uint64_t hash_block_num = block_num / HASHES_PER_BLOCK;
uint32_t hash_offset = (block_num % HASHES_PER_BLOCK) * DIGEST_SIZE;
// 1. 计算数据块的 SHA-256
const uint8_t *data_block = data_device + block_num * BLOCK_SIZE;
hash_block(data_block, BLOCK_SIZE, params->salt, params->salt_size, calculated);
// 2. 从哈希树中读取预期哈希值
const uint8_t *hash_block = hash_device + hash_block_num * BLOCK_SIZE;
memcpy(expected, hash_block + hash_offset, DIGEST_SIZE);
// 3. 比较
if (memcmp(calculated, expected, DIGEST_SIZE) != 0) {
fprintf(stderr, "Block %llu: VERIFICATION FAILED\n", block_num);
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <data_image>\n", argv[0]);
return 1;
}
// 1. 读取数据文件
FILE *fp = fopen(argv[1], "rb");
if (!fp) { perror("fopen"); return 1; }
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
uint8_t *data_device = aligned_alloc(BLOCK_SIZE,
(file_size + BLOCK_SIZE - 1) & ~(BLOCK_SIZE - 1));
memset(data_device, 0, (file_size + BLOCK_SIZE - 1) & ~(BLOCK_SIZE - 1));
fread(data_device, 1, file_size, fp);
fclose(fp);
uint64_t data_blocks = (file_size + BLOCK_SIZE - 1) / BLOCK_SIZE;
printf("Data blocks: %lu\n", data_blocks);
// 2. 计算哈希树各层的大小
uint64_t hash_blocks = data_blocks;
uint64_t total_hash_blocks = 0;
uint64_t level_blocks = data_blocks;
printf("Hash tree levels:\n");
for (int level = 0; ; level++) {
level_blocks = (level_blocks + HASHES_PER_BLOCK - 1) / HASHES_PER_BLOCK;
total_hash_blocks += level_blocks;
printf(" Level %d: %lu blocks\n", level, level_blocks);
if (level_blocks == 1)
break;
}
printf("Total hash blocks: %lu (%lu KB)\n",
total_hash_blocks, total_hash_blocks * BLOCK_SIZE / 1024);
// 3. 分配哈希设备
uint8_t *hash_device = calloc(total_hash_blocks, BLOCK_SIZE);
// 4. 构建 Level 0(叶子哈希层)
struct verity_params params = {0};
params.data_blocks = data_blocks;
params.hash_block_size = BLOCK_SIZE;
params.salt_size = 0; // 简化示例,无 salt
for (uint64_t i = 0; i < data_blocks; i++) {
hash_block(data_device + i * BLOCK_SIZE, BLOCK_SIZE,
NULL, 0,
hash_device + i * DIGEST_SIZE);
}
// 5. 递归构建上层哈希树
build_hash_tree(¶ms, hash_device, data_blocks - 1, data_blocks);
// 6. 验证所有数据块
printf("\nVerifying all blocks...\n");
int verified = 0;
for (uint64_t i = 0; i < data_blocks && i < 1000; i++) {
if (verify_data_block(data_device, hash_device, ¶ms, i) == 0)
verified++;
}
printf("Successfully verified %d/%d blocks\n", verified,
(int)(data_blocks < 1000 ? data_blocks : 1000));
// 7. 模拟篡改攻击
printf("\nSimulating tamper on block 10...\n");
data_device[10 * BLOCK_SIZE + 0x100] ^= 0xFF;
verify_data_block(data_device, hash_device, ¶ms, 10);
free(data_device);
free(hash_device);
return 0;
}
编译运行:
gcc -o verity_tool verity_tool.c -lcrypto -lssl
dd if=/dev/urandom of=test.img bs=1M count=10
./verity_tool test.img
六、安全模型与限制
dm-verity 提供的是离线完整性保护(offline integrity protection)。它的安全模型建立在以下假设之上:
- **根哈希可信**:根哈希必须通过安全信道获取(如数字签名、可信 bootloader 传递)
- **数据区暴露可接受**:原始数据块本身不包含机密信息(或额外加密保护)
- **只读假设**:数据写入必须发生在哈希树构建之前
dm-verity 不提供的保护:
- 不防重放攻击(可结合 `dm-integrity` 的 journal 模式)
- 不防数据删除(可结合写时复制 CoW 机制)
- 不提供机密性(需配合 `dm-crypt` 使用)
在生产环境中,dm-verity 通常与 dm-crypt 配合使用。内核支持设置 allow_discards 等参数优化 SSD 场景下的性能。
七、实战:容器镜像完整性验证
在 OCI 容器镜像中,dm-verity 正在成为镜像层完整性验证的重要方案。以下是使用 verity 保护容器存储层的完整流程:
# 1. 提取容器镜像层
skopeo copy docker://alpine:3.18 oci:alpine:3.18
cd alpine/blobs/sha256
# 2. 解压 tar 层,创建循环设备
layer_hash=$(cat ../index.json | jq -r '.manifests[0].digest' | cut -d: -f2)
tar xf ${layer_hash}
dd if=/dev/zero of=verity-data.img bs=1M count=20
mkfs.ext4 verity-data.img
# 3. 挂载并复制数据
mount -o loop verity-data.img /mnt/tmp
tar xf ${layer_hash} -C /mnt/tmp/
umount /mnt/tmp
# 4. 创建 verity 哈希树
veritysetup format verity-data.img verity-hash.img
# 5. 获取 root hash
HASH_INFO=$(veritysetup format verity-data.img verity-hash.img)
ROOT_HASH=$(echo "$HASH_INFO" | grep "Root Hash:" | awk '{print $NF}')
# 6. 通过 device mapper 加载
dmsetup create alpine-verity --table \
"0 $(blockdev --getsz verity-data.img) verity 1 \
verity-data.img verity-hash.img \
4096 4096 $(echo "$HASH_INFO" | grep "Data blocks:" | awk '{print $NF}') 1 \
sha256 $ROOT_HASH"
# 7. 挂载验证后的设备
mount /dev/mapper/alpine-verity /mnt/safe
# 8. 验证:篡改任何数据将导致 EIO
echo "test" > /dev/mapper/alpine-verity # 会失败,设备只读
八、总结
dm-verity 是 Linux 内核中一个精巧而强大的完整性子系统。它的核心思想简洁:通过 Merkle 哈希树将整个块设备的完整性归约为一个根哈希值。一旦根哈希被可信地锚定(签名、硬件信任根),任何后续的数据篡改都能被立即检测。
在多租户、容器化、边缘计算场景中,dm-verity 提供了比传统签名校验更细粒度的保护——它保护的是运行时的每一个 4KB 块,而不是整体镜像文件。理解其哈希树数据结构和内核验证流程,对于构建可信的 Linux 存储栈至关重要。
对于需要同时保证完整性和机密性的场景,建议组合使用 dm-verity + dm-crypt,或者使用更新的 dm-integrity(支持在线写入和 journaling),在不同的安全-性能权衡中找到最佳平衡点。

发表评论 取消回复