* [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 19:06 ` bot+bpf-ci
2026-08-31 10:05 ` Christian Brauner
2026-08-28 17:52 ` [PATCH bpf-next v4 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
` (11 subsequent siblings)
12 siblings, 2 replies; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
BPF program signatures can currently be verified against one of the
system keyrings (builtin, secondary, platform) or against an arbitrary
user/session caller-supplied keyring named through keyring_id. There
is nothing in between: the system keyrings need a kernel rebuild or a
vouched-for enrollment to rotate a key, while a caller-supplied keyring
is fully controlled by the loader and therefore carries no trust on
its own (unless explicitly combined with BPF LSM to protect against
key tampering).
Add a dedicated bpf keyring to fill that gap, modelled after the
dm-verity keyring which was added in commit 033724b1c627 ("dm-verity:
add dm-verity keyring") and which can eventually be used also via
systemd [0] through the same enrollment method as in dm-verity's case.
It is selected with the new KEY_SPEC_BPF_KEYRING special key id and
gives an operator a place to enroll a BPF-only signing key at boot,
specifically scoped to BPF program loading and nothing else in the
kernel's trust hierarchy.
The id is reserved from the KEY_SPEC space so that the latter is not
linked into any process keyring, and lookup_user_key() resolves
KEY_SPEC_BPF_KEYRING constant instead of having to look it up via
/proc/keys first.
By default the keyring is sealed empty at init. Systems that want to
provision keys pass bpf.keyring_unsealed=1, which leaves the keyring
open for the initrd to add keys to. The keyring is only ever consulted
once it is both non-empty and restricted. An unrestricted keyring is
ignored.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://github.com/systemd/systemd/pull/43549 [0]
---
.../admin-guide/kernel-parameters.txt | 8 +++
include/linux/bpf.h | 7 ++
include/linux/key.h | 2 +
include/uapi/linux/keyctl.h | 1 +
kernel/bpf/Makefile | 3 +
kernel/bpf/keys.c | 68 +++++++++++++++++++
kernel/bpf/verifier.c | 9 +++
security/keys/process_keys.c | 25 +++++++
8 files changed, 123 insertions(+)
create mode 100644 kernel/bpf/keys.c
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index e4643634a9b1..2beb61092bb3 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -667,6 +667,14 @@ Kernel parameters
See Documentation/admin-guide/bootconfig.rst
+ bpf.keyring_unsealed=
+ [BPF] When set to 1, leave the bpf keyring unsealed
+ after initialization so that userspace can provision
+ keys. Once the keyring is restricted it becomes active
+ and can be used for BPF program signature verification.
+
+ See Documentation/bpf/signing.rst
+
bttv.card= [HW,V4L] bttv (bt848 + bt878 based grabber cards)
bttv.radio= Most important insmod options are available as
kernel args too.
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index c2027688be3e..039674b2f875 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -1735,6 +1735,7 @@ enum bpf_sig_keyring {
BPF_SIG_KEYRING_SECONDARY,
BPF_SIG_KEYRING_PLATFORM,
BPF_SIG_KEYRING_USER,
+ BPF_SIG_KEYRING_BPF,
};
struct bpf_prog_aux {
@@ -3819,6 +3820,7 @@ struct bpf_key {
#if defined(CONFIG_KEYS) && defined(CONFIG_BPF_SYSCALL)
struct bpf_key *bpf_lookup_user_key(s32 serial, u64 flags);
struct bpf_key *bpf_lookup_system_key(u64 id);
+struct bpf_key *bpf_lookup_keyring(void);
void bpf_key_put(struct bpf_key *bkey);
int bpf_verify_pkcs7_signature(const struct bpf_dynptr *data_p,
const struct bpf_dynptr *sig_p,
@@ -3839,6 +3841,11 @@ static inline struct bpf_key *bpf_lookup_system_key(u64 id)
return NULL;
}
+static inline struct bpf_key *bpf_lookup_keyring(void)
+{
+ return NULL;
+}
+
static inline void bpf_key_put(struct bpf_key *bkey)
{
}
diff --git a/include/linux/key.h b/include/linux/key.h
index 81b8f05c6898..bd10fe45819d 100644
--- a/include/linux/key.h
+++ b/include/linux/key.h
@@ -440,6 +440,8 @@ extern key_ref_t keyring_search(key_ref_t keyring,
extern int keyring_restrict(key_ref_t keyring, const char *type,
const char *restriction);
+extern void key_register_bpf_keyring(struct key *keyring);
+
extern struct key *key_lookup(key_serial_t id);
static inline key_serial_t key_serial(const struct key *key)
diff --git a/include/uapi/linux/keyctl.h b/include/uapi/linux/keyctl.h
index 4c8884eea808..fa85b9760391 100644
--- a/include/uapi/linux/keyctl.h
+++ b/include/uapi/linux/keyctl.h
@@ -24,6 +24,7 @@
#define KEY_SPEC_GROUP_KEYRING -6 /* - key ID for GID-specific keyring */
#define KEY_SPEC_REQKEY_AUTH_KEY -7 /* - key ID for assumed request_key auth key */
#define KEY_SPEC_REQUESTOR_KEYRING -8 /* - key ID for request_key() dest keyring */
+#define KEY_SPEC_BPF_KEYRING -9 /* - key ID for the BPF-specific keyring */
/* request-key default keyrings */
#define KEY_REQKEY_DEFL_NO_CHANGE -1
diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
index 90255d80e5be..9a92c348bbda 100644
--- a/kernel/bpf/Makefile
+++ b/kernel/bpf/Makefile
@@ -27,6 +27,9 @@ obj-$(CONFIG_BPF_SYSCALL) += offload.o
obj-$(CONFIG_BPF_SYSCALL) += net_namespace.o
obj-$(CONFIG_BPF_SYSCALL) += tcx.o
endif
+ifeq ($(CONFIG_KEYS),y)
+obj-$(CONFIG_BPF_SYSCALL) += keys.o
+endif
ifeq ($(CONFIG_PERF_EVENTS),y)
obj-$(CONFIG_BPF_SYSCALL) += stackmap.o
endif
diff --git a/kernel/bpf/keys.c b/kernel/bpf/keys.c
new file mode 100644
index 000000000000..08549b3220c1
--- /dev/null
+++ b/kernel/bpf/keys.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Isovalent */
+
+#include <linux/bpf.h>
+#include <linux/cred.h>
+#include <linux/err.h>
+#include <linux/init.h>
+#include <linux/key.h>
+#include <linux/moduleparam.h>
+#include <linux/slab.h>
+
+#undef MODULE_PARAM_PREFIX
+#define MODULE_PARAM_PREFIX "bpf."
+
+static struct key *bpf_keyring;
+
+static bool bpf_keyring_unsealed __ro_after_init;
+module_param_named(keyring_unsealed, bpf_keyring_unsealed, bool, 0444);
+MODULE_PARM_DESC(keyring_unsealed, "Leave the bpf keyring unsealed");
+
+struct bpf_key *bpf_lookup_keyring(void)
+{
+ struct bpf_key *bkey;
+
+ if (!bpf_keyring)
+ return NULL;
+ if (!READ_ONCE(bpf_keyring->keys.nr_leaves_on_tree) ||
+ !READ_ONCE(bpf_keyring->restrict_link))
+ return NULL;
+
+ bkey = kmalloc_obj(*bkey);
+ if (!bkey)
+ return NULL;
+
+ bkey->key = bpf_keyring;
+ bkey->has_ref = false;
+ return bkey;
+}
+
+static int __init bpf_keyring_init(void)
+{
+ struct key *keyring;
+
+ keyring = keyring_alloc(".bpf",
+ GLOBAL_ROOT_UID, GLOBAL_ROOT_GID,
+ current_cred(), KEY_POS_SEARCH |
+ KEY_USR_VIEW | KEY_USR_READ |
+ KEY_USR_WRITE | KEY_USR_SEARCH |
+ KEY_USR_SETATTR, KEY_ALLOC_NOT_IN_QUOTA,
+ NULL, NULL);
+ if (IS_ERR(keyring)) {
+ pr_err("bpf: cannot allocate bpf keyring: %ld\n",
+ PTR_ERR(keyring));
+ return 0;
+ }
+ if (!bpf_keyring_unsealed &&
+ keyring_restrict(make_key_ref(keyring, true), NULL, NULL)) {
+ pr_err("bpf: cannot seal bpf keyring\n");
+ key_revoke(keyring);
+ key_put(keyring);
+ return 0;
+ }
+
+ bpf_keyring = keyring;
+ key_register_bpf_keyring(keyring);
+ return 0;
+}
+late_initcall(bpf_keyring_init);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index e036ae20bf6b..9c6624d2f5fb 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -24,6 +24,7 @@
#include <linux/bpf_lsm.h>
#include <linux/security.h>
#include <linux/verification.h>
+#include <linux/keyctl.h>
#include <linux/btf_ids.h>
#include <linux/poison.h>
#include <linux/module.h>
@@ -20981,6 +20982,8 @@ static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
return BPF_SIG_KEYRING_SECONDARY;
case (s32)(unsigned long)VERIFY_USE_PLATFORM_KEYRING:
return BPF_SIG_KEYRING_PLATFORM;
+ case KEY_SPEC_BPF_KEYRING:
+ return BPF_SIG_KEYRING_BPF;
default:
return BPF_SIG_KEYRING_USER;
}
@@ -21018,9 +21021,15 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
return -EINVAL;
if (system_keyring_id_check(attr->keyring_id) == 0)
key = bpf_lookup_system_key(attr->keyring_id);
+ else if (attr->keyring_id == KEY_SPEC_BPF_KEYRING)
+ key = bpf_lookup_keyring();
else
key = bpf_lookup_user_key(attr->keyring_id, 0);
if (!key) {
+ if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
+ verbose(env, "the bpf keyring is empty or has not been restricted\n");
+ return -ENOKEY;
+ }
verbose(env, "cannot resolve signing keyring with keyring_id %d\n",
attr->keyring_id);
return -EINVAL;
diff --git a/security/keys/process_keys.c b/security/keys/process_keys.c
index a63c46bb2d14..44358388e395 100644
--- a/security/keys/process_keys.c
+++ b/security/keys/process_keys.c
@@ -22,6 +22,9 @@
/* Session keyring create vs join semaphore */
static DEFINE_MUTEX(key_session_mutex);
+/* BPF keyring reachable through KEY_SPEC_BPF_KEYRING */
+static struct key *bpf_keyring __ro_after_init;
+
/* The root user's tracking struct */
struct key_user root_key_user = {
.usage = REFCOUNT_INIT(3),
@@ -590,6 +593,20 @@ bool lookup_user_key_possessed(const struct key *key,
return key == match_data->raw_data;
}
+/**
+ * key_register_bpf_keyring - Publish the BPF keyring for KEY_SPEC_BPF_KEYRING
+ * @keyring: The keyring to publish
+ *
+ * Make @keyring reachable by userspace through the KEY_SPEC_BPF_KEYRING
+ * special key ID, so that provisioning it does not require scraping its
+ * serial out of /proc/keys first. Called once, from an initcall, and never
+ * undone.
+ */
+void key_register_bpf_keyring(struct key *keyring)
+{
+ bpf_keyring = keyring;
+}
+
/*
* Look up a key ID given us by userspace with a given permissions mask to get
* the key it refers to.
@@ -741,6 +758,14 @@ key_ref_t lookup_user_key(key_serial_t id, unsigned long lflags,
key_ref = make_key_ref(key, 1);
break;
+ case KEY_SPEC_BPF_KEYRING:
+ key = bpf_keyring;
+ if (!key)
+ goto error;
+ __key_get(key);
+ key_ref = make_key_ref(key, 0);
+ break;
+
default:
key_ref = ERR_PTR(-EINVAL);
if (id < 1)
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation
2026-08-28 17:52 ` [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
@ 2026-08-28 19:06 ` bot+bpf-ci
2026-08-31 10:05 ` Christian Brauner
1 sibling, 0 replies; 21+ messages in thread
From: bot+bpf-ci @ 2026-08-28 19:06 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1804 bytes --]
> BPF program signatures can currently be verified against one of the
> system keyrings (builtin, secondary, platform) or against an arbitrary
> user/session caller-supplied keyring named through keyring_id. There
> is nothing in between: the system keyrings need a kernel rebuild or a
> vouched-for enrollment to rotate a key, while a caller-supplied keyring
> is fully controlled by the loader and therefore carries no trust on
> its own (unless explicitly combined with BPF LSM to protect against
> key tampering).
>
> Add a dedicated bpf keyring to fill that gap, modelled after the
> dm-verity keyring which was added in commit 033724b1c627 ("dm-verity:
> add dm-verity keyring") and which can eventually be used also via
> systemd [0] through the same enrollment method as in dm-verity's case.
> It is selected with the new KEY_SPEC_BPF_KEYRING special key id and
> gives an operator a place to enroll a BPF-only signing key at boot,
> specifically scoped to BPF program loading and nothing else in the
> kernel's trust hierarchy.
>
> The id is reserved from the KEY_SPEC space so that the latter is not
> linked into any process keyring, and lookup_user_key() resolves
> KEY_SPEC_BPF_KEYRING constant instead of having to look it up via
> /proc/keys first.
This isn't a bug, but could this sentence be clearer? The phrase "the
latter" grammatically refers back to "the KEY_SPEC space" rather than to
the bpf keyring itself, which reverses the intended meaning on first read.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33197746599
^ permalink raw reply [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation
2026-08-28 17:52 ` [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
2026-08-28 19:06 ` bot+bpf-ci
@ 2026-08-31 10:05 ` Christian Brauner
1 sibling, 0 replies; 21+ messages in thread
From: Christian Brauner @ 2026-08-31 10:05 UTC (permalink / raw)
To: Daniel Borkmann
Cc: memxor, brauner, kpsingh, ast, john.fastabend, a.s.protopopov,
bpf, dhowells, jarkko, keyrings
> BPF program signatures can currently be verified against one of the
> system keyrings (builtin, secondary, platform) or against an arbitrary
> user/session caller-supplied keyring named through keyring_id. There
> is nothing in between: the system keyrings need a kernel rebuild or a
> vouched-for enrollment to rotate a key, while a caller-supplied keyring
> is fully controlled by the loader and therefore carries no trust on
> its own (unless explicitly combined with BPF LSM to protect against
> key tampering).
>
> Add a dedicated bpf keyring to fill that gap, modelled after the
> dm-verity keyring which was added in commit 033724b1c627 ("dm-verity:
> add dm-verity keyring") and which can eventually be used also via
> systemd [0] through the same enrollment method as in dm-verity's case.
> It is selected with the new KEY_SPEC_BPF_KEYRING special key id and
> gives an operator a place to enroll a BPF-only signing key at boot,
> specifically scoped to BPF program loading and nothing else in the
> kernel's trust hierarchy.
>
> The id is reserved from the KEY_SPEC space so that the latter is not
> linked into any process keyring, and lookup_user_key() resolves
> KEY_SPEC_BPF_KEYRING constant instead of having to look it up via
> /proc/keys first.
>
> By default the keyring is sealed empty at init. Systems that want to
> provision keys pass bpf.keyring_unsealed=1, which leaves the keyring
> open for the initrd to add keys to. The keyring is only ever consulted
> once it is both non-empty and restricted. An unrestricted keyring is
> ignored.
>
> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
> Link: https://github.com/systemd/systemd/pull/43549 [0]
Looks good,
Reviewed-by: Christian Brauner (Amutable) <brauner@kernel.org>
--
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH bpf-next v4 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-31 10:05 ` Christian Brauner
2026-08-28 17:52 ` [PATCH bpf-next v4 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
` (10 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
Nothing changes for systems that do not use the bpf keyring. Without
bpf.keyring_unsealed=1 a caller-supplied keyring behaves exactly as
before, which also lets it serve as the staging step for software
installed onto a running system whose signing key is not enrolled
anywhere yet.
Passing bpf.keyring_unsealed=1 states that the bpf keyring is the trust
anchor for this boot, so from the first program load onwards a caller-
supplied keyring is refused with -EPERM. Deriving this from the boot
flag rather than from the keyring's runtime state keeps the decision
immutable from userspace.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
Documentation/admin-guide/kernel-parameters.txt | 8 ++++++++
include/linux/bpf.h | 6 ++++++
kernel/bpf/keys.c | 5 +++++
kernel/bpf/verifier.c | 10 +++++++---
4 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 2beb61092bb3..e70fb15757d4 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -673,6 +673,14 @@ Kernel parameters
keys. Once the keyring is restricted it becomes active
and can be used for BPF program signature verification.
+ Setting this also means that the bpf keyring is the
+ only non-system keyring a loader may select for the
+ rest of the boot: caller-supplied user/session
+ keyrings are refused with -EPERM, whether or not
+ provisioning actually completed. The system keyrings
+ stay selectable. Leaving it unset keeps the prior
+ behaviour, where a caller-supplied keyring is allowed.
+
See Documentation/bpf/signing.rst
bttv.card= [HW,V4L] bttv (bt848 + bt878 based grabber cards)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 039674b2f875..3a7eb2185c35 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3821,6 +3821,7 @@ struct bpf_key {
struct bpf_key *bpf_lookup_user_key(s32 serial, u64 flags);
struct bpf_key *bpf_lookup_system_key(u64 id);
struct bpf_key *bpf_lookup_keyring(void);
+bool bpf_keyring_enforced(void);
void bpf_key_put(struct bpf_key *bkey);
int bpf_verify_pkcs7_signature(const struct bpf_dynptr *data_p,
const struct bpf_dynptr *sig_p,
@@ -3846,6 +3847,11 @@ static inline struct bpf_key *bpf_lookup_keyring(void)
return NULL;
}
+static inline bool bpf_keyring_enforced(void)
+{
+ return false;
+}
+
static inline void bpf_key_put(struct bpf_key *bkey)
{
}
diff --git a/kernel/bpf/keys.c b/kernel/bpf/keys.c
index 08549b3220c1..bffc356839ac 100644
--- a/kernel/bpf/keys.c
+++ b/kernel/bpf/keys.c
@@ -18,6 +18,11 @@ static bool bpf_keyring_unsealed __ro_after_init;
module_param_named(keyring_unsealed, bpf_keyring_unsealed, bool, 0444);
MODULE_PARM_DESC(keyring_unsealed, "Leave the bpf keyring unsealed");
+bool bpf_keyring_enforced(void)
+{
+ return bpf_keyring_unsealed;
+}
+
struct bpf_key *bpf_lookup_keyring(void)
{
struct bpf_key *bkey;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 9c6624d2f5fb..2d50adf3800d 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21019,12 +21019,16 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
if (!attr->signature_size ||
attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
return -EINVAL;
- if (system_keyring_id_check(attr->keyring_id) == 0)
+ if (system_keyring_id_check(attr->keyring_id) == 0) {
key = bpf_lookup_system_key(attr->keyring_id);
- else if (attr->keyring_id == KEY_SPEC_BPF_KEYRING)
+ } else if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
key = bpf_lookup_keyring();
- else
+ } else if (bpf_keyring_enforced()) {
+ verbose(env, "caller-supplied keyring refused, use bpf keyring\n");
+ return -EPERM;
+ } else {
key = bpf_lookup_user_key(attr->keyring_id, 0);
+ }
if (!key) {
if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
verbose(env, "the bpf keyring is empty or has not been restricted\n");
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
2026-08-28 17:52 ` [PATCH bpf-next v4 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
@ 2026-08-31 10:05 ` Christian Brauner
0 siblings, 0 replies; 21+ messages in thread
From: Christian Brauner @ 2026-08-31 10:05 UTC (permalink / raw)
To: Daniel Borkmann
Cc: memxor, brauner, kpsingh, ast, john.fastabend, a.s.protopopov,
bpf, dhowells, jarkko, keyrings
> Nothing changes for systems that do not use the bpf keyring. Without
> bpf.keyring_unsealed=1 a caller-supplied keyring behaves exactly as
> before, which also lets it serve as the staging step for software
> installed onto a running system whose signing key is not enrolled
> anywhere yet.
>
> Passing bpf.keyring_unsealed=1 states that the bpf keyring is the trust
> anchor for this boot, so from the first program load onwards a caller-
> supplied keyring is refused with -EPERM. Deriving this from the boot
> flag rather than from the keyring's runtime state keeps the decision
> immutable from userspace.
>
> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Looks good,
Reviewed-by: Christian Brauner (Amutable) <brauner@kernel.org>
--
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH bpf-next v4 03/11] bpf: Raise the bound on a program's signature size
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
` (9 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
signature_size is bounded by KMALLOC_MAX_CACHE_SIZE, which is 8 KiB on a
4 KiB page system. Back then we chose it somewhat arbitrarily and was
picked when a BPF program signature was RSA or ECDSA. ML-DSA (FIPS-204)
verification is wired through the X.509 and PKCS#7 parsers, and BPF
reaches them too via verify_pkcs7_signature() without having to know the
concrete algorithm. The bound becomes a bit too small, thus add an
explicit BPF_PROG_MAX_SIGNATURE_SIZE of 64 KiB and use that instead
to cover all options.
KMALLOC_MAX_CACHE_SIZE is PAGE_SIZE-derived, so what was accepted so
far depended on the page size which is not optimal as it should be the
same behavior on every configuration. On 64 KiB page kernels this lowers
the ceiling from 128 KiB to 64 KiB. Nothing that could have been verified
is affected as the largest signature the kernel implements is ML-DSA-87
at 4627 bytes.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
kernel/bpf/verifier.c | 15 ++++++++++-----
.../selftests/bpf/prog_tests/signed_loader.c | 5 +++--
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 2d50adf3800d..d50a135466bb 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -20973,6 +20973,14 @@ int bpf_fixup_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
return 0;
}
+/*
+ * Upper bound on the PKCS#7 signature blob passed with a program. Comfortably
+ * above the largest signature the kernel can verify, and far below anything
+ * that would make rejecting a load expensive. Deliberately a fixed number so
+ * that what the syscall accepts does not depend on PAGE_SIZE.
+ */
+#define BPF_PROG_MAX_SIGNATURE_SIZE (64 * 1024)
+
static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
{
switch (keyring_id) {
@@ -21012,13 +21020,10 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
u64 data_sz;
int err = 0;
- /*
- * Don't attempt to use kmalloc_large or vmalloc for signatures.
- * Practical signature for BPF program should be below this limit.
- */
if (!attr->signature_size ||
- attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
+ attr->signature_size > BPF_PROG_MAX_SIGNATURE_SIZE)
return -EINVAL;
+
if (system_keyring_id_check(attr->keyring_id) == 0) {
key = bpf_lookup_system_key(attr->keyring_id);
} else if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 77381d345435..0c5294738d6c 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -571,8 +571,9 @@ static void signature_too_large(void)
if (gen_loader_fixture_init(&f) == 0) {
/*
- * signature_size beyond the kernel's bound (KMALLOC_MAX_CACHE_SIZE)
- * is rejected before the buffer is read.
+ * signature_size beyond the kernel's bound
+ * (BPF_PROG_MAX_SIGNATURE_SIZE) is rejected before the buffer
+ * is read.
*/
fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
64 << 20, KEY_SPEC_SESSION_KEYRING, 0);
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH bpf-next v4 04/11] bpftool: Support ML-DSA program signing
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (2 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
` (8 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
Add bpftool support for ML-DSA program signing and drop the flag for
ML-DSA keys on affected OpenSSL versions, the same way as commit
0ad9a71933e7 ("modsign: Enable ML-DSA module signing").
Also, an ML-DSA-87 signature is 4627 bytes on its own, so the blob does
not fit into the 4 KiB of MAX_SIG_SIZE anymore and signing would fail
otherwise. Bump to 16 KiB. MAX_SIG_SIZE only sizes the buffer for
what bpftool itself emits (unrelated to BPF_PROG_MAX_SIGNATURE_SIZE).
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
tools/bpf/bpftool/main.h | 2 +-
tools/bpf/bpftool/sign.c | 24 +++++++++++++++++++++---
2 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h
index 78b6e0ebb85d..9315a1db1f7c 100644
--- a/tools/bpf/bpftool/main.h
+++ b/tools/bpf/bpftool/main.h
@@ -57,7 +57,7 @@ static inline void *u64_to_ptr(__u64 ptr)
})
#define ERR_MAX_LEN 1024
-#define MAX_SIG_SIZE 4096
+#define MAX_SIG_SIZE 16384
#define BPF_TAG_FMT "%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx"
diff --git a/tools/bpf/bpftool/sign.c b/tools/bpf/bpftool/sign.c
index 88726a6db6d0..adbfd4ae5c02 100644
--- a/tools/bpf/bpftool/sign.c
+++ b/tools/bpf/bpftool/sign.c
@@ -130,6 +130,12 @@ __u32 register_session_key(const char *key_der_path)
int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
{
+ unsigned int signer_flags = CMS_NOCERTS | CMS_BINARY | CMS_NOSMIMECAP |
+#ifdef CMS_NO_SIGNING_TIME
+ CMS_NO_SIGNING_TIME |
+#endif
+ CMS_USE_KEYID | CMS_NOATTR;
+ const EVP_MD *cms_digest = EVP_sha256();
BIO *bd_in = NULL, *bd_out = NULL;
EVP_PKEY *private_key = NULL;
CMS_ContentInfo *cms = NULL;
@@ -167,6 +173,20 @@ int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
goto cleanup;
}
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L && OPENSSL_VERSION_NUMBER < 0x40000000L
+ if (EVP_PKEY_is_a(private_key, "ML-DSA-44") ||
+ EVP_PKEY_is_a(private_key, "ML-DSA-65") ||
+ EVP_PKEY_is_a(private_key, "ML-DSA-87")) {
+ /*
+ * See workaround in 0ad9a71933e7 ("modsign: Enable ML-DSA
+ * module signing"). Kernel only accepts sha512, see also
+ * 8bbdeb7a25b4 ("pkcs7, x509: Add ML-DSA support").
+ */
+ signer_flags &= ~CMS_NOATTR;
+ cms_digest = EVP_sha512();
+ }
+#endif
+
cms = CMS_sign(NULL, NULL, NULL, NULL,
CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY | CMS_DETACHED |
CMS_STREAM);
@@ -175,9 +195,7 @@ int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
goto cleanup;
}
- if (!CMS_add1_signer(cms, x509, private_key, EVP_sha256(),
- CMS_NOCERTS | CMS_BINARY | CMS_NOSMIMECAP |
- CMS_USE_KEYID | CMS_NOATTR)) {
+ if (!CMS_add1_signer(cms, x509, private_key, cms_digest, signer_flags)) {
err = -EINVAL;
goto cleanup;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH bpf-next v4 05/11] selftests/bpf: Add a test for the sealed bpf keyring
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (3 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 18:53 ` bot+bpf-ci
2026-08-28 17:52 ` [PATCH bpf-next v4 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
` (7 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
bpf_keyring_sealed checks that a load naming the bpf keyring fails while
the keyring has not been provisioned. It uses a junk signature as the
size check and the keyring lookup both happen before any crypto, so the
error under test is reached without a real signature and the ordering is
what gets verified:
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
[...]
#425/9 signed_loader/signed_module_kfunc_rejected:OK
#425/10 signed_loader/signature_failure_logs:OK
#425/11 signed_loader/signature_too_large:OK
#425/12 signed_loader/signature_zero_size:OK
#425/13 signed_loader/signature_bad_keyring:OK
#425/14 signed_loader/bpf_keyring_sealed:OK
#425/15 signed_loader/metadata_ctx_max_entries_ignored:OK
#425/16 signed_loader/metadata_ctx_initial_value_ignored:OK
#425/17 signed_loader/signature_authenticates_insns:OK
#425/18 signed_loader/signature_authenticates_metadata:OK
#425/19 signed_loader/hash_requires_frozen:OK
[...]
#425 signed_loader:OK
Summary: 1/30 PASSED, 0 SKIPPED, 0/0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../selftests/bpf/prog_tests/signed_loader.c | 84 +++++++++++++++++++
1 file changed, 84 insertions(+)
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 0c5294738d6c..e30166c00be6 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -32,8 +32,13 @@ enum {
BPF_SIG_KEYRING_SECONDARY,
BPF_SIG_KEYRING_PLATFORM,
BPF_SIG_KEYRING_USER,
+ BPF_SIG_KEYRING_BPF,
};
+#ifndef KEY_SPEC_BPF_KEYRING
+#define KEY_SPEC_BPF_KEYRING -9
+#endif
+
static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
const void *sig, __u32 sig_sz, __s32 keyring_id,
__u32 fd_array_cnt)
@@ -627,6 +632,83 @@ static void signature_bad_keyring(void)
gen_loader_fixture_fini(&f);
}
+static bool keyring_unsealed_boot(void)
+{
+ char val = 0;
+ int fd;
+
+ fd = open("/sys/module/bpf/parameters/keyring_unsealed", O_RDONLY);
+ if (fd < 0)
+ return false;
+ if (read(fd, &val, 1) != 1)
+ val = 0;
+ close(fd);
+ return val == 'Y' || val == '1';
+}
+
+static int bpf_keyring_lookup(int *nr_keys)
+{
+ char line[512], type[32], desc[64];
+ int serial = -ENOENT;
+ FILE *f;
+
+ f = fopen("/proc/keys", "r");
+ if (!f)
+ return -errno;
+
+ while (fgets(line, sizeof(line), f)) {
+ unsigned int hex;
+ char *sum;
+
+ if (sscanf(line, "%x %*s %*s %*s %*s %*s %*s %31s %63s",
+ &hex, type, desc) != 3)
+ continue;
+ if (strcmp(type, "keyring") || strcmp(desc, ".bpf:"))
+ continue;
+
+ serial = (int)hex;
+ if (nr_keys) {
+ sum = strstr(line, ".bpf: ");
+ *nr_keys = !sum || !strncmp(sum + 6, "empty", 5) ?
+ 0 : atoi(sum + 6);
+ }
+ break;
+ }
+ fclose(f);
+ return serial;
+}
+
+static void bpf_keyring_sealed(void)
+{
+ static const __u8 junk[64] = {};
+ struct gen_loader_fixture f;
+ int serial, key, fd;
+
+ if (keyring_unsealed_boot()) {
+ printf("%s:SKIP:the bpf keyring was unsealed at boot\n", __func__);
+ test__skip();
+ return;
+ }
+ serial = bpf_keyring_lookup(NULL);
+ if (serial >= 0) {
+ key = syscall(__NR_add_key, "user", "sealprobe", "x", 1,
+ KEY_SPEC_BPF_KEYRING);
+ if (key >= 0)
+ syscall(__NR_keyctl, KEYCTL_UNLINK, key,
+ KEY_SPEC_BPF_KEYRING);
+ ASSERT_EQ(key < 0 ? -errno : 0, -EPERM,
+ "nothing links into a sealed keyring");
+ }
+ if (gen_loader_fixture_init(&f) == 0) {
+ fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
+ sizeof(junk), KEY_SPEC_BPF_KEYRING, 0);
+ ASSERT_EQ(fd, -ENOKEY, "sealed bpf keyring rejected");
+ if (fd >= 0)
+ close(fd);
+ }
+ gen_loader_fixture_fini(&f);
+}
+
/*
* A signed loader must ignore ctx-supplied map dimensions: the host cannot
* resize a signed program's maps via the loader ctx. Drive a one-map program
@@ -1806,6 +1888,8 @@ void test_signed_loader(void)
signature_zero_size();
if (test__start_subtest("signature_bad_keyring"))
signature_bad_keyring();
+ if (test__start_subtest("bpf_keyring_sealed"))
+ bpf_keyring_sealed();
if (test__start_subtest("metadata_ctx_max_entries_ignored"))
metadata_ctx_max_entries_ignored();
if (test__start_subtest("metadata_ctx_initial_value_ignored"))
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 05/11] selftests/bpf: Add a test for the sealed bpf keyring
2026-08-28 17:52 ` [PATCH bpf-next v4 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
@ 2026-08-28 18:53 ` bot+bpf-ci
0 siblings, 0 replies; 21+ messages in thread
From: bot+bpf-ci @ 2026-08-28 18:53 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5632 bytes --]
> bpf_keyring_sealed checks that a load naming the bpf keyring fails while
> the keyring has not been provisioned. It uses a junk signature as the
> size check and the keyring lookup both happen before any crypto, so the
> error under test is reached without a real signature and the ordering is
> what gets verified:
>
> # LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
> [...]
> #425/9 signed_loader/signed_module_kfunc_rejected:OK
> #425/10 signed_loader/signature_failure_logs:OK
> #425/11 signed_loader/signature_too_large:OK
> #425/12 signed_loader/signature_zero_size:OK
> #425/13 signed_loader/signature_bad_keyring:OK
> #425/14 signed_loader/bpf_keyring_sealed:OK
> #425/15 signed_loader/metadata_ctx_max_entries_ignored:OK
> #425/16 signed_loader/metadata_ctx_initial_value_ignored:OK
> #425/17 signed_loader/signature_authenticates_insns:OK
> #425/18 signed_loader/signature_authenticates_metadata:OK
> #425/19 signed_loader/hash_requires_frozen:OK
> [...]
> #425 signed_loader:OK
> Summary: 1/30 PASSED, 0 SKIPPED, 0/0 FAILED
>
> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
This isn't a bug, but would it be worth a sentence on the add_key/-EPERM
probe and on why the subtest skips when bpf.keyring_unsealed=1? The
description covers the junk-signature/-ENOKEY check well, but the subtest
also verifies that add_key("user", "sealprobe", ..., KEY_SPEC_BPF_KEYRING)
returns -EPERM on a sealed keyring, and it skips via keyring_unsealed_boot()
by reading /sys/module/bpf/parameters/keyring_unsealed. A reviewer reading
only the changelog might miss these other behaviors.
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 0c5294738d6ce..e30166c00be6a 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> @@ -32,8 +32,13 @@ enum {
> BPF_SIG_KEYRING_SECONDARY,
> BPF_SIG_KEYRING_PLATFORM,
> BPF_SIG_KEYRING_USER,
> + BPF_SIG_KEYRING_BPF,
> };
>
> +#ifndef KEY_SPEC_BPF_KEYRING
> +#define KEY_SPEC_BPF_KEYRING -9
> +#endif
> +
> static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
> const void *sig, __u32 sig_sz, __s32 keyring_id,
> __u32 fd_array_cnt)
[ ... ]
> @@ -627,6 +632,83 @@ static void signature_bad_keyring(void)
> gen_loader_fixture_fini(&f);
> }
>
> +static bool keyring_unsealed_boot(void)
> +{
> + char val = 0;
> + int fd;
> +
> + fd = open("/sys/module/bpf/parameters/keyring_unsealed", O_RDONLY);
> + if (fd < 0)
> + return false;
> + if (read(fd, &val, 1) != 1)
> + val = 0;
> + close(fd);
> + return val == 'Y' || val == '1';
> +}
> +
> +static int bpf_keyring_lookup(int *nr_keys)
> +{
> + char line[512], type[32], desc[64];
> + int serial = -ENOENT;
> + FILE *f;
> +
> + f = fopen("/proc/keys", "r");
> + if (!f)
> + return -errno;
> +
> + while (fgets(line, sizeof(line), f)) {
> + unsigned int hex;
> + char *sum;
> +
> + if (sscanf(line, "%x %*s %*s %*s %*s %*s %*s %31s %63s",
> + &hex, type, desc) != 3)
> + continue;
> + if (strcmp(type, "keyring") || strcmp(desc, ".bpf:"))
> + continue;
> +
> + serial = (int)hex;
> + if (nr_keys) {
> + sum = strstr(line, ".bpf: ");
> + *nr_keys = !sum || !strncmp(sum + 6, "empty", 5) ?
> + 0 : atoi(sum + 6);
> + }
This isn't a bug, but could the nr_keys out-parameter (and the .bpf:
leaf-count parsing) move to the patch that first passes a non-NULL pointer?
The only caller in this commit is bpf_keyring_sealed(), which passes NULL:
serial = bpf_keyring_lookup(NULL). So the whole if (nr_keys) arm, together
with the char *sum local and the strstr/strncmp/atoi parsing, has no
consumer here. The serial return value is likewise only consumed as an
existence gate (if (serial >= 0)), so the precision of the parse is unused
too.
> + break;
> + }
> + fclose(f);
> + return serial;
> +}
> +
> +static void bpf_keyring_sealed(void)
> +{
> + static const __u8 junk[64] = {};
> + struct gen_loader_fixture f;
> + int serial, key, fd;
> +
> + if (keyring_unsealed_boot()) {
> + printf("%s:SKIP:the bpf keyring was unsealed at boot\n", __func__);
> + test__skip();
> + return;
> + }
> + serial = bpf_keyring_lookup(NULL);
> + if (serial >= 0) {
> + key = syscall(__NR_add_key, "user", "sealprobe", "x", 1,
> + KEY_SPEC_BPF_KEYRING);
> + if (key >= 0)
> + syscall(__NR_keyctl, KEYCTL_UNLINK, key,
> + KEY_SPEC_BPF_KEYRING);
> + ASSERT_EQ(key < 0 ? -errno : 0, -EPERM,
> + "nothing links into a sealed keyring");
> + }
> + if (gen_loader_fixture_init(&f) == 0) {
> + fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
> + sizeof(junk), KEY_SPEC_BPF_KEYRING, 0);
> + ASSERT_EQ(fd, -ENOKEY, "sealed bpf keyring rejected");
> + if (fd >= 0)
> + close(fd);
> + }
> + gen_loader_fixture_fini(&f);
> +}
> +
[ ... ]
> @@ -1806,6 +1888,8 @@ void test_signed_loader(void)
> signature_zero_size();
> if (test__start_subtest("signature_bad_keyring"))
> signature_bad_keyring();
> + if (test__start_subtest("bpf_keyring_sealed"))
> + bpf_keyring_sealed();
> if (test__start_subtest("metadata_ctx_max_entries_ignored"))
> metadata_ctx_max_entries_ignored();
> if (test__start_subtest("metadata_ctx_initial_value_ignored"))
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33197746599
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH bpf-next v4 06/11] selftests/bpf: Rebuild signed lskels when signing key changes
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (4 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
` (6 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
The signing key is regenerated whenever verify_sig_setup.sh changes,
but the signed light skeletons only depend on the BPF object and on
bpftool, not on the key they are signed with. Thus, add the certificate
as a prereq so a new key forces the skeletons to be signed again.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
tools/testing/selftests/bpf/Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index 5f1a3bfc0569..05b3ee64290f 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -663,7 +663,7 @@ $(TRUNNER_BPF_LSKELS): %.lskel.h: %.bpf.o $(BPFTOOL) | $(TRUNNER_OUTPUT)
}) && \
rm -f $$(<:.o=.llinked1.o) $$(<:.o=.llinked2.o) $$(<:.o=.llinked3.o)
-$(TRUNNER_BPF_LSKELS_SIGNED): %.lskel.h: %.bpf.o $(BPFTOOL) | $(TRUNNER_OUTPUT)
+$(TRUNNER_BPF_LSKELS_SIGNED): %.lskel.h: %.bpf.o $(BPFTOOL) $(VERIFICATION_CERT) | $(TRUNNER_OUTPUT)
$(Q)$(if $(PERMISSIVE),if [ ! -f $$< ]; then \
$$(RM) $$@; \
printf ' %-12s %s\n' 'SKIP-SKEL' '$$(notdir $$@)' 1>&2; \
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH bpf-next v4 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (5 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
` (5 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
The script's "setup" action generates an RSA key, enrolls it and builds
a keyring around it. The name says nothing about the algorithm, which is
fine while there is only one, but we'll add "setup-mldsa" soon, therefore
rename the existing one into "setup-rsa". No functional change.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../selftests/bpf/prog_tests/signed_loader.c | 18 +++++++++---------
.../bpf/prog_tests/verify_pkcs7_sig.c | 4 ++--
.../testing/selftests/bpf/verify_sig_setup.sh | 8 ++++----
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index e30166c00be6..fbf14c7dd560 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -462,7 +462,7 @@ static void signed_btf_fd_array_rejected(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
return;
}
@@ -914,7 +914,7 @@ static void signature_authenticates_insns(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
return;
}
@@ -1014,7 +1014,7 @@ static void signature_authenticates_metadata(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
return;
}
@@ -1350,7 +1350,7 @@ static void lsm_signature_verdict(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
goto out;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
dir = NULL;
goto out;
@@ -1533,7 +1533,7 @@ static void loadtime_verify(struct bpf_object *obj, int expect_maps)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
return;
}
@@ -1631,7 +1631,7 @@ static void signed_no_fd_array(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
return;
}
@@ -1702,7 +1702,7 @@ static void signed_map_by_fd_rejected(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
goto out_map;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
goto out_map;
}
@@ -1764,7 +1764,7 @@ static void signed_sparse_fd_array_rejected(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
goto out_map;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
goto out_map;
}
@@ -1818,7 +1818,7 @@ static void signed_module_kfunc_rejected(void)
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
- if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+ if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
rmdir(dir);
return;
}
diff --git a/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c b/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c
index f327feb8e38c..12b146d205d7 100644
--- a/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c
+++ b/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c
@@ -257,7 +257,7 @@ static void test_verify_pkcs7_sig_from_map(void)
if (!ASSERT_OK_PTR(tmp_dir, "mkdtemp"))
return;
- ret = _run_setup_process(tmp_dir, "setup");
+ ret = _run_setup_process(tmp_dir, "setup-rsa");
if (!ASSERT_OK(ret, "_run_setup_process"))
goto close_prog;
@@ -458,7 +458,7 @@ static void test_pkcs7_sig_fsverity(void)
snprintf(data_path, PATH_MAX, "%s/data-file", tmp_dir);
snprintf(sig_path, PATH_MAX, "%s/sig-file", tmp_dir);
- ret = _run_setup_process(tmp_dir, "setup");
+ ret = _run_setup_process(tmp_dir, "setup-rsa");
if (!ASSERT_OK(ret, "_run_setup_process"))
goto out;
diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
index 09179fb551f0..202e6e6418fe 100755
--- a/tools/testing/selftests/bpf/verify_sig_setup.sh
+++ b/tools/testing/selftests/bpf/verify_sig_setup.sh
@@ -28,7 +28,7 @@ authorityKeyIdentifier=keyid
usage()
{
- echo "Usage: $0 <setup|cleanup <existing_tmp_dir>"
+ echo "Usage: $0 <setup-rsa|cleanup <existing_tmp_dir>"
exit 1
}
@@ -47,7 +47,7 @@ genkey()
${tmp_dir}/signing_key.der -outform der
}
-setup()
+setup_rsa()
{
local tmp_dir="$1"
@@ -108,8 +108,8 @@ main()
[[ ! -d "${tmp_dir}" ]] && echo "Directory ${tmp_dir} doesn't exist" && exit 1
- if [[ "${action}" == "setup" ]]; then
- setup "${tmp_dir}"
+ if [[ "${action}" == "setup-rsa" ]]; then
+ setup_rsa "${tmp_dir}"
elif [[ "${action}" == "genkey" ]]; then
genkey "${tmp_dir}"
elif [[ "${action}" == "cleanup" ]]; then
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH bpf-next v4 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (6 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 18:53 ` bot+bpf-ci
2026-08-28 17:52 ` [PATCH bpf-next v4 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
` (4 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
The BPF signing is algorithm agnostic, but so far the BPF CI only
has tested a single one. BPF hands verify_pkcs7_signature() a keyring
and byte ranges, and everything below it already understands ML-DSA,
so add a test for ML-DSA signed program to validate it works as well.
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
[...]
#425/10 signed_loader/signature_failure_logs:OK
#425/11 signed_loader/signature_too_large:OK
#425/12 signed_loader/signature_zero_size:OK
#425/13 signed_loader/signature_bad_keyring:OK
#425/14 signed_loader/bpf_keyring_sealed:OK
#425/15 signed_loader/mldsa_signed_load:OK
#425/16 signed_loader/metadata_ctx_max_entries_ignored:OK
#425/17 signed_loader/metadata_ctx_initial_value_ignored:OK
#425/18 signed_loader/signature_authenticates_insns:OK
#425/19 signed_loader/signature_authenticates_metadata:OK
#425/20 signed_loader/hash_requires_frozen:OK
[...]
#425 signed_loader:OK
Summary: 1/31 PASSED, 0 SKIPPED, 0/0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
tools/testing/selftests/bpf/config | 2 +
.../selftests/bpf/prog_tests/signed_loader.c | 125 +++++++++++++++++-
.../testing/selftests/bpf/verify_sig_setup.sh | 57 +++++++-
3 files changed, 177 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc..2f79688dcf7c 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -12,7 +12,9 @@ CONFIG_BPF_SYSCALL=y
# CONFIG_BPF_UNPRIV_DEFAULT_OFF is not set
CONFIG_CGROUP_BPF=y
CONFIG_CRYPTO_HMAC=y
+CONFIG_CRYPTO_MLDSA=y
CONFIG_CRYPTO_SHA256=y
+CONFIG_CRYPTO_SHA512=y
CONFIG_CRYPTO_USER_API=y
CONFIG_CRYPTO_USER_API_HASH=y
CONFIG_CRYPTO_USER_API_SKCIPHER=y
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index fbf14c7dd560..87f155de0e65 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -39,6 +39,12 @@ enum {
#define KEY_SPEC_BPF_KEYRING -9
#endif
+/* verify_sig_setup.sh exits with this when openssl cannot do ML-DSA. */
+#define SETUP_SKIP (-77)
+
+/* FIPS-204 ML-DSA-87 signature size, see include/crypto/mldsa.h. */
+#define MLDSA87_SIGNATURE_SIZE 4627
+
static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
const void *sig, __u32 sig_sz, __s32 keyring_id,
__u32 fd_array_cnt)
@@ -161,12 +167,30 @@ static int run_setup(const char *cmd, const char *dir)
}
if (waitpid(pid, &status, 0) < 0)
return -errno;
- return (WIFEXITED(status) &&
- WEXITSTATUS(status) == 0) ? 0 : -EINVAL;
+ if (!WIFEXITED(status))
+ return -EINVAL;
+ return -WEXITSTATUS(status);
}
-static int sign_buf(const char *dir, const void *buf, __u32 len,
- void *sig, __u32 *sig_sz)
+static void genkey_dir_fini(const char *dir)
+{
+ static const char * const files[] = {
+ "signing_key.der", "signing_key.pem", "x509.genkey",
+ };
+ char path[PATH_MAX];
+ size_t i;
+
+ if (!dir)
+ return;
+ for (i = 0; i < ARRAY_SIZE(files); i++) {
+ snprintf(path, sizeof(path), "%s/%s", dir, files[i]);
+ unlink(path);
+ }
+ rmdir(dir);
+}
+
+static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
+ void *sig, __u32 *sig_sz, const char *digest)
{
char data_tmpl[PATH_MAX], key[PATH_MAX];
char sigpath[PATH_MAX + sizeof(".p7s")];
@@ -195,7 +219,7 @@ static int sign_buf(const char *dir, const void *buf, __u32 len,
}
if (pid == 0) {
snprintf(key, sizeof(key), "%s/signing_key.pem", dir);
- execlp("./sign-file", "./sign-file", "-d", "sha256",
+ execlp("./sign-file", "./sign-file", "-d", digest,
key, key, data_tmpl, NULL);
exit(1);
}
@@ -233,6 +257,12 @@ static int sign_buf(const char *dir, const void *buf, __u32 len,
return ret;
}
+static int sign_buf(const char *dir, const void *buf, __u32 len,
+ void *sig, __u32 *sig_sz)
+{
+ return sign_buf_digest(dir, buf, len, sig, sig_sz, "sha256");
+}
+
struct gen_loader_fixture {
struct test_signed_loader *skel;
struct gen_loader_opts gopts;
@@ -1607,6 +1637,89 @@ static void loadtime_with_map(void)
test_signed_loader_map__destroy(skel);
}
+/*
+ * End-to-end signed load with a post-quantum key. ML-DSA (FIPS-204) is wired
+ * through the X.509 and PKCS#7 parsers, and BPF reaches them via
+ * verify_pkcs7_signature() without knowing the algorithm, so an ML-DSA key in
+ * the keyring should verify an ML-DSA signed program with no BPF-side work.
+ */
+static void mldsa_signed_load(void)
+{
+ char dir_tmpl[] = "/tmp/bpfmldsaXXXXXX";
+ int map_fd = -1, prog_fd = -1, err;
+ __u8 *sig = NULL, *buf = NULL;
+ struct gen_loader_fixture f;
+ bool have_fixture = false;
+ __u32 sig_sz = 16384;
+ char *dir;
+
+ syscall(__NR_request_key, "keyring", "_uid.0", NULL,
+ KEY_SPEC_SESSION_KEYRING);
+ dir = mkdtemp(dir_tmpl);
+ if (!ASSERT_OK_PTR(dir, "mkdtemp"))
+ return;
+
+ err = run_setup("setup-mldsa", dir);
+ if (err == SETUP_SKIP) {
+ printf("%s:SKIP:no working ML-DSA signing, set SELFTESTS_VERBOSE=1\n",
+ __func__);
+ test__skip();
+ genkey_dir_fini(dir);
+ return;
+ }
+ if (!ASSERT_OK(err, "verify_sig_setup setup-mldsa")) {
+ genkey_dir_fini(dir);
+ return;
+ }
+
+ sig = malloc(sig_sz);
+ if (!ASSERT_OK_PTR(sig, "sig buf"))
+ goto out;
+ have_fixture = true;
+ if (gen_loader_fixture_init(&f) != 0)
+ goto out;
+
+ buf = malloc((size_t)f.gopts.insns_sz + f.data_sz);
+ if (!ASSERT_OK_PTR(buf, "signbuf"))
+ goto out;
+ memcpy(buf, f.gopts.insns, f.gopts.insns_sz);
+ memcpy(buf + f.gopts.insns_sz, f.blob, f.data_sz);
+
+ /*
+ * ML-DSA hashes the message itself, but openssl before 4.0 cannot
+ * produce a CMS message without signedAttrs for it, and with those in
+ * play only SHA-512 is permitted for the messageDigest attribute.
+ */
+ if (!ASSERT_OK(sign_buf_digest(dir, buf, f.gopts.insns_sz + f.data_sz,
+ sig, &sig_sz, "sha512"),
+ "sign insns||metadata with ML-DSA"))
+ goto out;
+
+ /*
+ * Guard against the setup silently handing back some other key type:
+ * an RSA or ECDSA signature is a few hundred bytes, where an ML-DSA-87
+ * one cannot be smaller than the raw signature it carries.
+ */
+ ASSERT_GT(sig_sz, MLDSA87_SIGNATURE_SIZE, "ML-DSA-87 signature size");
+
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, KEY_SPEC_SESSION_KEYRING, 1);
+ ASSERT_OK_FD(prog_fd, "ML-DSA signed loader load");
+out:
+ if (prog_fd >= 0)
+ close(prog_fd);
+ if (map_fd >= 0)
+ close(map_fd);
+ if (have_fixture)
+ gen_loader_fixture_fini(&f);
+ free(buf);
+ free(sig);
+ run_setup("cleanup", dir);
+}
+
/*
* A signed program need not bind any map. A plain BPF_PROG_TYPE_SYSCALL
* program with no fd_array is signed over its instructions alone: the kernel
@@ -1890,6 +2003,8 @@ void test_signed_loader(void)
signature_bad_keyring();
if (test__start_subtest("bpf_keyring_sealed"))
bpf_keyring_sealed();
+ if (test__start_subtest("mldsa_signed_load"))
+ mldsa_signed_load();
if (test__start_subtest("metadata_ctx_max_entries_ignored"))
metadata_ctx_max_entries_ignored();
if (test__start_subtest("metadata_ctx_initial_value_ignored"))
diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
index 202e6e6418fe..ba5921e4b12d 100755
--- a/tools/testing/selftests/bpf/verify_sig_setup.sh
+++ b/tools/testing/selftests/bpf/verify_sig_setup.sh
@@ -28,7 +28,7 @@ authorityKeyIdentifier=keyid
usage()
{
- echo "Usage: $0 <setup-rsa|cleanup <existing_tmp_dir>"
+ echo "Usage: $0 <setup-rsa|setup-mldsa|cleanup <existing_tmp_dir>"
exit 1
}
@@ -57,6 +57,57 @@ setup_rsa()
keyctl link $key_id $keyring_id
}
+mldsa_supported()
+{
+ local tmp_dir="$1"
+
+ genkey_mldsa "${tmp_dir}" || return 1
+ : > ${tmp_dir}/probe
+ # Same digest as the caller signs with, see sign_buf_digest().
+ ./sign-file -d sha512 ${tmp_dir}/signing_key.pem \
+ ${tmp_dir}/signing_key.pem ${tmp_dir}/probe || return 1
+ rm -f ${tmp_dir}/probe ${tmp_dir}/probe.p7s
+}
+
+genkey_mldsa()
+{
+ local tmp_dir="$1"
+
+ echo "${x509_genkey_content}" > ${tmp_dir}/x509.genkey
+
+ # No -<digest> here: ML-DSA hashes the message itself, so openssl
+ # ignores an explicit digest for it.
+ openssl req -new -nodes -utf8 -days 36500 \
+ -batch -x509 -newkey ML-DSA-87 \
+ -config ${tmp_dir}/x509.genkey \
+ -outform PEM -out ${tmp_dir}/signing_key.pem \
+ -keyout ${tmp_dir}/signing_key.pem 2>&1
+
+ openssl x509 -in ${tmp_dir}/signing_key.pem -out \
+ ${tmp_dir}/signing_key.der -outform der
+}
+
+mldsa_skip()
+{
+ local tmp_dir="$1"
+
+ rm -f ${tmp_dir}/x509.genkey ${tmp_dir}/signing_key.pem \
+ ${tmp_dir}/signing_key.der ${tmp_dir}/probe \
+ ${tmp_dir}/probe.p7s
+ exit 77
+}
+
+setup_mldsa()
+{
+ local tmp_dir="$1"
+
+ mldsa_supported "${tmp_dir}" || mldsa_skip "${tmp_dir}"
+ key_id=$(cat ${tmp_dir}/signing_key.der |
+ keyctl padd asymmetric ebpf_testing_key @s)
+ keyring_id=$(keyctl newring ebpf_testing_keyring @s)
+ keyctl link $key_id $keyring_id
+}
+
cleanup() {
local tmp_dir="$1"
@@ -91,7 +142,7 @@ catch()
local exit_code="$1"
local log_file="$2"
- if [[ "${exit_code}" -ne 0 ]]; then
+ if [[ "${exit_code}" -ne 0 && "${exit_code}" -ne 77 ]]; then
cat "${log_file}" >&3
fi
@@ -110,6 +161,8 @@ main()
if [[ "${action}" == "setup-rsa" ]]; then
setup_rsa "${tmp_dir}"
+ elif [[ "${action}" == "setup-mldsa" ]]; then
+ setup_mldsa "${tmp_dir}"
elif [[ "${action}" == "genkey" ]]; then
genkey "${tmp_dir}"
elif [[ "${action}" == "cleanup" ]]; then
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
2026-08-28 17:52 ` [PATCH bpf-next v4 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
@ 2026-08-28 18:53 ` bot+bpf-ci
0 siblings, 0 replies; 21+ messages in thread
From: bot+bpf-ci @ 2026-08-28 18:53 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2419 bytes --]
> diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
> index 202e6e6418fe2..ba5921e4b12d2 100755
> --- a/tools/testing/selftests/bpf/verify_sig_setup.sh
> +++ b/tools/testing/selftests/bpf/verify_sig_setup.sh
[ ... ]
> @@ -57,6 +57,57 @@ setup_rsa()
> keyctl link $key_id $keyring_id
> }
>
> +mldsa_supported()
> +{
> + local tmp_dir="$1"
> +
> + genkey_mldsa "${tmp_dir}" || return 1
> + : > ${tmp_dir}/probe
> + # Same digest as the caller signs with, see sign_buf_digest().
> + ./sign-file -d sha512 ${tmp_dir}/signing_key.pem \
> + ${tmp_dir}/signing_key.pem ${tmp_dir}/probe || return 1
> + rm -f ${tmp_dir}/probe ${tmp_dir}/probe.p7s
> +}
> +
> +genkey_mldsa()
> +{
> + local tmp_dir="$1"
> +
> + echo "${x509_genkey_content}" > ${tmp_dir}/x509.genkey
> +
> + # No -<digest> here: ML-DSA hashes the message itself, so openssl
> + # ignores an explicit digest for it.
> + openssl req -new -nodes -utf8 -days 36500 \
> + -batch -x509 -newkey ML-DSA-87 \
> + -config ${tmp_dir}/x509.genkey \
> + -outform PEM -out ${tmp_dir}/signing_key.pem \
> + -keyout ${tmp_dir}/signing_key.pem 2>&1
> +
> + openssl x509 -in ${tmp_dir}/signing_key.pem -out \
> + ${tmp_dir}/signing_key.der -outform der
> +}
> +
> +mldsa_skip()
> +{
> + local tmp_dir="$1"
> +
> + rm -f ${tmp_dir}/x509.genkey ${tmp_dir}/signing_key.pem \
> + ${tmp_dir}/signing_key.der ${tmp_dir}/probe \
> + ${tmp_dir}/probe.p7s
> + exit 77
> +}
> +
> +setup_mldsa()
> +{
> + local tmp_dir="$1"
> +
> + mldsa_supported "${tmp_dir}" || mldsa_skip "${tmp_dir}"
> + key_id=$(cat ${tmp_dir}/signing_key.der |
> + keyctl padd asymmetric ebpf_testing_key @s)
> + keyring_id=$(keyctl newring ebpf_testing_keyring @s)
> + keyctl link $key_id $keyring_id
> +}
> +
This isn't a bug, but genkey_mldsa() and setup_mldsa() look like
near-verbatim copies of genkey() and setup_rsa(), with only the openssl req
flags differing for genkey_mldsa() (-sha256 dropped, -newkey ML-DSA-87
added) and the probe call for setup_mldsa(). Would passing the extra openssl
req flags and the keygen function name as parameters to the existing helpers
work here instead?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33197746599
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH bpf-next v4 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (7 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 17:52 ` [PATCH bpf-next v4 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
` (3 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
vmtest.sh currently hardcodes the guest command line, so there is no
way to ask for such a setting without editing the script. Append
$KERNEL_CMDLINE_EXTRA when set so it can be used for testing the
BPF keyring:
# KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1" \
./vmtest.sh -- ./test_progs -t signed_loader
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
tools/testing/selftests/bpf/vmtest.sh | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/bpf/vmtest.sh b/tools/testing/selftests/bpf/vmtest.sh
index 6a3d026d76bd..e7e0b419a0b8 100755
--- a/tools/testing/selftests/bpf/vmtest.sh
+++ b/tools/testing/selftests/bpf/vmtest.sh
@@ -107,6 +107,14 @@ Options:
-s) Instead of powering off the VM, start an interactive
shell. If <command> is specified, the shell runs after
the command finishes executing
+
+Environment variables:
+
+ KERNEL_CMDLINE_EXTRA
+ Extra arguments to append to the guest kernel command
+ line, for tests that need a boot-time setting. e.g:
+
+ KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1" $0 -- ./test_progs -t signed_loader
EOF
}
@@ -286,6 +294,12 @@ EOF
QEMU_FLAGS=("${HOST_FLAGS[@]}")
fi
+ local kernel_cmdline="root=/dev/vda rw console=${QEMU_CONSOLE}"
+
+ if [[ -n "${KERNEL_CMDLINE_EXTRA:-}" ]]; then
+ kernel_cmdline+=" ${KERNEL_CMDLINE_EXTRA}"
+ fi
+
${QEMU_BINARY} \
-nodefaults \
-display none \
@@ -294,7 +308,7 @@ EOF
-m 4G \
-drive file="${rootfs_img}",format=raw,index=1,media=disk,if=virtio,cache=none \
-kernel "${kernel_bzimage}" \
- -append "root=/dev/vda rw console=${QEMU_CONSOLE}"
+ -append "${kernel_cmdline}"
}
copy_logs()
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH bpf-next v4 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (8 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 18:53 ` bot+bpf-ci
2026-08-28 17:52 ` [PATCH bpf-next v4 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
` (2 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
bpf_keyring_provisioned walks the keyring through its whole lifecycle in
one boot for ease of testing. It enrolls a freshly generated key into the
bpf keyring, confirms a load is still refused with -ENOKEY while the keyring
carries no restriction, then restricts it, and only then does the same
signed BPF program load with the bpf keyring. A caller-supplied keyring is
asserted to be refused both before and after the restriction, since what
refuses it is bpf.keyring_unsealed=1 rather than the state of the keyring.
Unsealing is a boot-time decision which also refuses the caller-supplied
keyrings that most subtests here sign against, so each subtest is tagged
with the boot it needs and the ones which cannot run report as skipped
instead of being dropped.
Regular run:
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
[...]
#425/12 signed_loader/signature_zero_size:OK
#425/13 signed_loader/signature_bad_keyring:OK
#425/14 signed_loader/bpf_keyring_sealed:OK
[...]
#425/30 signed_loader/signed_map_by_fd_rejected:OK
#425/31 signed_loader/signed_sparse_fd_array_rejected:OK
#425/32 signed_loader/bpf_keyring_provisioned:SKIP
#425 signed_loader:OK (SKIP: 1/32)
Summary: 1/31 PASSED, 1 SKIPPED, 0/0 FAILED
Unsealed run:
# KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1" \
LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
#425/1 signed_loader/loadtime_no_map:SKIP
#425/2 signed_loader/loadtime_with_map:SKIP
#425/3 signed_loader/metadata_match:OK
[...]
#425/30 signed_loader/signed_map_by_fd_rejected:SKIP
#425/31 signed_loader/signed_sparse_fd_array_rejected:SKIP
#425/32 signed_loader/bpf_keyring_provisioned:OK
#425 signed_loader:OK (SKIP: 17/32)
Summary: 1/15 PASSED, 17 SKIPPED, 0/0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../selftests/bpf/prog_tests/signed_loader.c | 376 +++++++++++++-----
1 file changed, 287 insertions(+), 89 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 87f155de0e65..a0f93756e717 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -45,9 +45,9 @@ enum {
/* FIPS-204 ML-DSA-87 signature size, see include/crypto/mldsa.h. */
#define MLDSA87_SIGNATURE_SIZE 4627
-static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
- const void *sig, __u32 sig_sz, __s32 keyring_id,
- __u32 fd_array_cnt)
+static int load_loader_log(const void *insns, __u32 insns_sz, int map_fd,
+ const void *sig, __u32 sig_sz, __s32 keyring_id,
+ __u32 fd_array_cnt, char *log_buf, __u32 log_sz)
{
union bpf_attr attr;
int fd;
@@ -59,18 +59,31 @@ static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
attr.license = ptr_to_u64("Dual BSD/GPL");
attr.prog_flags = BPF_F_SLEEPABLE;
attr.fd_array = ptr_to_u64(&map_fd);
+ attr.fd_array_cnt = fd_array_cnt;
if (sig) {
attr.signature = ptr_to_u64(sig);
attr.signature_size = sig_sz;
attr.keyring_id = keyring_id;
}
- attr.fd_array_cnt = fd_array_cnt;
+ if (log_buf) {
+ attr.log_level = 1;
+ attr.log_buf = ptr_to_u64(log_buf);
+ attr.log_size = log_sz;
+ }
memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
offsetofend(union bpf_attr, keyring_id));
return fd < 0 ? -errno : fd;
}
+static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
+ const void *sig, __u32 sig_sz, __s32 keyring_id,
+ __u32 fd_array_cnt)
+{
+ return load_loader_log(insns, insns_sz, map_fd, sig, sig_sz, keyring_id,
+ fd_array_cnt, NULL, 0);
+}
+
static int run_gen_loader(const void *insns, __u32 insns_sz,
const void *data, __u32 data_sz,
const void *excl, __u32 excl_sz,
@@ -205,6 +218,7 @@ static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
fd = mkstemp(data_tmpl);
if (fd < 0)
return -errno;
+ snprintf(sigpath, sizeof(sigpath), "%s.p7s", data_tmpl);
if (write(fd, buf, len) != (ssize_t)len) {
close(fd);
ret = -EIO;
@@ -229,30 +243,28 @@ static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
goto out;
}
- snprintf(sigpath, sizeof(sigpath), "%s.p7s", data_tmpl);
if (stat(sigpath, &st) < 0) {
ret = -errno;
goto out;
}
if (st.st_size > (off_t)*sig_sz) {
ret = -E2BIG;
- goto out_sig;
+ goto out;
}
fd = open(sigpath, O_RDONLY);
if (fd < 0) {
ret = -errno;
- goto out_sig;
+ goto out;
}
if (read(fd, sig, st.st_size) != st.st_size) {
close(fd);
ret = -EIO;
- goto out_sig;
+ goto out;
}
close(fd);
*sig_sz = st.st_size;
-out_sig:
- unlink(sigpath);
out:
+ unlink(sigpath);
unlink(data_tmpl);
return ret;
}
@@ -564,7 +576,6 @@ static void signature_failure_logs(void)
static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, };
char log_buf[1024] = {};
struct gen_loader_fixture f;
- union bpf_attr attr;
int fd;
if (gen_loader_fixture_init(&f) == 0) {
@@ -573,22 +584,9 @@ static void signature_failure_logs(void)
* failure is reported through the verifier log. A present-but-
* invalid signature is rejected and the log says why.
*/
- memset(&attr, 0, sizeof(attr));
- attr.prog_type = BPF_PROG_TYPE_SYSCALL;
- attr.insns = ptr_to_u64(f.gopts.insns);
- attr.insn_cnt = f.gopts.insns_sz / sizeof(struct bpf_insn);
- attr.license = ptr_to_u64("Dual BSD/GPL");
- attr.prog_flags = BPF_F_SLEEPABLE;
- attr.signature = ptr_to_u64(junk);
- attr.signature_size = sizeof(junk);
- attr.keyring_id = KEY_SPEC_SESSION_KEYRING;
- attr.log_level = 1;
- attr.log_buf = ptr_to_u64(log_buf);
- attr.log_size = sizeof(log_buf);
- memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
-
- fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
- offsetofend(union bpf_attr, keyring_id));
+ fd = load_loader_log(f.gopts.insns, f.gopts.insns_sz, -1, junk,
+ sizeof(junk), KEY_SPEC_SESSION_KEYRING, 0,
+ log_buf, sizeof(log_buf));
ASSERT_LT(fd, 0, "invalid signature rejected at load");
if (fd >= 0)
close(fd);
@@ -708,6 +706,23 @@ static int bpf_keyring_lookup(int *nr_keys)
return serial;
}
+static long keyctl_ret(int cmd, unsigned long arg2, unsigned long arg3)
+{
+ long ret = syscall(__NR_keyctl, cmd, arg2, arg3);
+
+ return ret < 0 ? -errno : ret;
+}
+
+/*
+ * What the bpf keyring still needs once it got provisioned: KEY_POS_SEARCH
+ * for the in-kernel search during verification, and the user view/read bits
+ * so it stays visible in /proc/keys, rest is dropped so the enrolled is
+ * therefore final.
+ */
+#define BPF_KEYRING_PERM_LOCKED 0x08030000
+/* What bpf_keyring_init() grants at boot. */
+#define BPF_KEYRING_PERM_INITIAL 0x082f0000
+
static void bpf_keyring_sealed(void)
{
static const __u8 junk[64] = {};
@@ -721,6 +736,9 @@ static void bpf_keyring_sealed(void)
}
serial = bpf_keyring_lookup(NULL);
if (serial >= 0) {
+ ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID,
+ KEY_SPEC_BPF_KEYRING, 0), serial,
+ "KEY_SPEC_BPF_KEYRING resolves to the bpf keyring");
key = syscall(__NR_add_key, "user", "sealprobe", "x", 1,
KEY_SPEC_BPF_KEYRING);
if (key >= 0)
@@ -739,6 +757,183 @@ static void bpf_keyring_sealed(void)
gen_loader_fixture_fini(&f);
}
+static int try_load(const struct gen_loader_fixture *f, const void *sig,
+ __u32 sig_sz, __s32 keyring_id, char *log_buf, __u32 log_sz)
+{
+ int map_fd, prog_fd;
+
+ map_fd = setup_meta_map(f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map"))
+ return map_fd;
+ prog_fd = load_loader_log(f->gopts.insns, f->gopts.insns_sz, map_fd,
+ sig, sig_sz, keyring_id, 1, log_buf, log_sz);
+ close(map_fd);
+ if (prog_fd >= 0)
+ close(prog_fd);
+ return prog_fd;
+}
+
+/*
+ * This needs bpf.keyring_unsealed=1 on the guest kernel command line, which
+ * vmtest.sh can pass via KERNEL_CMDLINE_EXTRA. There is no way to unseal the
+ * keyring from here, so without it the test skips. It also only works once
+ * per boot, as restricting a keyring cannot be undone.
+ */
+static void bpf_keyring_provisioned(void)
+{
+ char dir_tmpl[] = "/tmp/bpfkeyringXXXXXX";
+ char bad_tmpl[] = "/tmp/bpfkeyringbadXXXXXX";
+ __u8 *sig = NULL, *bad = NULL, *buf = NULL;
+ int serial, err;
+ int nr_keys = 0, der_fd = -1;
+ struct gen_loader_fixture f;
+ __u32 sig_sz = 8192, bad_sz;
+ bool have_fixture = false;
+ char *dir, *bad_dir = NULL;
+ char log_buf[1024] = {};
+ char path[PATH_MAX];
+ __u8 der[4096];
+ ssize_t der_sz;
+
+ serial = bpf_keyring_lookup(&nr_keys);
+ if (serial < 0) {
+ printf("%s:SKIP:no bpf keyring (needs CONFIG_KEYS)\n", __func__);
+ test__skip();
+ return;
+ }
+ if (nr_keys != 0) {
+ printf("%s:SKIP:the bpf keyring has already been provisioned\n",
+ __func__);
+ test__skip();
+ return;
+ }
+
+ dir = mkdtemp(dir_tmpl);
+ if (!ASSERT_OK_PTR(dir, "mkdtemp"))
+ return;
+ if (!ASSERT_OK(run_setup("genkey", dir), "verify_sig_setup genkey"))
+ goto rmdir;
+
+ snprintf(path, sizeof(path), "%s/signing_key.der", dir);
+ der_fd = open(path, O_RDONLY);
+ if (!ASSERT_OK_FD(der_fd, "open signing_key.der"))
+ goto rmdir;
+ der_sz = read(der_fd, der, sizeof(der));
+ close(der_fd);
+ if (!ASSERT_GT(der_sz, 0, "read signing_key.der"))
+ goto rmdir;
+
+ ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID, KEY_SPEC_BPF_KEYRING, 0),
+ serial, "KEY_SPEC_BPF_KEYRING resolves to the bpf keyring");
+
+ err = syscall(__NR_add_key, "asymmetric", "", der, (size_t)der_sz,
+ KEY_SPEC_BPF_KEYRING);
+ if (err < 0 && errno == EPERM) {
+ printf("%s:SKIP:the bpf keyring is sealed, need bpf.keyring_unsealed=1\n",
+ __func__);
+ test__skip();
+ goto rmdir;
+ }
+ if (!ASSERT_GE(err, 0, "add the signing key to the bpf keyring"))
+ goto rmdir;
+
+ sig = malloc(sig_sz);
+ if (!ASSERT_OK_PTR(sig, "sig buf"))
+ goto out;
+ have_fixture = true;
+ if (gen_loader_fixture_init(&f) != 0)
+ goto out;
+
+ buf = malloc((size_t)f.gopts.insns_sz + f.data_sz);
+ if (!ASSERT_OK_PTR(buf, "signbuf"))
+ goto out;
+ memcpy(buf, f.gopts.insns, f.gopts.insns_sz);
+ memcpy(buf + f.gopts.insns_sz, f.blob, f.data_sz);
+ if (!ASSERT_OK(sign_buf(dir, buf, f.gopts.insns_sz + f.data_sz, sig,
+ &sig_sz), "sign insns||metadata"))
+ goto out;
+
+ ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0),
+ -ENOKEY, "unrestricted keyring still not consulted");
+
+ ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, NULL, 0),
+ -EPERM, "caller-supplied keyring refused before provisioning");
+
+ if (!ASSERT_OK(syscall(__NR_keyctl, KEYCTL_RESTRICT_KEYRING,
+ KEY_SPEC_BPF_KEYRING, NULL, NULL),
+ "restrict bpf keyring"))
+ goto out;
+
+ if (!ASSERT_OK_FD(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING,
+ NULL, 0),
+ "load signed by a key in the .bpf keyring"))
+ goto out;
+
+ bad_dir = mkdtemp(bad_tmpl);
+ if (!ASSERT_OK_PTR(bad_dir, "mkdtemp unenrolled"))
+ goto out;
+ if (!ASSERT_OK(run_setup("genkey", bad_dir), "verify_sig_setup genkey unenrolled"))
+ goto out;
+ bad_sz = 8192;
+ bad = malloc(bad_sz);
+ if (!ASSERT_OK_PTR(bad, "bad sig buf"))
+ goto out;
+ if (!ASSERT_OK(sign_buf(bad_dir, buf, f.gopts.insns_sz + f.data_sz, bad,
+ &bad_sz), "sign with an unenrolled key"))
+ goto out;
+
+ ASSERT_EQ(try_load(&f, bad, bad_sz, KEY_SPEC_BPF_KEYRING, log_buf,
+ sizeof(log_buf)), -ENOKEY,
+ "key outside the bpf keyring refused");
+ ASSERT_HAS_SUBSTR(log_buf, "signature verification failed",
+ "the bpf keyring was consulted");
+
+ f.blob[0] ^= 0xff;
+ err = try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0);
+ f.blob[0] ^= 0xff;
+ ASSERT_EQ(err, -EKEYREJECTED, "tampered metadata refused");
+
+ ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, NULL, 0),
+ -EPERM, "caller-supplied keyring refused once .bpf is in use");
+
+ err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
+ ASSERT_EQ(err, -ENOENT, "keyring writable while the user bits are there");
+
+ err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_LOCKED);
+ if (!ASSERT_OK(err, "drop the user bits on the bpf keyring"))
+ goto out;
+
+ ASSERT_OK_FD(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0),
+ "load still verified against the locked keyring");
+ ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID, KEY_SPEC_BPF_KEYRING, 0),
+ -EACCES, "the special id grants no rights of its own");
+
+ err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
+ ASSERT_EQ(err, -EACCES, "unlink refused");
+ err = keyctl_ret(KEYCTL_CLEAR, serial, 0);
+ ASSERT_EQ(err, -EACCES, "clear refused");
+ err = keyctl_ret(KEYCTL_REVOKE, serial, 0);
+ ASSERT_EQ(err, -EACCES, "revoke refused");
+ err = keyctl_ret(KEYCTL_INVALIDATE, serial, 0);
+ ASSERT_EQ(err, -EACCES, "invalidate refused");
+ err = keyctl_ret(KEYCTL_SET_TIMEOUT, serial, 1);
+ ASSERT_EQ(err, -EACCES, "timeout refused");
+ err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_INITIAL);
+ ASSERT_EQ(err, -EACCES, "the bits cannot be granted back");
+
+ ASSERT_EQ(bpf_keyring_lookup(&nr_keys), serial, "keyring still there");
+ ASSERT_EQ(nr_keys, 1, "the enrolled key survived");
+out:
+ if (have_fixture)
+ gen_loader_fixture_fini(&f);
+ genkey_dir_fini(bad_dir);
+ free(buf);
+ free(bad);
+ free(sig);
+rmdir:
+ genkey_dir_fini(dir);
+}
+
/*
* A signed loader must ignore ctx-supplied map dimensions: the host cannot
* resize a signed program's maps via the loader ctx. Drive a one-map program
@@ -1973,68 +2168,71 @@ static void signed_module_kfunc_rejected(void)
run_setup("cleanup", dir);
}
+enum subtest_boot {
+ BOOT_ANY,
+ BOOT_SEALED,
+ BOOT_UNSEALED,
+};
+
+static const struct {
+ const char *name;
+ void (*fn)(void);
+ enum subtest_boot boot;
+} subtests[] = {
+ { "loadtime_no_map", loadtime_no_map, BOOT_SEALED },
+ { "loadtime_with_map", loadtime_with_map, BOOT_SEALED },
+ { "metadata_match", metadata_match, BOOT_ANY },
+ { "signature_enforced", signature_enforced, BOOT_SEALED },
+ { "signed_nonexcl_fd_array_rejected", signed_nonexcl_fd_array_rejected, BOOT_SEALED },
+ { "signed_unfrozen_fd_array_rejected", signed_unfrozen_fd_array_rejected, BOOT_SEALED },
+ { "signed_nonarray_fd_array_rejected", signed_nonarray_fd_array_rejected, BOOT_SEALED },
+ { "signed_btf_fd_array_rejected", signed_btf_fd_array_rejected, BOOT_ANY },
+ { "signed_module_kfunc_rejected", signed_module_kfunc_rejected, BOOT_SEALED },
+ { "signature_failure_logs", signature_failure_logs, BOOT_SEALED },
+ { "signature_too_large", signature_too_large, BOOT_ANY },
+ { "signature_zero_size", signature_zero_size, BOOT_ANY },
+ { "signature_bad_keyring", signature_bad_keyring, BOOT_SEALED },
+ { "bpf_keyring_sealed", bpf_keyring_sealed, BOOT_ANY },
+ { "mldsa_signed_load", mldsa_signed_load, BOOT_SEALED },
+ { "metadata_ctx_max_entries_ignored", metadata_ctx_max_entries_ignored, BOOT_ANY },
+ { "metadata_ctx_initial_value_ignored", metadata_ctx_initial_value_ignored, BOOT_ANY },
+ { "signature_authenticates_insns", signature_authenticates_insns, BOOT_SEALED },
+ { "signature_authenticates_metadata", signature_authenticates_metadata, BOOT_SEALED },
+ { "hash_requires_frozen", hash_requires_frozen, BOOT_ANY },
+ { "no_update_after_freeze", no_update_after_freeze, BOOT_ANY },
+ { "freeze_writable_mmap", freeze_writable_mmap, BOOT_ANY },
+ { "no_writable_mmap_frozen", no_writable_mmap_frozen, BOOT_ANY },
+ { "map_hash_matches_libbpf", map_hash_matches_libbpf, BOOT_ANY },
+ { "map_hash_multi_element", map_hash_multi_element, BOOT_ANY },
+ { "map_hash_bad_size", map_hash_bad_size, BOOT_ANY },
+ { "map_hash_unsupported_type", map_hash_unsupported_type, BOOT_ANY },
+ { "lsm_signature_verdict", lsm_signature_verdict, BOOT_SEALED },
+ { "signed_no_fd_array", signed_no_fd_array, BOOT_SEALED },
+ { "signed_map_by_fd_rejected", signed_map_by_fd_rejected, BOOT_SEALED },
+ { "signed_sparse_fd_array_rejected", signed_sparse_fd_array_rejected, BOOT_SEALED },
+ { "bpf_keyring_provisioned", bpf_keyring_provisioned, BOOT_UNSEALED },
+};
+
void test_signed_loader(void)
{
- if (test__start_subtest("loadtime_no_map"))
- loadtime_no_map();
- if (test__start_subtest("loadtime_with_map"))
- loadtime_with_map();
- if (test__start_subtest("metadata_match"))
- metadata_match();
- if (test__start_subtest("signature_enforced"))
- signature_enforced();
- if (test__start_subtest("signed_nonexcl_fd_array_rejected"))
- signed_nonexcl_fd_array_rejected();
- if (test__start_subtest("signed_unfrozen_fd_array_rejected"))
- signed_unfrozen_fd_array_rejected();
- if (test__start_subtest("signed_nonarray_fd_array_rejected"))
- signed_nonarray_fd_array_rejected();
- if (test__start_subtest("signed_btf_fd_array_rejected"))
- signed_btf_fd_array_rejected();
- if (test__start_subtest("signed_module_kfunc_rejected"))
- signed_module_kfunc_rejected();
- if (test__start_subtest("signature_failure_logs"))
- signature_failure_logs();
- if (test__start_subtest("signature_too_large"))
- signature_too_large();
- if (test__start_subtest("signature_zero_size"))
- signature_zero_size();
- if (test__start_subtest("signature_bad_keyring"))
- signature_bad_keyring();
- if (test__start_subtest("bpf_keyring_sealed"))
- bpf_keyring_sealed();
- if (test__start_subtest("mldsa_signed_load"))
- mldsa_signed_load();
- if (test__start_subtest("metadata_ctx_max_entries_ignored"))
- metadata_ctx_max_entries_ignored();
- if (test__start_subtest("metadata_ctx_initial_value_ignored"))
- metadata_ctx_initial_value_ignored();
- if (test__start_subtest("signature_authenticates_insns"))
- signature_authenticates_insns();
- if (test__start_subtest("signature_authenticates_metadata"))
- signature_authenticates_metadata();
- if (test__start_subtest("hash_requires_frozen"))
- hash_requires_frozen();
- if (test__start_subtest("no_update_after_freeze"))
- no_update_after_freeze();
- if (test__start_subtest("freeze_writable_mmap"))
- freeze_writable_mmap();
- if (test__start_subtest("no_writable_mmap_frozen"))
- no_writable_mmap_frozen();
- if (test__start_subtest("map_hash_matches_libbpf"))
- map_hash_matches_libbpf();
- if (test__start_subtest("map_hash_multi_element"))
- map_hash_multi_element();
- if (test__start_subtest("map_hash_bad_size"))
- map_hash_bad_size();
- if (test__start_subtest("map_hash_unsupported_type"))
- map_hash_unsupported_type();
- if (test__start_subtest("lsm_signature_verdict"))
- lsm_signature_verdict();
- if (test__start_subtest("signed_no_fd_array"))
- signed_no_fd_array();
- if (test__start_subtest("signed_map_by_fd_rejected"))
- signed_map_by_fd_rejected();
- if (test__start_subtest("signed_sparse_fd_array_rejected"))
- signed_sparse_fd_array_rejected();
+ bool unsealed = keyring_unsealed_boot();
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(subtests); i++) {
+ if (!test__start_subtest(subtests[i].name))
+ continue;
+ if (subtests[i].boot == BOOT_SEALED && unsealed) {
+ printf("%s:SKIP:needs a boot without bpf.keyring_unsealed=1\n",
+ subtests[i].name);
+ test__skip();
+ continue;
+ }
+ if (subtests[i].boot == BOOT_UNSEALED && !unsealed) {
+ printf("%s:SKIP:needs bpf.keyring_unsealed=1\n",
+ subtests[i].name);
+ test__skip();
+ continue;
+ }
+ subtests[i].fn();
+ }
}
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
2026-08-28 17:52 ` [PATCH bpf-next v4 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
@ 2026-08-28 18:53 ` bot+bpf-ci
0 siblings, 0 replies; 21+ messages in thread
From: bot+bpf-ci @ 2026-08-28 18:53 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2933 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 87f155de0e656..a0f93756e717b 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
[ ... ]
> +enum subtest_boot {
> + BOOT_ANY,
> + BOOT_SEALED,
> + BOOT_UNSEALED,
> +};
> +
> +static const struct {
> + const char *name;
> + void (*fn)(void);
> + enum subtest_boot boot;
> +} subtests[] = {
> + { "loadtime_no_map", loadtime_no_map, BOOT_SEALED },
> + { "loadtime_with_map", loadtime_with_map, BOOT_SEALED },
> + { "metadata_match", metadata_match, BOOT_ANY },
[ ... ]
> + { "signed_sparse_fd_array_rejected", signed_sparse_fd_array_rejected, BOOT_SEALED },
> + { "bpf_keyring_provisioned", bpf_keyring_provisioned, BOOT_UNSEALED },
> +};
> +
> void test_signed_loader(void)
> {
> + bool unsealed = keyring_unsealed_boot();
> + unsigned int i;
> +
> + for (i = 0; i < ARRAY_SIZE(subtests); i++) {
> + if (!test__start_subtest(subtests[i].name))
> + continue;
> + if (subtests[i].boot == BOOT_SEALED && unsealed) {
> + printf("%s:SKIP:needs a boot without bpf.keyring_unsealed=1\n",
> + subtests[i].name);
> + test__skip();
> + continue;
> + }
> + if (subtests[i].boot == BOOT_UNSEALED && !unsealed) {
> + printf("%s:SKIP:needs bpf.keyring_unsealed=1\n",
> + subtests[i].name);
> + test__skip();
> + continue;
> + }
> + subtests[i].fn();
> + }
> }
The commit message documents running './test_progs -t signed_loader' with
KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1", making the unsealed boot a
supported configuration. Does this need to account for other signed light
skeleton tests in the same binary?
tools/testing/selftests/bpf/Makefile lists three signed light skeletons:
LSKELS_SIGNED := fentry_test.c fexit_test.c atomics.c
Each hard-asserts successful load with a caller-supplied keyring:
prog_tests/atomics.c:
skel->keyring_id = KEY_SPEC_SESSION_KEYRING;
err = atomics_lskel__load(skel);
if (!ASSERT_OK(err, "atomics skeleton load"))
prog_tests/fentry_test.c, prog_tests/fexit_test.c, and
prog_tests/fentry_fexit.c have the same pattern. When the kernel is booted
with bpf.keyring_unsealed=1, bpf_prog_verify_signature() refuses
caller-supplied keyrings (-EPERM), so those tests fail rather than skip.
A full './test_progs' (without '-t signed_loader') under the unsealed boot
would report failures for atomics, fentry_test, fexit_test, and
fentry_fexit. Is the unsealed boot meant only for '-t signed_loader', or
should those other signed-lskel tests check keyring_unsealed_boot() and
skip when appropriate?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33197746599
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH bpf-next v4 11/11] Documentation/bpf: Document the bpf keyring and improve examples
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (9 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
@ 2026-08-28 17:52 ` Daniel Borkmann
2026-08-28 18:53 ` bot+bpf-ci
2026-08-30 1:20 ` [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support patchwork-bot+netdevbpf
2026-08-31 10:05 ` Christian Brauner
12 siblings, 1 reply; 21+ messages in thread
From: Daniel Borkmann @ 2026-08-28 17:52 UTC (permalink / raw)
To: memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings
Key generation is detailled for RSA and ML-DSA, the load example sets
keyring_id to the bpf keyring with the session keyring shown only as
the staging variant, and the LSM admission example anchors on the bpf
keyring while allowlisting staged serials rather than treating a user
keyring as ordinary trust.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
Documentation/bpf/signing.rst | 283 +++++++++++++++++++++++++++++-----
1 file changed, 246 insertions(+), 37 deletions(-)
diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
index e73eaaebd8b1..27fdad3df96f 100644
--- a/Documentation/bpf/signing.rst
+++ b/Documentation/bpf/signing.rst
@@ -254,21 +254,24 @@ 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.)
+A more realistic admission policy than "is it signed at all": base trust in
+the bpf keyring or an explicit allowlist of a caller-supplied staging key/
+keyring, and emit a record of every decision including denied attempts.
+(Illustrative - error checking elided.)
.. code-block:: c
- /* Serials of user keys/keyrings we additionally trust. */
+ /*
+ * Serials of caller-supplied keyrings we are willing to stage. Empty
+ * on a system that has committed to the bpf keyring, where the kernel
+ * refuses them anyway.
+ */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __s32); /* keyring_serial */
__type(value, __u8);
__uint(max_entries, 64);
- } trusted_user_keys SEC(".maps");
+ } staging_keys SEC(".maps");
/* Audit stream consumed by a userspace logger. */
struct {
@@ -291,11 +294,18 @@ auditable. (Illustrative - error checking elided.)
if (kernel)
return 0; /* trust in-kernel loads */
- if (verdict != BPF_SIG_VERIFIED)
+ 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 */
+ } else switch (ktype) {
+ case BPF_SIG_KEYRING_BPF:
+ break;
+ case BPF_SIG_KEYRING_USER:
+ if (!bpf_map_lookup_elem(&staging_keys, &serial))
+ ret = -EPERM;
+ break;
+ default:
+ ret = -EPERM; /* keyring not in policy */
+ }
d = bpf_ringbuf_reserve(&audit, sizeof(*d), 0);
if (d) {
@@ -309,6 +319,11 @@ auditable. (Illustrative - error checking elided.)
return ret;
}
+Such a policy is what makes a caller-supplied keyring usable at all on a system
+that does not boot with ``bpf.keyring_unsealed=1``: the allowlist bounds which
+staged keys count, and the LSM itself has to protect them from being tampered
+with.
+
Observing a verified load: ``security_bpf_prog()``
--------------------------------------------------
@@ -381,8 +396,9 @@ 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
+let a policy distinguish "verified against the operator's bpf keyring" from
+"verified against a keyring the loader supplied itself", which is the
+distinction that matters most. A policy LSM such as IPE could consume the same
hooks to enforce system policy without writing any BPF, though none implements
this today.
@@ -390,33 +406,158 @@ 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.
+against. Four values are well-known; anything else is resolved as a caller-
+supplied key or keyring, named either by its serial or by one of the other
+``KEY_SPEC_*`` ids:
+
+.. list-table::
+ :header-rows: 1
+
+ * - ``keyring_id``
+ - Keyring
+ * - ``0``
+ - builtin trusted keyring
+ * - ``VERIFY_USE_SECONDARY_KEYRING`` (``1``)
+ - secondary trusted keyring
+ * - ``VERIFY_USE_PLATFORM_KEYRING`` (``2``)
+ - platform keyring
+ * - ``KEY_SPEC_BPF_KEYRING`` (``-9``)
+ - the bpf keyring
+ * - anything else
+ - a caller-supplied user/session key or keyring
+
+The keyring is resolved first, before the signature bytes are examined, so a
+signature naming a keyring that cannot be used is rejected up front, and a
+failed verification aborts the load - a program that loads successfully with
+a signature therefore always has consistent keyring fields recorded.
+
+The bpf keyring
+---------------
+
+A system keyring needs a kernel rebuild or a vouched-for enrollment to rotate a
+key, and grants BPF-signing trust to keys trusted for everything else in the
+kernel too. A caller-supplied keyring, at the other extreme, is filled by the
+very process that loads the program and so carries no trust of its own.
+
+The bpf keyring fills that gap and is the trust anchor which a signed BPF
+deployment should be built on top of: a keyring named ``.bpf``, selected with
+``KEY_SPEC_BPF_KEYRING``, that an operator provisions at boot with a key
+scoped to BPF program loading and nothing else in the kernel's trust hierarchy.
+It is owned by the operator rather than by the loader, and rotatable across a
+reboot without touching the kernel image. It is modelled after the dm-verity
+keyring (see ``dm_verity.keyring_unsealed=``) and provisioned the same way: an
+initrd runs the ``keyctl`` steps below before handing off to the rootfs.
+
+Provisioning
+~~~~~~~~~~~~
+
+The keyring is created during ``late_initcall`` and is **sealed empty** by
+default: it carries a reject-all restriction, so no key can ever be added and
+``KEY_SPEC_BPF_KEYRING`` fails with ``-ENOKEY`` for the whole boot.
+
+``bpf.keyring_unsealed=1`` leaves it unrestricted at init so the initrd can
+provision it. The keyring is not linked into any process keyring, but the
+``KEY_SPEC_BPF_KEYRING`` special key id addresses it directly, so its serial
+does not have to be looked up first. Steps would be as follows::
+
+ keyctl padd asymmetric "" -9 < signing_key.der
+ keyctl restrict_keyring -9
+
+Both steps are required: the keyring is consulted only once it is **non-empty
+and restricted**. An unrestricted keyring is ignored even when it holds keys,
+so a half-provisioned keyring is inert rather than a weaker trust anchor, and a
+load naming it fails with ``-ENOKEY`` and a verifier log. Restricting cannot
+be undone.
+
+More than one key is enrolled by repeating the ``keyctl padd`` step; the
+restriction is applied once, after the last of them::
+
+ for key in /etc/bpf/keys/*.der; do
+ keyctl padd asymmetric "" -9 < $key
+ done
+
+ keyctl restrict_keyring -9
+ keyctl show -9
+
+The restriction bounds what can be added, never what can be taken away. A key
+that is already enrolled can still be unlinked, and the keyring cleared or
+revoked, by anything running as root. That does not weaken the anchor, since
+a keyring left empty is no longer consulted and a load naming it fails with
+``-ENOKEY``, but it does take signed loading out until the next boot. Dropping
+the user permissions the keyring no longer needs would close that; as a third
+step in the initrd::
+
+ keyctl setperm -9 0x08030000
+
+What remains is ``KEY_POS_SEARCH`` for the in-kernel search during verification,
+plus ``KEY_USR_VIEW`` and ``KEY_USR_READ`` so the keyring stays visible in
+``/proc/keys``. ``KEY_SPEC_BPF_KEYRING`` names the keyring but conveys no rights
+of its own - the keyring is not possessed by anyone, so the ``KEY_POS_*`` bits
+are only ever reached by the in-kernel search, and userspace is left with what
+the ``KEY_USR_*`` bits grant. Once they are dropped, addressing it by the serial
+``/proc/keys`` reports gets no further than the special id does.
+
+Provisioning has to complete before control passes to the rootfs. The keyring
+takes any number of keys for as long as it is unrestricted, so it is the
+restriction that bounds the enrolled set, not the first enrollment. Before
+the initrd hands off control, it must therefore restrict the keyring after
+the last enrollment.
+
+Enforcement
+~~~~~~~~~~~
+
+``bpf.keyring_unsealed=1`` states that the bpf keyring is *the* trust anchor for
+this boot, so it does more than unseal. From the first program load onwards a
+caller-supplied user/session keyring is refused with ``-EPERM`` and a verifier
+log message, whether or not provisioning ever completed. The system keyrings
+stay selectable.
+
+Enforcement is readable at ``/sys/module/bpf/parameters/keyring_unsealed``, and
+read-only there: the flag is ``__ro_after_init`` behind a 0444 parameter. It is
+also derived from the boot flag rather than from the keyring's runtime state,
+so there is no window early in boot during which a caller-supplied keyring is
+still accepted.
+
+Caller-supplied keyrings are for staging
+----------------------------------------
+
+A ``keyring_id`` naming a user or session key or keyring is a *staging*
+mechanism, not a trust anchor: it is filled by the same userspace that loads the
+program, so verifying against it establishes only that the loader signed what it
+loaded. Its purpose is to let software installed onto a running system - whose
+signing key is not enrolled anywhere yet - run signed until that key reaches the
+bpf keyring on the next boot.
+
+A system that has committed to the bpf keyring refuses this path outright (see
+`Enforcement`_). A system that has not can still allow it, but a policy must
+never treat ``BPF_SIG_KEYRING_USER`` as equivalent to the bpf or system
+keyrings; it should allowlist the specific serials it is willing to stage and
+pair that with a BPF LSM policy protecting those keys from tampering, as in
+`Enforcement via LSMs`_.
+
+Recorded fields
+---------------
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.
+ keyrings, ``_BPF`` for the bpf keyring, or ``_USER`` for a caller-supplied
+ 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
+ **caller-supplied 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".
+ serial. The system keyrings and the bpf keyring are trusted as a whole and
+ expose no serial here, so the serial is ``0`` for them, and ``0`` for
+ unsigned programs. A non-zero ``keyring_serial`` is therefore exactly
+ "verified against the caller-supplied key/keyring with this serial", which
+ is exactly the case a policy has to scrutinise.
.. list-table::
:header-rows: 1
@@ -436,16 +577,51 @@ Two fields are recorded in ``prog->aux->sig`` for an LSM to inspect:
* - ``VERIFY_USE_PLATFORM_KEYRING``
- ``BPF_SIG_KEYRING_PLATFORM``
- ``0``
- * - other (a user/session key serial)
+ * - ``KEY_SPEC_BPF_KEYRING``
+ - ``BPF_SIG_KEYRING_BPF``
+ - ``0``
+ * - other (a caller-supplied key serial)
- ``BPF_SIG_KEYRING_USER``
- serial of the resolved key/keyring
-Producing a signed object
-==========================
+Producing and loading a signed object
+=====================================
+
+Generating a signing key
+------------------------
+
+Signing is algorithm agnostic: the algorithm comes from the X.509 certificate
+and the PKCS#7 ``SignerInfo``. Anything the X.509 and PKCS#7 parsers understand
+works with no BPF-side change. Both examples below read the certificate request
+config from ``x509.genkey``, for which ``certs/x509.genkey`` serves as a
+template (see Documentation/admin-guide/module-signing.rst). RSA::
+
+ openssl req -new -nodes -utf8 -sha256 -days 36500 -batch -x509 \
+ -config x509.genkey -outform PEM \
+ -out signing_key.pem -keyout signing_key.pem
+ openssl x509 -in signing_key.pem -outform der -out signing_key.der
+
+ML-DSA-87 (FIPS-204), which needs openssl 3.5 or later, and both
+``CONFIG_CRYPTO_MLDSA`` and ``CONFIG_CRYPTO_SHA512`` in the kernel - the latter
+for the signedAttrs digest described below, which ``CONFIG_CRYPTO_MLDSA`` does
+not select. Note the absence of a digest option: ML-DSA hashes the message
+itself, so openssl ignores an explicit digest for it::
+
+ openssl req -new -nodes -utf8 -days 36500 -batch -x509 \
+ -newkey ML-DSA-87 -config x509.genkey -outform PEM \
+ -out signing_key.pem -keyout signing_key.pem
+ openssl x509 -in signing_key.pem -outform der -out signing_key.der
+
+``bpftool`` handles the following internally: openssl before 4.0 cannot combine
+ML-DSA with ``CMS_NOATTR``, so it falls back to signedAttrs, where only SHA-512
+is permitted. This mirrors what module signing does as well.
+
+Signing
+-------
``bpftool`` generates and signs a light skeleton in one step::
- bpftool gen skeleton -L -S -k <private_key.pem> -i <certificate.x509> \
+ bpftool gen skeleton -L -S -k signing_key.pem -i signing_key.der \
obj.bpf.o > obj.lskel.h
``-L`` selects the light-skeleton (``gen_loader``) backend and ``-S`` enables
@@ -454,12 +630,36 @@ signing; ``-k`` and ``-i`` supply the signing key and its X.509 certificate.
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.
+signing and is not included.
+
+Loading
+-------
+
+The generated skeleton exposes ``keyring_id``, which selects the keyring the
+kernel verifies against. Set it between open and load; loading then 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.
+ struct obj *skel = obj__open();
+
+ skel->keyring_id = KEY_SPEC_BPF_KEYRING;
+ err = obj__load(skel);
+
+For the staging case the same object is loaded against a keyring the caller
+populated itself, which only works on a system that has not set
+``bpf.keyring_unsealed=1``::
+
+ /*
+ * Staging only: this keyring is under the loader's own control and
+ * carries no trust of its own. See "Caller-supplied keyrings are for
+ * staging".
+ */
+ key_id = add_key("asymmetric", "", der, der_sz, KEY_SPEC_SESSION_KEYRING);
+ skel->keyring_id = KEY_SPEC_SESSION_KEYRING;
+ err = obj__load(skel);
+
+Either way the trusted public key must already be in the chosen keyring for
+verification to succeed. For the bpf keyring that enrollment happens once at
+boot, see `Provisioning`_.
UAPI reference
==============
@@ -487,6 +687,13 @@ UAPI reference
The map content is not hashed separately at all - it is covered, as bytes,
by the program signature.
+Kernel command line:
+
+``bpf.keyring_unsealed=``
+ Set to ``1`` to leave the bpf keyring unsealed for provisioning, and to make
+ it the only non-system keyring a loader may select for the rest of the boot
+ (see `The bpf keyring`_).
+
Notes and limitations
======================
@@ -495,3 +702,5 @@ Notes and limitations
exceed it.
- The metadata container is a single-element array map, accessed through
``map_direct_value_addr``.
+- The bpf keyring needs ``CONFIG_KEYS``; without it there is no bpf keyring
+ and ``KEY_SPEC_BPF_KEYRING`` never resolves.
--
2.43.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 11/11] Documentation/bpf: Document the bpf keyring and improve examples
2026-08-28 17:52 ` [PATCH bpf-next v4 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
@ 2026-08-28 18:53 ` bot+bpf-ci
0 siblings, 0 replies; 21+ messages in thread
From: bot+bpf-ci @ 2026-08-28 18:53 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4294 bytes --]
> diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
> index e73eaaebd8b15..27fdad3df96f1 100644
> --- a/Documentation/bpf/signing.rst
> +++ b/Documentation/bpf/signing.rst
> @@ -254,21 +254,24 @@ 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.)
> +A more realistic admission policy than "is it signed at all": base trust in
> +the bpf keyring or an explicit allowlist of a caller-supplied staging key/
^^
> +keyring, and emit a record of every decision including denied attempts.
Does this line break leave "key/" at the end of a line? This is body text
in an RST paragraph, so the soft line break renders as whitespace and the
output would read "staging key/ keyring" with a stray space. Rewrapping so
"key/keyring" stays on one line should fix it.
> +(Illustrative - error checking elided.)
[ ... ]
> @@ -390,33 +406,158 @@ 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.
> +against. Four values are well-known; anything else is resolved as a caller-
> +supplied key or keyring, named either by its serial or by one of the other
> +``KEY_SPEC_*`` ids:
[ ... ]
> +Provisioning
> +~~~~~~~~~~~~
> +
> +The keyring is created during ``late_initcall`` and is **sealed empty** by
> +default: it carries a reject-all restriction, so no key can ever be added and
> +``KEY_SPEC_BPF_KEYRING`` fails with ``-ENOKEY`` for the whole boot.
Can the phrasing here be tightened? When the keyring is sealed,
bpf_keyring_init() still publishes it, so lookup_user_key() resolves the
special id fine.
Looking at security/keys/process_keys.c:
case KEY_SPEC_BPF_KEYRING:
key = bpf_keyring;
if (!key)
goto error;
__key_get(key);
key_ref = make_key_ref(key, 0);
break;
What actually returns -ENOKEY is a program load naming the keyring:
bpf_lookup_keyring() returns NULL because nr_leaves_on_tree is 0, and
the verifier turns that into -ENOKEY. Two paragraphs later the doc uses
the precise wording ("a load naming it fails with -ENOKEY"); would the
same phrasing work here?
[ ... ]
> +What remains is ``KEY_POS_SEARCH`` for the in-kernel search during verification,
> +plus ``KEY_USR_VIEW`` and ``KEY_USR_READ`` so the keyring stays visible in
> +``/proc/keys``. ``KEY_SPEC_BPF_KEYRING`` names the keyring but conveys no rights
Is KEY_USR_READ attributed to the right facility? /proc/keys visibility
is gated only by KEY_NEED_VIEW in security/keys/proc.c:
rc = key_task_permission(key_ref, ctx.cred, KEY_NEED_VIEW);
if (rc < 0)
return 0;
KEY_NEED_READ is what KEYCTL_READ requires in security/keys/keyctl.c,
which is the "keyctl show" step the same section recommends. So the mask
0x08030000 is correct, but would it read more clearly as "KEY_USR_VIEW so
the keyring stays visible in /proc/keys, and KEY_USR_READ so keyctl show
can still list it"?
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33197746599
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (10 preceding siblings ...)
2026-08-28 17:52 ` [PATCH bpf-next v4 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
@ 2026-08-30 1:20 ` patchwork-bot+netdevbpf
2026-08-31 10:05 ` Christian Brauner
12 siblings, 0 replies; 21+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-08-30 1:20 UTC (permalink / raw)
To: Daniel Borkmann
Cc: memxor, brauner, kpsingh, ast, john.fastabend, a.s.protopopov,
bpf, dhowells, jarkko, keyrings
Hello:
This series was applied to bpf/bpf-next.git (master)
by Alexei Starovoitov <ast@kernel.org>:
On Fri, 28 Aug 2026 19:52:16 +0200 you wrote:
> Add a dedicated BPF keyring which is integrated for BPF signing. It
> is modelled after the dm-verity keyring which was added in commit
> 033724b ("dm-verity: add dm-verity keyring") and which can eventually
> be used also via systemd through the same enrollment method as in
> dm-verity's case. It is selected with a new well-known keyring_id
> VERIFY_USE_BPF_KEYRING and gives an operator a place to enroll a
> BPF-only signing key at boot, specifically scoped to signed BPF
> program loading. Next, to demonstrate that BPF signing is algorithm
> agnostic, we add a few small tooling changes to support ML-DSA and
> integrate everything into BPF selftest for BPF CI to ensure nothing
> breaks with future changes. For more details, see individual commits.
>
> [...]
Here is the summary with links:
- [bpf-next,v4,01/11] bpf, keys: Add a bpf keyring for program signature validation
https://git.kernel.org/bpf/bpf-next/c/264d8fd2794f
- [bpf-next,v4,02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
https://git.kernel.org/bpf/bpf-next/c/abaa0835b4f3
- [bpf-next,v4,03/11] bpf: Raise the bound on a program's signature size
https://git.kernel.org/bpf/bpf-next/c/300b348e6d5e
- [bpf-next,v4,04/11] bpftool: Support ML-DSA program signing
https://git.kernel.org/bpf/bpf-next/c/d37168c99361
- [bpf-next,v4,05/11] selftests/bpf: Add a test for the sealed bpf keyring
https://git.kernel.org/bpf/bpf-next/c/6754aeca6022
- [bpf-next,v4,06/11] selftests/bpf: Rebuild signed lskels when signing key changes
https://git.kernel.org/bpf/bpf-next/c/e7ada0800ce2
- [bpf-next,v4,07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa
https://git.kernel.org/bpf/bpf-next/c/1841d6cbc5c7
- [bpf-next,v4,08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
https://git.kernel.org/bpf/bpf-next/c/478cf384def8
- [bpf-next,v4,09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh
https://git.kernel.org/bpf/bpf-next/c/b638a82d64f1
- [bpf-next,v4,10/11] selftests/bpf: Add tests for bpf keyring in signed loader
https://git.kernel.org/bpf/bpf-next/c/1c8cb8bf7329
- [bpf-next,v4,11/11] Documentation/bpf: Document the bpf keyring and improve examples
https://git.kernel.org/bpf/bpf-next/c/8588fa592985
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 21+ messages in thread* Re: [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support
2026-08-28 17:52 [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (11 preceding siblings ...)
2026-08-30 1:20 ` [PATCH bpf-next v4 00/11] BPF keyring and signed loader ML-DSA support patchwork-bot+netdevbpf
@ 2026-08-31 10:05 ` Christian Brauner
12 siblings, 0 replies; 21+ messages in thread
From: Christian Brauner @ 2026-08-31 10:05 UTC (permalink / raw)
To: Daniel Borkmann
Cc: memxor, brauner, kpsingh, ast, john.fastabend, a.s.protopopov,
bpf, dhowells, jarkko, keyrings
> Add a dedicated BPF keyring which is integrated for BPF signing. It
> is modelled after the dm-verity keyring which was added in commit
> 033724b ("dm-verity: add dm-verity keyring") and which can eventually
> be used also via systemd through the same enrollment method as in
> dm-verity's case. It is selected with a new well-known keyring_id
> VERIFY_USE_BPF_KEYRING and gives an operator a place to enroll a
> BPF-only signing key at boot, specifically scoped to signed BPF
> program loading. Next, to demonstrate that BPF signing is algorithm
> agnostic, we add a few small tooling changes to support ML-DSA and
> integrate everything into BPF selftest for BPF CI to ensure nothing
> breaks with future changes. For more details, see individual commits.
Looks great to me. Note that the I already added support for enrolling
keys into the .bpf keyring into systemd-keyring-update:
https://github.com/systemd/systemd/pull/43549
The current mechanism is built so that you are allowed to enroll further
keys post-sealing provided the key is signed by a key that has already
been enrolled into the keyring prior to sealing it.
--
^ permalink raw reply [flat|nested] 21+ messages in thread