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 <[email protected]>
> ---
>  .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 <[email protected]>
> + * Copyright (C) 2025 Thomas Weißschuh <[email protected]>
> + *
> + */
> +#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

Reply via email to