[PATCH v4 15/17] module: Introduce hash-based integrity checking
Nicolas Schier
nsc at kernel.org
Sat Feb 21 21:38:29 UTC 2026
Hi Thomas,
On Tue, Jan 13, 2026 at 01:28:59PM +0100, Thomas Weißschuh wrote:
> The current signature-based module integrity checking has some drawbacks
> in combination with reproducible builds. Either the module signing key
> is generated at build time, which makes the build unreproducible, or a
> static signing key is used, which precludes rebuilds by third parties
> and makes the whole build and packaging process much more complicated.
>
> The goal is to reach bit-for-bit reproducibility. Excluding certain
> parts of the build output from the reproducibility analysis would be
> error-prone and force each downstream consumer to introduce new tooling.
>
> Introduce a new mechanism to ensure only well-known modules are loaded
> by embedding a merkle tree root of all modules built as part of the full
> kernel build into vmlinux.
>
> Non-builtin modules can be validated as before through signatures.
>
> Normally the .ko module files depend on a fully built vmlinux to be
> available for modpost validation and BTF generation. With
> CONFIG_MODULE_HASHES, vmlinux now depends on the modules
> to build a merkle tree. This introduces a dependency cycle which is
> impossible to satisfy. Work around this by building the modules during
> link-vmlinux.sh, after vmlinux is complete enough for modpost and BTF
> but before the final module hashes are
>
> The PKCS7 format which is used for regular module signatures can not
> represent Merkle proofs, so a new kind of module signature is
> introduced. As this signature type is only ever used for builtin
> modules, no compatibility issues can arise.
>
> Signed-off-by: Thomas Weißschuh <linux at weissschuh.net>
> ---
> .gitignore | 1 +
> Documentation/kbuild/reproducible-builds.rst | 5 +-
> Makefile | 8 +-
> include/asm-generic/vmlinux.lds.h | 11 +
> include/linux/module_hashes.h | 25 ++
> include/linux/module_signature.h | 1 +
> kernel/module/Kconfig | 21 +-
> kernel/module/Makefile | 1 +
> kernel/module/hashes.c | 92 ++++++
> kernel/module/hashes_root.c | 6 +
> kernel/module/internal.h | 1 +
> kernel/module/main.c | 4 +-
> scripts/.gitignore | 1 +
> scripts/Makefile | 3 +
> scripts/Makefile.modfinal | 11 +
> scripts/Makefile.modinst | 13 +
> scripts/Makefile.vmlinux | 5 +
> scripts/link-vmlinux.sh | 14 +-
> scripts/modules-merkle-tree.c | 467 +++++++++++++++++++++++++++
> security/lockdown/Kconfig | 2 +-
> 20 files changed, 685 insertions(+), 7 deletions(-)
>
[...]
> diff --git a/kernel/module/hashes_root.c b/kernel/module/hashes_root.c
> new file mode 100644
> index 000000000000..1abfcd3aa679
> --- /dev/null
> +++ b/kernel/module/hashes_root.c
> @@ -0,0 +1,6 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +
> +#include <linux/module_hashes.h>
> +
> +/* Blank dummy data. Will be overridden by link-vmlinux.sh */
> +const struct module_hashes_root module_hashes_root __module_hashes_section = {};
> diff --git a/kernel/module/internal.h b/kernel/module/internal.h
> index e2d49122c2a1..e22837d3ac76 100644
> --- a/kernel/module/internal.h
> +++ b/kernel/module/internal.h
> @@ -338,6 +338,7 @@ void module_mark_ro_after_init(const Elf_Ehdr *hdr, Elf_Shdr *sechdrs,
> const char *secstrings);
>
> int module_sig_check(struct load_info *info, const u8 *sig, size_t sig_len);
> +int module_hash_check(struct load_info *info, const u8 *sig, size_t sig_len);
>
> #ifdef CONFIG_DEBUG_KMEMLEAK
> void kmemleak_load_module(const struct module *mod, const struct load_info *info);
> diff --git a/kernel/module/main.c b/kernel/module/main.c
> index 2a28a0ece809..fa30b6387936 100644
> --- a/kernel/module/main.c
> +++ b/kernel/module/main.c
> @@ -3362,8 +3362,10 @@ static int module_integrity_check(struct load_info *info, int flags)
>
> if (IS_ENABLED(CONFIG_MODULE_SIG) && sig_type == PKEY_ID_PKCS7) {
> err = module_sig_check(info, sig, sig_len);
> + } else if (IS_ENABLED(CONFIG_MODULE_HASHES) && sig_type == PKEY_ID_MERKLE) {
> + err = module_hash_check(info, sig, sig_len);
> } else {
> - pr_err("module: not signed with expected PKCS#7 message\n");
> + pr_err("module: not signed with signature mechanism\n");
> err = -ENOPKG;
To prevent others from running into the same issue:
My first test got stuck here, as I tested with virtme-ng, which symlinks
modules from build tree to /lib/modules/$(uname -r)/..., resulting in
[ 15.956855] module: not signed with signature mechanism
modprobe: ERROR: could not insert 'efivarfs': Package not installed
As the modules_install step was missing, modules were not being signed.
[...]
> diff --git a/scripts/modules-merkle-tree.c b/scripts/modules-merkle-tree.c
> new file mode 100644
> index 000000000000..a6ec0e21213b
> --- /dev/null
> +++ b/scripts/modules-merkle-tree.c
> @@ -0,0 +1,467 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Compute hashes for modules files and build a merkle tree.
> + *
> + * Copyright (C) 2025 Sebastian Andrzej Siewior <sebastian at breakpoint.cc>
> + * Copyright (C) 2025 Thomas Weißschuh <linux at weissschuh.net>
> + *
> + */
> +#define _GNU_SOURCE 1
> +#include <arpa/inet.h>
> +#include <err.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <stdarg.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <stdbool.h>
> +#include <stdlib.h>
> +
> +#include <sys/stat.h>
> +#include <sys/mman.h>
> +
> +#include <openssl/evp.h>
> +#include <openssl/err.h>
> +
> +#include "ssl-common.h"
> +
> +static int hash_size;
> +static EVP_MD_CTX *ctx;
> +
> +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 */
> +};
> +
> +#define PKEY_ID_MERKLE 3
> +
> +static const char magic_number[] = "~Module signature appended~\n";
This here will be the forth definition of struct module_signature,
increasing the risk of unwanted diversion. I second Petr's suggestion
to reuse a _common_ definition instead.
(Here, even include/linux/module_signature.h could be included itself.)
> +
> +struct file_entry {
> + char *name;
> + unsigned int pos;
> + unsigned char hash[EVP_MAX_MD_SIZE];
> +};
> +
> +static struct file_entry *fh_list;
> +static size_t num_files;
> +
> +struct leaf_hash {
> + unsigned char hash[EVP_MAX_MD_SIZE];
> +};
> +
> +struct mtree {
> + struct leaf_hash **l;
> + unsigned int *entries;
> + unsigned int levels;
> +};
> +
> +static inline void *xcalloc(size_t n, size_t size)
> +{
> + void *p;
> +
> + p = calloc(n, size);
> + if (!p)
> + errx(1, "Memory allocation failed");
> +
> + return p;
> +}
> +
> +static void *xmalloc(size_t size)
> +{
> + void *p;
> +
> + p = malloc(size);
> + if (!p)
> + errx(1, "Memory allocation failed");
> +
> + return p;
> +}
> +
> +static inline void *xreallocarray(void *oldp, size_t n, size_t size)
> +{
> + void *p;
> +
> + p = reallocarray(oldp, n, size);
> + if (!p)
> + errx(1, "Memory allocation failed");
> +
> + return p;
> +}
> +
> +static inline char *xasprintf(const char *fmt, ...)
> +{
> + va_list ap;
> + char *strp;
> + int ret;
> +
> + va_start(ap, fmt);
> + ret = vasprintf(&strp, fmt, ap);
> + va_end(ap);
> + if (ret == -1)
> + err(1, "Memory allocation failed");
> +
> + return strp;
> +}
Please consider moving these x* functions into scripts/include/xalloc.h
for reuse. (I am sure someone else wrote this already, but I can't find
it...)
>
>
thanks for all your efforts for reproducibility!
As I have no clue about that: Is the patent for merkle trees [1] a
problem when integrating that here?
Can you verify if I get the mechanics roughly correct?
* Modules are merkle tree leaves. Modules are built and logically
paired by the order from modules.order; a single left-over module is
paired with itself.
* Hashes of paired modules are hashed again (branch node hash);
hashes of pairs of branch nodes' hashes are hashed again;
repeat until we reach the single merkle tree root hash
* The final merkle tree root hash (and the count of tree levels) is
included in vmlinux
'make && find . -name '*.ko' -exec rm {} \; && make' does not rebuild
the in-tree modules. Shifting the module-hashes support from
scripts/link-vmlinux.sh to scripts/Makefile.vmlinux might (make it
easier) to fix this again.
Kind regards,
Nicolas
[1]: https://worldwide.espacenet.com/patent/search/family/022107098/publication/US4309569A?q=pn%3DUS4309569
--
Nicolas
More information about the Linux-security-module-archive
mailing list