| .. SPDX-License-Identifier: GPL-2.0 |
| |
| ============ |
| BPF signing |
| ============ |
| |
| This document describes how BPF programs are cryptographically signed, how the |
| kernel verifies them at load time, and how Linux Security Modules (LSMs) - |
| including the BPF LSM - use the resulting verdict to enforce policy. It is |
| written for developers who want to produce signed BPF objects, understand what |
| the signature actually guarantees, or build a policy on top of it. |
| |
| Motivation |
| ========== |
| |
| A signed BPF program lets the kernel establish that the bytecode being loaded |
| originates from a trusted producer and was not modified in transit. On its own |
| the kernel does not *require* signatures - an unsigned program loads exactly as |
| before - but it records a verdict (see `The verdict`_) that an LSM can gate on. |
| This is the building block for policies such as "only run BPF that was signed by |
| a key in the trusted keyring", as could in the future be enforced by an LSM |
| such as IPE. |
| |
| Signing is orthogonal to the existing permission model: it does not replace the |
| capability checks or the verifier. A signed load still requires the usual |
| privileges (``CAP_BPF`` and any program-type-specific capability, subject to |
| ``kernel.unprivileged_bpf_disabled``), and the loader's instructions are still |
| checked by the verifier like any other program. A valid signature establishes |
| *origin and integrity*, not safety - it lets a policy trust where the bytecode |
| came from, it does not let a load skip any check it would otherwise face. |
| |
| The hard part is *what* gets signed. A naive scheme would sign a program's |
| instruction buffer at build time and verify that signature at |
| ``BPF_PROG_LOAD``. That does not survive contact with real BPF objects, because |
| the bytes the kernel finally loads are not the bytes the developer built and |
| signed. Between the two, libbpf and the kernel rewrite the program: |
| |
| - **map file descriptors** are patched into ``ld_imm64`` instructions |
| (``BPF_PSEUDO_MAP_FD``), and a map's fd is assigned at load time, so it |
| differs on every run; |
| - **CO-RE relocations** rewrite field offsets, sizes and existence flags against |
| the *running* kernel's BTF, so the result differs from one kernel to the next; |
| - **kfunc and ksym references** are resolved to ids/addresses in the running |
| kernel; |
| - **global data** (``.rodata``/``.data``/``.bss``) is created and seeded as maps |
| at load. |
| |
| So a signature over the original instructions cannot match the relocated |
| instructions the verifier ends up checking, and the relocated form cannot be |
| produced ahead of time because it depends on the target kernel. There is no |
| fixed byte string that is both signable at build time and what the kernel |
| actually loads - which is why a program cannot simply be signed and loaded |
| directly. |
| |
| The trusted loader |
| ================== |
| |
| The solution is to move that setup work *into* a small BPF program - the |
| **loader** - and sign the loader instead of the individual programs. libbpf's |
| ``gen_loader`` machinery (``bpftool gen skeleton -L``, the "light skeleton") |
| emits a ``BPF_PROG_TYPE_SYSCALL`` program whose body performs the bpf() syscalls |
| that create maps, apply relocations, and load the real programs. The payload it |
| installs - the serialized programs, map descriptions, relocation data and |
| initial values - lives in a separate array map, the **metadata map** |
| (``__loader.map``). |
| |
| So the unit of trust is the loader, and the signing contract is:: |
| |
| Sig(I_loader || D_meta) |
| |
| where ``I_loader`` is the loader's instruction stream and ``D_meta`` is the |
| content of the metadata map. Verifying the loader's signature establishes that |
| both the loader *and* the payload it is about to install are authentic. The |
| loader is reproducible: ``gen_loader`` builds it from primitives so the same |
| object yields the same bytes on any build host. |
| |
| Why the loader is signable when the program is not |
| -------------------------------------------------- |
| |
| The loader sidesteps every rewrite listed above, because the bytes that are |
| signed are *relocation-invariant*: |
| |
| - The loader's own instructions are a fixed sequence of bpf() syscalls emitted |
| by ``gen_loader``; they carry no CO-RE relocations and resolve no ksyms, so |
| they are identical on every kernel. The metadata map is referenced by *index* |
| into ``fd_array`` (``BPF_PSEUDO_MAP_IDX_VALUE``), not by a baked-in file |
| descriptor, so even that reference does not change between build and load. |
| The loader instruction bytes the kernel verifies are exactly the bytes that |
| were signed. |
| - The metadata map is opaque, frozen data - the serialized target programs, |
| their relocation records, map descriptions and initial values. Its bytes are |
| identical at build time and at load time, so they are simply appended to the |
| instructions and covered by the same signature (there is no separate metadata |
| hash to compute or compare). |
| |
| All the host-specific rewriting - creating maps, patching their fds into the |
| target programs, applying CO-RE, resolving ksyms, seeding global data - still |
| happens, but it happens *inside the loader at runtime*, on the verified |
| metadata, **after** the kernel has verified the ``insns || metadata`` signature. |
| The kernel never has to verify the relocated target programs: it verifies the |
| loader and its inputs once, and trust transfers to whatever that now-trusted, |
| deterministic loader installs. The relocation step is moved from "before the |
| signature can be checked" to "after a trusted program runs" - which is exactly |
| what makes it signable. |
| |
| Because the metadata map is the loader's only untrusted input, two existing map |
| properties are reused to keep it trustworthy across the load: |
| |
| Exclusive maps |
| A map created with ``excl_prog_hash`` (see ``BPF_MAP_CREATE``) may only be |
| accessed by a program whose digest matches that hash. The verifier enforces |
| ``map->excl_prog_sha == prog->digest`` for every map a program uses, so the |
| metadata map is bound to exactly the signed loader and cannot be shared with |
| or mutated by another program. |
| |
| Frozen maps |
| The metadata map is frozen (``BPF_MAP_FREEZE``) before the loader is loaded. |
| Freezing blocks further userspace writes, so the bytes folded into the |
| signature cannot change before the loader runs. (Freezing does not make the |
| map read-only to the loader program itself, which still writes created file |
| descriptors back into the blob's scratch area.) |
| |
| Load-time verification |
| ======================= |
| |
| Rather than have the loader check its own metadata from within BPF, the kernel |
| verifies it directly at ``BPF_PROG_LOAD``, with no new UAPI. The mechanism |
| reuses the existing ``fd_array``: |
| |
| #. Userspace creates the metadata map with ``excl_prog_hash`` set to the |
| loader's digest, populates it, and freezes it. |
| #. The loader is loaded with ``signature``/``signature_size``/``keyring_id`` |
| set, the metadata map referenced through ``fd_array``, and ``fd_array_cnt`` |
| set so the kernel knows the array's length. |
| #. Signature verification runs inside the verifier (``bpf_check()``), once it |
| has resolved the ``fd_array`` entries into the program's ``used_maps``. The |
| maps folded into the signature are therefore the very objects the program |
| binds - a single resolution of ``fd_array``, not a separate read, so the |
| verified bytes cannot be swapped for a different map after the check (no |
| time-of-check/time-of-use window). Each folded map must be exclusive (carry |
| ``excl_prog_sha``) and a plain array map (``BPF_MAP_TYPE_ARRAY``); only an |
| array map exposes its value buffer through ``map_direct_value_addr()`` as a |
| kernel address spanning ``value_size`` bytes. A map that is not exclusive, not |
| frozen, or not a plain array is rejected, with a verifier log message naming |
| the offending map. The kernel appends each map's frozen |
| contents to the instruction buffer and verifies the PKCS#7 signature over the |
| concatenation ``insns || metadata_0 || metadata_1 || ...`` in ``used_maps`` |
| order, before it rewrites the (signed) instructions. |
| |
| A signed program therefore takes one of exactly two shapes, both fully |
| supported: |
| |
| - **No bound maps** (``fd_array_cnt == 0``): there is nothing to append, so the |
| kernel verifies the signature over the instructions alone. A valid signature |
| yields ``BPF_SIG_VERIFIED`` and the program loads. This is the ordinary case |
| for a directly-loaded signed program with no separate payload; it is *not* |
| rejected for "missing" metadata, because it has none to cover. |
| - **Exclusive bound maps** (``fd_array_cnt > 0``): every entry is exclusive and |
| folded, so the signature covers ``insns || metadata``. |
| |
| There is no third shape: a non-exclusive map in a signed program's ``fd_array`` |
| is rejected rather than silently left out of the signature, so a signed loader |
| never binds a map its signature does not cover. |
| |
| The digest binding (``excl_prog_sha == prog->digest``) is enforced by the |
| verifier as usual; because that check runs while ``fd_array`` is resolved - |
| before the verifier would otherwise compute the tag - ``prog->digest`` is |
| computed up front in the verifier, over the unmodified (signature-covered) |
| instructions, for any signed load. |
| |
| Coverage is then enforced as the verifier resolves instructions, at the point |
| each object is bound rather than by a count taken afterwards. Once the signature |
| has been verified, binding any further map is refused: a map reached by a |
| directly-referenced fd, or a map swapped into an ``fd_array`` slot the loader |
| reads, is not among those already folded, so it is rejected the moment the |
| verifier tries to bind it. A BTF is refused outright for a signed program - a |
| ksym or a BTF fd in ``fd_array``, whether resolved up front or lazily for a |
| module kfunc, is rejected when it would be bound. Together with the fold rule |
| above this keeps the verdict binary: a signed program cannot use a map its |
| signature does not cover, and a different but equally digest-bound map cannot be |
| substituted at an ``fd_array`` slot. Non-exclusive maps are never folded, so a |
| signed program cannot use one at all. |
| |
| The verdict |
| =========== |
| |
| A program is either unsigned or fully verified - there is no intermediate |
| state. The outcome is recorded in ``prog->aux->sig.verdict``: |
| |
| .. code-block:: c |
| |
| enum bpf_sig_verdict { |
| BPF_SIG_UNSIGNED = 0, |
| BPF_SIG_VERIFIED, |
| }; |
| |
| ``BPF_SIG_VERIFIED`` means the signature is valid and covers the instructions |
| *and* the frozen contents of every exclusive map the program uses: |
| |
| - For an ordinary, directly-loaded signed program the instructions are the whole |
| artifact and it uses no exclusive maps, so a valid instruction signature is |
| the complete verification. |
| - For a signed loader the metadata map is exclusive, so its contents are folded |
| in and the signature covers ``insns || metadata``. |
| |
| There is deliberately no "instructions verified but metadata not" verdict: a |
| signed loader that fails to cover its metadata is *rejected* (see above), not |
| recorded with a weaker verdict. ``BPF_SIG_VERIFIED`` therefore always means the |
| program and everything the signature is responsible for are authentic, which is |
| what a policy can rely on. |
| |
| Alongside the verdict the kernel records which keyring validated the signature; |
| see `Keyrings`_. |
| |
| Enforcement via LSMs |
| ==================== |
| |
| Signing only *records* a verdict; an LSM turns it into policy. The verdict and |
| keyring fields live in ``struct bpf_prog_aux``, so a BPF LSM program can read |
| them directly (see Documentation/bpf/prog_lsm.rst for writing and attaching BPF |
| LSM programs); the same fields are equally available to in-tree LSMs. Two hooks |
| are useful at different points of the load: the dedicated |
| ``security_bpf_prog_load()`` gates admission before the main verification work, |
| and the existing ``security_bpf_prog()`` observes a program that has fully |
| loaded. |
| |
| Admission: ``security_bpf_prog_load()`` |
| --------------------------------------- |
| |
| This hook gates admission **for every load**, from a single call site inside the |
| verifier (``bpf_check()``), before the main verification work. It runs after the |
| optional signature verification, so the verdict and keyring fields are final - the |
| hook can see whether, and how strongly, the program was signed, which keyring |
| validated it, the load ``attr``, the BPF token and whether the load came from the |
| kernel. For a signed load the verdict is ``BPF_SIG_VERIFIED`` here (the signature |
| has just been checked); for an unsigned load it is ``BPF_SIG_UNSIGNED``. |
| |
| This is the place for *coarse admission* that must also see unsigned and |
| not-yet-verified loads: require a signature at all, restrict the acceptable |
| keyring, restrict which token/credentials may load BPF, apply per-program-type |
| rules, or audit every load attempt that makes it past signature verification - |
| attempts failing the signature or the metadata binding abort before this hook |
| fires. It is the primary deny point. |
| |
| One subtlety: this hook runs *before* the verifier finishes its work, so |
| ``BPF_SIG_VERIFIED`` *here* means only "validly signed" - not "loaded". Allowing |
| a load at this point lets it *proceed*; it does not guarantee the program will |
| load. A validly signed program can still be rejected afterwards on two |
| independent grounds: the verifier may reject it like any other program (unsafe |
| memory access, bad control flow, resource limits, ...), and the kernel separately |
| refuses - as the verifier resolves instructions and binds each object - any map |
| the signature does not cover or any BTF at all, regardless of what this hook |
| returned. Only after the program has fully loaded, at the next hook |
| (``security_bpf_prog()``), does ``BPF_SIG_VERIFIED`` carry its full meaning: |
| validly signed *and* fully verified. |
| |
| A more realistic admission policy than "is it signed at all": accept programs |
| signed by a system keyring, accept a user-keyring signature only if the |
| key/keyring it was verified against is on an explicit allowlist, and emit a |
| tamper-evident record of every decision so that even denied attempts are |
| auditable. (Illustrative - error checking elided.) |
| |
| .. code-block:: c |
| |
| /* Serials of user keys/keyrings we additionally trust. */ |
| struct { |
| __uint(type, BPF_MAP_TYPE_HASH); |
| __type(key, __s32); /* keyring_serial */ |
| __type(value, __u8); |
| __uint(max_entries, 64); |
| } trusted_user_keys SEC(".maps"); |
| |
| /* Audit stream consumed by a userspace logger. */ |
| struct { |
| __uint(type, BPF_MAP_TYPE_RINGBUF); |
| __uint(max_entries, 1 << 16); |
| } audit SEC(".maps"); |
| |
| struct decision { __u32 prog_type, verdict, ktype; __s32 serial, ret; }; |
| |
| SEC("lsm/bpf_prog_load") |
| int BPF_PROG(admit, struct bpf_prog *prog, union bpf_attr *attr, |
| struct bpf_token *token, bool kernel) |
| { |
| __u32 verdict = prog->aux->sig.verdict; |
| __u32 ktype = prog->aux->sig.keyring_type; |
| __s32 serial = prog->aux->sig.keyring_serial; |
| struct decision *d; |
| int ret = 0; |
| |
| if (kernel) |
| return 0; /* trust in-kernel loads */ |
| |
| if (verdict != BPF_SIG_VERIFIED) |
| ret = -EPERM; /* must be validly signed */ |
| else if (ktype == BPF_SIG_KEYRING_USER && |
| !bpf_map_lookup_elem(&trusted_user_keys, &serial)) |
| ret = -EPERM; /* key/keyring not allowlisted */ |
| |
| d = bpf_ringbuf_reserve(&audit, sizeof(*d), 0); |
| if (d) { |
| d->prog_type = attr->prog_type; |
| d->verdict = verdict; |
| d->ktype = ktype; |
| d->serial = serial; |
| d->ret = ret; |
| bpf_ringbuf_submit(d, 0); /* record allow *and* deny */ |
| } |
| return ret; |
| } |
| |
| Observing a verified load: ``security_bpf_prog()`` |
| -------------------------------------------------- |
| |
| There is deliberately no separate "metadata attested" hook. The coverage check |
| above is enforced by the kernel unconditionally, so a signed loader that fails |
| to cover its metadata never loads and an LSM never has to re-establish that |
| fact. To *act on* a program that has successfully and fully loaded, use the |
| existing ``security_bpf_prog()`` hook (``lsm/bpf_prog``), which fires from |
| ``bpf_prog_new_fd()`` - after the verifier, after the coverage check, and after |
| ``bpf_prog_alloc_id()``. Relative to the admission hook this point is strictly |
| later and stronger: |
| |
| - the program has an id (``prog->aux->id``), so it can be recorded or correlated |
| with later events; |
| - ``verdict == BPF_SIG_VERIFIED`` *here* means **fully** verified - a program |
| that used a map the signature does not cover was already rejected, so it cannot |
| reach this point; |
| - it observes only programs that actually loaded; a failed load never mints an |
| fd, so it never reaches this hook. |
| |
| It takes only the ``prog`` and a non-zero return still aborts (the fd is not |
| handed out), so it can veto as well as observe. One wrinkle: it also fires on |
| other paths that mint a new program fd - notably ``bpf_prog_get_fd_by_id()`` - |
| not just on a fresh load. Because the program already has its id here, an LSM |
| can tell the two apart with a small hash map: the *first* time an id is seen is |
| the load; a later sighting of the same id is just another fd to a program that |
| already exists. |
| |
| To bound the map and let a reused id read as a fresh load, this can be paired |
| with ``security_bpf_prog_free()`` (``lsm/bpf_prog_free``), which deletes the |
| entry on teardown - keyed by the same ``prog`` pointer, since |
| ``bpf_prog_free_id()`` has already cleared ``prog->aux->id`` to ``0`` by the time |
| that hook runs. (Illustrative - privileged LSM, error checking elided.) |
| |
| .. code-block:: c |
| |
| struct rec { __u32 id, ktype; __s32 serial; }; |
| |
| struct { |
| __uint(type, BPF_MAP_TYPE_HASH); |
| __type(key, __u64); /* struct bpf_prog * -- stable id */ |
| __type(value, struct rec); |
| __uint(max_entries, 4096); |
| } live SEC(".maps"); |
| |
| SEC("lsm/bpf_prog") /* fires after load and on every later fd */ |
| int BPF_PROG(observe, struct bpf_prog *prog) |
| { |
| __u64 key = (__u64)(unsigned long)prog; |
| struct rec r; |
| |
| if (prog->aux->sig.verdict != BPF_SIG_VERIFIED) |
| return 0; |
| if (bpf_map_lookup_elem(&live, &key)) |
| return 0; /* seen before: a later fd, not a load */ |
| |
| /* First sighting == this program just loaded; id is valid here. */ |
| r.id = prog->aux->id; |
| r.ktype = prog->aux->sig.keyring_type; |
| r.serial = prog->aux->sig.keyring_serial; |
| bpf_map_update_elem(&live, &key, &r, BPF_NOEXIST); |
| /* ... newly-loaded verified-program action, e.g. record r.id ... */ |
| return 0; |
| } |
| |
| Putting them together: to *require* verified BPF, deny at the admission hook |
| unless the verdict is ``BPF_SIG_VERIFIED`` (and, if desired, restrict the |
| keyring). The kernel then guarantees that any program which actually loads with |
| that verdict covered all of its exclusive maps, rejecting any that did not - so |
| a deny-by-default admission policy needs no second enforcement point. Use |
| ``security_bpf_prog()`` to record or finally gate the verified programs once |
| they carry an id. The ``verdict``, ``keyring_type`` and ``keyring_serial`` fields |
| let a policy distinguish, for example, "verified and signed by a builtin key" |
| from "verified by a user key". A policy LSM such as IPE could consume the same |
| hooks to enforce system policy without writing any BPF, though none implements |
| this today. |
| |
| Keyrings |
| ======== |
| |
| ``keyring_id`` selects the trusted keyring the PKCS#7 signature is verified |
| against. The well-known ids ``0`` (builtin), ``VERIFY_USE_SECONDARY_KEYRING`` |
| and ``VERIFY_USE_PLATFORM_KEYRING`` select the corresponding system keyrings; |
| any other value is treated as the serial of a user/session key or keyring. |
| The keyring is looked up first, before the signature bytes are examined, so a |
| signature naming a non-existent keyring is rejected up front, and a failed |
| verification aborts the load - so a program that loads successfully with a |
| signature always has consistent keyring fields recorded. |
| |
| Two fields are recorded in ``prog->aux->sig`` for an LSM to inspect: |
| |
| ``keyring_type`` (``enum bpf_sig_keyring``) |
| Classified purely from ``keyring_id`` whenever the program is signed: |
| ``BPF_SIG_KEYRING_BUILTIN``, ``_SECONDARY``, ``_PLATFORM`` for the system |
| keyrings, or ``_USER`` for a user/session keyring. It is |
| ``BPF_SIG_KEYRING_NONE`` for an unsigned program. |
| |
| ``keyring_serial`` (``s32``) |
| Set **only** on a successful verification, to the serial of the |
| **user/session key or keyring** that ``keyring_id`` resolved to - the |
| object the signature was verified against, not the individual asymmetric |
| key inside it that matched the signer. Passing |
| ``KEY_SPEC_SESSION_KEYRING``, for example, records the session keyring's |
| serial. The system keyrings are trusted as a whole and expose no serial |
| here, so the serial is ``0`` for builtin, secondary and platform |
| signatures, and ``0`` for unsigned programs. In other words, a non-zero |
| ``keyring_serial`` is exactly "verified against the user key/keyring with |
| this serial". |
| |
| .. list-table:: |
| :header-rows: 1 |
| |
| * - ``keyring_id`` |
| - ``keyring_type`` |
| - ``keyring_serial`` |
| * - (no signature) |
| - ``BPF_SIG_KEYRING_NONE`` |
| - ``0`` |
| * - ``0`` |
| - ``BPF_SIG_KEYRING_BUILTIN`` |
| - ``0`` |
| * - ``VERIFY_USE_SECONDARY_KEYRING`` |
| - ``BPF_SIG_KEYRING_SECONDARY`` |
| - ``0`` |
| * - ``VERIFY_USE_PLATFORM_KEYRING`` |
| - ``BPF_SIG_KEYRING_PLATFORM`` |
| - ``0`` |
| * - other (a user/session key serial) |
| - ``BPF_SIG_KEYRING_USER`` |
| - serial of the resolved key/keyring |
| |
| Producing a signed object |
| ========================== |
| |
| ``bpftool`` generates and signs a light skeleton in one step:: |
| |
| bpftool gen skeleton -L -S -k <private_key.pem> -i <certificate.x509> \ |
| obj.bpf.o > obj.lskel.h |
| |
| ``-L`` selects the light-skeleton (``gen_loader``) backend and ``-S`` enables |
| signing; ``-k`` and ``-i`` supply the signing key and its X.509 certificate. |
| ``bpftool`` signs ``insns || metadata`` - the exact bytes the kernel |
| reconstructs - and also computes ``excl_prog_hash`` as the digest of the loader |
| instructions so the metadata map can be bound to the loader. The signature and |
| hash are embedded in the generated header; the certificate is used only for |
| signing and is not included. Loading the skeleton performs the |
| create/populate/freeze/load sequence described above. |
| |
| At runtime the trusted public key must be present in the chosen keyring (for |
| example added to the session keyring, or built into the kernel's builtin trusted |
| keyring) for verification to succeed. |
| |
| UAPI reference |
| ============== |
| |
| ``BPF_PROG_LOAD`` (``union bpf_attr``): |
| |
| ``signature``, ``signature_size`` |
| Pointer to and length of the PKCS#7 signature blob. |
| |
| ``keyring_id`` |
| Trusted keyring selector (see `Keyrings`_). |
| |
| ``fd_array``, ``fd_array_cnt`` |
| Array of map (and module BTF) file descriptors bound to the program. |
| ``fd_array_cnt`` must be set for the kernel to scan the array. When a |
| signature is present, a BTF entry is rejected outright, and every map must |
| be exclusive; its frozen contents are folded into the verified buffer, and |
| a non-exclusive entry is rejected. |
| |
| ``BPF_MAP_CREATE`` (``union bpf_attr``): |
| |
| ``excl_prog_hash``, ``excl_prog_hash_size`` |
| SHA-256 digest of the program permitted to access this (exclusive) map. This |
| binds the metadata map to the loader; it is not a hash of the map *content*. |
| The map content is not hashed separately at all - it is covered, as bytes, |
| by the program signature. |
| |
| Notes and limitations |
| ====================== |
| |
| - The instructions plus folded metadata are verified as one ``bpf_dynptr``, |
| which bounds the combined size (currently ~16 MiB); very large objects can |
| exceed it. |
| - The metadata container is a single-element array map, accessed through |
| ``map_direct_value_addr``. |