Linux 内核模块签名与 Secure Boot 信任链深度分析:从 UEFI 固件到运行时模块验证

在现代计算基础设施中,操作系统内核的安全性是整个系统安全的基础。一旦内核被攻破,攻击者就获得了系统的最高权限——Ring 0 级别的控制。为了防止恶意代码在内核层面执行,Linux 引入了内核模块签名机制(Kernel Module Signing)和 Secure Boot 信任链。这两者共同构建了一道从系统启动到运行时模块加载的完整防线。

本文将深入剖析这一信任链的每一个环节:从 UEFI 固件的安全启动开始,到内核模块签名的密码学实现,再到生产环境中的部署与排错,力求给读者提供一套完整的工程实践指南。

一、Secure Boot 的基础原理

Secure Boot 是由 UEFI 规范定义的一项安全功能,其核心目标是在系统启动的每个阶段验证下一阶段代码的完整性和真实性。其核心理念是:"永远不要执行未经信任的代码"。

整个启动链可以简化为:

UEFI Firmware → Shim/GRUB → Kernel → Initramfs → Userspace
     ↓              ↓          ↓          ↓
   [db 验证]    [shim 验证]  [vmlinuz]  [dm-verity]

在 UEFI Secure Boot 中,信任根存储在固件的密钥数据库中:

  • db (authorized signatures database):存储受信任的签名证书或哈希值。UEFI 应用程序必须被 db 中的证书签名才能执行。
  • dbx (forbidden signatures database):存储已被吊销的证书或哈希值。即使一个签名证书在 db 中,如果它也在 dbx 中,该证书签名的代码仍然会被拒绝执行。
  • KEK (Key Exchange Key):用于更新 db 和 dbx 的密钥,形成二级信任链。
  • Platform Key (PK):顶层的根密钥,用于更新 KEK。一旦设置 PK,系统进入"用户模式"(User Mode);如果 PK 被清空,系统进入"设置模式"(Setup Mode)。

理解这些概念是接下来的基础。实际上,Linux 发行版通常通过 Shim 这个小型 EFI 引导加载程序来解决自身与 UEFI 信任模型的兼容性问题。

二、Shim —— 连接 UEFI 与 Linux 的桥梁

Microsoft 是 UEFI 安全启动生态的主导者,几乎所有出厂支持 Secure Boot 的 PC 设备都预装了 Microsoft UEFI CA。然而,Linux 发行版不可能对每个内核更新都向 Microsoft 申请签名,因此社区创造了 Shim 这一巧妙的解决方案。

Shim 是一个极其精简的 EFI 应用程序,它的作用类似于一个"翻译层":

  1. Shim 本身由 Microsoft 的 UEFI CA 签名(这花费了 Linux 社区很长时间才说服 Microsoft)。
  2. Shim 内置了每个 Linux 发行版自己的公钥(例如 Canonical 的公钥、Debian 的公钥等),或者是 Machine Owner Key(MOK)。
  3. Shim 加载 GRUB 时,使用内置的公钥或 MOK 对 GRUB 进行验证。
  4. GRUB 再使用发行版公签名的镜像后,继续加载内核。

这种机制允许每个發行版维护自己的密钥对,而不必每次都走 Microsoft 的签名流程。对于使用自编译内核或自定义模块的场景,用户可以通过 MOK Manager 将自己的公钥导入 Shim 的信任链中。

实战:查看当前 Shim 的 MOK 列表

在 Linux 系统中,你可以直接查看当前已注册的 MOK:

# 列出当前 MOK 列表
mokutil --list-enrolled

# 输出示例:
[Root]  MOKlistRT: 1 entries
[Root]   sig: Microsoft Corporation UEFI CA 2011
[Root]   sig: Canonical Ltd. Master CA Certificate
[Root]   sig: My Custom CA Key (自签名)

三、内核模块签名机制详解

3.1 为什么需要模块签名?

Linux 是一个宏内核(Monolithic Kernel),但通过可加载内核模块(Loadable Kernel Module, LKM)实现了优秀的模块化设计。任何人都可以编写一个 .ko 文件并尝试通过 insmod 或 modprobe 加载到内核中。如果没有签名验证机制,攻击者可以轻易地:

  • 加载一个 rootkit 模块实现后门驻留
  • 绕过安全策略(如 SELinux、AppArmor)
  • 窃取内核态敏感数据(密钥、密码等)

内核模块签名通过密码学手段确保:只有持有对应私钥的人签名的模块才能被加载。

3.2 密码学实现

内核模块签名使用的是 X.509 证书链。具体流程如下:

  1. 构建内核时,CONFIG_MODULE_SIG 配置选项启用签名验证。
  2. 内核在编译过程中,使用 scripts/sign-file 工具对每个 .ko 文件进行签名。
  3. 签名的数学过程采用 SHA-512 哈希算法 + RSA-2048(或 ECDSA)公私钥对。
  4. 签名数据嵌入到 ELF 文件的特殊段中(通常在模块文件末尾)。

sign-file 工具的签名的核心步骤:

sign-file sha512 <私钥> <公钥证书> <模块文件.ko>

实际上,内核源码树中的 scripts/sign-file.c 负责生成一个 PKCS#7 格式的签名,然后将其追加到 ELF 文件的特定段中。内核在加载模块时,会自动提取这个签名并验证其合法性。

3.3 内核配置选项

与模块签名相关的关键 Kconfig 选项包括:

配置项 说明
CONFIG_MODULE_SIG 启用模块签名验证
CONFIG_MODULE_SIG_FORCE 强制模式,拒绝加载任何未签名或签名无效的模块
CONFIG_MODULE_SIG_ALL 编译时对所有模块签名
CONFIG_MODULE_SIG_HASH 使用的哈希算法(sha256/sha384/sha512)
CONFIG_MODULE_SIG_KEY 默认签名密钥路径

在强制模式(CONFIG_MODULE_SIG_FORCE=y)下,即使加载模块的用户是 root,如果模块没有有效的签名,内核将直接返回 -EKEYREJECTED(129)。

3.4 内置密钥环

内核维护了一个 内置密钥环(.builtin_keyring),其中包含构建时指定的公钥证书链。你可以通过以下方式查看:

# 查看已加载到内核中的密钥
keyctl list @u

# 查看二进制证书信息
cat /proc/keys | grep -i module

你也可以在编译内核时通过 CONFIG_SYSTEM_TRUSTED_KEYS 指定额外的 PEM 格式公钥文件,这些公钥会被嵌到 vmlinuz 中,成为内置信任的一部分。

四、信任链的完整构建

从工程角度看,一个完整的签名信任链包含以下几个关键组件:

4.1 密钥层级设计

在生产环境中,不建议直接使用根密钥(Root CA)对日常模块签名。推荐的做法是:

Root CA (HSM 中离线存储)
    └── Intermediate CA 1 (用于内核签名)
         └── Kernel Signing Key (日尝使用, CI/CD 中使用)
    └── Intermediate CA 2 (用于第三方驱动)
         └── Vendor Signing Key

这种层级结构有以下优势:

  • Root CA 可以离线保存,极大降低泄露风险
  • Intermediate CA 可以根据用途隔离,互不干扰
  • 吊销某个 Intermediate CA 不会影响其他路径的信任

4.2 编译时签名流程

以 Gentoo 或 Arch Linux 编译第三方内核为例,构建时的签名流程大致如下:

# 1. 生成密钥对(首次做一次)
openssl req -new -x509 -newkey rsa:4096 \
  -keyout MOK.key -out MOK.crt \
  -nodes -days 3650 \
  -subj "/CN=My Linux Module Signing Key/"

# 转换为 DER 格式供 mokutil 使用
openssl x509 -in MOK.crt -outform DER -out MOK.der

# 2. 将公钥注册到 Shim/MOK
mokutil --import MOK.der
# 需要设置一个一次性密码,下次重启时进入 MOK Manager 确认

# 3. 编译内核时指定密钥
make CONFIG_MODULE_SIG_KEY=/path/to/MOK.priv modules_install

# 4. 对 DKMS 模块自动签名
# 在 /etc/dkms/framework.conf 中添加:
sign_tool="/usr/lib/$(uname -r)/build/scripts/sign-file sha512 /path/to/MOK.priv /path/to/MOK.pub"

4.3 DKMS 模块的签名挑战

Dynamic Kernel Module Support (DKMS) 允许第三方模块(如显卡驱动、文件系统驱动等)在内核升级时自动重新编译。然而,DKMS 编译后的模块也需要签名。如果没有正确配置,每次内核升级后模块签名就会失效,导致模块无法加载。

解决方案是使用 DKMS 的 sign_tool 配置:

# /etc/dkms/framework.conf
sign_tool="/etc/dkms/sign-kernel-modules \
  /path/to/MOK.key /path/to/MOK.der $kernelver $(uname -m)"

对于 NVIDIA 专有驱动,内核提供了 CONFIG_SYSTEM_EXTRA_CERTIFICATE 选项,或者可以通过 dkms autoinstall 的 post-install hook 来自动签名。

五、运行时模块加载的签名验证

当用户执行 modprobe <module> 或 insmod <module.ko> 时,以下验证流程被触发:

  1. 内核读取 ELF 文件末尾特殊段的 PKCS#7 签名数据。
  2. 使用签名中的公钥(或证书链中更高一级的公钥)验证签名哈希。
  3. 将来验证的公钥与内核的 secondary信任密钥环(.secondary 或 .module_sign)进行比对。
  4. 验证模块的哈希值是否被篡改。

在内核源码中,这个过程由 kernel/module/signing.c 实现,核心函数为 mod_verify_sig()。

5.1 密钥环优先级

当多个密钥环存在时,内核按以下顺序查找信任锚点:

  1. .builtin_keyring —— 编译时内置的公钥,不可修改
  2. .secondary 或 .module_sign —— 通过 keyctl 或系统配置加载的密钥
  3. .platform —— 由 UEFI 提供的平台密钥(PK、KEK、db、dbx)

这种分层的信任模型保证了:即使攻击者控制了用户态,也无法注入自己签名的密钥——除非他们有平台的物理访问权限。

5.2 内核 Lockdown 模式

从 Linux 5.4 开始,内核引入了 Lockdown 功能,进一步增强内核安全。Lockdown 有以下几种模式:

  • none(默认):不限制用户态对内核的访问
  • integrity:禁止用户态通过 /dev/mem、/dev/kmem、kexec 等方式修改内核内存
  • confidentiality:在 integrity 基础上进一步限制,甚至禁止读取部分内核数据(如内核地址空间布局)

在 Secure Boot 环境下,内核会自动进入 integrity 级别的 Lockdown,确保运行时的系统完整性。

# 检查当前 Lockdown 模式
cat /sys/kernel/security/lockdown
# 输出:none [integrity] confidentiality
# 中括号中的为当前启用模式

六、生产环境部署实战

6.1 场景一:企业内部的 Linux 镜像管理

在企业环境中,通常需要为内部 Linux 部署构建安全启动链。推荐步骤如下:

Step 1:生成企业 Root CA

# 在离线环境中生成 Root CA
openssl genrsa -aes256 -out root-ca.key -rand /dev/urandom 8192
openssl req -x509 -new -nodes -key root-ca.key \
  -sha512 -days 7300 \
  -out root-ca.crt \
  -subj "/O=MyCorp/CN=MyCorp Internal Root CA"

Step 2:生成内核模块签名中间证书

# 生成 Intermediate CA
openssl genrsa -aes256 -out module-ca.key 4096
openssl req -new -key module-ca.key \
  -out module-ca.csr \
  -subj "/O=MyCorp/CN=MyCorp Module Signing CA v1"

# 使用 Root CA 签名 Intermediate CA
openssl x509 -req -in module-ca.csr \
  -CA root-ca.crt -CAkey root-ca.key \
  -CAcreateserial -out module-ca.crt \
  -days 1825 -sha384 \
  -extfile <(printf "critical,CA:TRUE,pathlen:0\nkeyUsage:critical,digitalSignature,cRLSign,keyCertSign")

Step 3:在内核编译配置中指定公钥

.config 文件
CONFIG_MODULE_SIG=y
CONFIG_MODULE_SIG_FORCE=y
CONFIG_MODULE_SIG_HASH="sha512"
CONFIG_SYSTEM_TRUSTED_KEYS="/path/to/module-ca.crt"

Step 4:在部署机器上注册公钥到 MOK

# 将公钥打包为 DER 格式
openssl x509 -in module-ca.crt -outform DER -out module-ca.der

# 导入 MOK(需要重启)
mokutil --import module-ca.der

6.2 场景二:CI/CD 中的自动化签名

在自动化的内核构建流水线中,集成内核模块签名需要注意密钥的安全性。不应将 Root CA 直接存储在 CI/CD 系统中,而是应该:

  • 使用专门的密钥管理服务(如 HashiCorp Vault、AWS KMS)
  • 在构建过程中使用临时 Intermediate CA 签名
  • 签名操作完成后立即销毁 Intermediate CA 私钥

使用 Vault 签名的示例脚本:

#!/bin/bash
# 从 Vault 获取签名的中间证书(有效期仅 1 小时)
vault write -field=cert transit/sign/module-key \
  input=$(openssl dgst -sha512 -binary "$1" | base64) \
  > "$1.sig"

# 使用临时证书中的公钥组合成可用于验证的 X.509 证书链
cat root-ca.crt > module-ca-chain.pem
vault write -field=cert transit/read/module-key >> module-ca-chain.pem

# 使用组合后的密钥链签名模块
sign-file sha512 module-ca-chain.pem module-ca-chain.pem "$1"

6.3 场景三:容器化环境中的内核模块处理

在 Docker/Kubernetes 容器环境中,通常不建议在宿主机上动态加载模块。然而,在某些场景下(如运行需要特定内核模块的容器),需要安全的模块加载方式。

推荐的工程实践包括:

  1. 在 Host 上预先加载并签名模块,避免容器内 modprobe
  2. 为容器设置特定的 capability(如 CAP_SYS_MODULE),但仅限于受信任的容器
  3. 使用 CompST 或 Landlock 限制容器对模块加载的访问
# Pod Security Policy 示例
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restrict-modprobe
spec:
  allowedCapabilities: []
  requiredDropCapabilities:
    - ALL
  volumes: [...]
  hostNetwork: false
  hostPID: false
  runAsUser:
    rule: MustRunAsNonRoot
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: MustRunAs
  fsGroup:
    rule: MustRunAs

七、常见陷阱与排错

7.1 模块签名失败的所有权问题

在排查"Required key not available"错误时,应首先检查:

# 查看模块是否已签名
modinfo <module_name> | grep sig_
# 应该有:sig_id:         PKCS#7
#          signer:        ...
#          sig_key:       ...

# 如果 signer 字段为空,说明模块未被签名

7.2 内核升级后的签名丢失

使用 DKMS 时,内核升级后模块需要重新编译和签名。如果签名配置不正确,会导致以下症状:

  • 系统启动后某些功能丢失(如 GPU 加速、网络驱动)
  • dmesg 中出现类似 "module verification failed: signature and/or required key missing" 的错误

排查方法:

# 查看系统日志
journalctl -k | grep -i "module\|signature"

# 检查 DKMS 模块状态
dkms status

# 手动重新签名
dkms autoinstall --kernelsource /usr/src/linux

7.3 密钥生命周期管理

密钥的吊销与更新是 Secure Boot 中最容易忽视的环节。一旦旧的签名私钥泄露,必须:

  1. 立即将公钥加入 dbx(固件吊销列表)
  2. 更新 db 中的公钥为新的证书
  3. 重新签名所有相关的内核模块
# 吊销旧的 MOK 密钥
mokutil --delete <old-key.der>

# 或使用 efivar 管理 UEFI 密钥数据库
efivar -w -n 8BE4DF61-93CA-11D2-AA0D-00E098032B8c-dbx -t file dbx-update.bin

八、交叉编译场景下的模块签名

在嵌入式或 ARM 架构的交叉编译场景中,模块签名的配置有一些特殊之处。主要挑战在于:

  1. 签名工具的路径可能在编译主机上不同(scripts/sign-file 在编译主机上运行,但内核是为目标架构编译的)
  2. 内核配置中的密钥路径可能需要绝对路径

交叉编译时推荐的签名配置:

# 在内核 .config 中设置签名工具参数
CONFIG_MODULE_SIG_KEY="/absolute/path/to/signing_key.pem"
# signing_key.pem 包含私钥和证书(PEM 格式,两者合并)

# 编译
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules

# 安装模块
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- INSTALL_MOD_PATH=/tmp/target modules_install

九、性能与安全性的权衡

有人可能会质疑:对每个模块进行签名和运行时验证是否会影响系统性能?

实际测试表明:

  • 签名验证的开销:每个模块加载时增加约 0.5ms - 2ms(取决于使用的哈希算法和密钥长度)
  • SHA-256 vs SHA-512:在 x86_64 架构上,SHA-512 性能甚至可能优于 SHA-256(因为 x86 有原生 SHA 指令扩展支持 512 位操作)
  • 缓存效应:内核会对已验证的模块进行缓存,同一模块不会重复验证
  • 总体影响:在服务器场景中,模块加载通常只发生一次(启动时),对日常运行几乎没有可感知的影响

因此,在生产环境中强烈建议启用 CONFIG_MODULE_SIG_FORCE=y,将完整性作为最高优先级。

十、总结与展望

Linux 内核模块签名与 Secure Boot 信任链是一个涉及硬件、固件、引导加载器、内核和工具链的复杂系统工程。构建一个完整且安全的签名体系需要:

  1. 正确理解信任链:从 Platform Key 到内核模块的每一级信任传递
  2. 层级化密钥管理:Root CA 离线保存,Intermediate CA 按用途隔离
  3. 自动化签名流程:将签名集成到内核编译和 CI/CD 流水线中
  4. 持续的密钥生命周期管理:定期轮换、及时吊销泄露密钥

随着机密计算(Confidential Computing)技术的发展,未来可能会看到更多基于硬件安全模块(TPM/Pluton/CCA)的细粒度模块验证机制。而 eBPF 安全模块(LSM eBPF)的成熟也为动态的模块行为监控提供了新的可能。

对于需要在生产环境部署 Secure Boot 的组织,建议从最小可行方案(MVP)开始:先启用编译时签名,再逐步推进到自定义 CA 和硬件 Key 存储(HSM/TPM),最终实现全自动化的安全启动流水线。信任链的建设不是一蹴而就的,而是一个持续演进的过程。


参考资料:

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部