|
| 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