mirror of
https://github.com/torvalds/linux.git
synced 2026-09-22 04:34:03 +02:00
Documentation/bpf: Add BPF signing and enforcement doc
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.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
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]
Link: https://lore.kernel.org/bpf/20260708075343.358712-9-daniel@iogearbox.net
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
This commit is contained in:
parent
99b321dde7
commit
84c42f515f
|
|
@ -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
|
||||
|
|
|
|||
497
Documentation/bpf/signing.rst
Normal file
497
Documentation/bpf/signing.rst
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
.. 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``.
|
||||
Loading…
Reference in New Issue
Block a user