This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature verification. Signature data can be easily generated for the binary data that is generated via bpftool gen -L. This signature can be appended to a skeleton executable via scripts/sign-ebpf. Hornet checks the signature against a binary buffer containing the lskel instructions that the loader maps use. Maps are frozen to prevent TOCTOU bugs where a sufficiently privileged user could rewrite map data between the calls to BPF_PROG_LOAD and BPF_PROG_RUN. Additionally, both sparse-array-based and fd_array_cnt-based map fd arrays are supported for signature verification.
Blaise Boscaccy (4): security: Hornet LSM hornet: Introduce sign-ebpf hornet: Add an example lskel data extactor script selftests/hornet: Add a selftest for the hornet LSM
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 + scripts/Makefile | 1 + scripts/hornet/Makefile | 5 + scripts/hornet/extract-skel.sh | 29 ++ scripts/hornet/sign-ebpf.c | 420 +++++++++++++++++++ security/Kconfig | 3 +- security/Makefile | 1 + security/hornet/Kconfig | 11 + security/hornet/Makefile | 4 + security/hornet/hornet_lsm.c | 239 +++++++++++ tools/testing/selftests/Makefile | 1 + tools/testing/selftests/hornet/Makefile | 51 +++ tools/testing/selftests/hornet/loader.c | 21 + tools/testing/selftests/hornet/trivial.bpf.c | 33 ++ 18 files changed, 882 insertions(+), 1 deletion(-) create mode 100644 Documentation/admin-guide/LSM/Hornet.rst create mode 100644 scripts/hornet/Makefile create mode 100755 scripts/hornet/extract-skel.sh create mode 100644 scripts/hornet/sign-ebpf.c create mode 100644 security/hornet/Kconfig create mode 100644 security/hornet/Makefile create mode 100644 security/hornet/hornet_lsm.c create mode 100644 tools/testing/selftests/hornet/Makefile create mode 100644 tools/testing/selftests/hornet/loader.c create mode 100644 tools/testing/selftests/hornet/trivial.bpf.c
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 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 programs which where loaded from inside kernel rather than userspace, 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 bboscaccy@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 bboscaccy@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) +{ + 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, +};
Blaise Boscaccy bboscaccy@linux.microsoft.com writes:
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 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 programs which where loaded from inside kernel rather than userspace, 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 bboscaccy@linux.microsoft.com
Documentation/admin-guide/LSM/Hornet.rst | 51 +++++
You will need to add that file to .../index.rst, or it won't be included in the docs build.
Thanks,
jon
Jonathan Corbet corbet@lwn.net writes:
Blaise Boscaccy bboscaccy@linux.microsoft.com writes:
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 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 programs which where loaded from inside kernel rather than userspace, 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 bboscaccy@linux.microsoft.com
Documentation/admin-guide/LSM/Hornet.rst | 51 +++++
You will need to add that file to .../index.rst, or it won't be included in the docs build.
Thanks,
jon
Good catch, will get that fixed. Thanks Jon.
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 bboscaccy@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;
}
case VERIFYING_UNSPECIFIED_SIGNATURE: if (pkcs7->data_type != OID_data) { pr_warn("Invalid unspecified sig (not pkcs7-data)\n");break;
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 bboscaccy@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 :)
+{
- 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
sergeh@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 bboscaccy@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;
}
case VERIFYING_UNSPECIFIED_SIGNATURE: if (pkcs7->data_type != OID_data) { pr_warn("Invalid unspecified sig (not pkcs7-data)\n");break;
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 bboscaccy@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
On Fri, Mar 21, 2025 at 12:46 PM Blaise Boscaccy bboscaccy@linux.microsoft.com 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 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 programs which where loaded from inside kernel rather than userspace, 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 bboscaccy@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
A reminder that you'll need to take responsibility for maintaining Hornet and provide a corresponding entry in the MAINTAINERS file too. I'm not nice enough to maintain Hornet for you ;) If you have any questions about any of the fields, let me know.
I believe you've seen this already, but as a general FYI we do have some guidelines for new LSMs:
https://web.git.kernel.org/pub/scm/linux/kernel/git/pcmoore/lsm.git/tree/REA...
This introduces the sign-ebpf tool. It is very similar to the existing sign-file script, with one key difference, it will sign a file with with a signature computed off of arbitrary input data. This can used to sign an ebpf light skeleton loader program for verification via hornet.
Typical usage is to provide a payload containing the lskel ebpf syscall program binary and it's associated maps, which can be extracted from the auto-generated skel header.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com --- scripts/Makefile | 1 + scripts/hornet/Makefile | 5 + scripts/hornet/sign-ebpf.c | 420 +++++++++++++++++++++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 scripts/hornet/Makefile create mode 100644 scripts/hornet/sign-ebpf.c
diff --git a/scripts/Makefile b/scripts/Makefile index 46f860529df5e..a2cace05d7342 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -57,6 +57,7 @@ subdir-$(CONFIG_GENKSYMS) += genksyms subdir-$(CONFIG_GENDWARFKSYMS) += gendwarfksyms subdir-$(CONFIG_SECURITY_SELINUX) += selinux subdir-$(CONFIG_SECURITY_IPE) += ipe +subdir-$(CONFIG_SECURITY_HORNET) += hornet
# Let clean descend into subdirs subdir- += basic dtc gdb kconfig mod diff --git a/scripts/hornet/Makefile b/scripts/hornet/Makefile new file mode 100644 index 0000000000000..ab71dbb8688e4 --- /dev/null +++ b/scripts/hornet/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +hostprogs-always-y := sign-ebpf + +HOSTCFLAGS_sign-ebpf.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null) +HOSTLDLIBS_sign-ebpf = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto) diff --git a/scripts/hornet/sign-ebpf.c b/scripts/hornet/sign-ebpf.c new file mode 100644 index 0000000000000..ff98fddb79a15 --- /dev/null +++ b/scripts/hornet/sign-ebpf.c @@ -0,0 +1,420 @@ +/* Sign ebpf programs and skeletons using the given key. + * + * This program is heavily based on the kernel's sign-file tool + * with some minor additions to support the signing of eBPF lskels. + * + * Copyright © 2014-2016 Red Hat, Inc. All Rights Reserved. + * Copyright © 2015 Intel Corporation. + * Copyright © 2016 Hewlett Packard Enterprise Development LP + * Copyright © 2025 Microsoft Corporation. + * + * Authors: David Howells dhowells@redhat.com + * David Woodhouse dwmw2@infradead.org + * Juerg Haefliger juerg.haefliger@hpe.com + * Blaise Boscaccy bboscaccy@linux.microsoft.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 + * of the licence, or (at your option) any later version. + */ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <stdbool.h> +#include <string.h> +#include <getopt.h> +#include <err.h> +#include <arpa/inet.h> +#include <openssl/opensslv.h> +#include <openssl/bio.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/err.h> +#if OPENSSL_VERSION_MAJOR >= 3 +# define USE_PKCS11_PROVIDER +# include <openssl/provider.h> +# include <openssl/store.h> +#else +# if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) +# define USE_PKCS11_ENGINE +# include <openssl/engine.h> +# endif +#endif +#include "../ssl-common.h" + +/* + * Use CMS if we have openssl-1.0.0 or newer available - otherwise we have to + * assume that it's not available and its header file is missing and that we + * should use PKCS#7 instead. Switching to the older PKCS#7 format restricts + * the options we have on specifying the X.509 certificate we want. + * + * Further, older versions of OpenSSL don't support manually adding signers to + * the PKCS#7 message so have to accept that we get a certificate included in + * the signature message. Nor do such older versions of OpenSSL support + * signing with anything other than SHA1 - so we're stuck with that if such is + * the case. + */ +#if defined(LIBRESSL_VERSION_NUMBER) || \ + OPENSSL_VERSION_NUMBER < 0x10000000L || \ + defined(OPENSSL_NO_CMS) +#define USE_PKCS7 +#endif +#ifndef USE_PKCS7 +#include <openssl/cms.h> +#else +#include <openssl/pkcs7.h> +#endif + +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_PKCS7 2 + +static char magic_number[] = "~eBPF signature appended~\n"; + +static __attribute__((noreturn)) +void format(void) +{ + fprintf(stderr, + "Usage: scripts/sign-ebpf [-dp] <hash algo> <key> <x509> <bin> <loader> [<dest>]\n"); + exit(2); +} + +static const char *key_pass; + +static int pem_pw_cb(char *buf, int len, int w, void *v) +{ + int pwlen; + + if (!key_pass) + return -1; + + pwlen = strlen(key_pass); + if (pwlen >= len) + return -1; + + strcpy(buf, key_pass); + + /* If it's wrong, don't keep trying it. */ + key_pass = NULL; + + return pwlen; +} + +static EVP_PKEY *read_private_key_pkcs11(const char *private_key_name) +{ + EVP_PKEY *private_key = NULL; +#ifdef USE_PKCS11_PROVIDER + OSSL_STORE_CTX *store; + + if (!OSSL_PROVIDER_try_load(NULL, "pkcs11", true)) + ERR(1, "OSSL_PROVIDER_try_load(pkcs11)"); + if (!OSSL_PROVIDER_try_load(NULL, "default", true)) + ERR(1, "OSSL_PROVIDER_try_load(default)"); + + store = OSSL_STORE_open(private_key_name, NULL, NULL, NULL, NULL); + ERR(!store, "OSSL_STORE_open"); + + while (!OSSL_STORE_eof(store)) { + OSSL_STORE_INFO *info = OSSL_STORE_load(store); + + if (!info) { + drain_openssl_errors(__LINE__, 0); + continue; + } + if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) { + private_key = OSSL_STORE_INFO_get1_PKEY(info); + ERR(!private_key, "OSSL_STORE_INFO_get1_PKEY"); + } + OSSL_STORE_INFO_free(info); + if (private_key) + break; + } + OSSL_STORE_close(store); +#elif defined(USE_PKCS11_ENGINE) + ENGINE *e; + + ENGINE_load_builtin_engines(); + drain_openssl_errors(__LINE__, 1); + e = ENGINE_by_id("pkcs11"); + ERR(!e, "Load PKCS#11 ENGINE"); + if (ENGINE_init(e)) + drain_openssl_errors(__LINE__, 1); + else + ERR(1, "ENGINE_init"); + if (key_pass) + ERR(!ENGINE_ctrl_cmd_string(e, "PIN", key_pass, 0), "Set PKCS#11 PIN"); + private_key = ENGINE_load_private_key(e, private_key_name, NULL, NULL); + ERR(!private_key, "%s", private_key_name); +#else + fprintf(stderr, "no pkcs11 engine/provider available\n"); + exit(1); +#endif + return private_key; +} + +static EVP_PKEY *read_private_key(const char *private_key_name) +{ + if (!strncmp(private_key_name, "pkcs11:", 7)) { + return read_private_key_pkcs11(private_key_name); + } else { + EVP_PKEY *private_key; + BIO *b; + + b = BIO_new_file(private_key_name, "rb"); + ERR(!b, "%s", private_key_name); + private_key = PEM_read_bio_PrivateKey(b, NULL, pem_pw_cb, + NULL); + ERR(!private_key, "%s", private_key_name); + BIO_free(b); + + return private_key; + } +} + +static X509 *read_x509(const char *x509_name) +{ + unsigned char buf[2]; + X509 *x509; + BIO *b; + int n; + + b = BIO_new_file(x509_name, "rb"); + ERR(!b, "%s", x509_name); + + /* Look at the first two bytes of the file to determine the encoding */ + n = BIO_read(b, buf, 2); + if (n != 2) { + if (BIO_should_retry(b)) { + fprintf(stderr, "%s: Read wanted retry\n", x509_name); + exit(1); + } + if (n >= 0) { + fprintf(stderr, "%s: Short read\n", x509_name); + exit(1); + } + ERR(1, "%s", x509_name); + } + + ERR(BIO_reset(b) != 0, "%s", x509_name); + + if (buf[0] == 0x30 && buf[1] >= 0x81 && buf[1] <= 0x84) + /* Assume raw DER encoded X.509 */ + x509 = d2i_X509_bio(b, NULL); + else + /* Assume PEM encoded X.509 */ + x509 = PEM_read_bio_X509(b, NULL, NULL, NULL); + + BIO_free(b); + ERR(!x509, "%s", x509_name); + + return x509; +} + +int main(int argc, char **argv) +{ + struct module_signature sig_info = { .id_type = PKEY_ID_PKCS7 }; + char *hash_algo = NULL; + char *private_key_name = NULL, *raw_sig_name = NULL; + char *x509_name, *bin_name, *loader_name, *dest_name; + bool save_sig = false, replace_orig; + bool sign_only = false; + bool raw_sig = false; + unsigned char buf[4096]; + unsigned long loader_size, sig_size; + unsigned int use_signed_attrs; + const EVP_MD *digest_algo; + EVP_PKEY *private_key; +#ifndef USE_PKCS7 + CMS_ContentInfo *cms = NULL; + unsigned int use_keyid = 0; +#else + PKCS7 *pkcs7 = NULL; +#endif + X509 *x509; + BIO *bd, *bm, *bl; + int opt, n; + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); + ERR_clear_error(); + + key_pass = getenv("KBUILD_SIGN_PIN"); + +#ifndef USE_PKCS7 + use_signed_attrs = CMS_NOATTR; +#else + use_signed_attrs = PKCS7_NOATTR; +#endif + + do { + opt = getopt(argc, argv, "sdpk"); + switch (opt) { + case 's': raw_sig = true; break; + case 'p': save_sig = true; break; + case 'd': sign_only = true; save_sig = true; break; +#ifndef USE_PKCS7 + case 'k': use_keyid = CMS_USE_KEYID; break; +#endif + case -1: break; + default: format(); + } + } while (opt != -1); + + argc -= optind; + argv += optind; + if (argc < 5 || argc > 6) + format(); + + if (raw_sig) { + raw_sig_name = argv[0]; + hash_algo = argv[1]; + } else { + hash_algo = argv[0]; + private_key_name = argv[1]; + } + x509_name = argv[2]; + bin_name = argv[3]; + loader_name = argv[4]; + if (argc == 6 && strcmp(argv[4], argv[5]) != 0) { + dest_name = argv[5]; + replace_orig = false; + } else { + ERR(asprintf(&dest_name, "%s.~signed~", loader_name) < 0, + "asprintf"); + replace_orig = true; + } + +#ifdef USE_PKCS7 + if (strcmp(hash_algo, "sha1") != 0) { + fprintf(stderr, "sign-file: %s only supports SHA1 signing\n", + OPENSSL_VERSION_TEXT); + exit(3); + } +#endif + + /* Open the bin file */ + bm = BIO_new_file(bin_name, "rb"); + ERR(!bm, "%s", bin_name); + + if (!raw_sig) { + /* Read the private key and the X.509 cert the PKCS#7 message + * will point to. + */ + private_key = read_private_key(private_key_name); + x509 = read_x509(x509_name); + + /* Digest the module data. */ + OpenSSL_add_all_digests(); + drain_openssl_errors(__LINE__, 0); + digest_algo = EVP_get_digestbyname(hash_algo); + ERR(!digest_algo, "EVP_get_digestbyname"); + +#ifndef USE_PKCS7 + /* Load the signature message from the digest buffer. */ + cms = CMS_sign(NULL, NULL, NULL, NULL, + CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY | + CMS_DETACHED | CMS_STREAM); + ERR(!cms, "CMS_sign"); + + ERR(!CMS_add1_signer(cms, x509, private_key, digest_algo, + CMS_NOCERTS | CMS_BINARY | + CMS_NOSMIMECAP | use_keyid | + use_signed_attrs), + "CMS_add1_signer"); + ERR(CMS_final(cms, bm, NULL, CMS_NOCERTS | CMS_BINARY) != 1, + "CMS_final"); + +#else + pkcs7 = PKCS7_sign(x509, private_key, NULL, bm, + PKCS7_NOCERTS | PKCS7_BINARY | + PKCS7_DETACHED | use_signed_attrs); + ERR(!pkcs7, "PKCS7_sign"); +#endif + + if (save_sig) { + char *sig_file_name; + BIO *b; + + ERR(asprintf(&sig_file_name, "%s.p7s", bin_name) < 0, + "asprintf"); + b = BIO_new_file(sig_file_name, "wb"); + ERR(!b, "%s", sig_file_name); +#ifndef USE_PKCS7 + ERR(i2d_CMS_bio_stream(b, cms, NULL, 0) != 1, + "%s", sig_file_name); +#else + ERR(i2d_PKCS7_bio(b, pkcs7) != 1, + "%s", sig_file_name); +#endif + BIO_free(b); + } + + if (sign_only) { + BIO_free(bm); + return 0; + } + } + + /* Open the destination file now so that we can shovel the loader data + * across as we read it. + */ + bd = BIO_new_file(dest_name, "wb"); + ERR(!bd, "%s", dest_name); + + bl = BIO_new_file(loader_name, "rb"); + ERR(!bl, "%s", loader_name); + + + /* Append the marker and the PKCS#7 message to the destination file */ + ERR(BIO_reset(bm) < 0, "%s", bin_name); + ERR(BIO_reset(bl) < 0, "%s", loader_name); + while ((n = BIO_read(bl, buf, sizeof(buf))), + n > 0) { + ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name); + } + BIO_free(bl); + BIO_free(bm); + ERR(n < 0, "%s", loader_name); + loader_size = BIO_number_written(bd); + + if (!raw_sig) { +#ifndef USE_PKCS7 + ERR(i2d_CMS_bio_stream(bd, cms, NULL, 0) != 1, "%s", dest_name); +#else + ERR(i2d_PKCS7_bio(bd, pkcs7) != 1, "%s", dest_name); +#endif + } else { + BIO *b; + + /* Read the raw signature file and write the data to the + * destination file + */ + b = BIO_new_file(raw_sig_name, "rb"); + ERR(!b, "%s", raw_sig_name); + while ((n = BIO_read(b, buf, sizeof(buf))), n > 0) + ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name); + BIO_free(b); + } + + sig_size = BIO_number_written(bd) - loader_size; + sig_info.sig_len = htonl(sig_size); + ERR(BIO_write(bd, &sig_info, sizeof(sig_info)) < 0, "%s", dest_name); + ERR(BIO_write(bd, magic_number, sizeof(magic_number) - 1) < 0, "%s", dest_name); + + ERR(BIO_free(bd) != 1, "%s", dest_name); + + /* Finally, if we're signing in place, replace the original. */ + if (replace_orig) + ERR(rename(dest_name, loader_name) < 0, "%s", dest_name); + + return 0; +}
On Fri, Mar 21, 2025 at 09:45:04AM -0700, Blaise Boscaccy wrote:
This introduces the sign-ebpf tool. It is very similar to the existing sign-file script, with one key difference, it will sign a file with with a signature computed off of arbitrary input data. This can used to sign an ebpf light skeleton loader program for verification via hornet.
Typical usage is to provide a payload containing the lskel ebpf syscall program binary and it's associated maps, which can be extracted from the auto-generated skel header.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com
scripts/Makefile | 1 + scripts/hornet/Makefile | 5 + scripts/hornet/sign-ebpf.c | 420 +++++++++++++++++++++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 scripts/hornet/Makefile create mode 100644 scripts/hornet/sign-ebpf.c
diff --git a/scripts/Makefile b/scripts/Makefile index 46f860529df5e..a2cace05d7342 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -57,6 +57,7 @@ subdir-$(CONFIG_GENKSYMS) += genksyms subdir-$(CONFIG_GENDWARFKSYMS) += gendwarfksyms subdir-$(CONFIG_SECURITY_SELINUX) += selinux subdir-$(CONFIG_SECURITY_IPE) += ipe +subdir-$(CONFIG_SECURITY_HORNET) += hornet # Let clean descend into subdirs subdir- += basic dtc gdb kconfig mod diff --git a/scripts/hornet/Makefile b/scripts/hornet/Makefile new file mode 100644 index 0000000000000..ab71dbb8688e4 --- /dev/null +++ b/scripts/hornet/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +hostprogs-always-y := sign-ebpf
+HOSTCFLAGS_sign-ebpf.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null) +HOSTLDLIBS_sign-ebpf = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto) diff --git a/scripts/hornet/sign-ebpf.c b/scripts/hornet/sign-ebpf.c new file mode 100644 index 0000000000000..ff98fddb79a15 --- /dev/null +++ b/scripts/hornet/sign-ebpf.c @@ -0,0 +1,420 @@ +/* Sign ebpf programs and skeletons using the given key.
- This program is heavily based on the kernel's sign-file tool
- with some minor additions to support the signing of eBPF lskels.
- Copyright © 2014-2016 Red Hat, Inc. All Rights Reserved.
- Copyright © 2015 Intel Corporation.
- Copyright © 2016 Hewlett Packard Enterprise Development LP
- Copyright © 2025 Microsoft Corporation.
- Authors: David Howells dhowells@redhat.com
David Woodhouse <dwmw2@infradead.org>
Juerg Haefliger <juerg.haefliger@hpe.com>
Blaise Boscaccy <bboscaccy@linux.microsoft.com>
I don't think that for new code ad-hoc authors lists in source code make any sense.
- This program is free software; you can redistribute it and/or
- modify it under the terms of the GNU Lesser General Public License
- as published by the Free Software Foundation; either version 2.1
- of the licence, or (at your option) any later version.
Redundant given SPDX.
- */
+#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <stdbool.h> +#include <string.h> +#include <getopt.h> +#include <err.h> +#include <arpa/inet.h> +#include <openssl/opensslv.h> +#include <openssl/bio.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/err.h> +#if OPENSSL_VERSION_MAJOR >= 3 +# define USE_PKCS11_PROVIDER +# include <openssl/provider.h> +# include <openssl/store.h> +#else +# if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) +# define USE_PKCS11_ENGINE +# include <openssl/engine.h> +# endif +#endif +#include "../ssl-common.h"
+/*
- Use CMS if we have openssl-1.0.0 or newer available - otherwise we have to
- assume that it's not available and its header file is missing and that we
- should use PKCS#7 instead. Switching to the older PKCS#7 format restricts
- the options we have on specifying the X.509 certificate we want.
- Further, older versions of OpenSSL don't support manually adding signers to
- the PKCS#7 message so have to accept that we get a certificate included in
- the signature message. Nor do such older versions of OpenSSL support
- signing with anything other than SHA1 - so we're stuck with that if such is
- the case.
- */
+#if defined(LIBRESSL_VERSION_NUMBER) || \
- OPENSSL_VERSION_NUMBER < 0x10000000L || \
- defined(OPENSSL_NO_CMS)
+#define USE_PKCS7 +#endif +#ifndef USE_PKCS7 +#include <openssl/cms.h> +#else +#include <openssl/pkcs7.h> +#endif
+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_PKCS7 2
+static char magic_number[] = "~eBPF signature appended~\n";
+static __attribute__((noreturn)) +void format(void) +{
- fprintf(stderr,
"Usage: scripts/sign-ebpf [-dp] <hash algo> <key> <x509> <bin> <loader> [<dest>]\n");
- exit(2);
+}
+static const char *key_pass;
+static int pem_pw_cb(char *buf, int len, int w, void *v) +{
- int pwlen;
- if (!key_pass)
return -1;
- pwlen = strlen(key_pass);
- if (pwlen >= len)
return -1;
- strcpy(buf, key_pass);
- /* If it's wrong, don't keep trying it. */
- key_pass = NULL;
- return pwlen;
+}
+static EVP_PKEY *read_private_key_pkcs11(const char *private_key_name) +{
- EVP_PKEY *private_key = NULL;
+#ifdef USE_PKCS11_PROVIDER
- OSSL_STORE_CTX *store;
- if (!OSSL_PROVIDER_try_load(NULL, "pkcs11", true))
ERR(1, "OSSL_PROVIDER_try_load(pkcs11)");
- if (!OSSL_PROVIDER_try_load(NULL, "default", true))
ERR(1, "OSSL_PROVIDER_try_load(default)");
- store = OSSL_STORE_open(private_key_name, NULL, NULL, NULL, NULL);
- ERR(!store, "OSSL_STORE_open");
- while (!OSSL_STORE_eof(store)) {
OSSL_STORE_INFO *info = OSSL_STORE_load(store);
if (!info) {
drain_openssl_errors(__LINE__, 0);
continue;
}
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
private_key = OSSL_STORE_INFO_get1_PKEY(info);
ERR(!private_key, "OSSL_STORE_INFO_get1_PKEY");
}
OSSL_STORE_INFO_free(info);
if (private_key)
break;
- }
- OSSL_STORE_close(store);
+#elif defined(USE_PKCS11_ENGINE)
- ENGINE *e;
- ENGINE_load_builtin_engines();
- drain_openssl_errors(__LINE__, 1);
- e = ENGINE_by_id("pkcs11");
- ERR(!e, "Load PKCS#11 ENGINE");
- if (ENGINE_init(e))
drain_openssl_errors(__LINE__, 1);
- else
ERR(1, "ENGINE_init");
- if (key_pass)
ERR(!ENGINE_ctrl_cmd_string(e, "PIN", key_pass, 0), "Set PKCS#11 PIN");
- private_key = ENGINE_load_private_key(e, private_key_name, NULL, NULL);
- ERR(!private_key, "%s", private_key_name);
+#else
- fprintf(stderr, "no pkcs11 engine/provider available\n");
- exit(1);
+#endif
- return private_key;
+}
+static EVP_PKEY *read_private_key(const char *private_key_name) +{
- if (!strncmp(private_key_name, "pkcs11:", 7)) {
return read_private_key_pkcs11(private_key_name);
- } else {
EVP_PKEY *private_key;
BIO *b;
b = BIO_new_file(private_key_name, "rb");
ERR(!b, "%s", private_key_name);
private_key = PEM_read_bio_PrivateKey(b, NULL, pem_pw_cb,
NULL);
ERR(!private_key, "%s", private_key_name);
BIO_free(b);
return private_key;
- }
+}
+static X509 *read_x509(const char *x509_name) +{
- unsigned char buf[2];
- X509 *x509;
- BIO *b;
- int n;
- b = BIO_new_file(x509_name, "rb");
- ERR(!b, "%s", x509_name);
- /* Look at the first two bytes of the file to determine the encoding */
- n = BIO_read(b, buf, 2);
- if (n != 2) {
if (BIO_should_retry(b)) {
fprintf(stderr, "%s: Read wanted retry\n", x509_name);
exit(1);
}
if (n >= 0) {
fprintf(stderr, "%s: Short read\n", x509_name);
exit(1);
}
ERR(1, "%s", x509_name);
- }
- ERR(BIO_reset(b) != 0, "%s", x509_name);
- if (buf[0] == 0x30 && buf[1] >= 0x81 && buf[1] <= 0x84)
/* Assume raw DER encoded X.509 */
x509 = d2i_X509_bio(b, NULL);
- else
/* Assume PEM encoded X.509 */
x509 = PEM_read_bio_X509(b, NULL, NULL, NULL);
- BIO_free(b);
- ERR(!x509, "%s", x509_name);
- return x509;
+}
+int main(int argc, char **argv) +{
- struct module_signature sig_info = { .id_type = PKEY_ID_PKCS7 };
- char *hash_algo = NULL;
- char *private_key_name = NULL, *raw_sig_name = NULL;
- char *x509_name, *bin_name, *loader_name, *dest_name;
- bool save_sig = false, replace_orig;
- bool sign_only = false;
- bool raw_sig = false;
- unsigned char buf[4096];
- unsigned long loader_size, sig_size;
- unsigned int use_signed_attrs;
- const EVP_MD *digest_algo;
- EVP_PKEY *private_key;
+#ifndef USE_PKCS7
- CMS_ContentInfo *cms = NULL;
- unsigned int use_keyid = 0;
+#else
- PKCS7 *pkcs7 = NULL;
+#endif
- X509 *x509;
- BIO *bd, *bm, *bl;
- int opt, n;
- OpenSSL_add_all_algorithms();
- ERR_load_crypto_strings();
- ERR_clear_error();
- key_pass = getenv("KBUILD_SIGN_PIN");
+#ifndef USE_PKCS7
- use_signed_attrs = CMS_NOATTR;
+#else
- use_signed_attrs = PKCS7_NOATTR;
+#endif
- do {
opt = getopt(argc, argv, "sdpk");
switch (opt) {
case 's': raw_sig = true; break;
case 'p': save_sig = true; break;
case 'd': sign_only = true; save_sig = true; break;
+#ifndef USE_PKCS7
case 'k': use_keyid = CMS_USE_KEYID; break;
+#endif
case -1: break;
default: format();
}
- } while (opt != -1);
- argc -= optind;
- argv += optind;
- if (argc < 5 || argc > 6)
format();
- if (raw_sig) {
raw_sig_name = argv[0];
hash_algo = argv[1];
- } else {
hash_algo = argv[0];
private_key_name = argv[1];
- }
- x509_name = argv[2];
- bin_name = argv[3];
- loader_name = argv[4];
- if (argc == 6 && strcmp(argv[4], argv[5]) != 0) {
dest_name = argv[5];
replace_orig = false;
- } else {
ERR(asprintf(&dest_name, "%s.~signed~", loader_name) < 0,
"asprintf");
replace_orig = true;
- }
+#ifdef USE_PKCS7
- if (strcmp(hash_algo, "sha1") != 0) {
fprintf(stderr, "sign-file: %s only supports SHA1 signing\n",
OPENSSL_VERSION_TEXT);
exit(3);
- }
+#endif
- /* Open the bin file */
- bm = BIO_new_file(bin_name, "rb");
- ERR(!bm, "%s", bin_name);
- if (!raw_sig) {
/* Read the private key and the X.509 cert the PKCS#7 message
* will point to.
*/
private_key = read_private_key(private_key_name);
x509 = read_x509(x509_name);
/* Digest the module data. */
OpenSSL_add_all_digests();
drain_openssl_errors(__LINE__, 0);
digest_algo = EVP_get_digestbyname(hash_algo);
ERR(!digest_algo, "EVP_get_digestbyname");
+#ifndef USE_PKCS7
/* Load the signature message from the digest buffer. */
cms = CMS_sign(NULL, NULL, NULL, NULL,
CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY |
CMS_DETACHED | CMS_STREAM);
ERR(!cms, "CMS_sign");
ERR(!CMS_add1_signer(cms, x509, private_key, digest_algo,
CMS_NOCERTS | CMS_BINARY |
CMS_NOSMIMECAP | use_keyid |
use_signed_attrs),
"CMS_add1_signer");
ERR(CMS_final(cms, bm, NULL, CMS_NOCERTS | CMS_BINARY) != 1,
"CMS_final");
+#else
pkcs7 = PKCS7_sign(x509, private_key, NULL, bm,
PKCS7_NOCERTS | PKCS7_BINARY |
PKCS7_DETACHED | use_signed_attrs);
ERR(!pkcs7, "PKCS7_sign");
+#endif
if (save_sig) {
char *sig_file_name;
BIO *b;
ERR(asprintf(&sig_file_name, "%s.p7s", bin_name) < 0,
"asprintf");
b = BIO_new_file(sig_file_name, "wb");
ERR(!b, "%s", sig_file_name);
+#ifndef USE_PKCS7
ERR(i2d_CMS_bio_stream(b, cms, NULL, 0) != 1,
"%s", sig_file_name);
+#else
ERR(i2d_PKCS7_bio(b, pkcs7) != 1,
"%s", sig_file_name);
+#endif
BIO_free(b);
}
if (sign_only) {
BIO_free(bm);
return 0;
}
- }
- /* Open the destination file now so that we can shovel the loader data
* across as we read it.
*/
- bd = BIO_new_file(dest_name, "wb");
- ERR(!bd, "%s", dest_name);
- bl = BIO_new_file(loader_name, "rb");
- ERR(!bl, "%s", loader_name);
- /* Append the marker and the PKCS#7 message to the destination file */
- ERR(BIO_reset(bm) < 0, "%s", bin_name);
- ERR(BIO_reset(bl) < 0, "%s", loader_name);
- while ((n = BIO_read(bl, buf, sizeof(buf))),
n > 0) {
ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name);
- }
- BIO_free(bl);
- BIO_free(bm);
- ERR(n < 0, "%s", loader_name);
- loader_size = BIO_number_written(bd);
- if (!raw_sig) {
+#ifndef USE_PKCS7
ERR(i2d_CMS_bio_stream(bd, cms, NULL, 0) != 1, "%s", dest_name);
+#else
ERR(i2d_PKCS7_bio(bd, pkcs7) != 1, "%s", dest_name);
+#endif
- } else {
BIO *b;
/* Read the raw signature file and write the data to the
* destination file
*/
b = BIO_new_file(raw_sig_name, "rb");
ERR(!b, "%s", raw_sig_name);
while ((n = BIO_read(b, buf, sizeof(buf))), n > 0)
ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name);
BIO_free(b);
- }
- sig_size = BIO_number_written(bd) - loader_size;
- sig_info.sig_len = htonl(sig_size);
- ERR(BIO_write(bd, &sig_info, sizeof(sig_info)) < 0, "%s", dest_name);
- ERR(BIO_write(bd, magic_number, sizeof(magic_number) - 1) < 0, "%s", dest_name);
- ERR(BIO_free(bd) != 1, "%s", dest_name);
- /* Finally, if we're signing in place, replace the original. */
- if (replace_orig)
ERR(rename(dest_name, loader_name) < 0, "%s", dest_name);
- return 0;
+}
2.48.1
BR, Jarkko
Jarkko Sakkinen jarkko@kernel.org writes:
On Fri, Mar 21, 2025 at 09:45:04AM -0700, Blaise Boscaccy wrote:
This introduces the sign-ebpf tool. It is very similar to the existing sign-file script, with one key difference, it will sign a file with with a signature computed off of arbitrary input data. This can used to sign an ebpf light skeleton loader program for verification via hornet.
Typical usage is to provide a payload containing the lskel ebpf syscall program binary and it's associated maps, which can be extracted from the auto-generated skel header.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com
scripts/Makefile | 1 + scripts/hornet/Makefile | 5 + scripts/hornet/sign-ebpf.c | 420 +++++++++++++++++++++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 scripts/hornet/Makefile create mode 100644 scripts/hornet/sign-ebpf.c
diff --git a/scripts/Makefile b/scripts/Makefile index 46f860529df5e..a2cace05d7342 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -57,6 +57,7 @@ subdir-$(CONFIG_GENKSYMS) += genksyms subdir-$(CONFIG_GENDWARFKSYMS) += gendwarfksyms subdir-$(CONFIG_SECURITY_SELINUX) += selinux subdir-$(CONFIG_SECURITY_IPE) += ipe +subdir-$(CONFIG_SECURITY_HORNET) += hornet # Let clean descend into subdirs subdir- += basic dtc gdb kconfig mod diff --git a/scripts/hornet/Makefile b/scripts/hornet/Makefile new file mode 100644 index 0000000000000..ab71dbb8688e4 --- /dev/null +++ b/scripts/hornet/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +hostprogs-always-y := sign-ebpf
+HOSTCFLAGS_sign-ebpf.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null) +HOSTLDLIBS_sign-ebpf = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto) diff --git a/scripts/hornet/sign-ebpf.c b/scripts/hornet/sign-ebpf.c new file mode 100644 index 0000000000000..ff98fddb79a15 --- /dev/null +++ b/scripts/hornet/sign-ebpf.c @@ -0,0 +1,420 @@ +/* Sign ebpf programs and skeletons using the given key.
- This program is heavily based on the kernel's sign-file tool
- with some minor additions to support the signing of eBPF lskels.
- Copyright © 2014-2016 Red Hat, Inc. All Rights Reserved.
- Copyright © 2015 Intel Corporation.
- Copyright © 2016 Hewlett Packard Enterprise Development LP
- Copyright © 2025 Microsoft Corporation.
- Authors: David Howells dhowells@redhat.com
David Woodhouse <dwmw2@infradead.org>
Juerg Haefliger <juerg.haefliger@hpe.com>
Blaise Boscaccy <bboscaccy@linux.microsoft.com>
I don't think that for new code ad-hoc authors lists in source code make any sense.
I'll remove that. This script is mostly copy-paste of sign-file with some new args and usage. I'll leave the copywrite and nuke the author list.
- This program is free software; you can redistribute it and/or
- modify it under the terms of the GNU Lesser General Public License
- as published by the Free Software Foundation; either version 2.1
- of the licence, or (at your option) any later version.
Redundant given SPDX.
Sounds good.
- */
+#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <stdbool.h> +#include <string.h> +#include <getopt.h> +#include <err.h> +#include <arpa/inet.h> +#include <openssl/opensslv.h> +#include <openssl/bio.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/err.h> +#if OPENSSL_VERSION_MAJOR >= 3 +# define USE_PKCS11_PROVIDER +# include <openssl/provider.h> +# include <openssl/store.h> +#else +# if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) +# define USE_PKCS11_ENGINE +# include <openssl/engine.h> +# endif +#endif +#include "../ssl-common.h"
+/*
- Use CMS if we have openssl-1.0.0 or newer available - otherwise we have to
- assume that it's not available and its header file is missing and that we
- should use PKCS#7 instead. Switching to the older PKCS#7 format restricts
- the options we have on specifying the X.509 certificate we want.
- Further, older versions of OpenSSL don't support manually adding signers to
- the PKCS#7 message so have to accept that we get a certificate included in
- the signature message. Nor do such older versions of OpenSSL support
- signing with anything other than SHA1 - so we're stuck with that if such is
- the case.
- */
+#if defined(LIBRESSL_VERSION_NUMBER) || \
- OPENSSL_VERSION_NUMBER < 0x10000000L || \
- defined(OPENSSL_NO_CMS)
+#define USE_PKCS7 +#endif +#ifndef USE_PKCS7 +#include <openssl/cms.h> +#else +#include <openssl/pkcs7.h> +#endif
+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_PKCS7 2
+static char magic_number[] = "~eBPF signature appended~\n";
+static __attribute__((noreturn)) +void format(void) +{
- fprintf(stderr,
"Usage: scripts/sign-ebpf [-dp] <hash algo> <key> <x509> <bin> <loader> [<dest>]\n");
- exit(2);
+}
+static const char *key_pass;
+static int pem_pw_cb(char *buf, int len, int w, void *v) +{
- int pwlen;
- if (!key_pass)
return -1;
- pwlen = strlen(key_pass);
- if (pwlen >= len)
return -1;
- strcpy(buf, key_pass);
- /* If it's wrong, don't keep trying it. */
- key_pass = NULL;
- return pwlen;
+}
+static EVP_PKEY *read_private_key_pkcs11(const char *private_key_name) +{
- EVP_PKEY *private_key = NULL;
+#ifdef USE_PKCS11_PROVIDER
- OSSL_STORE_CTX *store;
- if (!OSSL_PROVIDER_try_load(NULL, "pkcs11", true))
ERR(1, "OSSL_PROVIDER_try_load(pkcs11)");
- if (!OSSL_PROVIDER_try_load(NULL, "default", true))
ERR(1, "OSSL_PROVIDER_try_load(default)");
- store = OSSL_STORE_open(private_key_name, NULL, NULL, NULL, NULL);
- ERR(!store, "OSSL_STORE_open");
- while (!OSSL_STORE_eof(store)) {
OSSL_STORE_INFO *info = OSSL_STORE_load(store);
if (!info) {
drain_openssl_errors(__LINE__, 0);
continue;
}
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
private_key = OSSL_STORE_INFO_get1_PKEY(info);
ERR(!private_key, "OSSL_STORE_INFO_get1_PKEY");
}
OSSL_STORE_INFO_free(info);
if (private_key)
break;
- }
- OSSL_STORE_close(store);
+#elif defined(USE_PKCS11_ENGINE)
- ENGINE *e;
- ENGINE_load_builtin_engines();
- drain_openssl_errors(__LINE__, 1);
- e = ENGINE_by_id("pkcs11");
- ERR(!e, "Load PKCS#11 ENGINE");
- if (ENGINE_init(e))
drain_openssl_errors(__LINE__, 1);
- else
ERR(1, "ENGINE_init");
- if (key_pass)
ERR(!ENGINE_ctrl_cmd_string(e, "PIN", key_pass, 0), "Set PKCS#11 PIN");
- private_key = ENGINE_load_private_key(e, private_key_name, NULL, NULL);
- ERR(!private_key, "%s", private_key_name);
+#else
- fprintf(stderr, "no pkcs11 engine/provider available\n");
- exit(1);
+#endif
- return private_key;
+}
+static EVP_PKEY *read_private_key(const char *private_key_name) +{
- if (!strncmp(private_key_name, "pkcs11:", 7)) {
return read_private_key_pkcs11(private_key_name);
- } else {
EVP_PKEY *private_key;
BIO *b;
b = BIO_new_file(private_key_name, "rb");
ERR(!b, "%s", private_key_name);
private_key = PEM_read_bio_PrivateKey(b, NULL, pem_pw_cb,
NULL);
ERR(!private_key, "%s", private_key_name);
BIO_free(b);
return private_key;
- }
+}
+static X509 *read_x509(const char *x509_name) +{
- unsigned char buf[2];
- X509 *x509;
- BIO *b;
- int n;
- b = BIO_new_file(x509_name, "rb");
- ERR(!b, "%s", x509_name);
- /* Look at the first two bytes of the file to determine the encoding */
- n = BIO_read(b, buf, 2);
- if (n != 2) {
if (BIO_should_retry(b)) {
fprintf(stderr, "%s: Read wanted retry\n", x509_name);
exit(1);
}
if (n >= 0) {
fprintf(stderr, "%s: Short read\n", x509_name);
exit(1);
}
ERR(1, "%s", x509_name);
- }
- ERR(BIO_reset(b) != 0, "%s", x509_name);
- if (buf[0] == 0x30 && buf[1] >= 0x81 && buf[1] <= 0x84)
/* Assume raw DER encoded X.509 */
x509 = d2i_X509_bio(b, NULL);
- else
/* Assume PEM encoded X.509 */
x509 = PEM_read_bio_X509(b, NULL, NULL, NULL);
- BIO_free(b);
- ERR(!x509, "%s", x509_name);
- return x509;
+}
+int main(int argc, char **argv) +{
- struct module_signature sig_info = { .id_type = PKEY_ID_PKCS7 };
- char *hash_algo = NULL;
- char *private_key_name = NULL, *raw_sig_name = NULL;
- char *x509_name, *bin_name, *loader_name, *dest_name;
- bool save_sig = false, replace_orig;
- bool sign_only = false;
- bool raw_sig = false;
- unsigned char buf[4096];
- unsigned long loader_size, sig_size;
- unsigned int use_signed_attrs;
- const EVP_MD *digest_algo;
- EVP_PKEY *private_key;
+#ifndef USE_PKCS7
- CMS_ContentInfo *cms = NULL;
- unsigned int use_keyid = 0;
+#else
- PKCS7 *pkcs7 = NULL;
+#endif
- X509 *x509;
- BIO *bd, *bm, *bl;
- int opt, n;
- OpenSSL_add_all_algorithms();
- ERR_load_crypto_strings();
- ERR_clear_error();
- key_pass = getenv("KBUILD_SIGN_PIN");
+#ifndef USE_PKCS7
- use_signed_attrs = CMS_NOATTR;
+#else
- use_signed_attrs = PKCS7_NOATTR;
+#endif
- do {
opt = getopt(argc, argv, "sdpk");
switch (opt) {
case 's': raw_sig = true; break;
case 'p': save_sig = true; break;
case 'd': sign_only = true; save_sig = true; break;
+#ifndef USE_PKCS7
case 'k': use_keyid = CMS_USE_KEYID; break;
+#endif
case -1: break;
default: format();
}
- } while (opt != -1);
- argc -= optind;
- argv += optind;
- if (argc < 5 || argc > 6)
format();
- if (raw_sig) {
raw_sig_name = argv[0];
hash_algo = argv[1];
- } else {
hash_algo = argv[0];
private_key_name = argv[1];
- }
- x509_name = argv[2];
- bin_name = argv[3];
- loader_name = argv[4];
- if (argc == 6 && strcmp(argv[4], argv[5]) != 0) {
dest_name = argv[5];
replace_orig = false;
- } else {
ERR(asprintf(&dest_name, "%s.~signed~", loader_name) < 0,
"asprintf");
replace_orig = true;
- }
+#ifdef USE_PKCS7
- if (strcmp(hash_algo, "sha1") != 0) {
fprintf(stderr, "sign-file: %s only supports SHA1 signing\n",
OPENSSL_VERSION_TEXT);
exit(3);
- }
+#endif
- /* Open the bin file */
- bm = BIO_new_file(bin_name, "rb");
- ERR(!bm, "%s", bin_name);
- if (!raw_sig) {
/* Read the private key and the X.509 cert the PKCS#7 message
* will point to.
*/
private_key = read_private_key(private_key_name);
x509 = read_x509(x509_name);
/* Digest the module data. */
OpenSSL_add_all_digests();
drain_openssl_errors(__LINE__, 0);
digest_algo = EVP_get_digestbyname(hash_algo);
ERR(!digest_algo, "EVP_get_digestbyname");
+#ifndef USE_PKCS7
/* Load the signature message from the digest buffer. */
cms = CMS_sign(NULL, NULL, NULL, NULL,
CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY |
CMS_DETACHED | CMS_STREAM);
ERR(!cms, "CMS_sign");
ERR(!CMS_add1_signer(cms, x509, private_key, digest_algo,
CMS_NOCERTS | CMS_BINARY |
CMS_NOSMIMECAP | use_keyid |
use_signed_attrs),
"CMS_add1_signer");
ERR(CMS_final(cms, bm, NULL, CMS_NOCERTS | CMS_BINARY) != 1,
"CMS_final");
+#else
pkcs7 = PKCS7_sign(x509, private_key, NULL, bm,
PKCS7_NOCERTS | PKCS7_BINARY |
PKCS7_DETACHED | use_signed_attrs);
ERR(!pkcs7, "PKCS7_sign");
+#endif
if (save_sig) {
char *sig_file_name;
BIO *b;
ERR(asprintf(&sig_file_name, "%s.p7s", bin_name) < 0,
"asprintf");
b = BIO_new_file(sig_file_name, "wb");
ERR(!b, "%s", sig_file_name);
+#ifndef USE_PKCS7
ERR(i2d_CMS_bio_stream(b, cms, NULL, 0) != 1,
"%s", sig_file_name);
+#else
ERR(i2d_PKCS7_bio(b, pkcs7) != 1,
"%s", sig_file_name);
+#endif
BIO_free(b);
}
if (sign_only) {
BIO_free(bm);
return 0;
}
- }
- /* Open the destination file now so that we can shovel the loader data
* across as we read it.
*/
- bd = BIO_new_file(dest_name, "wb");
- ERR(!bd, "%s", dest_name);
- bl = BIO_new_file(loader_name, "rb");
- ERR(!bl, "%s", loader_name);
- /* Append the marker and the PKCS#7 message to the destination file */
- ERR(BIO_reset(bm) < 0, "%s", bin_name);
- ERR(BIO_reset(bl) < 0, "%s", loader_name);
- while ((n = BIO_read(bl, buf, sizeof(buf))),
n > 0) {
ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name);
- }
- BIO_free(bl);
- BIO_free(bm);
- ERR(n < 0, "%s", loader_name);
- loader_size = BIO_number_written(bd);
- if (!raw_sig) {
+#ifndef USE_PKCS7
ERR(i2d_CMS_bio_stream(bd, cms, NULL, 0) != 1, "%s", dest_name);
+#else
ERR(i2d_PKCS7_bio(bd, pkcs7) != 1, "%s", dest_name);
+#endif
- } else {
BIO *b;
/* Read the raw signature file and write the data to the
* destination file
*/
b = BIO_new_file(raw_sig_name, "rb");
ERR(!b, "%s", raw_sig_name);
while ((n = BIO_read(b, buf, sizeof(buf))), n > 0)
ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name);
BIO_free(b);
- }
- sig_size = BIO_number_written(bd) - loader_size;
- sig_info.sig_len = htonl(sig_size);
- ERR(BIO_write(bd, &sig_info, sizeof(sig_info)) < 0, "%s", dest_name);
- ERR(BIO_write(bd, magic_number, sizeof(magic_number) - 1) < 0, "%s", dest_name);
- ERR(BIO_free(bd) != 1, "%s", dest_name);
- /* Finally, if we're signing in place, replace the original. */
- if (replace_orig)
ERR(rename(dest_name, loader_name) < 0, "%s", dest_name);
- return 0;
+}
2.48.1
BR, Jarkko
This script eases lskel developments against hornet by generating the data payload used for code signing. It extracts the data out of the autogenerated lskel header that gets created via bpftool.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com --- scripts/hornet/extract-skel.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100755 scripts/hornet/extract-skel.sh
diff --git a/scripts/hornet/extract-skel.sh b/scripts/hornet/extract-skel.sh new file mode 100755 index 0000000000000..9ace78794b85e --- /dev/null +++ b/scripts/hornet/extract-skel.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (c) 2025 Microsoft Corporation +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation. + +function usage() { + echo "Sample script for extracting instructions and map data out of" + echo "autogenerated eBPF lskel headers" + echo "" + echo "USAGE: header_file output_file" + exit +} + +ARGC=$# + +EXPECTED_ARGS=2 + +if [ $ARGC -ne $EXPECTED_ARGS ] ; then + usage +else + printf $(gcc -E $1 | grep "static const char opts_insn" | \ + awk -F"=" '{print $2}' | sed 's/;+$//' | sed 's/"//g') > $2 + printf $(gcc -E $1 | grep "static const char opts_data" | \ + awk -F"=" '{print $2}' | sed 's/;+$//' | sed 's/"//g') >> $2 +fi
On Fri, Mar 21, 2025 at 09:45:05AM -0700, Blaise Boscaccy wrote:
This script eases lskel developments against hornet by generating the
1. What iskel? 2. Why hornet is here in lower case?
data payload used for code signing. It extracts the data out of the autogenerated lskel header that gets created via bpftool.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com
scripts/hornet/extract-skel.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100755 scripts/hornet/extract-skel.sh
diff --git a/scripts/hornet/extract-skel.sh b/scripts/hornet/extract-skel.sh new file mode 100755 index 0000000000000..9ace78794b85e --- /dev/null +++ b/scripts/hornet/extract-skel.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (c) 2025 Microsoft Corporation +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation.
+function usage() {
- echo "Sample script for extracting instructions and map data out of"
- echo "autogenerated eBPF lskel headers"
- echo ""
- echo "USAGE: header_file output_file"
- exit
+}
+ARGC=$#
+EXPECTED_ARGS=2
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
- usage
+else
- printf $(gcc -E $1 | grep "static const char opts_insn" | \
awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g') > $2
- printf $(gcc -E $1 | grep "static const char opts_data" | \
awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g') >> $2
+fi
2.48.1
BR, Jarkko
Jarkko Sakkinen jarkko@kernel.org writes:
On Fri, Mar 21, 2025 at 09:45:05AM -0700, Blaise Boscaccy wrote:
This script eases lskel developments against hornet by generating the
- What iskel?
It's a "light-skeleton". I'll remove the abbreviations from this patchset's commit messages. The jargon is hard enough to grok as-is.
- Why hornet is here in lower case?
Typo. Thanks for finding that.
data payload used for code signing. It extracts the data out of the autogenerated lskel header that gets created via bpftool.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com
scripts/hornet/extract-skel.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100755 scripts/hornet/extract-skel.sh
diff --git a/scripts/hornet/extract-skel.sh b/scripts/hornet/extract-skel.sh new file mode 100755 index 0000000000000..9ace78794b85e --- /dev/null +++ b/scripts/hornet/extract-skel.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (c) 2025 Microsoft Corporation +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation.
+function usage() {
- echo "Sample script for extracting instructions and map data out of"
- echo "autogenerated eBPF lskel headers"
- echo ""
- echo "USAGE: header_file output_file"
- exit
+}
+ARGC=$#
+EXPECTED_ARGS=2
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
- usage
+else
- printf $(gcc -E $1 | grep "static const char opts_insn" | \
awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g') > $2
- printf $(gcc -E $1 | grep "static const char opts_data" | \
awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g') >> $2
+fi
2.48.1
BR, Jarkko
This selftest tests contains a simple testcase that utilizes an lskel loader. One version of the lskel is signed with the autogenerated module signing key, another is not. A test driver attempts to load the lskels. With hornet enabled, the signed version should successfully be loaded, and the unsigned version should fail.
Signed-off-by: Blaise Boscaccy bboscaccy@linux.microsoft.com --- tools/testing/selftests/Makefile | 1 + tools/testing/selftests/hornet/Makefile | 51 ++++++++++++++++++++ tools/testing/selftests/hornet/loader.c | 21 ++++++++ tools/testing/selftests/hornet/trivial.bpf.c | 33 +++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 tools/testing/selftests/hornet/Makefile create mode 100644 tools/testing/selftests/hornet/loader.c create mode 100644 tools/testing/selftests/hornet/trivial.bpf.c
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 8daac70c2f9d2..fce32ee4de328 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -41,6 +41,7 @@ TARGETS += ftrace TARGETS += futex TARGETS += gpio TARGETS += hid +TARGETS += hornet TARGETS += intel_pstate TARGETS += iommu TARGETS += ipc diff --git a/tools/testing/selftests/hornet/Makefile b/tools/testing/selftests/hornet/Makefile new file mode 100644 index 0000000000000..93da70f41d40c --- /dev/null +++ b/tools/testing/selftests/hornet/Makefile @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: GPL-2.0 +include ../../../build/Build.include +include ../../../scripts/Makefile.arch +include ../../../scripts/Makefile.include + +CLANG ?= clang +CFLAGS := -g -O2 -Wall +BPFTOOL ?= bpftool +SCRIPTSDIR := $(abspath ../../../../scripts/hornet) +TOOLSDIR := $(abspath ../../..) +LIBDIR := $(TOOLSDIR)/lib +BPFDIR := $(LIBDIR)/bpf +TOOLSINCDIR := $(TOOLSDIR)/include +APIDIR := $(TOOLSINCDIR)/uapi +CERTDIR := $(abspath ../../../../certs) + +TEST_GEN_PROGS_EXTENDED := loader +TEST_GEN_PROGS := signed_loader +TEST_PROGS := fail_loader +TEST_GEN_FILES := vmlinux.h loader.h trivial.bin trivial.bpf.o +$(TEST_GEN_PROGS): LDLIBS += -lbpf +$(TEST_GEN_PROGS): $(TEST_GEN_FILES) + +include ../lib.mk + +BPF_CFLAGS := -target bpf \ + -D__TARGET_ARCH_$(ARCH) \ + -I/usr/include/$(shell uname -m)-linux-gnu \ + $(KHDR_INCLUDES) +vmlinux.h: + $(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h + +trivial.bpf.o: trivial.bpf.c vmlinux.h + $(CLANG) $(CFLAGS) $(BPF_CFLAGS) -c $< -o $@ + +loader.h: trivial.bpf.o + $(BPFTOOL) gen skeleton -L $< name trivial > $@ + +trivial.bin: loader.h + $(SCRIPTSDIR)/extract-skel.sh $< $@ + +loader: loader.c loader.h + $(CC) $(CFLAGS) -I$(LIBDIR) -I$(APIDIR) $< -o $@ -lbpf + +fail_loader: fail_loader.c loader.h + $(CC) $(CFLAGS) -I$(LIBDIR) -I$(APIDIR) $< -o $@ -lbpf + +signed_loader: trivial.bin loader fail_loader + $(SCRIPTSDIR)/sign-ebpf sha256 $(CERTDIR)/signing_key.pem $(CERTDIR)/signing_key.x509 \ + trivial.bin loader signed_loader + chmod u+x $@ diff --git a/tools/testing/selftests/hornet/loader.c b/tools/testing/selftests/hornet/loader.c new file mode 100644 index 0000000000000..9a43bb012d1b2 --- /dev/null +++ b/tools/testing/selftests/hornet/loader.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause + +#include <stdio.h> +#include <unistd.h> +#include <stddef.h> +#include <sys/resource.h> +#include <bpf/libbpf.h> +#include <errno.h> +#include "loader.h" + +int main(int argc, char **argv) +{ + struct trivial *skel; + + skel = trivial__open_and_load(); + if (!skel) + return -1; + + trivial__destroy(skel); + return 0; +} diff --git a/tools/testing/selftests/hornet/trivial.bpf.c b/tools/testing/selftests/hornet/trivial.bpf.c new file mode 100644 index 0000000000000..d38c5b53ff932 --- /dev/null +++ b/tools/testing/selftests/hornet/trivial.bpf.c @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause + +#include "vmlinux.h" + +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> +#include <bpf/bpf_core_read.h> + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +int monitored_pid = 0; + +SEC("tracepoint/syscalls/sys_enter_unlinkat") +int handle_enter_unlink(struct trace_event_raw_sys_enter *ctx) +{ + char filename[128] = { 0 }; + struct task_struct *task; + unsigned long start_time = 0; + int pid = bpf_get_current_pid_tgid() >> 32; + char *pathname_ptr = (char *) BPF_CORE_READ(ctx, args[1]); + + bpf_probe_read_str(filename, sizeof(filename), pathname_ptr); + task = (struct task_struct *)bpf_get_current_task(); + start_time = BPF_CORE_READ(task, start_time); + + bpf_printk("BPF triggered unlinkat by PID: %d, start_time %ld. pathname = %s", + pid, start_time, filename); + + if (monitored_pid == pid) + bpf_printk("target pid found"); + + return 0; +}
On Fri, Mar 21, 2025 at 12:45 PM Blaise Boscaccy bboscaccy@linux.microsoft.com wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature verification. Signature data can be easily generated for the binary data that is generated via bpftool gen -L. This signature can be appended to a skeleton executable via scripts/sign-ebpf. Hornet checks the signature against a binary buffer containing the lskel instructions that the loader maps use. Maps are frozen to prevent TOCTOU bugs where a sufficiently privileged user could rewrite map data between the calls to BPF_PROG_LOAD and BPF_PROG_RUN. Additionally, both sparse-array-based and fd_array_cnt-based map fd arrays are supported for signature verification.
Blaise Boscaccy (4): security: Hornet LSM hornet: Introduce sign-ebpf hornet: Add an example lskel data extactor script selftests/hornet: Add a selftest for the hornet LSM
Thanks Blaise, I noticed a few minor things, but nothing critical. As I understand it, you'll be presenting Hornet at LSFMMBPF next week? Assuming that's the case, I'm going to hold off on reviewing this until we hear how that went next week; please report back after the conference.
However, to be clear, the Hornet LSM proposed here seems very reasonable to me and I would have no conceptual objections to merging it upstream. Based on off-list discussions I believe there is a lot of demand for something like this, and I believe many people will be happy to have BPF signature verification in-tree.
On Fri, Mar 21, 2025 at 09:45:02AM -0700, Blaise Boscaccy wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature
Can you define "light-skeleton-based" before using the term.
This is the first time in my life when I hear about it.
verification. Signature data can be easily generated for the binary
s/easily//
Useless word having no measure.
data that is generated via bpftool gen -L. This signature can be
I have no idea what that command does.
"Signature data can be generated for the binary data as follows:
bpftool gen -L
<explanation>"
Here you'd need to answer to couple of unknowns:
1. What is in exact terms "signature data"? 2. What does "bpftool gen -L" do?
This feedback maps to other examples too in the cover letter.
BR, Jarkko
On Sat, Mar 22, 2025 at 1:22 PM Jarkko Sakkinen jarkko@kernel.org wrote:
On Fri, Mar 21, 2025 at 09:45:02AM -0700, Blaise Boscaccy wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature
Can you define "light-skeleton-based" before using the term.
This is the first time in my life when I hear about it.
I was in the same situation a few months ago when I first heard about it :)
Blaise can surely provide a much better answer that what I'm about to write, but since Blaise is going to be at LSFMMBPF this coming week I suspect he might not have a lot of time to respond to email in the next few days so I thought I would do my best to try and answer :)
An eBPF "light skeleton" is basically a BPF loader program and while I'm sure there are several uses for a light skeleton, or lskel for brevity, the single use case that we are interested in here, and the one that Hornet deals with, is the idea of using a lskel to enable signature verification of BPF programs as it seems to be the one way that has been deemed acceptable by the BPF maintainers.
Once again, skipping over a lot of details, the basic idea is that you take your original BPF program (A), feed it into a BPF userspace tool to encapsulate the original program A into a BPF map and generate a corresponding light skeleton BPF program (B), and then finally sign the resulting binary containing the lskel program (B) and map corresponding to the original program A. At runtime, the lskel binary is loaded into the kernel, and if Hornet is enabled, the signature of both the lskel program A and original program B is verified. If the signature verification passes, lskel program A performs the necessary BPF CO-RE transforms on BPF program A stored in the BPF map and then attempts to load the original BPF program B, all from within the kernel, and with the map frozen to prevent tampering from userspace.
Hopefully that helps fill in some gaps until someone more knowledgeable can provide a better answer and/or correct any mistakes in my explanation above ;)
On Sat, Mar 22, 2025 at 4:44 PM Paul Moore paul@paul-moore.com wrote:
On Sat, Mar 22, 2025 at 1:22 PM Jarkko Sakkinen jarkko@kernel.org wrote:
On Fri, Mar 21, 2025 at 09:45:02AM -0700, Blaise Boscaccy wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature
Can you define "light-skeleton-based" before using the term.
This is the first time in my life when I hear about it.
I was in the same situation a few months ago when I first heard about it :)
Blaise can surely provide a much better answer that what I'm about to write, but since Blaise is going to be at LSFMMBPF this coming week I suspect he might not have a lot of time to respond to email in the next few days so I thought I would do my best to try and answer :)
An eBPF "light skeleton" is basically a BPF loader program and while I'm sure there are several uses for a light skeleton, or lskel for brevity, the single use case that we are interested in here, and the one that Hornet deals with, is the idea of using a lskel to enable signature verification of BPF programs as it seems to be the one way that has been deemed acceptable by the BPF maintainers.
Once again, skipping over a lot of details, the basic idea is that you take your original BPF program (A), feed it into a BPF userspace tool to encapsulate the original program A into a BPF map and generate a corresponding light skeleton BPF program (B), and then finally sign the resulting binary containing the lskel program (B) and map corresponding to the original program A.
Forgive me, I mixed up my "A" and "B" above :/
At runtime, the lskel binary is loaded into the kernel, and if Hornet is enabled, the signature of both the lskel program A and original program B is verified.
... and I did again here
If the signature verification passes, lskel program A performs the necessary BPF CO-RE transforms on BPF program A stored in the BPF map and then attempts to load the original BPF program B, all from within the kernel, and with the map frozen to prevent tampering from userspace.
... and once more here because why not? :)
Hopefully that helps fill in some gaps until someone more knowledgeable can provide a better answer and/or correct any mistakes in my explanation above ;)
On Sat, Mar 22, 2025 at 04:48:14PM -0400, Paul Moore wrote:
On Sat, Mar 22, 2025 at 4:44 PM Paul Moore paul@paul-moore.com wrote:
On Sat, Mar 22, 2025 at 1:22 PM Jarkko Sakkinen jarkko@kernel.org wrote:
On Fri, Mar 21, 2025 at 09:45:02AM -0700, Blaise Boscaccy wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature
Can you define "light-skeleton-based" before using the term.
This is the first time in my life when I hear about it.
I was in the same situation a few months ago when I first heard about it :)
Blaise can surely provide a much better answer that what I'm about to write, but since Blaise is going to be at LSFMMBPF this coming week I suspect he might not have a lot of time to respond to email in the next few days so I thought I would do my best to try and answer :)
An eBPF "light skeleton" is basically a BPF loader program and while I'm sure there are several uses for a light skeleton, or lskel for brevity, the single use case that we are interested in here, and the one that Hornet deals with, is the idea of using a lskel to enable signature verification of BPF programs as it seems to be the one way that has been deemed acceptable by the BPF maintainers.
Once again, skipping over a lot of details, the basic idea is that you take your original BPF program (A), feed it into a BPF userspace tool to encapsulate the original program A into a BPF map and generate a corresponding light skeleton BPF program (B), and then finally sign the resulting binary containing the lskel program (B) and map corresponding to the original program A.
Forgive me, I mixed up my "A" and "B" above :/
At runtime, the lskel binary is loaded into the kernel, and if Hornet is enabled, the signature of both the lskel program A and original program B is verified.
... and I did again here
If the signature verification passes, lskel program A performs the necessary BPF CO-RE transforms on BPF program A stored in the BPF map and then attempts to load the original BPF program B, all from within the kernel, and with the map frozen to prevent tampering from userspace.
... and once more here because why not? :)
No worries I was able to decipher this :-)
Hopefully that helps fill in some gaps until someone more knowledgeable can provide a better answer and/or correct any mistakes in my explanation above ;)
-- paul-moore.com
BR, Jarkko
On Sat, Mar 22, 2025 at 04:44:13PM -0400, Paul Moore wrote:
On Sat, Mar 22, 2025 at 1:22 PM Jarkko Sakkinen jarkko@kernel.org wrote:
On Fri, Mar 21, 2025 at 09:45:02AM -0700, Blaise Boscaccy wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature
Can you define "light-skeleton-based" before using the term.
This is the first time in my life when I hear about it.
I was in the same situation a few months ago when I first heard about it :)
Blaise can surely provide a much better answer that what I'm about to write, but since Blaise is going to be at LSFMMBPF this coming week I suspect he might not have a lot of time to respond to email in the next few days so I thought I would do my best to try and answer :)
Yeah, I don't think there is anything largely wrong in the feature itself but it speaks language that would fit to eBPF subsystem list, not here :-)
I.e. assume only very basic knowledge of eBPF and explain what stuff mentioned actually does. Like bpftool statement should be opened up fully.
An eBPF "light skeleton" is basically a BPF loader program and while I'm sure there are several uses for a light skeleton, or lskel for brevity, the single use case that we are interested in here, and the one that Hornet deals with, is the idea of using a lskel to enable signature verification of BPF programs as it seems to be the one way that has been deemed acceptable by the BPF maintainers.
I got some grip but the term only should be used IMHO in the commit message, if it is defined at first :-)
Once again, skipping over a lot of details, the basic idea is that you take your original BPF program (A), feed it into a BPF userspace tool to encapsulate the original program A into a BPF map and generate a corresponding light skeleton BPF program (B), and then finally sign the resulting binary containing the lskel program (B) and map corresponding to the original program A. At runtime, the lskel binary is loaded into the kernel, and if Hornet is enabled, the signature of both the lskel program A and original program B is verified. If the signature verification passes, lskel program A performs the necessary BPF CO-RE transforms on BPF program A stored in the BPF map and then attempts to load the original BPF program B, all from within the kernel, and with the map frozen to prevent tampering from userspace.
When you speak about corresponding lskel program what does that program contain? Is it some kind of new version of the same program with modifications, or?
I neither did not know what BPF CO-RE is but I googled it ;-)
Hopefully that helps fill in some gaps until someone more knowledgeable can provide a better answer and/or correct any mistakes in my explanation above ;)
Sure... Thanks for the explanations!
-- paul-moore.com
BR, Jarkko
Jarkko Sakkinen jarkko@kernel.org writes:
Hi Jarkko,
Thanks for the comments. Paul did a very nice job providing some background info, allow me to provide some additional data.
On Fri, Mar 21, 2025 at 09:45:02AM -0700, Blaise Boscaccy wrote:
This patch series introduces the Hornet LSM.
Hornet takes a simple approach to light-skeleton-based eBPF signature
Can you define "light-skeleton-based" before using the term.
This is the first time in my life when I hear about it.
Sure. Here is the patchset where this stuff got introduced if you are curious. https://lore.kernel.org/bpf/20220209054315.73833-1-alexei.starovoitov@gmail....
eBPF has similar requirements to that of modules when it comes to loading: find kallysym addresses, fix up elf relocations, some struct field offset handing stuff called CO-RE (compile-one run-anywhere), and some other miscellaneous bookkeeping. During eBPF program compilation, pseudo-values get written to the immedate operands of instructions. During loading, those pseudo-values get rewritten with concrete addresses or data applicable to the currently running system, e.g. a kallsym address or a fd for a map. This needs to happen before the instructions for a bpf program are loaded into the kernel via the bpf() syscall.
Unlike modules, an in-kernel loader unfortunately doesn't exist. Typically, the instruction rewriting is done dynamically in userspace via libbpf (or the rust/go/python loader). What skeletons do is generate a script of required instruction-rewriting operations which then gets played back at load-time against a hard-coded blob of raw instruction data. This removes the need to distribute source-code or object files.
There are two flavors of skeletons, normal skeletons, and light skeletons. Normal skeletons utilize relocation logic that lives in libbpf, and the relocations/instruction rewriting happen in userspace. The second flavor, light skeletons, uses a small eBPF program that contains the relocation lookup logic. As it's running in in the kernel, it unpacks the target program, peforms the instruction rewriting, and loads the target program. Light skeletons are currently utilized for some drivers, and BPF_PRELOAD functionionality since they can operate without userspace.
Light skeletons were recommended on various mailing list discussions as the preffered path to performing signature verification. There are some PoCs floating around that used light-skeletons in concert with fs-verity/IMA and eBPF LSMs. We took a slightly different approach to Hornet, by utilizing the existing PCKS#7 signing scheme that is used for kernel modules.
verification. Signature data can be easily generated for the binary
s/easily//
Useless word having no measure.
Ack, thanks.
data that is generated via bpftool gen -L. This signature can be
I have no idea what that command does.
"Signature data can be generated for the binary data as follows:
bpftool gen -L
<explanation>"
Here you'd need to answer to couple of unknowns:
- What is in exact terms "signature data"?
That is a PKCS#7 signature of a data buffer containing the raw instructions of an eBPF program, followed by the initial values of any maps used by the program.
- What does "bpftool gen -L" do?
eBPF programs often have 2 parts. An orchestrator/loader program that provides load -> attach/run -> i/o -> teardown logic and the in-kernel program.
That command is used to generate a skeleton which can be used by the orchestrator prgoram. Skeletons get generated as a C header file, that contains various autogenerated functions that open and load bpf programs as decribed above. That header file ends up being included in a userspace orchestrator program or possibly a kernel module.
This feedback maps to other examples too in the cover letter.
BR, Jarkko
I'll rework this with some definitions of the eBPF subsystem jargon along with your suggestions.
-blaise
On Mon, Mar 31, 2025 at 01:57:15PM -0700, Blaise Boscaccy wrote:
There are two flavors of skeletons, normal skeletons, and light skeletons. Normal skeletons utilize relocation logic that lives in libbpf, and the relocations/instruction rewriting happen in userspace. The second flavor, light skeletons, uses a small eBPF program that contains the relocation lookup logic. As it's running in in the kernel, it unpacks the target program, peforms the instruction rewriting, and loads the target program. Light skeletons are currently utilized for some drivers, and BPF_PRELOAD functionionality since they can operate without userspace.
Light skeletons were recommended on various mailing list discussions as the preffered path to performing signature verification. There are some PoCs floating around that used light-skeletons in concert with fs-verity/IMA and eBPF LSMs. We took a slightly different approach to Hornet, by utilizing the existing PCKS#7 signing scheme that is used for kernel modules.
Right, because in the normal skeletons relocation logic remains unsigned?
I have to admit I don't fully cope how the relocation process translates into eBPF program but I do get how it is better for signatures if it does :-)
verification. Signature data can be easily generated for the binary
s/easily//
Useless word having no measure.
Ack, thanks.
data that is generated via bpftool gen -L. This signature can be
I have no idea what that command does.
"Signature data can be generated for the binary data as follows:
bpftool gen -L
<explanation>"
Here you'd need to answer to couple of unknowns:
- What is in exact terms "signature data"?
That is a PKCS#7 signature of a data buffer containing the raw instructions of an eBPF program, followed by the initial values of any maps used by the program.
Got it, thanks. This motivates to refine my TPM2 asymmetric keys series so that TPM2 could anchor these :-)
https://lore.kernel.org/linux-integrity/20240528210823.28798-1-jarkko@kernel...
- What does "bpftool gen -L" do?
eBPF programs often have 2 parts. An orchestrator/loader program that provides load -> attach/run -> i/o -> teardown logic and the in-kernel program.
That command is used to generate a skeleton which can be used by the orchestrator prgoram. Skeletons get generated as a C header file, that contains various autogenerated functions that open and load bpf programs as decribed above. That header file ends up being included in a userspace orchestrator program or possibly a kernel module.
I did read the man page now too, but thanks for the commentary!
This feedback maps to other examples too in the cover letter.
BR, Jarkko
I'll rework this with some definitions of the eBPF subsystem jargon along with your suggestions.
Yeah, you should be able to put the gist a factor better to nutshell :-)
-blaise
BR, Jarkko
Jarkko Sakkinen jarkko@kernel.org writes:
On Mon, Mar 31, 2025 at 01:57:15PM -0700, Blaise Boscaccy wrote:
There are two flavors of skeletons, normal skeletons, and light skeletons. Normal skeletons utilize relocation logic that lives in libbpf, and the relocations/instruction rewriting happen in userspace. The second flavor, light skeletons, uses a small eBPF program that contains the relocation lookup logic. As it's running in in the kernel, it unpacks the target program, peforms the instruction rewriting, and loads the target program. Light skeletons are currently utilized for some drivers, and BPF_PRELOAD functionionality since they can operate without userspace.
Light skeletons were recommended on various mailing list discussions as the preffered path to performing signature verification. There are some PoCs floating around that used light-skeletons in concert with fs-verity/IMA and eBPF LSMs. We took a slightly different approach to Hornet, by utilizing the existing PCKS#7 signing scheme that is used for kernel modules.
Right, because in the normal skeletons relocation logic remains unsigned?
Yup, Exactly.
I have to admit I don't fully cope how the relocation process translates into eBPF program but I do get how it is better for signatures if it does :-)
verification. Signature data can be easily generated for the binary
s/easily//
Useless word having no measure.
Ack, thanks.
data that is generated via bpftool gen -L. This signature can be
I have no idea what that command does.
"Signature data can be generated for the binary data as follows:
bpftool gen -L
<explanation>"
Here you'd need to answer to couple of unknowns:
- What is in exact terms "signature data"?
That is a PKCS#7 signature of a data buffer containing the raw instructions of an eBPF program, followed by the initial values of any maps used by the program.
Got it, thanks. This motivates to refine my TPM2 asymmetric keys series so that TPM2 could anchor these :-)
https://lore.kernel.org/linux-integrity/20240528210823.28798-1-jarkko@kernel...
Oooh. That would be very nice :)
- What does "bpftool gen -L" do?
eBPF programs often have 2 parts. An orchestrator/loader program that provides load -> attach/run -> i/o -> teardown logic and the in-kernel program.
That command is used to generate a skeleton which can be used by the orchestrator prgoram. Skeletons get generated as a C header file, that contains various autogenerated functions that open and load bpf programs as decribed above. That header file ends up being included in a userspace orchestrator program or possibly a kernel module.
I did read the man page now too, but thanks for the commentary!
This feedback maps to other examples too in the cover letter.
BR, Jarkko
I'll rework this with some definitions of the eBPF subsystem jargon along with your suggestions.
Yeah, you should be able to put the gist a factor better to nutshell :-)
-blaise
BR, Jarkko
linux-kselftest-mirror@lists.linaro.org