Skip to content

Commit bff9b8b

Browse files
committed
cover letter
Signed-off-by: KP Singh <[email protected]>
1 parent 2a0e5cd commit bff9b8b

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed

README

Whitespace-only changes.

README.md

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#v1 -> v2
2+
3+
* Addressed feedback on excl maps and their implementation
4+
* libbpf feedback
5+
* fixed s390x and other tests that were failing in the CI
6+
* using the kernel's sha256 API since it now uses acceleration if available
7+
* simple signing test case, this can be extended to inject a false SHA into
8+
the loader
9+
10+
BPF Signing has gone over multiple discussions in various conferences with the
11+
kernel and BPF community and the following patch series is a culmination
12+
of the current of discussion and signed BPF programs. Once signing is
13+
implemented, the next focus would be to implement the right security policies
14+
for all BPF use-cases (dynamically generated bpf programs, simple non CO-RE
15+
programs).
16+
17+
Signing also paves the way for allowing unrivileged users to
18+
load vetted BPF programs and helps in adhering to the principle of least
19+
privlege by avoiding unnecessary elevation of privileges to CAP_BPF and
20+
CAP_SYS_ADMIN (ofcourse, with the appropriate security policy active).
21+
22+
A early version of this design was proposed in [1]:
23+
24+
# General Idea: Trusted Hash Chain
25+
26+
The key idea of the design is to use a signing algorithm that allows
27+
us to integrity-protect a number of future payloads, including their
28+
order, by creating a chain of trust.
29+
30+
Consider that Alice needs to send messages M_1, M_2, ..., M_n to Bob.
31+
We define blocks of data such that:
32+
33+
B_n = M_n || H(termination_marker)
34+
35+
(Each block contains its corresponding message and the hash of the
36+
*next* block in the chain.)
37+
38+
B_{n-1} = M_{n-1} || H(B_n)
39+
B_{n-2} = M_{n-2} || H(B_{n-1})
40+
41+
...
42+
43+
B_2 = M_2 || H(B_3)
44+
B_1 = M_1 || H(B_2)
45+
46+
Alice does the following (e.g., on a build system where all payloads
47+
are available):
48+
49+
* Assembles the blocks B_1, B_2, ..., B_n.
50+
* Calculates H(B_1) and signs it, yielding Sig(H(B_1)).
51+
52+
Alice sends the following to Bob:
53+
54+
M_1, H(B_2), Sig(H(B_1))
55+
56+
Bob receives this payload and does the following:
57+
58+
* Reconstructs B_1 as B_1' using the received M_1 and H(B_2)
59+
(i.e., B_1' = M_1 || H(B_2)).
60+
* Recomputes H(B_1') and verifies the signature against the
61+
received Sig(H(B_1)).
62+
* If the signature verifies, it establishes the integrity of M_1
63+
and H(B_2) (and transitively, the integrity of the entire chain). Bob
64+
now stores the verified H(B_2) until it receives the next message.
65+
* When Bob receives M_2 (and H(B_3) if n > 2), it reconstructs
66+
B_2' (e.g., B_2' = M_2 || H(B_3), or if n=2, B_2' = M_2 ||
67+
H(termination_marker)). Bob then computes H(B_2') and compares it
68+
against the stored H(B_2) that was verified in the previous step.
69+
70+
This process continues until the last block is received and verified.
71+
72+
Now, applying this to the BPF signing use-case, we simplify to two messages:
73+
74+
M_1 = I_loader (the instructions of the loader program)
75+
M_2 = M_metadata (the metadata for the loader program, passed in a
76+
map, which includes the programs to be loaded and other context)
77+
78+
For this specific BPF case, we will directly sign a composite of the
79+
first message and the hash of the second. Let H_meta = H(M_metadata).
80+
The block to be signed is effectively:
81+
82+
B_signed = I_loader || H_meta
83+
84+
The signature generated is Sig(B_signed).
85+
86+
The process then follows a similar pattern to the Alice and Bob model,
87+
where the kernel (Bob) verifies I_loader and H_meta using the
88+
signature. Then, the trusted I_loader is responsible for verifying
89+
M_metadata against the trusted H_meta.
90+
91+
From an implementation standpoint:
92+
93+
# Build
94+
95+
bpftool (or some other tool in a trusted build environment) knows
96+
about the metadata (M_metadata) and the loader program (I_loader). It
97+
first calculates H_meta = H(M_metadata). Then it constructs the object
98+
to be signed and computes the signature:
99+
100+
Sig(I_loader || H_meta)
101+
102+
# Loader
103+
104+
The loader program and the metadata are a hermetic representation of the source
105+
of the eBPF program, its maps and context. The loader program is generated by
106+
libbpf as a part of a standard API i.e. bpf_object__gen_loader.
107+
108+
## Supply chain
109+
110+
While users can use light skeletons as a convenient method to use signing
111+
support, they can directly use the loader program generation using libbpf
112+
(bpf_object__gen_loader) into their own trusted toolchains.
113+
114+
libbpf, which has access to the program's instruction buffer is a key part of
115+
the TCB of the build environment
116+
117+
An advanced threat model that does not intend to depend on libbpf (or any provenant
118+
userspace BPF libraries) due to supply chain risks despite it being developed
119+
in the kernel source and by the kernel community will require reimplmenting a
120+
lot of the core BPF userspace support (like instruction relocation, map handling).
121+
122+
Such an advanced user would also need to integrate the generation of the loader
123+
into their toolchain.
124+
125+
Given that many use-cases (e.g. Cilium) generate trusted BPF programs,
126+
trusted loaders are an inevitability and a requirement for signing support, a
127+
entrusting loader programs will be a fundamental requirement for an security
128+
policy.
129+
130+
The initial instructions of the loader program verify the SHA256 hash
131+
of the metadata (M_metadata) that will be passed in a map. These instructions
132+
effectively embed the precomputed H_meta as immediate values.
133+
134+
ld_imm64 r1, const_ptr_to_map // insn[0].src_reg == BPF_PSEUDO_MAP_IDX
135+
r2 = *(u64 *)(r1 + 0);
136+
ld_imm64 r3, sha256_of_map_part1 // precomputed by bpf_object__gen_load/libbpf (H_meta_1)
137+
if r2 != r3 goto out;
138+
139+
r2 = *(u64 *)(r1 + 8);
140+
ld_imm64 r3, sha256_of_map_part2 // precomputed by bpf_object__gen_load/libbpf (H_meta_2)
141+
if r2 != r3 goto out;
142+
143+
r2 = *(u64 *)(r1 + 16);
144+
ld_imm64 r3, sha256_of_map_part3 // precomputed by bpf_object__gen_load/libbpf (H_meta_3)
145+
if r2 != r3 goto out;
146+
147+
r2 = *(u64 *)(r1 + 24);
148+
ld_imm64 r3, sha256_of_map_part4 // precomputed by bpf_object__gen_load/libbpf (H_meta_4)
149+
if r2 != r3 goto out;
150+
...
151+
152+
This implicitly makes the payload equivalent to the signed block (B_signed)
153+
154+
I_loader || H_meta
155+
156+
bpftool then generates the signature of this I_loader payload (which
157+
now contains the expected H_meta) using a key and an identity:
158+
159+
This signature is stored in bpf_attr, which is extended as follows for
160+
the BPF_PROG_LOAD command:
161+
162+
__aligned_u64 signature;
163+
__u32 signature_size;
164+
__u32 keyring_id;
165+
166+
The reasons for a simpler UAPI is that it's more future proof (e.g.) with more
167+
stable instruction buffers, loader programs being directly into the compilers.
168+
A simple API also allows simple programs e.g. for networking that don't need
169+
loader programs to directly use signing.
170+
171+
# Extending OBJ_GET_INFO_BY_FD for hashes
172+
173+
OBJ_GET_INFO_BY_FD is used to get information about BPF objects (maps, programs, links) and
174+
returning the hash of the map is a natural extension of the UAPI as it can be
175+
helpful for debugging, fingerprinting etc.
176+
177+
Currently, it's only implemented for BPF_MAP_TYPE_ARRAY. It can be trivially
178+
extended for BPF programs to return the complete SHA256 along with the tag.
179+
180+
The SHA is stored in struct bpf_map for exclusive and frozen maps
181+
182+
struct bpf_map {
183+
+ u64 sha[4];
184+
const struct bpf_map_ops *ops;
185+
struct bpf_map *inner_map_meta;
186+
};
187+
188+
## Exclusive BPF maps
189+
190+
Exclusivity ensures that the map can only be used by a future BPF
191+
program whose SHA256 hash matches sha256_of_future_prog.
192+
193+
First, bpf_prog_calc_tag() is updated to compute the SHA256 instead of
194+
SHA1, and this hash is stored in struct bpf_prog_aux:
195+
196+
@@ -1588,6 +1588,7 @@ struct bpf_prog_aux {
197+
int cgroup_atype; /* enum cgroup_bpf_attach_type */
198+
struct bpf_map *cgroup_storage[MAX_BPF_CGROUP_STORAGE_TYPE];
199+
char name[BPF_OBJ_NAME_LEN];
200+
+ u64 sha[4];
201+
u64 (*bpf_exception_cb)(u64 cookie, u64 sp, u64 bp, u64, u64);
202+
// ...
203+
};
204+
205+
An exclusive is created by passing an excl_prog_hash
206+
(and excl_prog_hash_size) in the BPF_MAP_CREATE command.
207+
When a BPF program is subsequently loaded and it attempts to use this map,
208+
the kernel will compare the program's own SHA256 hash against the one
209+
registered with the map, if matching, it will be added to prog->used_maps[].
210+
211+
The program load will fail if the hashes do not match or if the map is
212+
already in use by another (non-matching) exclusive program.
213+
214+
Exclusive maps ensure that no other BPF programs and compromise the intergity of
215+
the map post the signature verification.
216+
217+
NOTE: Exclusive maps cannot be added as inner maps.
218+
219+
# Light Skeleton Sequence (Userspace Example)
220+
221+
err = map_fd = skel_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map",
222+
opts->excl_prog_hash,
223+
opts->excl_prog_hash_sz, 4,
224+
opts->data_sz, 1);
225+
err = skel_map_update_elem(map_fd, &key, opts->data, 0);
226+
227+
err = skel_map_freeze(map_fd);
228+
229+
// Kernel computes the hash of the map.
230+
err = skel_obj_get_info_by_fd(map_fd);
231+
232+
memset(&attr, 0, prog_load_attr_sz);
233+
attr.prog_type = BPF_PROG_TYPE_SYSCALL;
234+
attr.insns = (long) opts->insns;
235+
attr.insn_cnt = opts->insns_sz / sizeof(struct bpf_insn);
236+
attr.signature = (long) opts->signature;
237+
attr.signature_size = opts->signature_sz;
238+
attr.keyring_id = opts->keyring_id;
239+
attr.license = (long) "Dual BSD/GPL";
240+
241+
The kernel will:
242+
243+
* Compute the hash of the provided I_loader bytecode.
244+
* Verify the signature against this computed hash.
245+
* Check if the metadata map (now exclusive) is intended for this
246+
program's hash.
247+
248+
The signature check happens in BPF_PROG_LOAD before the security_bpf_prog
249+
LSM hook.
250+
251+
This ensures that the loaded loader program (I_loader), including the
252+
embedded expected hash of the metadata (H_meta), is trusted.
253+
Since the loader program is now trusted, it can be entrusted to verify
254+
the actual metadata (M_metadata) read from the (now exclusive and
255+
frozen) map against the embedded (and trusted) H_meta. There is no
256+
Time-of-Check-Time-of-Use (TOCTOU) vulnerability here because:
257+
258+
* The signature covers the I_loader and its embedded H_meta.
259+
* The metadata map M_metadata is frozen before the loader program is loaded
260+
and associated with it.
261+
* The map is made exclusive to the specific (signed and verified)
262+
loader program.
263+
264+
[1] https://lore.kernel.org/bpf/CACYkzJ6VQUExfyt0=-FmXz46GHJh3d=FXh5j4KfexcEFbHV-vg@mail.gmail.com/#t

0 commit comments

Comments
 (0)