ser...@kernel.org writes:

> On Fri, Mar 21, 2025 at 09:45:03AM -0700, Blaise Boscaccy wrote:
>> This adds the Hornet Linux Security Module which provides signature
>> verification of eBPF programs.
>> 
>> Hornet uses a similar signature verification scheme similar to that of
>
> used 'similar' twice
>
>> kernel modules. A pkcs#7 signature is appended to the end of an
>> executable file. During an invocation of bpf_prog_load, the signature
>> is fetched from the current task's executable file. That signature is
>> used to verify the integrity of the bpf instructions and maps which
>> where passed into the kernel. Additionally, Hornet implicitly trusts any
>
> s/where/were
>
>> programs which where loaded from inside kernel rather than userspace,
>
> s/where/were
>
>> which allows BPF_PRELOAD programs along with outputs for BPF_SYSCALL
>> programs to run.
>> 
>> Hornet allows users to continue to maintain an invariant that all code
>> running inside of the kernel has been signed and works well with
>> light-skeleton based loaders, or any statically generated program that
>> doesn't require userspace instruction rewriting.
>> 
>> Signed-off-by: Blaise Boscaccy <bbosca...@linux.microsoft.com>
>> ---
>>  Documentation/admin-guide/LSM/Hornet.rst |  51 +++++
>>  crypto/asymmetric_keys/pkcs7_verify.c    |  10 +
>>  include/linux/kernel_read_file.h         |   1 +
>>  include/linux/verification.h             |   1 +
>>  include/uapi/linux/lsm.h                 |   1 +
>>  security/Kconfig                         |   3 +-
>>  security/Makefile                        |   1 +
>>  security/hornet/Kconfig                  |  11 ++
>>  security/hornet/Makefile                 |   4 +
>>  security/hornet/hornet_lsm.c             | 239 +++++++++++++++++++++++
>>  10 files changed, 321 insertions(+), 1 deletion(-)
>>  create mode 100644 Documentation/admin-guide/LSM/Hornet.rst
>>  create mode 100644 security/hornet/Kconfig
>>  create mode 100644 security/hornet/Makefile
>>  create mode 100644 security/hornet/hornet_lsm.c
>> 
>> diff --git a/Documentation/admin-guide/LSM/Hornet.rst 
>> b/Documentation/admin-guide/LSM/Hornet.rst
>> new file mode 100644
>> index 0000000000000..fa112412638f1
>> --- /dev/null
>> +++ b/Documentation/admin-guide/LSM/Hornet.rst
>> @@ -0,0 +1,51 @@
>> +======
>> +Hornet
>> +======
>> +
>> +Hornet is a Linux Security Module that provides signature verification
>> +for eBPF programs. This is selectable at build-time with
>> +``CONFIG_SECURITY_HORNET``.
>> +
>> +Overview
>> +========
>> +
>> +Hornet provides signature verification for eBPF programs by utilizing
>> +the existing PKCS#7 infrastructure that's used for module signature
>> +verification. Hornet works by creating a buffer containing the eBPF
>> +program instructions along with its associated maps and checking a
>> +signature against that buffer. The signature is appended to the end of
>> +the lskel executable file and is extracted at runtime via
>> +get_task_exe_file. Hornet works by hooking into the
>> +security_bpf_prog_load hook. Load invocations that originate from the
>> +kernel (bpf preload, results of bpf_syscall programs, etc.) are
>> +allowed to run unconditionally. Calls that originate from userspace
>> +require signature verification. If signature verification fails, the
>> +program will fail to load.
>> +
>> +Instruction/Map Ordering
>> +========================
>> +
>> +Hornet supports both sparse-array based maps via map discovery along
>> +with the newly added fd_array_cnt API for continuous map arrays. The
>> +buffer used for signature verification is assumed to be the
>> +instructions followed by all maps used, ordered by their index in
>> +fd_array.
>> +
>> +Tooling
>> +=======
>> +
>> +Some tooling is provided to aid with the development of signed eBPF lskels.
>> +
>> +extract-skel.sh
>> +---------------
>> +
>> +This simple shell script extracts the instructions and map data used
>> +by the light skeleton from the autogenerated header file created by
>> +bpftool.
>> +
>> +sign-ebpf
>> +---------
>> +
>> +sign-ebpf works similarly to the sign-file script with one key
>> +difference: it takes a separate input binary used for signature
>> +verification and will append the signature to a different output file.
>> diff --git a/crypto/asymmetric_keys/pkcs7_verify.c 
>> b/crypto/asymmetric_keys/pkcs7_verify.c
>> index f0d4ff3c20a83..1a5fbb3612188 100644
>> --- a/crypto/asymmetric_keys/pkcs7_verify.c
>> +++ b/crypto/asymmetric_keys/pkcs7_verify.c
>> @@ -428,6 +428,16 @@ int pkcs7_verify(struct pkcs7_message *pkcs7,
>>              }
>>              /* Authattr presence checked in parser */
>>              break;
>> +    case VERIFYING_EBPF_SIGNATURE:
>> +            if (pkcs7->data_type != OID_data) {
>> +                    pr_warn("Invalid ebpf sig (not pkcs7-data)\n");
>> +                    return -EKEYREJECTED;
>> +            }
>> +            if (pkcs7->have_authattrs) {
>> +                    pr_warn("Invalid ebpf sig (has authattrs)\n");
>> +                    return -EKEYREJECTED;
>> +            }
>> +            break;
>>      case VERIFYING_UNSPECIFIED_SIGNATURE:
>>              if (pkcs7->data_type != OID_data) {
>>                      pr_warn("Invalid unspecified sig (not pkcs7-data)\n");
>> diff --git a/include/linux/kernel_read_file.h 
>> b/include/linux/kernel_read_file.h
>> index 90451e2e12bd1..7ed9337be5423 100644
>> --- a/include/linux/kernel_read_file.h
>> +++ b/include/linux/kernel_read_file.h
>> @@ -14,6 +14,7 @@
>>      id(KEXEC_INITRAMFS, kexec-initramfs)    \
>>      id(POLICY, security-policy)             \
>>      id(X509_CERTIFICATE, x509-certificate)  \
>> +    id(EBPF, ebpf)                          \
>>      id(MAX_ID, )
>>  
>>  #define __fid_enumify(ENUM, dummy) READING_ ## ENUM,
>> diff --git a/include/linux/verification.h b/include/linux/verification.h
>> index 4f3022d081c31..812be8ad5f744 100644
>> --- a/include/linux/verification.h
>> +++ b/include/linux/verification.h
>> @@ -35,6 +35,7 @@ enum key_being_used_for {
>>      VERIFYING_KEXEC_PE_SIGNATURE,
>>      VERIFYING_KEY_SIGNATURE,
>>      VERIFYING_KEY_SELF_SIGNATURE,
>> +    VERIFYING_EBPF_SIGNATURE,
>>      VERIFYING_UNSPECIFIED_SIGNATURE,
>>      NR__KEY_BEING_USED_FOR
>>  };
>> diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
>> index 938593dfd5daf..2ff9bcdd551e2 100644
>> --- a/include/uapi/linux/lsm.h
>> +++ b/include/uapi/linux/lsm.h
>> @@ -65,6 +65,7 @@ struct lsm_ctx {
>>  #define LSM_ID_IMA          111
>>  #define LSM_ID_EVM          112
>>  #define LSM_ID_IPE          113
>> +#define LSM_ID_HORNET               114
>>  
>>  /*
>>   * LSM_ATTR_XXX definitions identify different LSM attributes
>> diff --git a/security/Kconfig b/security/Kconfig
>> index f10dbf15c2947..0030f0224c7ab 100644
>> --- a/security/Kconfig
>> +++ b/security/Kconfig
>> @@ -230,6 +230,7 @@ source "security/safesetid/Kconfig"
>>  source "security/lockdown/Kconfig"
>>  source "security/landlock/Kconfig"
>>  source "security/ipe/Kconfig"
>> +source "security/hornet/Kconfig"
>>  
>>  source "security/integrity/Kconfig"
>>  
>> @@ -273,7 +274,7 @@ config LSM
>>      default 
>> "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,ipe,bpf"
>>  if DEFAULT_SECURITY_APPARMOR
>>      default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,ipe,bpf" if 
>> DEFAULT_SECURITY_TOMOYO
>>      default "landlock,lockdown,yama,loadpin,safesetid,ipe,bpf" if 
>> DEFAULT_SECURITY_DAC
>> -    default 
>> "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf"
>> +    default 
>> "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,hornet,bpf"
>>      help
>>        A comma-separated list of LSMs, in initialization order.
>>        Any LSMs left off this list, except for those with order
>> diff --git a/security/Makefile b/security/Makefile
>> index 22ff4c8bd8cec..e24bccd951f88 100644
>> --- a/security/Makefile
>> +++ b/security/Makefile
>> @@ -26,6 +26,7 @@ obj-$(CONFIG_CGROUPS)                      += 
>> device_cgroup.o
>>  obj-$(CONFIG_BPF_LSM)                       += bpf/
>>  obj-$(CONFIG_SECURITY_LANDLOCK)             += landlock/
>>  obj-$(CONFIG_SECURITY_IPE)          += ipe/
>> +obj-$(CONFIG_SECURITY_HORNET)               += hornet/
>>  
>>  # Object integrity file lists
>>  obj-$(CONFIG_INTEGRITY)                     += integrity/
>> diff --git a/security/hornet/Kconfig b/security/hornet/Kconfig
>> new file mode 100644
>> index 0000000000000..19406aa237ac6
>> --- /dev/null
>> +++ b/security/hornet/Kconfig
>> @@ -0,0 +1,11 @@
>> +# SPDX-License-Identifier: GPL-2.0-only
>> +config SECURITY_HORNET
>> +    bool "Hornet support"
>> +    depends on SECURITY
>> +    default n
>> +    help
>> +      This selects Hornet.
>> +      Further information can be found in
>> +      Documentation/admin-guide/LSM/Hornet.rst.
>> +
>> +      If you are unsure how to answer this question, answer N.
>> diff --git a/security/hornet/Makefile b/security/hornet/Makefile
>> new file mode 100644
>> index 0000000000000..79f4657b215fa
>> --- /dev/null
>> +++ b/security/hornet/Makefile
>> @@ -0,0 +1,4 @@
>> +# SPDX-License-Identifier: GPL-2.0-only
>> +obj-$(CONFIG_SECURITY_HORNET) := hornet.o
>> +
>> +hornet-y := hornet_lsm.o
>> diff --git a/security/hornet/hornet_lsm.c b/security/hornet/hornet_lsm.c
>> new file mode 100644
>> index 0000000000000..3616c68b76fbc
>> --- /dev/null
>> +++ b/security/hornet/hornet_lsm.c
>> @@ -0,0 +1,239 @@
>> +// SPDX-License-Identifier: GPL-2.0-only
>> +/*
>> + * Hornet Linux Security Module
>> + *
>> + * Author: Blaise Boscaccy <bbosca...@linux.microsoft.com>
>> + *
>> + * Copyright (C) 2025 Microsoft Corporation
>> + */
>> +
>> +#include <linux/lsm_hooks.h>
>> +#include <uapi/linux/lsm.h>
>> +#include <linux/bpf.h>
>> +#include <linux/verification.h>
>> +#include <crypto/public_key.h>
>> +#include <linux/module_signature.h>
>> +#include <crypto/pkcs7.h>
>> +#include <linux/bpf_verifier.h>
>> +#include <linux/sort.h>
>> +
>> +#define EBPF_SIG_STRING "~eBPF signature appended~\n"
>> +
>> +struct hornet_maps {
>> +    u32 used_idx[MAX_USED_MAPS];
>> +    u32 used_map_cnt;
>> +    bpfptr_t fd_array;
>> +};
>> +
>> +static int cmp_idx(const void *a, const void *b)
>> +{
>> +    return *(const u32 *)a - *(const u32 *)b;
>> +}
>> +
>> +static int add_used_map(struct hornet_maps *maps, int idx)
>> +{
>> +    int i;
>> +
>> +    for (i = 0; i < maps->used_map_cnt; i++)
>> +            if (maps->used_idx[i] == idx)
>> +                    return i;
>> +
>> +    if (maps->used_map_cnt >= MAX_USED_MAPS)
>> +            return -E2BIG;
>> +
>> +    maps->used_idx[maps->used_map_cnt] = idx;
>> +    return maps->used_map_cnt++;
>> +}
>> +
>> +static int hornet_find_maps(struct bpf_prog *prog, struct hornet_maps *maps)
>> +{
>> +    struct bpf_insn *insn = prog->insnsi;
>> +    int insn_cnt = prog->len;
>> +    int i;
>> +    int err;
>> +
>> +    for (i = 0; i < insn_cnt; i++, insn++) {
>> +            if (insn[0].code == (BPF_LD | BPF_IMM | BPF_DW)) {
>> +                    switch (insn[0].src_reg) {
>> +                    case BPF_PSEUDO_MAP_IDX_VALUE:
>> +                    case BPF_PSEUDO_MAP_IDX:
>> +                            err = add_used_map(maps, insn[0].imm);
>> +                            if (err < 0)
>> +                                    return err;
>> +                            break;
>> +                    default:
>> +                            break;
>> +                    }
>> +            }
>> +    }
>> +    /* Sort the spare-array indices. This should match the map ordering 
>> used during
>> +     * signature generation
>> +     */
>> +    sort(maps->used_idx, maps->used_map_cnt, sizeof(*maps->used_idx),
>> +         cmp_idx, NULL);
>> +
>> +    return 0;
>> +}
>> +
>> +static int hornet_populate_fd_array(struct hornet_maps *maps, u32 
>> fd_array_cnt)
>> +{
>> +    int i;
>> +
>> +    if (fd_array_cnt > MAX_USED_MAPS)
>> +            return -E2BIG;
>> +
>> +    for (i = 0; i < fd_array_cnt; i++)
>> +            maps->used_idx[i] = i;
>> +
>> +    maps->used_map_cnt = fd_array_cnt;
>> +    return 0;
>> +}
>> +
>> +/* kern_sys_bpf is declared as an EXPORT_SYMBOL in kernel/bpf/syscall.c, 
>> however no definition is
>> + * provided in any bpf header files. If/when this function has a proper 
>> definition provided
>> + * somewhere this declaration should be removed
>> + */
>> +int kern_sys_bpf(int cmd, union bpf_attr *attr, unsigned int size);
>> +
>> +static int hornet_verify_lskel(struct bpf_prog *prog, struct hornet_maps 
>> *maps,
>> +                           void *sig, size_t sig_len)
>> +{
>> +    int fd;
>> +    u32 i;
>> +    void *buf;
>> +    void *new;
>> +    size_t buf_sz;
>> +    struct bpf_map *map;
>> +    int err = 0;
>> +    int key = 0;
>> +    union bpf_attr attr = {0};
>> +
>> +    buf = kmalloc_array(prog->len, sizeof(struct bpf_insn), GFP_KERNEL);
>> +    if (!buf)
>> +            return -ENOMEM;
>> +    buf_sz = prog->len * sizeof(struct bpf_insn);
>> +    memcpy(buf, prog->insnsi, buf_sz);
>> +
>> +    for (i = 0; i < maps->used_map_cnt; i++) {
>> +            err = copy_from_bpfptr_offset(&fd, maps->fd_array,
>> +                                          maps->used_idx[i] * sizeof(fd),
>> +                                          sizeof(fd));
>> +            if (err < 0)
>> +                    continue;
>> +            if (fd < 1)
>> +                    continue;
>> +
>> +            map = bpf_map_get(fd);
>> +            if (IS_ERR(map))
>> +                    continue;
>> +
>> +            /* don't allow userspace to change map data used for signature 
>> verification */
>> +            if (!map->frozen) {
>> +                    attr.map_fd = fd;
>> +                    err = kern_sys_bpf(BPF_MAP_FREEZE, &attr, sizeof(attr));
>> +                    if (err < 0)
>> +                            goto out;
>> +            }
>> +
>> +            new = krealloc(buf, buf_sz + map->value_size, GFP_KERNEL);
>> +            if (!new) {
>> +                    err = -ENOMEM;
>> +                    goto out;
>> +            }
>> +            buf = new;
>> +            new = map->ops->map_lookup_elem(map, &key);
>> +            if (!new) {
>> +                    err = -ENOENT;
>> +                    goto out;
>> +            }
>> +            memcpy(buf + buf_sz, new, map->value_size);
>> +            buf_sz += map->value_size;
>> +    }
>> +
>> +    err = verify_pkcs7_signature(buf, buf_sz, sig, sig_len,
>> +                                 VERIFY_USE_SECONDARY_KEYRING,
>> +                                 VERIFYING_EBPF_SIGNATURE,
>> +                                 NULL, NULL);
>> +out:
>> +    kfree(buf);
>> +    return err;
>> +}
>> +
>> +static int hornet_check_binary(struct bpf_prog *prog, union bpf_attr *attr,
>> +                           struct hornet_maps *maps)
>> +{
>> +    struct file *file = get_task_exe_file(current);
>> +    const unsigned long markerlen = sizeof(EBPF_SIG_STRING) - 1;
>> +    void *buf = NULL;
>> +    size_t sz = 0, sig_len, prog_len, buf_sz;
>> +    int err = 0;
>> +    struct module_signature sig;
>> +
>> +    buf_sz = kernel_read_file(file, 0, &buf, INT_MAX, &sz, READING_EBPF);
>> +    fput(file);
>> +    if (!buf_sz)
>> +            return -1;
>> +
>> +    prog_len = buf_sz;
>> +
>> +    if (prog_len > markerlen &&
>> +        memcmp(buf + prog_len - markerlen, EBPF_SIG_STRING, markerlen) == 0)
>> +            prog_len -= markerlen;
>> +
>> +    memcpy(&sig, buf + (prog_len - sizeof(sig)), sizeof(sig));
>> +    sig_len = be32_to_cpu(sig.sig_len);
>> +    prog_len -= sig_len + sizeof(sig);
>> +
>> +    err = mod_check_sig(&sig, prog->len * sizeof(struct bpf_insn), "ebpf");
>> +    if (err)
>> +            return err;
>> +    return hornet_verify_lskel(prog, maps, buf + prog_len, sig_len);
>> +}
>> +
>> +static int hornet_check_signature(struct bpf_prog *prog, union bpf_attr 
>> *attr,
>> +                              struct bpf_token *token, bool is_kernel)
>
> It's a little confusing that you are passing is_kernel in here, when the
> only caller will always pass in true.  Is there a good reason not to
> drop the arg here and pass 'true' in to make_bpfptr().  Of course, then
> people will ask why not define an IS_KERNEL to true as passing true to
> second argument is cryptic...  Maybe you just can't win here :)
>

Initially during development churn, this code was using a bpfptr_t that
ended up becoming a boolean flag in the LSM hooks and this appears to be a
relic of that. I think I'll remove the boolean param to
hornet_check_signature since this code is only interested in checking
stuff that originiated in userspace.  

>> +{
>> +    struct hornet_maps maps = {0};
>> +    int err;
>> +
>> +    /* support both sparse arrays and explicit continuous arrays of map fds 
>> */
>> +    if (attr->fd_array_cnt)
>> +            err = hornet_populate_fd_array(&maps, attr->fd_array_cnt);
>> +    else
>> +            err = hornet_find_maps(prog, &maps);
>> +
>> +    if (err < 0)
>> +            return err;
>> +
>> +    maps.fd_array = make_bpfptr(attr->fd_array, is_kernel);
>> +    return hornet_check_binary(prog, attr, &maps);
>> +}
>> +
>> +static int hornet_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr,
>> +                            struct bpf_token *token, bool is_kernel)
>> +{
>> +    if (is_kernel)
>> +            return 0;
>> +    return hornet_check_signature(prog, attr, token, is_kernel);
>> +}
>> +
>> +static struct security_hook_list hornet_hooks[] __ro_after_init = {
>> +    LSM_HOOK_INIT(bpf_prog_load, hornet_bpf_prog_load),
>> +};
>> +
>> +static const struct lsm_id hornet_lsmid = {
>> +    .name = "hornet",
>> +    .id = LSM_ID_HORNET,
>> +};
>> +
>> +static int __init hornet_init(void)
>> +{
>> +    pr_info("Hornet: eBPF signature verification enabled\n");
>> +    security_add_hooks(hornet_hooks, ARRAY_SIZE(hornet_hooks), 
>> &hornet_lsmid);
>> +    return 0;
>> +}
>> +
>> +DEFINE_LSM(hornet) = {
>> +    .name = "hornet",
>> +    .init = hornet_init,
>> +};
>> -- 
>> 2.48.1
>> 

Reply via email to