Skip to content

Commit 58a5820

Browse files
author
Alexei Starovoitov
committed
Merge branch 'signed-bpf-programs'
KP Singh says: ==================== Signed BPF programs BPF Signing has gone over multiple discussions in various conferences with the kernel and BPF community and the following patch series is a culmination of the current of discussion and signed BPF programs. Once signing is implemented, the next focus would be to implement the right security policies for all BPF use-cases (dynamically generated bpf programs, simple non CO-RE programs). Signing also paves the way for allowing unrivileged users to load vetted BPF programs and helps in adhering to the principle of least privlege by avoiding unnecessary elevation of privileges to CAP_BPF and CAP_SYS_ADMIN (ofcourse, with the appropriate security policy active). A early version of this design was proposed in [1]: The key idea of the design is to use a signing algorithm that allows us to integrity-protect a number of future payloads, including their order, by creating a chain of trust. Consider that Alice needs to send messages M_1, M_2, ..., M_n to Bob. We define blocks of data such that: B_n = M_n || H(termination_marker) (Each block contains its corresponding message and the hash of the *next* block in the chain.) B_{n-1} = M_{n-1} || H(B_n) B_{n-2} = M_{n-2} || H(B_{n-1}) ... B_2 = M_2 || H(B_3) B_1 = M_1 || H(B_2) Alice does the following (e.g., on a build system where all payloads are available): * Assembles the blocks B_1, B_2, ..., B_n. * Calculates H(B_1) and signs it, yielding Sig(H(B_1)). Alice sends the following to Bob: M_1, H(B_2), Sig(H(B_1)) Bob receives this payload and does the following: * Reconstructs B_1 as B_1' using the received M_1 and H(B_2) (i.e., B_1' = M_1 || H(B_2)). * Recomputes H(B_1') and verifies the signature against the received Sig(H(B_1)). * If the signature verifies, it establishes the integrity of M_1 and H(B_2) (and transitively, the integrity of the entire chain). Bob now stores the verified H(B_2) until it receives the next message. * When Bob receives M_2 (and H(B_3) if n > 2), it reconstructs B_2' (e.g., B_2' = M_2 || H(B_3), or if n=2, B_2' = M_2 || H(termination_marker)). Bob then computes H(B_2') and compares it against the stored H(B_2) that was verified in the previous step. This process continues until the last block is received and verified. Now, applying this to the BPF signing use-case, we simplify to two messages: M_1 = I_loader (the instructions of the loader program) M_2 = M_metadata (the metadata for the loader program, passed in a map, which includes the programs to be loaded and other context) For this specific BPF case, we will directly sign a composite of the first message and the hash of the second. Let H_meta = H(M_metadata). The block to be signed is effectively: B_signed = I_loader || H_meta The signature generated is Sig(B_signed). The process then follows a similar pattern to the Alice and Bob model, where the kernel (Bob) verifies I_loader and H_meta using the signature. Then, the trusted I_loader is responsible for verifying M_metadata against the trusted H_meta. From an implementation standpoint: bpftool (or some other tool in a trusted build environment) knows about the metadata (M_metadata) and the loader program (I_loader). It first calculates H_meta = H(M_metadata). Then it constructs the object to be signed and computes the signature: Sig(I_loader || H_meta) The loader program and the metadata are a hermetic representation of the source of the eBPF program, its maps and context. The loader program is generated by libbpf as a part of a standard API i.e. bpf_object__gen_loader. While users can use light skeletons as a convenient method to use signing support, they can directly use the loader program generation using libbpf (bpf_object__gen_loader) into their own trusted toolchains. libbpf, which has access to the program's instruction buffer is a key part of the TCB of the build environment An advanced threat model that does not intend to depend on libbpf (or any provenant userspace BPF libraries) due to supply chain risks despite it being developed in the kernel source and by the kernel community will require reimplmenting a lot of the core BPF userspace support (like instruction relocation, map handling). Such an advanced user would also need to integrate the generation of the loader into their toolchain. Given that many use-cases (e.g. Cilium) generate trusted BPF programs, trusted loaders are an inevitability and a requirement for signing support, a entrusting loader programs will be a fundamental requirement for an security policy. The initial instructions of the loader program verify the SHA256 hash of the metadata (M_metadata) that will be passed in a map. These instructions effectively embed the precomputed H_meta as immediate values. ld_imm64 r1, const_ptr_to_map // insn[0].src_reg == BPF_PSEUDO_MAP_IDX r2 = *(u64 *)(r1 + 0); ld_imm64 r3, sha256_of_map_part1 // precomputed by bpf_object__gen_load/libbpf (H_meta_1) if r2 != r3 goto out; r2 = *(u64 *)(r1 + 8); ld_imm64 r3, sha256_of_map_part2 // precomputed by bpf_object__gen_load/libbpf (H_meta_2) if r2 != r3 goto out; r2 = *(u64 *)(r1 + 16); ld_imm64 r3, sha256_of_map_part3 // precomputed by bpf_object__gen_load/libbpf (H_meta_3) if r2 != r3 goto out; r2 = *(u64 *)(r1 + 24); ld_imm64 r3, sha256_of_map_part4 // precomputed by bpf_object__gen_load/libbpf (H_meta_4) if r2 != r3 goto out; ... This implicitly makes the payload equivalent to the signed block (B_signed) I_loader || H_meta bpftool then generates the signature of this I_loader payload (which now contains the expected H_meta) using a key and an identity: This signature is stored in bpf_attr, which is extended as follows for the BPF_PROG_LOAD command: __aligned_u64 signature; __u32 signature_size; __u32 keyring_id; The reasons for a simpler UAPI is that it's more future proof (e.g.) with more stable instruction buffers, loader programs being directly into the compilers. A simple API also allows simple programs e.g. for networking that don't need loader programs to directly use signing. OBJ_GET_INFO_BY_FD is used to get information about BPF objects (maps, programs, links) and returning the hash of the map is a natural extension of the UAPI as it can be helpful for debugging, fingerprinting etc. Currently, it's only implemented for BPF_MAP_TYPE_ARRAY. It can be trivially extended for BPF programs to return the complete SHA256 along with the tag. The SHA is stored in struct bpf_map for exclusive and frozen maps struct bpf_map { + u64 sha[4]; const struct bpf_map_ops *ops; struct bpf_map *inner_map_meta; }; Exclusivity ensures that the map can only be used by a future BPF program whose SHA256 hash matches sha256_of_future_prog. First, bpf_prog_calc_tag() is updated to compute the SHA256 instead of SHA1, and this hash is stored in struct bpf_prog_aux: @@ -1588,6 +1588,7 @@ struct bpf_prog_aux { int cgroup_atype; /* enum cgroup_bpf_attach_type */ struct bpf_map *cgroup_storage[MAX_BPF_CGROUP_STORAGE_TYPE]; char name[BPF_OBJ_NAME_LEN]; + u64 sha[4]; u64 (*bpf_exception_cb)(u64 cookie, u64 sp, u64 bp, u64, u64); // ... }; An exclusive is created by passing an excl_prog_hash (and excl_prog_hash_size) in the BPF_MAP_CREATE command. When a BPF program is subsequently loaded and it attempts to use this map, the kernel will compare the program's own SHA256 hash against the one registered with the map, if matching, it will be added to prog->used_maps[]. The program load will fail if the hashes do not match or if the map is already in use by another (non-matching) exclusive program. Exclusive maps ensure that no other BPF programs and compromise the intergity of the map post the signature verification. NOTE: Exclusive maps cannot be added as inner maps. err = map_fd = skel_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map", opts->excl_prog_hash, opts->excl_prog_hash_sz, 4, opts->data_sz, 1); err = skel_map_update_elem(map_fd, &key, opts->data, 0); err = skel_map_freeze(map_fd); // Kernel computes the hash of the map. err = skel_obj_get_info_by_fd(map_fd); memset(&attr, 0, prog_load_attr_sz); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = (long) opts->insns; attr.insn_cnt = opts->insns_sz / sizeof(struct bpf_insn); attr.signature = (long) opts->signature; attr.signature_size = opts->signature_sz; attr.keyring_id = opts->keyring_id; attr.license = (long) "Dual BSD/GPL"; The kernel will: * Compute the hash of the provided I_loader bytecode. * Verify the signature against this computed hash. * Check if the metadata map (now exclusive) is intended for this program's hash. The signature check happens in BPF_PROG_LOAD before the security_bpf_prog LSM hook. This ensures that the loaded loader program (I_loader), including the embedded expected hash of the metadata (H_meta), is trusted. Since the loader program is now trusted, it can be entrusted to verify the actual metadata (M_metadata) read from the (now exclusive and frozen) map against the embedded (and trusted) H_meta. There is no Time-of-Check-Time-of-Use (TOCTOU) vulnerability here because: * The signature covers the I_loader and its embedded H_meta. * The metadata map M_metadata is frozen before the loader program is loaded and associated with it. * The map is made exclusive to the specific (signed and verified) loader program. [1] https://lore.kernel.org/bpf/CACYkzJ6VQUExfyt0=-FmXz46GHJh3d=FXh5j4KfexcEFbHV-vg@mail.gmail.com/ ==================== Link: https://patch.msgid.link/[email protected] Signed-off-by: Alexei Starovoitov <[email protected]>
2 parents 5a427fd + b720903 commit 58a5820

File tree

28 files changed

+660
-33
lines changed

28 files changed

+660
-33
lines changed

crypto/asymmetric_keys/pkcs7_verify.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ int pkcs7_verify(struct pkcs7_message *pkcs7,
429429
/* Authattr presence checked in parser */
430430
break;
431431
case VERIFYING_UNSPECIFIED_SIGNATURE:
432+
case VERIFYING_BPF_SIGNATURE:
432433
if (pkcs7->data_type != OID_data) {
433434
pr_warn("Invalid unspecified sig (not pkcs7-data)\n");
434435
return -EKEYREJECTED;

include/linux/verification.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ enum key_being_used_for {
3636
VERIFYING_KEY_SIGNATURE,
3737
VERIFYING_KEY_SELF_SIGNATURE,
3838
VERIFYING_UNSPECIFIED_SIGNATURE,
39+
VERIFYING_BPF_SIGNATURE,
3940
NR__KEY_BEING_USED_FOR
4041
};
4142
#ifdef CONFIG_SYSTEM_DATA_VERIFICATION

include/uapi/linux/bpf.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,16 @@ union bpf_attr {
16111611
* continuous.
16121612
*/
16131613
__u32 fd_array_cnt;
1614+
/* Pointer to a buffer containing the signature of the BPF
1615+
* program.
1616+
*/
1617+
__aligned_u64 signature;
1618+
/* Size of the signature buffer in bytes. */
1619+
__u32 signature_size;
1620+
/* ID of the kernel keyring to be used for signature
1621+
* verification.
1622+
*/
1623+
__s32 keyring_id;
16141624
};
16151625

16161626
struct { /* anonymous struct used by BPF_OBJ_* commands */

kernel/bpf/helpers.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3898,7 +3898,7 @@ __bpf_kfunc int bpf_verify_pkcs7_signature(struct bpf_dynptr *data_p,
38983898

38993899
return verify_pkcs7_signature(data, data_len, sig, sig_len,
39003900
trusted_keyring->key,
3901-
VERIFYING_UNSPECIFIED_SIGNATURE, NULL,
3901+
VERIFYING_BPF_SIGNATURE, NULL,
39023902
NULL);
39033903
#else
39043904
return -EOPNOTSUPP;

kernel/bpf/syscall.c

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include <linux/tracepoint.h>
4040
#include <linux/overflow.h>
4141
#include <linux/cookie.h>
42+
#include <linux/verification.h>
4243

4344
#include <net/netfilter/nf_bpf_link.h>
4445
#include <net/netkit.h>
@@ -2785,8 +2786,44 @@ static bool is_perfmon_prog_type(enum bpf_prog_type prog_type)
27852786
}
27862787
}
27872788

2789+
static int bpf_prog_verify_signature(struct bpf_prog *prog, union bpf_attr *attr,
2790+
bool is_kernel)
2791+
{
2792+
bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
2793+
struct bpf_dynptr_kern sig_ptr, insns_ptr;
2794+
struct bpf_key *key = NULL;
2795+
void *sig;
2796+
int err = 0;
2797+
2798+
if (system_keyring_id_check(attr->keyring_id) == 0)
2799+
key = bpf_lookup_system_key(attr->keyring_id);
2800+
else
2801+
key = bpf_lookup_user_key(attr->keyring_id, 0);
2802+
2803+
if (!key)
2804+
return -EINVAL;
2805+
2806+
sig = kvmemdup_bpfptr(usig, attr->signature_size);
2807+
if (IS_ERR(sig)) {
2808+
bpf_key_put(key);
2809+
return -ENOMEM;
2810+
}
2811+
2812+
bpf_dynptr_init(&sig_ptr, sig, BPF_DYNPTR_TYPE_LOCAL, 0,
2813+
attr->signature_size);
2814+
bpf_dynptr_init(&insns_ptr, prog->insnsi, BPF_DYNPTR_TYPE_LOCAL, 0,
2815+
prog->len * sizeof(struct bpf_insn));
2816+
2817+
err = bpf_verify_pkcs7_signature((struct bpf_dynptr *)&insns_ptr,
2818+
(struct bpf_dynptr *)&sig_ptr, key);
2819+
2820+
bpf_key_put(key);
2821+
kvfree(sig);
2822+
return err;
2823+
}
2824+
27882825
/* last field in 'union bpf_attr' used by this command */
2789-
#define BPF_PROG_LOAD_LAST_FIELD fd_array_cnt
2826+
#define BPF_PROG_LOAD_LAST_FIELD keyring_id
27902827

27912828
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
27922829
{
@@ -2950,6 +2987,12 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
29502987
/* eBPF programs must be GPL compatible to use GPL-ed functions */
29512988
prog->gpl_compatible = license_is_gpl_compatible(license) ? 1 : 0;
29522989

2990+
if (attr->signature) {
2991+
err = bpf_prog_verify_signature(prog, attr, uattr.is_kernel);
2992+
if (err)
2993+
goto free_prog;
2994+
}
2995+
29532996
prog->orig_prog = NULL;
29542997
prog->jited = 0;
29552998

tools/bpf/bpftool/Documentation/bpftool-gen.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ SYNOPSIS
1616

1717
**bpftool** [*OPTIONS*] **gen** *COMMAND*
1818

19-
*OPTIONS* := { |COMMON_OPTIONS| | { **-L** | **--use-loader** } }
19+
*OPTIONS* := { |COMMON_OPTIONS| | { **-L** | **--use-loader** } | [ { **-S** | **--sign** } {**-k** <private_key.pem>} **-i** <certificate.x509> ] }
2020

2121
*COMMAND* := { **object** | **skeleton** | **help** }
2222

@@ -186,6 +186,17 @@ OPTIONS
186186
skeleton). A light skeleton contains a loader eBPF program. It does not use
187187
the majority of the libbpf infrastructure, and does not need libelf.
188188

189+
-S, --sign
190+
For skeletons, generate a signed skeleton. This option must be used with
191+
**-k** and **-i**. Using this flag implicitly enables **--use-loader**.
192+
193+
-k <private_key.pem>
194+
Path to the private key file in PEM format, required for signing.
195+
196+
-i <certificate.x509>
197+
Path to the X.509 certificate file in PEM or DER format, required for
198+
signing.
199+
189200
EXAMPLES
190201
========
191202
**$ cat example1.bpf.c**

tools/bpf/bpftool/Documentation/bpftool-prog.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ SYNOPSIS
1818

1919
*OPTIONS* := { |COMMON_OPTIONS| |
2020
{ **-f** | **--bpffs** } | { **-m** | **--mapcompat** } | { **-n** | **--nomount** } |
21-
{ **-L** | **--use-loader** } }
21+
{ **-L** | **--use-loader** } | [ { **-S** | **--sign** } **-k** <private_key.pem> **-i** <certificate.x509> ] }
2222

2323
*COMMANDS* :=
2424
{ **show** | **list** | **dump xlated** | **dump jited** | **pin** | **load** |
@@ -248,6 +248,18 @@ OPTIONS
248248
creating the maps, and loading the programs (see **bpftool prog tracelog**
249249
as a way to dump those messages).
250250

251+
-S, --sign
252+
Enable signing of the BPF program before loading. This option must be
253+
used with **-k** and **-i**. Using this flag implicitly enables
254+
**--use-loader**.
255+
256+
-k <private_key.pem>
257+
Path to the private key file in PEM format, required when signing.
258+
259+
-i <certificate.x509>
260+
Path to the X.509 certificate file in PEM or DER format, required when
261+
signing.
262+
251263
EXAMPLES
252264
========
253265
**# bpftool prog show**

tools/bpf/bpftool/Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ include $(FEATURES_DUMP)
130130
endif
131131
endif
132132

133-
LIBS = $(LIBBPF) -lelf -lz
134-
LIBS_BOOTSTRAP = $(LIBBPF_BOOTSTRAP) -lelf -lz
133+
LIBS = $(LIBBPF) -lelf -lz -lcrypto
134+
LIBS_BOOTSTRAP = $(LIBBPF_BOOTSTRAP) -lelf -lz -lcrypto
135135

136136
ifeq ($(feature-libelf-zstd),1)
137137
LIBS += -lzstd
@@ -194,7 +194,7 @@ endif
194194

195195
BPFTOOL_BOOTSTRAP := $(BOOTSTRAP_OUTPUT)bpftool
196196

197-
BOOTSTRAP_OBJS = $(addprefix $(BOOTSTRAP_OUTPUT),main.o common.o json_writer.o gen.o btf.o)
197+
BOOTSTRAP_OBJS = $(addprefix $(BOOTSTRAP_OUTPUT),main.o common.o json_writer.o gen.o btf.o sign.o)
198198
$(BOOTSTRAP_OBJS): $(LIBBPF_BOOTSTRAP)
199199

200200
OBJS = $(patsubst %.c,$(OUTPUT)%.o,$(SRCS)) $(OUTPUT)disasm.o

tools/bpf/bpftool/cgroup.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
// Copyright (C) 2017 Facebook
33
// Author: Roman Gushchin <[email protected]>
44

5+
#undef GCC_VERSION
6+
#ifndef _GNU_SOURCE
7+
#define _GNU_SOURCE
8+
#endif
59
#define _XOPEN_SOURCE 500
610
#include <errno.h>
711
#include <fcntl.h>

tools/bpf/bpftool/gen.c

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -688,10 +688,17 @@ static void codegen_destroy(struct bpf_object *obj, const char *obj_name)
688688
static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *header_guard)
689689
{
690690
DECLARE_LIBBPF_OPTS(gen_loader_opts, opts);
691+
struct bpf_load_and_run_opts sopts = {};
692+
char sig_buf[MAX_SIG_SIZE];
693+
__u8 prog_sha[SHA256_DIGEST_LENGTH];
691694
struct bpf_map *map;
695+
692696
char ident[256];
693697
int err = 0;
694698

699+
if (sign_progs)
700+
opts.gen_hash = true;
701+
695702
err = bpf_object__gen_loader(obj, &opts);
696703
if (err)
697704
return err;
@@ -701,6 +708,7 @@ static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *h
701708
p_err("failed to load object file");
702709
goto out;
703710
}
711+
704712
/* If there was no error during load then gen_loader_opts
705713
* are populated with the loader program.
706714
*/
@@ -780,8 +788,52 @@ static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *h
780788
print_hex(opts.insns, opts.insns_sz);
781789
codegen("\
782790
\n\
783-
\"; \n\
784-
\n\
791+
\";\n");
792+
793+
if (sign_progs) {
794+
sopts.insns = opts.insns;
795+
sopts.insns_sz = opts.insns_sz;
796+
sopts.excl_prog_hash = prog_sha;
797+
sopts.excl_prog_hash_sz = sizeof(prog_sha);
798+
sopts.signature = sig_buf;
799+
sopts.signature_sz = MAX_SIG_SIZE;
800+
801+
err = bpftool_prog_sign(&sopts);
802+
if (err < 0) {
803+
p_err("failed to sign program");
804+
goto out;
805+
}
806+
807+
codegen("\
808+
\n\
809+
static const char opts_sig[] __attribute__((__aligned__(8))) = \"\\\n\
810+
");
811+
print_hex((const void *)sig_buf, sopts.signature_sz);
812+
codegen("\
813+
\n\
814+
\";\n");
815+
816+
codegen("\
817+
\n\
818+
static const char opts_excl_hash[] __attribute__((__aligned__(8))) = \"\\\n\
819+
");
820+
print_hex((const void *)prog_sha, sizeof(prog_sha));
821+
codegen("\
822+
\n\
823+
\";\n");
824+
825+
codegen("\
826+
\n\
827+
opts.signature = (void *)opts_sig; \n\
828+
opts.signature_sz = sizeof(opts_sig) - 1; \n\
829+
opts.excl_prog_hash = (void *)opts_excl_hash; \n\
830+
opts.excl_prog_hash_sz = sizeof(opts_excl_hash) - 1; \n\
831+
opts.keyring_id = skel->keyring_id; \n\
832+
");
833+
}
834+
835+
codegen("\
836+
\n\
785837
opts.ctx = (struct bpf_loader_ctx *)skel; \n\
786838
opts.data_sz = sizeof(opts_data) - 1; \n\
787839
opts.data = (void *)opts_data; \n\
@@ -1240,7 +1292,7 @@ static int do_skeleton(int argc, char **argv)
12401292
err = -errno;
12411293
libbpf_strerror(err, err_buf, sizeof(err_buf));
12421294
p_err("failed to open BPF object file: %s", err_buf);
1243-
goto out;
1295+
goto out_obj;
12441296
}
12451297

12461298
bpf_object__for_each_map(map, obj) {
@@ -1355,6 +1407,13 @@ static int do_skeleton(int argc, char **argv)
13551407
printf("\t} links;\n");
13561408
}
13571409

1410+
if (sign_progs) {
1411+
codegen("\
1412+
\n\
1413+
__s32 keyring_id; \n\
1414+
");
1415+
}
1416+
13581417
if (btf) {
13591418
err = codegen_datasecs(obj, obj_name);
13601419
if (err)
@@ -1552,6 +1611,7 @@ static int do_skeleton(int argc, char **argv)
15521611
err = 0;
15531612
out:
15541613
bpf_object__close(obj);
1614+
out_obj:
15551615
if (obj_data)
15561616
munmap(obj_data, mmap_sz);
15571617
close(fd);
@@ -1930,7 +1990,7 @@ static int do_help(int argc, char **argv)
19301990
" %1$s %2$s help\n"
19311991
"\n"
19321992
" " HELP_SPEC_OPTIONS " |\n"
1933-
" {-L|--use-loader} }\n"
1993+
" {-L|--use-loader} | [ {-S|--sign } {-k} <private_key.pem> {-i} <certificate.x509> ]}\n"
19341994
"",
19351995
bin_name, "gen");
19361996

0 commit comments

Comments
 (0)