Khorina · 2023年06月08日 · 香港

深入分析Linux kernel安全特性: 内核模块签名

来源:乾越

概述

顾名思义,在开启该功能之后,内核在加载内核模块时,会对内核模块的签名进行检查。

如果内核模块本身没有经过签名,或者签名值与预期值不符,这两种情况都会被认为是签名认证失败。根据策略的不同,签名认证失败可能会导致模块被拒绝加载,也可能是继续正常加载但内核会显示一条警告信息。

内核模块签名功能的本质是限制root用户载入恶意的内核模块。当root用户加载一个内核模块时,内核在分辨是系统管理员还是攻击者的时候,依靠的就是能够进行身份认证的可信的X.509证书和与之对应的私钥。只要私钥存储妥当不发生泄露,攻击者就无法伪造X.509证书,因此也就不可能提供含有正确签名的内核模块;而系统管理员是唯一合法的X.509证书的使用者,是可以用合法的证书对应的私钥对内核模块进行签名的。

最佳实践

  • 用openssl命令生成PEM格式的签名key文件。
openssl req -new -nodes -utf8 -sha256 -days 36500 \
        -batch -x509 -config x509.genkey \
        -outform PEM -out system_key.pem \
        -keyout system_key.pem

其中x509.genkey的内容如下:

[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = v3_req

[ req_distinguished_name ]
O = Alibaba Group
OU = <your_organization>
CN = Test modsign key
emailAddress = <your_user_name>@alibaba-inc.com

[ v3_req ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always
  • 配置内核

    CONFIG_MODULE_SIG=y
    CONFIG_MODULE_SIG_FORCE=y
    CONFIG_MODULE_SIG_KEY=<前一个步骤生成的system_key.pem文件的路径>
    CONFIG_MODULE_SIG_HASH="sha256"
    CONFIG_MODULE_SIG_SHA256=y
    CONFIG_CRYPTO_SHA256=y
    CONFIG_MODULE_SIG_ALL=y

  • 编译内核和模块
make bzImage modules
  • 安装内核和内核模块
sudo make modules_install install
  • 对内核模块进行签名

在指定了CONFIG_MODULE_SIG_ALL=y的情况下,kbuild系统可以自动对模块进行签名,而且该步骤通常无需手动运行,会在module_install时自动执行签名。

make modules_sign
  • 用openssl全手动签名和验签
openssl smime -sign -nocerts -noattr -binary -in <module> -inkey \
    <key> -signer <x509> -outform der -out <raw sig>

openssl smime -verify -in <raw sig> -inform der -content <module> \
    -certfile <x509> -noverify -out /dev/null

内核配置选项

CONFIG_MODULE_SIG:Module signature verification
如果开启了该选项,在内核在加载内核模块时,会对内核模块的签名进行检查。

默认情况下,在加载没有签名或者是签名不正确的内核模块时,内核仅仅是打印一条提示信息,比如:

k_netlink: module verification failed: signature and/or required key missing - tainting kernel

同时将内核标记为tainted,然后继续正常加载签名有问题的模块。

CONFIG_MODULE_SIG_FORCE: Require modules to be validly signed
如果开启了该选项,在加载没有签名或者是签名不正确的内核模块时,内核会直接拒绝加载签名有问题的内核模块。

CONFIG_MODULE_SIG_HASH: Which hash algorithm should modules be signed with?
该选项用于决定签名时使用的摘要算法。每一种算法都有对应的内核选项:

  • CONFIG_MODULE_SIG_SHA1:sha1
  • CONFIG_MODULE_SIG_SHA224:sha224
  • CONFIG_MODULE_SIG_SHA256:sha256
  • CONFIG_MODULE_SIG_SHA384:sha384
  • CONFIG_MODULE_SIG_SHA512:sha512

注意:内核会使用crypto子系统中实现的摘要算法内核模块来计算摘要值,这就意味着这些实现摘要算法的模块必须已经事先编译为内置,否则就会发生“为了计算签名摘要值而要加载对应的模块但该模块的签名因缺少摘要算法模块而无法计算“的窘况。

CONFIG_MODULE_SIG_KEY: File name or PKCS#11 URI of module signing key

该选项指定了用于对内核模块签名的PEM格式的签名key文件的路径,或者是一个PKCS#11 URI(在实际进行签名前,签名工具会先把这个URI指定的key下载下来)。该选项的默认值是"certs/signing_key.pem"。如果没有修改这个默认值,内核会自动创建"certs/signing_key.pem"文件用于内核模块签名。

kbuild会将这个X.509证书文件转换为system certificate list并编译进内核中,然后在system trusted keyring初始化阶段将这个list中的每一个X.509证书都添加到builtin trusted keyring中。此时,每一个X.509证书就是一把system trusted key。因此,该选项指定的key文件中的所有X.509证书还可以当成system trusted key]来用。key文件至少包含一个PEM格式的私钥和与之关联的、PEM格式的X.509证书。可以通过级联的方式将更多的X.509证书添加到该key文件中。

但是反过来却是不成立的:并不是所有的system trusted key都可以用来验证内核模块的签名,比如secondary trusted keyring中的key就不能用来验证内核模块的签名;只有builtin trusted keyring中的key才可以用来验证内核模块的签名。

最后要提醒的是,用于模块签名的签名key可以是自签名的,也可以不是。如果不是自签名的,只要确保签名key和父key能被导入到builtin trusted keyring即可。不要求父key的父key也必须在builtin trusted keyring中。

CONFIG_MODULE_SIG_ALL: Automatically sign all modules

选中该选项后,kbuild会在执行make modules_install的时候对所有的内核模块进行签名。如果没有选中该选项,则需要使用者自己手动调用scripts/sign-file签名工具对内核模块进行签名。

内核启动参数

module.sig_enforce

如果将该参数传给内核,表示强制验证内核模块签名,效果上等价于CONFIG_MODULE_SIG_FORCE=y。如果内核在编译时已经将CONFIG_MODULE_SIG_FORCE设为了y,那这里的内核选项是不会起任何作用的。

该选项为内核强制验证功能在策略上提供了一定的灵活性,比如运行系统需要DKMS或者SystemTap支持的话,如果没有实现配套的PKI签名服务机制,最好将CONFIG_MODULE_SIG_FORCE设为n,同时为了保证安全在内核命令行参数中指定module.sig_enforce。在必要时,可以临时去掉module.sig_enforce,以便系统维护或调试。

实现细节

内核编译阶段

如果CONFIG_MODULE_SIG_KEY参数为默认值,自动生成内核模块签名key文件certs/signing_key.pem

如果CONFIG_MODULE_SIG_KEY的值为默认值certs/signing_key.pem,表示用户希望使用由kbuild自动生成的key文件对内核模块进行签名。

kbuild会自动执行以下命令生成key文件:

openssl req -new -nodes -utf8 -$(CONFIG_MODULE_SIG_HASH) -days 36500 \
    -batch -x509 -config certs/x509.genkey \
    -outform PEM -out $(obj)/signing_key.pem \
    -keyout certs/signing_key.pem

该命令的含义是:根据X509证书请求配置模板文件的内容,生成一个自签名的X509证书。生成的key文件保存为certs/signing_key.pem, 同时将自签名的X509证书附在key内容的后面。

其中生成X509证书请求时使用的配置模板文件的内容为:

[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = myexts

[ req_distinguished_name ]
#O = Unspecified company
CN = Build time autogenerated kernel key
#emailAddress = unspecified.user@unspecified.company

[ myexts ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid

将内核模块签名key文件build到system certificate list中

kbuild会调用scripts/extract-cert程序将CONFIG_MODULE_SIG_KEY指定的PEM格式的X.509证书文件转换为DER格式的certs/signing_key.x509文件:

scripts/extract-cert $(CONFIG_MODULE_SIG_KEY) signing_key.x509

如果CONFIG_MODULE_SIG_KEY指定的一个PKCS#11 URI,scripts/extract-cert程序也会在签名之前先下载好再使用。

接下来,上一个步骤生成的certs/signing_key.x509文件会连同system_certificates.S一起被编译为certs/system_certificates.o。该object文件的内容会被包含到内核image的.init.rodata节中。

__INITRODATA

        .align 8
        .globl VMLINUX_SYMBOL(system_certificate_list)
VMLINUX_SYMBOL(system_certificate_list):
__cert_list_start:
#ifdef CONFIG_MODULE_SIG
        .incbin "certs/signing_key.x509"
#endif
        .incbin "certs/x509_certificate_list"
__cert_list_end:

该system certificate list中的所有X.509证书最终都会被加载到builtin trusted keyring中,并作为system trusted key来使用。

顺便一提,由内核选项CONFIG_SYSTEM_TRUSTED_KEYS指定的system trusted key文件(即certs/x509_certificate_list)也会成为system certificate list的一部分。准确来说,它会在内核初始化system trusted klseyring时加入到builtin trusted keyring中。

Kernel启动阶段

初始化system trusted keyrings

由内核选项CONFIG_MODULE_SIG_KEY指定的对内核模块进行签名的key文件会在这个阶段被加载到builtin trusted keyring中。

用户态运行时阶段

加载内核模块

  • 当加载内核模块时,会对内核模块的签名进行认证。最终认证的结果会保存在info->sig_ok中。
static int load_module(struct load_info *info, const char __user *uargs,
                       int flags)
...
        err = module_sig_check(info, flags);
        if (err)
                goto free_copy;
...
  • 如果认证失败,info->sig_ok返回0,因此内核会打印一条信息:\<module_name>: module verification failed: signature and/or required key missing - tainting kernel,然后会将kernel状态标记为TAINT_UNSIGNED_MODULE。
...
#ifdef CONFIG_MODULE_SIG
        mod->sig_ok = info->sig_ok;
        if (!mod->sig_ok) {
                pr_notice_once("%s: module verification failed: signature "
                               "and/or required key missing - tainting "
                               "kernel\n", mod->name);
                add_taint_module(mod, TAINT_UNSIGNED_MODULE, LOCKDEP_STILL_OK);
        }
#endif
  • 对内核模块的签名认证的过程如下:
  1. 首先检查flags是否为0。当执行modprobe -f时,会要求内核忽略所有的版本检查。这会导致作为内核模块的数据的一部分的版本信息将被完全忽略。因此如果一个曾经签过名的模块有安全漏洞,那么攻击者可以在新内核上强行加载这个有漏洞的模块以便攻击新内核。因此,内核认为flags为非0值的时候,是可能存在上述跨内核版本攻击的可能性的,因此执行modprobe -f一定会导致签名认证失败。
  2. 检查内核模块尾部的mark字段是否为“~MODULE_SIG_STRING~\n”。
  3. 如果mark字段存在,进行签名检查。
  4. 如果mark不存在,根据当前的模块签名检查策略,决定返回值;如果使用了强制签名检查策略(CONFIG_MODULE_SIG_FORCE=y或指定了module.sig_enforce),则返回-ENOKEY;如果没有使用强制签名检查策略,返回0。
  5. 检查flags是否为0

结论:执行modprobe -f一定会导致签名认证失败, 这一点所有的责任人都要注意。原因是:作为内核模块的数据的一部分的版本信息将被modprobe -f完全忽略,如果一个曾经签过名的模块有安全漏洞,那么攻击者可以在新内核上强行加载这个有漏洞的模块以便攻击新内核。

#ifdef CONFIG_MODULE_SIG
static int module_sig_check(struct load_info *info, int flags)
{
        int err = -ENOKEY;
        const unsigned long markerlen = sizeof(MODULE_SIG_STRING) - 1;
        const void *mod = info->hdr;

        /*
         * Require flags == 0, as a module with version information
         * removed is no longer the module that was signed
         */
        if (flags == 0 &&
            info->len > markerlen &&
            memcmp(mod + info->len - markerlen, MODULE_SIG_STRING, markerlen) == 0) {
                /* We truncate the module to discard the signature */
                info->len -= markerlen;
                err = mod_verify_sig(mod, &info->len);
        }

        if (!err) {
                info->sig_ok = true;
                return 0;
        }

        /* Not having a signature is only an error if we're strict. */
        if (err == -ENOKEY && !sig_enforce)
                err = 0;

        return err;
}
#endif
  • 检查module签名格式中的各个字段,然后调用verify_pkcs7_signature()验证PKCS#7消息。
int mod_verify_sig(const void *mod, unsigned long *_modlen)
{
        struct module_signature ms;
        size_t modlen = *_modlen, sig_len;

        pr_devel("==>%s(,%zu)\n", __func__, modlen);

        if (modlen <= sizeof(ms))
                return -EBADMSG;

        memcpy(&ms, mod + (modlen - sizeof(ms)), sizeof(ms));
        modlen -= sizeof(ms);  // modlen包括模块内容+PKCS#7消息

        sig_len = be32_to_cpu(ms.sig_len); // PKCS#7消息的长度
        if (sig_len >= modlen)
                return -EBADMSG;
        modlen -= sig_len; // 仅包含模块内容的长度
        *_modlen = modlen; // 返回模块内容的长度

        if (ms.id_type != PKEY_ID_PKCS7) {
                pr_err("Module is not signed with expected PKCS#7 message\n");
                return -ENOPKG;
        }

        if (ms.algo != 0 ||
            ms.hash != 0 ||
            ms.signer_len != 0 ||
            ms.key_id_len != 0 ||
            ms.__pad[0] != 0 ||
            ms.__pad[1] != 0 ||
            ms.__pad[2] != 0) {
                pr_err("PKCS#7 signature info has unexpected non-zero params\n");
                return -EBADMSG;
        }

        return verify_pkcs7_signature(mod, modlen, mod + modlen, sig_len,
                                      NULL, VERIFYING_MODULE_SIGNATURE,
                                      NULL, NULL);
}
  • 对内核模块中的PKCS#7格式的消息进行验证。注意其中的trusted_keys参数为NULL,因此能够用于验证内核模块签名的,仅限builtin trusted keyring中的key;而导入到secondary trusted keyring中的key是不能用于内核模块签名的。
#ifdef CONFIG_SYSTEM_DATA_VERIFICATION

/**
 * verify_pkcs7_signature - Verify a PKCS#7-based signature on system data.
 * @data: The data to be verified (NULL if expecting internal data).
 * @len: Size of @data.
 * @raw_pkcs7: The PKCS#7 message that is the signature.
 * @pkcs7_len: The size of @raw_pkcs7.
 * @trusted_keys: Trusted keys to use (NULL for builtin trusted keys only,
 *                                      (void *)1UL for all trusted keys).
 * @usage: The use to which the key is being put.
 * @view_content: Callback to gain access to content.
 * @ctx: Context for callback.
 */
int verify_pkcs7_signature(const void *data, size_t len,
                           const void *raw_pkcs7, size_t pkcs7_len,
                           struct key *trusted_keys,
                           enum key_being_used_for usage,
                           int (*view_content)(void *ctx,
                                               const void *data, size_t len,
                                               size_t asn1hdrlen),
                           void *ctx)
{
        struct pkcs7_message *pkcs7;
        int ret;

        // 解析内核模块签名中的PKCS#7消息(DER格式),并将相关字段
        // 填充到pkcs7数据结构中。
        // struct pkcs7_message {
        //     PKCS#7中的signer证书列表(可以为空)
        //     struct x509_certificate *certs;
        //     证书撤销列表CRL(可以为空)
        //     struct x509_certificate *crl;
        //     Signer信息列表
        //     struct pkcs7_signed_info *signed_infos;
        //     PKCS#7消息格式版本号。1:  PKCS#7或CMS; 3:CMS
        //     u8       version;
        //     表示PKCS#7消息中是否包含authenticatedAttributes字段
        //     bool     have_authattrs;
        //     内容类型
        //     enum OID data_type;
        //     被签名的内容的长度
        //     size_t       data_len;
        //     被签名的内容的ASN.1 header的长度
        //     size_t       data_hdrlen;
        //     被签名的内容(如果为NULL表示是detached content)
        //     const void   *data;
        // };
        // 
        // Signer信息
        // struct pkcs7_signed_info {
        //   指向下一个signer信息
        //   struct pkcs7_signed_info *next;
        //   指向位于pkcs7_message.certs的signer证书(如果有的话)
        //   struct x509_certificate *signer;
        //   当前signer信息在signer信息列表中的位置
        //   unsigned   index;
        //   True if not usable due to missing crypto
        //   bool       unsupported_crypto;
        //
        //   Auth属性值: Message digest - the digest of the Content Data (or NULL)
        //   const void *msgdigest;
        //   unsigned   msgdigest_len;
        //
        //   Authenticated Attribute data (or NULL) */
        //   unsigned   authattrs_len;
        //   const void *authattrs;
        //
        //   Auth属性值
        //   unsigned long  aa_set;
        //   Auth属性值
        //   time64_t   signing_time;
        //
        //   指向signer生成的签名信息
        //   struct public_key_signature *sig;
        // };
        //   
        // struct public_key_signature {
        //   [0]包含的是由signer的issuerAndSerialNumber字段生成的key id
        //   [1]包含的则是skid(可以在X.509证书中的X509v3 Subject Key Identifier字段找到该值)
        //   struct asymmetric_key_id *auth_ids[2];
        //   具体的签名值
        //   u8 *s;
        //   签名值的长度
        //   u32 s_size;
        //   摘要值
        //   u8 *digest;
        //   摘要值的长度
        //   u8 digest_size;
        //   签名算法,目前仅支持"rsa"
        //   const char *pkey_algo;
        //   摘要算法,比如"sha256"等
        //   const char *hash_algo;
        // };
        pkcs7 = pkcs7_parse_message(raw_pkcs7, pkcs7_len);
        if (IS_ERR(pkcs7))
                return PTR_ERR(pkcs7);

        /* The data should be detached - so we need to supply it. */
        // 对于内核模块来说,content总是detached的。
        if (data && pkcs7_supply_detached_data(pkcs7, data, len) < 0) {
                pr_err("PKCS#7 signature with non-detached data\n");
                ret = -EBADMSG;
                goto error;
        }

        // 验证PKCS#7签名。过程如下:
        // 1. 计算内核模块主体内容的摘要值
        // 2. 遍历每一个signer信息,如果PKCS#7中包含了signer的X.509证书的话,使用其
        //     证书中的公钥验证signer的签名值
        // 3. 如果signer信息中不包含signer的X.509证书的话,这里同样也会返回0,目的是
        //     为了让接下来的system trusted key来验证signer中的的签名值
        ret = pkcs7_verify(pkcs7, usage);
        if (ret < 0)
                goto error;

        // 确定验证签名的、且可信的issuer证书的来源
        if (!trusted_keys) {
                trusted_keys = builtin_trusted_keys;
        } else if (trusted_keys == (void *)1UL) {
#ifdef CONFIG_SECONDARY_TRUSTED_KEYRING
                trusted_keys = secondary_trusted_keys;
#else
                trusted_keys = builtin_trusted_keys;
#endif
        }

        // 用system trusted keyring中的system trusted key作为signer的X.509证书来验证
        // PKCS#7中包含的每一个signer的签名值。这里的逻辑很简单,既然PKCS#7中可能
        // 没有包含signer的证书,那么如果能在system trusted keyring中找到signer的证书
        // 来验证每一个signer的签名值也是可以的。
        // 这里只考虑最简单的情况:signer信息中不包含X.509证书的情况。
        // 首先用auth_ids[0]、即通过signer.IssuerAndSerialNumbe字段
        // 计算出的key id在system trusted keyring中搜索与之匹配的system trusted key。
        // 如果没有找到,返回-ENOKEY,同时打印错误;如果找到匹配的system trusted key,
        // 则使用该key验证PKCS#7中的签名。
        ret = pkcs7_validate_trust(pkcs7, trusted_keys);
        if (ret < 0) {
                if (ret == -ENOKEY)
                        pr_err("PKCS#7 signature not signed with a trusted key\n");
                goto error;
        }

        if (view_content) {
                size_t asn1hdrlen;

                ret = pkcs7_get_content_data(pkcs7, &data, &len, &asn1hdrlen);
                if (ret < 0) {
                        if (ret == -ENODATA)
                                pr_devel("PKCS#7 message does not contain data\n");
                        goto error;
                }

                ret = view_content(ctx, data, len, asn1hdrlen);
        }

error:
        pkcs7_free_message(pkcs7);
        pr_devel("<==%s() = %d\n", __func__, ret);
        return ret;
}
EXPORT_SYMBOL_GPL(verify_pkcs7_signature);

#endif /* CONFIG_SYSTEM_DATA_VERIFICATION */

查看builtin trusted keyring

$ sudo keyctl list %:.builtin_trusted_keys
6 keys in keyring:
...

所有builtin trusted keyring中的key都可以用来对内核模块进行签名。

FAQ

如何判断一个内核模块是否签过名?
有一种不太准确的方法是直接检查内核模块的二进制内容:

tail <module.ko>

如果有出现“~Module signature appended~”字样,大多数情况下可以认为该内核模块是有签名的。之所以这里的措辞很谨慎,原因是这里没有用科学的方法去解析该模块的签名格式是否正确,也没有去验证里面的key是否真的有效。

如果添加额外的内核模块签名key?

  • 在重编内核的前提下,向CONFIG_MODULE_SIG_KEY指定的key文件级联更多的key即可。
  • 在不重编内核的前提下,借用CONFIG_SYSTEM_EXTRA_CERTIFICATE可以添加额外的内核模块签名key。这种方法要求内核在build时要事先保留足够大的空间。如果该空间的总容量本身就很小,拿只能精简要导入的X.509证书的内容;如果是因为已经添加过别的额外的system trusted key或内核模块签名key导致空间不足的话,可以考虑从内核image中删除其他不再使用的key。

如何删除内核模块的签名?

strip --keep-file-symbols <module_name>

Troubleshooting

加载内核模块时出现错误“module verification failed: signature and/or required key missing - tainting kernel”
该错误信息表示该内核模块没有经过签名,但是由于内核没有开启强制验证,因此该内核模块还是被加载了。

加载内核模块时出现错误“ERROR: could not insert module \<module_name>: Required key not available”(errno为-ENOKEY)
如果伴随着这个错误的同时,内核还报“PKCS#7 signature not signed with a trusted key”这个错误的话,说明被加载的模块虽然经过了签名,但签名key并不存在于builtin trusted keyring中,因此内核无法验证签名的有效性。

如果内核没有报“PKCS#7 signature not signed with a trusted key”这个错误,说明被加载的模块根本没有经过签名。

加载内核模块时出现错误“ERROR: could not insert module \<module_name>: Key was rejected by service”(errno为-EKEYREJECTED)

说明被加载的模块虽然经过了签名,但签名key并不存在于builtin system trusted keyring中,因此内核拒绝加载该内核模块。

注意这个错误与错误“Required key not available”+“PKCS#7 signature not signed with a trusted key”的区别:后者表示签名key与位于builtin system trusted keyring中的key是有关联的,比如builtin system trusted keyring中只包含了父key,而签名key是由父key签发的;或者是builtin system trusted keyring中只包含了由父key签发的签名key,而签名key是父key。前者则表示签名key与位于builtin system trusted keyring中的key(如果有key的话)是没有任何关联性的。

另外之前碰到过一个case也会返回-EKEYREJECTED:trusted keyring里带了2个不同的证书,但是却具有相同的issuer和serial number。问题现象是:用这2个证书对应的私钥来签名,总有一个会失败。根因是认证PKCS#7 signer签名的时候,是用生成签名的证书key id(issuer+证书序列号)作为KEY来search trusted keyring的。因此,如果用第一个被search到的证书key id对应的证书不是生成签名的证书,就会导致-EKEYREJECTED错误。解决方法很简单:用不同的serial number来生成同一个CA生成的子证书。

对内核模块签名后又strip导致内核模块签名验证失败

模块一旦被签名好,再次对其内容的修改会导致签名验证失败。

这种修改可能有多种来源: - rpmbuild可能会考虑到减小initramfs的尺寸进而在打包时strip掉内核模块的debuginfo。

RHEL 3.10的内核模块签名格式

RHEL 3.10的签名格式是redhat自己弄的,和standard linux kernel的格式不兼容。

连modinfo这个开源工具,对模块签名格式的支持的code,也是redhat写的。最新的modinfo对5.x的模块签名都不支持显示,但能正确显示3.10的redhat自己实现和port的模块签名的格式。

TODO

使用PKCS#11 URI进行内核模块签名

PKCS#11由RFC7512定义。它主要是定义了HSM的使用接口。虽然使用HSM不太方便,但是安全性还是有一定保障的,而且如果现场出了严重,必须亲自上阵的话,用HSM可能反而是最稳妥的办法。

BUG

  • 如果内核不支持内核模块签名,但插入的内核模块是经过签名的,则插入失败并返回-ENOPKG。
  • 在没有启用内核签名强制验证的情况下,如果模块的签名key不在builtin trusted keyring中,会被拒绝加载。
  • 在启用内核签名强制验证的情况下,如果模块没有签名的话,会报“PKCS#7 signature not signed with a trusted key”这个有误导性的错误信息。

附录

签名工具sign-file
sign-file工具的源码位于kernel源码目录中的script目录下。

用法

scripts/sign-file [-dp] <hash_algo> <private_key_name> \
    <x509_name> <module_name> [<dest_name>]
scripts/sign-file -s <raw_sig_name> <hash_algo> <x509_name> <module_name> [<dest>]

\<hash_algo>是签名使用的摘要算法;\<private_key_name>是签名的私钥文件,支持pkcs#11或PEM格式;\<x509_name>是与私钥文件关联的X.509证书文件(PEM或DER格式都可以);\<module_name>是被签名的内核模块的名字;\<dest_name>是签名后的内核模块文件。

参数

-k
没有指定-k参数的话,OpenSSL使用issuer name和issuer序列号来标识签名证书;如果指定了-k参数的话,则使用subject key identifier(SKID)来标识签名证书。(注释:证书的AKID表示父CA证书的SKID;如果一个证书的AKID等于SKID,表示该证书是自签名的;因为issuer name和issuer都存在重名的情况,因此x509 v3增加了SKID和AKID扩展)。

默认情况下,kbuild没有设置该参数。

具体来说,在认证PKCS#7 signer签名的时候,是用生成签名的证书key id(issuer+证书序列号,由于没有指定-k参数)作为KEY来search trusted keyring的。

-p
将PKCS#7消息单独存为以.p7s为后缀的文件。可以用openssl pkcs7 -text -print -inform der -in .p7s来查看PKCS#7消息的详细内容。

默认情况下,kbuild没有设置该参数。

-s
指定由-p参数生成的.p7s文件作为内核模块签名中的\<PKCS#7消息>部分。

默认情况下,kbuild没有设置该参数。

-d
只是走一下签名的流程,并不真的对模块进行签名;同时兼具-p的功能。

默认情况下,kbuild没有设置该参数。

环境变量

KBUILD_SIGN_PIN:
该环境变量有两个作用: - 在不使用PKCS#11访问签名用私钥的情况下,用来指定私钥文件的解密口令。 - 在使用PKCS#11访问签名用私钥的情况下,用来指定PKCS#11的PIN码。

源码分析

sign-file会使用X.509证书中的issuer和serial来设置PKCS#7 signer info中的issuer和serial中的字段。因此,如果内核模块是由子key签的,但放到system trusted keyring中只有父key的话,内核在验证签名时会使用issuer为父key+序号为子key证书的key id组合到system trusted keyring中查找匹配的key,结果肯定是不匹配。

模块签名的格式

<内核模块的内容>
<PKCS#7消息>
<模块签名>
<Mark字符串"~Module signature appended~\n"(共28字节,不包括结尾的NULL字符)>

PKCS#7消息的格式

PKCS#7是一种加密消息的语法标准,由RSA安全体系在公钥加密系统中交换数字证书产生的一种加密标准。有关其详细格式,请参考RFC 2315 - PKCS #7: Cryptographic Message Syntax Version 1.5。

PKCS#7消息的格式的主体是content,共支持6种content类型:

  • data
  • signedData
  • envelopedData
  • signedAndEnvelopedData
  • digestedData
  • encryptedData

content的具体格式由content类型的定义来决定。目前内核中的PKCS#7 parser仅支持content类型为signedData的PKCS#7消息。

下面是PKCS#7的格式定义:

ContentInfo ::= SEQUENCE {
     // 定义了content的类型。
     // 该字段的类型为:ContentType ::= OBJECT IDENTIFIER
     contentType ContentType,
     // content的具体格式由contentType字段的定义来决定。
     content
       [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }

生成signedData类型的PKCS#7消息

生成signedData类型的PKCS#7消息的过程如下:

  • Signer选定digest-encryption算法。
  • Signer将被签名的数据以及authenticated attribute(如果有的话)作为输入,计算出摘要值。 注意:authenticated attribute中真正参与摘要计算的只有DER格式的Attributes的值:
authenticatedAttributes
          [0] IMPLICIT Attributes OPTIONAL,

Attributes值的tag是SET OF,不包括前面的IMPLICIT [0] tag。

  • Signer将计算出来的摘要值和使用的摘要算法组织成下面的BER格式:
DigestInfo ::= SEQUENCE {
     digestAlgorithm DigestAlgorithmIdentifier, // 摘要算法OID
     digest Digest // 摘要值, 类型为OCTET STRING
   }
  • Signer用自己的私钥对DigestInfo进行加密。
  • 上述操作通常无需signer自己手动完成,因为PKCS#1 v1.5在底层会自动完成上述操作。
  • 将Signer的证书以及关联的issuer证书以及其他相关信息封装成PKCS#7消息格式。

PKCS#7允许多个signer对相同的输入内容进行签名,每个签名以及signer信息都会封装到PKCS#7消息中。因此一个PKCS#7消息可以同时对多个signer进行认证。

signedData类型的PKCS#7消息格式详解

signedData类型的PKCS#7消息由一组signer信息以及一组signer/issuer的证书组成。signer信息包括特定signer提供的签名值,signer证书包括signer的实体信息以及公钥。issuer证书用于对signer进行身份认证。

// PKCS#7类型为signedData的格式定义
   SignedData ::= SEQUENCE {
     // 表示PKCS#7消息格式语法的版本号
     // 该字段的类型为:Version ::= INTEGER
     version Version,

     // 表示所有signer使用的摘要算法的集合。
     // 该字段的类型为:
     // DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
     // DigestAlgorithmIdentifier ::= AlgorithmIdentifier
     digestAlgorithms DigestAlgorithmIdentifiers,

     // 包含被签名的内容。
     // contentType表示内容的类型;content表示实际被签名的内容。
     // 注意:真正被签名的只有content自身,不包括DER编码中的id和长度字段。
     // 如果content置空,表示真正被签名的content不在PKCS#7中,这种情况被称
     // 为detached content。
     // 该字段的类型为:
     // ContentInfo ::= SEQUENCE {
     //   contentType ContentType,
     //   content
     //     [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }
     contentInfo ContentInfo,

     // 一组证书PKCS#6格式的扩展证书或X.509证书。理论上来说,signer的
     // 证书,以及所有与signer相关联的全部issuer证书甚至连根证书都可以放
     // 在该字段中,从而构成一个证书层级结构;但实际上该字段也是可以为空的。
     // 该字段的类型为:
     // ExtendedCertificatesAndCertificates ::=
     //   SET OF ExtendedCertificateOrCertificate
     //   可以是PKCS#6格式的扩展证书或X.509证书。
     //   ExtendedCertificateOrCertificate ::= CHOICE {
     //     certificate Certificate, -- X.509
     //     extendedCertificate [0] IMPLICIT ExtendedCertificate }
     certificates
        [0] IMPLICIT ExtendedCertificatesAndCertificates
          OPTIONAL,

     // 包含一组撤销证书。
     // 该字段的类型为:
     // CertificateRevocationLists ::= SET OF CertificateRevocationList
     crls
       [1] IMPLICIT CertificateRevocationLists OPTIONAL,

     // 包含一组signer的相关信息
     signerInfos SignerInfos
   }

   // 包含一组signer的相关信息
   SignerInfos ::= SET OF SignerInfo
      // 包含每个signer的相关信息:signer使用的签名证书,摘要算法,
      // 签名算法,签名以及属性字段等。
      SignerInfo ::= SEQUENCE {
        // 表示PKCS#7消息格式语法的版本号
        // 本字段的类型为: Version ::= INTEGER
        version Version,

        // 每个证书issuer都会为其所颁发的证书分配一个证书序列号,且该
        // 序列号在其所颁发的所有证书中是唯一的。因此在给定证书issuer的
        // 专有名称以及其颁发的证书序列号的共同作用下,本字段可以为用
        // 于唯一标识指定证书issuer颁发的某个特定的证书。
        // 本字段的类型为: 
        // IssuerAndSerialNumber ::= SEQUENCE {
        //   issuer Name,  证书issuer的专有名称
        //   serialNumber CertificateSerialNumber
        //  }
        issuerAndSerialNumber IssuerAndSerialNumber,

        // Signer使用的摘要算法
        // 本字段的类型为: DigestAlgorithmIdentifier ::= AlgorithmIdentifier
        digestAlgorithm DigestAlgorithmIdentifier,

        // 一组经过签名或认证的属性信息
        authenticatedAttributes
          [0] IMPLICIT Attributes OPTIONAL,

        //  Signer使用的签名算法,比如PKCS#1 RSA。
        // 本字段的类型为: 
        //   DigestEncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
        digestEncryptionAlgorithm
          DigestEncryptionAlgorithmIdentifier,

        // 用signer的私钥加密后的结果,即所谓的签名值
        encryptedDigest EncryptedDigest,

        // 一组未经签名或认证的属性信息
        unauthenticatedAttributes
          [1] IMPLICIT Attributes OPTIONAL }

内核模块签名中的PKCS#7消息

内核对内核模块签名中的PKCS#7消息格式有如下硬性要求:

  • PKCS#7消息的content类型必须是signedData。
  • contentInfo包含的data的content类型必须是data。
  • 版本号必须为1(表示PKCS#7 v1, 见RFC2315 9.1节;或CMS v1,见RFC5652 5.1节)或3( CMS v 3 RFC2315 5.1节)。
  • 必须包含至少一个signer的信息。
  • 所有的signer信息都不能包含authenticatedAttributes字段。
  • 忽略所有的unauthenticatedAttributes字段。
  • 签名算法必须是PKCS#1 RSA加密算法。

module_signature的格式

该签名格式包含了模块签名的各种元数据,并采用big endian编码。

struct module_signature {
        uint8_t         algo;           /* Public-key crypto algorithm [0] */
        uint8_t         hash;           /* Digest algorithm [0] */
        uint8_t         id_type;        /* Key identifier type [PKEY_ID_PKCS7] */
        uint8_t         signer_len;     /* Length of signer's name [0] */
        uint8_t         key_id_len;     /* Length of key identifier [0] */
        uint8_t         __pad[3];
        uint32_t        sig_len;        /* Length of signature data */ htonl(size of .p7s)
};
  • algo 表示签名算法ID,必须为0。
  • hash 表示摘要算法ID,必须为0。
  • id_type module的签名格式,当前仅支持PKEY_ID_PKCS7(2)。
  • signer_len 未使用,必须为0。
  • key_id_len 未使用,必须为0。
  • __pad[3] 填充字段,不使用,必须为0.
  • sig_len 表示PKCS#7消息字段的字节长度。

参考

  • Documentation/module-signing.txt
  • PKCS #7: Cryptographic Message Syntax Version 1.5
  • Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile
作者: 乾越
文章来源:乾越

推荐阅读

预期功能安全专栏 | 智能汽车中人工智能算法应用及其安全综述
汽车安全之声 | 汽车智能化带来的安全新挑战及其应对思路
精品译文 | 域控制器(DC)ECU的功能安全性
精品译文 | 如何理解SOTIF场景

更多物联网安全,PSA等技术干货请关注平台安全架构(PSA)专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入PSA技术交流群,请备注研究方向。
推荐阅读
关注数
4550
内容数
127
Arm发布的PSA旨在为物联网安全提供一套全面的安全指导方针,使从芯片制造商到设备开发商等价值链中的每位成员都能成功实现安全运行。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息