【译】如何为 Secure Boot 签名

本文最早于 2017 年 8 月 1 日发表于 Matt's blog: How to sign things for Secure Boot,译于 2022 年 9 月 30 日,到今天为止内容可能已有更新,仅作参考。

Secure Boot 签名

Secure Boot(安全启动)的概念要求从最早的硬件加载的固件代码,一直到作为内核的一部分加载的内核模块之间存在一个信任链。换句话说,不仅是固件和 bootloader(引导加载程序)需要签名,内核和内核模块同样需要签名。人们通常不会更改固件或 bootloader,但是很可能会重新构建内核或添加硬件制造商提供的额外模块。

在 Ubuntu 中可能会发生这样的事情:你可能想构建自己的内核(但我们希望你可以只使用我们在源中提供的通用内核),并且你可能要安装自己的内核模块。这意味着要对 UEFI 二进制文件和内核模块进行签名,这可以使用自己的一套工具来完成。

但是首先,要了解用于 Secure Boot 的信任链的更多信息。

shim 中的证书

想要为 UEFI Secure Boot 签名,你需要创建一个可以在固件中导入的 X509 证书,随后可以直接通过制造商固件导入,也可以很容易地通过 shim 导入。

创建用于 UEFI Secure Boot 的证书相对简单。openssl 可以通过运行几个 SSL 命令来完成。现在,我们需要为模块签名创建一个 SSL 证书。

首先创建一些配置,让 openssl 知道我们想要创建什么(命名为 openssl.cnf):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# This definition stops the following lines choking if HOME isn't
# defined.
HOME = .
RANDFILE = $ENV::HOME/.rnd
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3
string_mask = utf8only
prompt = no

[ req_distinguished_name ]
countryName = CA
stateOrProvinceName = Quebec
localityName = Montreal
0.organizationName = cyphermox
commonName = Secure Boot Signing
emailAddress = example@example.com

[ v3 ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:FALSE
extendedKeyUsage = codeSigning,1.3.6.1.4.1.311.10.3.6,1.3.6.1.4.1.2312.16.1.2
nsComment = "OpenSSL Generated Certificate"

可以选择修改 [ req_distinguished_name ] 下的值,或者完全删除该部分与 distinguished_nameprompt 字段,此时 openssl 会要求你输入值来为证书设置标识。

标识本身并不重要,但后面的一些值很重要:例如,请确保 extendedKeyUsage 字段中有 1.3.6.1.4.1.2312.16.1.2,这是一个 OID,可以告诉 shim 生成的是一个模块签名证书

然后,我们可以开始有趣的部分:创建私钥和公钥。

1
2
3
4
5
openssl req -config ./openssl.cnf \
-new -x509 -newkey rsa:2048 \
-nodes -days 36500 -outform DER \
-keyout "MOK.priv" \
-out "MOK.der"

此命令将创建证书的私钥和公钥来进行签名。你需要这两个文件来签名,并且在 shim 中只需要导入公钥(MOK.der)。

译者注: 1. 若生成密钥时出现类似 Can't load /home/username/.rnd into RNG 的错误,可能是因为没有生成 .rnd 随机数文件,可以在 home 目录使用 openssl rand -writerand .rnd 生成,或修改配置文件中 RANDFILE 字段的值。 2. extendedKeyUsage 字段限定了该证书的用途。 1. 1.3.6.1.4.1.2312.16.1.2 是一个只能用于模块签名的 OID,若将生成的密钥用于签名内核是无效的,即使签名了也会无法通过 shim 的启动验证。Linux 内核相关的常用 OID 号: - 1.3.6.1.4.1.2312.16 用于内核的 OID - 1.3.6.1.4.1.2312.16.1 - X.509 extendedKeyUsage 限制集 - 1.3.6.1.4.1.2312.16.1.1 - 仅可用于固件签名 - 1.3.6.1.4.1.2312.16.1.2 - 仅可用于模块签名 - 1.3.6.1.4.1.2312.16.1.3 - 仅可用于 Kexecable image(内核)签名 2. 更多 OID 选项可以参考 Global OID reference database

注册密钥

现在,让我们在 shim 中注册我们刚刚创建的密钥。这使得它可以被接受为内核想要加载的任何模块的有效签名密钥,或者你想要构建自己的 bootloader 或内核的有效密钥(前提是在 openssl.cnf 中没有设置前面讨论的 1.3.6.1.4.1.2312.16.1.2 OID)。

要注册密钥,请使用 mokutil 命令:

1
sudo mokutil --import MOK.der

按照提示输入密码。

完成此操作后,重新启动系统。就在加载 GRUB 之前,shim 将显示蓝屏(实际上是 shim 项目的另一部分,称为 MokManager)。在该界面选择“注册 MOK”,然后跟随菜单完成注册过程。你还可以使用“View key”查看要添加的密钥的某些属性,以确保添加正确。MokManager 会询问你在早些时候运行 mokutil 时输入的密码,并保存密钥,再次重新启动。

译者注: 1. MokManager 启动(蓝屏出现)后需要在短时间内按下任意键进入,否则会跳过导入向导直接进入系统,此时需要重新运行 mokutil 命令导入。 2. 导入完成后可以在系统中使用 mokutil --list-enrolled 查看到已导入的证书。 3. 重复导入已导入的证书时也会提示 SKIP: MOK.der is already enrolled

对模块签名

在签名之前,可以查看 /proc/keys 以确保我们添加的密钥已经在内核中可见。

1
2
3
4
5
6
7
8
9
$ sudo cat /proc/keys
0020f22a I--Q--- 1 perm 0b0b0000 0 0 user invocation_id: 16
0022a089 I------ 2 perm 1f0b0000 0 0 keyring .builtin_trusted_keys: 1
003462c9 I--Q--- 2 perm 3f030000 0 0 keyring _ses: 1
00709f1c I--Q--- 1 perm 0b0b0000 0 0 user invocation_id: 16
00f488cc I--Q--- 2 perm 3f030000 0 0 keyring _ses: 1
[...]
1dcb85e2 I------ 1 perm 1f030000 0 0 asymmetri Build time autogenerated kernel key: eae8fa5ee6c91603c031c81226b2df4b135df7d2: X509.rsa 135df7d2 []
[...]

确保里面存在一个带有之前输入的属性(如 commonName 等)的密钥。

要对内核模块进行签名,可以使用 kmodsign 命令:

1
kmodsign sha512 MOK.priv MOK.der module.ko

其中 module.ko 是要签名的内核模块文件的文件名。kmodsign 会将签名附加到文件中。如果你希望保持签名独立并手动将其对应到模块,请参见 kmosign –help

你可以通过检查模块是否包含字符串 ~Module signature appended~ 来验证模块是否已签名成功:

1
2
3
4
5
6
$ hexdump -Cv module.ko | tail -n 5
00002c20 10 14 08 cd eb 67 a8 3d ac 82 e1 1d 46 b5 5c 91 |.....g.=....F.\.|
00002c30 9c cb 47 f7 c9 77 00 00 02 00 00 00 00 00 00 00 |..G..w..........|
00002c40 02 9e 7e 4d 6f 64 75 6c 65 20 73 69 67 6e 61 74 |..~Module signat|
00002c50 75 72 65 20 61 70 70 65 6e 64 65 64 7e 0a |ure appended~.|
00002c5e

你也可以用这种方式(使用 hexdump)来检查签名的密钥是否是你创建的密钥。

对内核与 bootloader 签名

要对希望用 shim 加载的自定义内核或其他 EFI 二进制文件进行签名,需要使用不同的命令:sbsign。此时我们需要不同格式的证书。

将之前创建的证书转换为 PEM:

1
openssl x509 -in MOK.der -inform DER -outform PEM -out MOK.pem

用 PEM 文件签署 EFI 二进制文件:

1
sbsign --key MOK.priv --cert MOK.pem my_binary.efi --output my_binary.efi.signed

只要签名密钥注册在 shim 中,并且不包含前面的 OID(因为这限制了密钥只能用于内核模块签名,译者注:指不包含 1.3.6.1.4.1.2312.16.1.2),就可以用 shim 加载二进制文件。

译者注:可以使用 sbverify 命令检查文件是否签名成功,具体用法请参考 sbverify -help

不使用 shim 处理签名

如果你不想使用 shim 来处理(译者注:导入)密钥(但我建议你使用它),你需要创建不同的证书:一个是系统的 PK (Platform Key),你可以直接通过 KeyTool 或系统提供的固件工具注册密钥到固件中。本文不会详细说明在固件中注册密钥的步骤,因为它往往因系统而异,但主要思想是让系统进入 Secure Boot 的 “Setup Mode”;运行 KeyTool,并注册密钥——首先安装 KEK 和 DB 密钥,然后是 PK。这些文件需要被放在某些 FAT 分区。

我有个脚本来生成正确的证书和文件,在这里分享(它们本身是从某个地方复制过来的,记不清了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
echo -n "Enter a Common Name to embed in the keys: "
read NAME
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME PK/" -keyout PK.key \
-out PK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME KEK/" -keyout KEK.key \
-out KEK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME DB/" -keyout DB.key \
-out DB.crt -days 3650 -nodes -sha256
openssl x509 -in PK.crt -out PK.cer -outform DER
openssl x509 -in KEK.crt -out KEK.cer -outform DER
openssl x509 -in DB.crt -out DB.cer -outform DER
GUID=`python -c 'import uuid; print str(uuid.uuid1())'`
echo $GUID > myGUID.txt
cert-to-efi-sig-list -g $GUID PK.crt PK.esl
cert-to-efi-sig-list -g $GUID KEK.crt KEK.esl
cert-to-efi-sig-list -g $GUID DB.crt DB.esl
rm -f noPK.esl
touch noPK.esl
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k PK.key -c PK.crt PK PK.esl PK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k PK.key -c PK.crt PK noPK.esl noPK.auth
chmod 0600 *.key
echo ""
echo ""
echo "For use with KeyTool, copy the *.auth and *.esl files to a FAT USB"
echo "flash drive or to your EFI System Partition (ESP)."
echo "For use with most UEFIs' built-in key managers, copy the *.cer files."
echo ""

与以前相同的逻辑相同:根据需要使用 sbsignkmodsign 签名(sbsign 使用 .crt 文件,kmodsign 使用 .cer 文件);只要密钥正确地注册到固件或 shim 中,它们就能成功加载。

Ubuntu 的 Secure Boot 有何进展

签名是一件复杂的事情 —— 你需要创建 SSL 证书,将它们注册到固件或 shim 中……你需要对 Secure Boot 的工作原理以及要使用的命令有相当的了解。很明显,这并不是每个人都能做到的,而且一开始就有点糟糕。出于这个原因,我们正在努力使安装 DKMS 模块时的密钥创建、注册和签名更加容易。

update-secureboot-policy 应该很快就能让你生成并注册一个密钥,并且 DKMS 将能够使用该密钥自行签名。