From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from www62.your-server.de (www62.your-server.de [213.133.104.62]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 07BB72D7DDD; Wed, 24 Jun 2026 14:27:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=213.133.104.62 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1782311260; cv=none; b=IOOnFXA9V9xiYMXbnXnTeuPQziyjFtFZN+Ea4gsKeVAVvc4/ldEyYmVhUiDWz/QkvH04PZ9S10giP1rueI8Qri/mmNTaz3wQvl5NABj8XVEXUCs7vYUy2BWyQkIgV4mooSt4/pAwy42+gv7sMvJlpGqEztc2xGyoCVYWfrGkZmA= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1782311260; c=relaxed/simple; bh=9jrBKVwVVGyxlIH6jRm28eFzHpVMg8Kdm4xnAtU/F4M=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Bf+5oQPE+IM13DTH0N+szZoso63oaPmb7qQ3xB890UnY4UHKLjXZxPl7RZKj1+ve3f6mj7p1W/NTR1GdEBWVwcZ+Mt67x+hOss2bM6s4ZUZ/Wx10CjvcHgUtb6LJ8LBZsZTMPueoRbdqNtXLPY2YtIXd0gS0szJe71apkmOr0RE= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=iogearbox.net; spf=pass smtp.mailfrom=iogearbox.net; dkim=pass (2048-bit key) header.d=iogearbox.net header.i=@iogearbox.net header.b=NJSDBnhh; arc=none smtp.client-ip=213.133.104.62 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=iogearbox.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=iogearbox.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=iogearbox.net header.i=@iogearbox.net header.b="NJSDBnhh" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=iogearbox.net; s=default2302; h=Content-Transfer-Encoding:MIME-Version: References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Sender:Reply-To: Content-Type:Content-ID:Content-Description:Resent-Date:Resent-From: Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID; bh=jxRl9rDr6c7ppiWcEf6Dl5FojtkGGKp8YeMSZjgj0nI=; b=NJSDBnhh9dg9mQxcz28UeBzjGP p/CciKy0YMdJQZ5cqReP59jGyBdu2CXjWs0a1omMzzMxES9SPWTYZ/58XfjNGfzC+JTY/EHZkcJwz Dlvgric97UKi/pFtl05yc45mn7E2rS9tMbzxNtK4hmDUCU0UX6lx+IN0hMAel2ske8toOEZ1siKNj 96kDYG9qP0qSVd0KqizL1YxL+uShfRNDTAoZ5fekC5LanrfFYtpnTrn7Pf6WDk0H+9YWj2X61j5wv b8KHIYe8rHH4PGt/LrwUuZUyfblzJiVV8wx5nWaMFjHyxAPAu6La48n0xKkBlSmjKkOlBuY7HFbTh vccJwzZQ==; Received: from localhost ([127.0.0.1]) by www62.your-server.de with esmtpsa (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.96.2) (envelope-from ) id 1wcOC2-000GIN-26; Wed, 24 Jun 2026 16:03:06 +0200 From: Daniel Borkmann To: ast@kernel.org Cc: kpsingh@kernel.org, James.Bottomley@hansenpartnership.com, paul@paul-moore.com, bboscaccy@linux.microsoft.com, memxor@gmail.com, torvalds@linux-foundation.org, bpf@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH bpf-next v2 5/5] Documentation/bpf: Add BPF signing and enforcement doc Date: Wed, 24 Jun 2026 16:03:01 +0200 Message-ID: <20260624140301.93421-6-daniel@iogearbox.net> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260624140301.93421-1-daniel@iogearbox.net> References: <20260624140301.93421-1-daniel@iogearbox.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Virus-Scanned: Clear (ClamAV 1.4.3/28041/Wed Jun 24 08:24:54 2026) Describe the BPF signing design end to end: why a trusted loader is needed, the signature(insns || metadata) contract, load-time verification via fd_array (exclusive + frozen maps), the binary BPF_SIG_{UNSIGNED,VERIFIED} verdict, and how [BPF] LSMs can enforce policy on it. This writes down the contract on the discussion points with the LSM / integrity folks [0][1]: by the time security_bpf_prog_load() is called, signature verification has fully completed and covers the instructions plus the frozen contents of every bound exclusive map; there is no intermediate "loader verified, payload pending" state to reason about; and what BPF_SIG_VERIFIED means at each hook is spelled out explicitly, including the post-verifier coverage check that keeps the verdict binary. Signed-off-by: Daniel Borkmann Link: https://lore.kernel.org/bpf/bc823ddbaf63e0e177eb46d1cc15076e4e2e689d.camel@HansenPartnership.com [0] Link: https://lore.kernel.org/bpf/CAHC9VhSDkwGgPfrBUh7EgBKEJj_JjnY68c0YAmuuLT_i--GskQ@mail.gmail.com [1] --- Documentation/bpf/index.rst | 1 + Documentation/bpf/signing.rst | 490 ++++++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 Documentation/bpf/signing.rst diff --git a/Documentation/bpf/index.rst b/Documentation/bpf/index.rst index 0d5c6f659266..638a00d42bc2 100644 --- a/Documentation/bpf/index.rst +++ b/Documentation/bpf/index.rst @@ -28,6 +28,7 @@ that goes into great technical depth about the BPF Architecture. classic_vs_extended.rst bpf_iterators bpf_licensing + signing test_debug clang-notes linux-notes diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst new file mode 100644 index 000000000000..71ca26f8fd2a --- /dev/null +++ b/Documentation/bpf/signing.rst @@ -0,0 +1,490 @@ +.. 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 enforced for instance by 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``), 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 program bound +to a signed loader never has a map the 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. + +Once the verifier has finished resolving instructions, it requires that the +program use no map beyond those folded into the signature, and that it +reference no BTF at all, and rejects the load otherwise. A map resolved from +an instruction after verification - for example an exclusive map reached by a +directly-referenced fd, or a map swapped into an ``fd_array`` slot the loader +reads - shows up as an extra ``used_maps`` entry and is caught here; a BTF +reference (a ksym, or a BTF fd in ``fd_array``) likewise shows up in +``used_btfs`` and is rejected. 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 such as +IPE. 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 attempt. 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 +confirms - once the verifier resolves instructions - that the program only uses +maps the signature covers, rejecting a violating load 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". Policy LSMs such as IPE consume the same hooks to +enforce system policy without writing any BPF. + +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 -i \ + 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, +certificate and hash are embedded in the generated header; 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 file descriptors bound to the program. ``fd_array_cnt`` must be + set for the kernel to scan the array. When a signature is present, every map + in the array 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``. -- 2.43.0