Skip to contentSkip to conversation

Reintroduce Hornet LSM

PATCH Superseded Blaise Boscaccy 4 participants 25 messages Apr 20 – Apr 29

Subsystems (9)
Documentation Kbuild Kselftest BPF +5 more subsystems

v5 of 7 Superseded Latest is v7 →

lore
Share & tools
lore
#0 lore raw

v5 cover Reintroduce Hornet LSM

This patch series introduces the next iteration of the Hornet LSM. Hornet’s goal is to provide a secure and extensible in-kernel signature verification mechanism for eBPF programs.

Hornet addresses concerns from users who require strict audit trails and verification guarantees for eBPF programs, especially in security-sensitive environments. Many production systems need assurance that only authorized, unmodified eBPF programs are loaded into the kernel. Hornet provides this assurance through cryptographic signature verification.

The currently accepted loader-plus-map signature verification scheme, mandated by Alexei and KP, is simple to implement and generally acceptable if users and administrators are satisfied with it. However, verifying both the loader and the maps offers additional benefits beyond verifying the loader alone:

1. Security and Audit Integrity

A key advantage is that the LSM hook for authorizing BPF program loads can operate after signature verification. This ensures:

* Access control decisions are based on verified signature status.
* Accurate system state measurement and logging.
* Log entries claiming a verified signature are truthful, avoiding misleading records where only the loader was verified while the actual BPF program verification occurs later without logging.

2. TOCTOU Attack Prevention

The current map hash implementation may be vulnerable to a TOCTOU attack because it allows unfrozen maps to cache a previously calculated hash. The accepted “trusted loader” scheme cannot detect this and may permit loading altered maps.

3. Supply Chain Integrity

Verify that eBPF programs and their associated map data have not been modified since they were built and signed, in the kernel proper, may aid in protecting against supply chain attacks.

This approach addresses concerns from users who require strict audit trails and verification guarantees, especially in security-sensitive environments. Map hashes for extended verification are passed via the existing PKCS#7 UAPI and verified by the crypto subsystem. Hornet then calculates the program’s verification state. Hornet itself does not enforce a policy on whether unsigned or partially signed programs should be rejected. It delegates that decision to downstream LSMs hook, making it a composable building block in a larger security architecture.

Changes in V5:
- Added IPE documentation

Link to V4: https://lore.kernel.org/linux-security-module/[email protected]/

Changes in V4:
- IPE integration
- Arbitrary keyring support

Link to V3: https://lore.kernel.org/linux-security-module/[email protected]/

Changes in V3:
- Updated for signed attribute patch series changes
- Added some new result enum values
- Minor documentation clarification
- Misc style fixes
- Added missing signed-off-by tags

Link to V2: https://lore.kernel.org/linux-security-module/[email protected]/

Changes in V2:
- Addressed possible TocTou races in hash verification
- Improved documentation and tooling
- Added Alexie's nack

Link to RFC: https://lore.kernel.org/linux-security-module/[email protected]/

Blaise Boscaccy (6):
lsm: security: Add additional enum values for bpf integrity checks
security: Hornet LSM
hornet: Introduce gen_sig
hornet: Add a light skeleton data extractor scripts selftests/hornet: Add a selftest for the Hornet LSM
ipe: Add BPF program load policy enforcement via Hornet integration

James Bottomley (3):
crypto: pkcs7: add flag for validated trust on a signed info block
crypto: pkcs7: add ability to extract signed attributes by OID
crypto: pkcs7: add tests for pkcs7_get_authattr

Paul Moore (1):
lsm: framework for BPF integrity verification

 Documentation/admin-guide/LSM/Hornet.rst     | 321 +++++++++++++++
 Documentation/admin-guide/LSM/index.rst      |   1 +
 Documentation/admin-guide/LSM/ipe.rst        | 162 +++++++-
 Documentation/security/ipe.rst               |  39 ++
 MAINTAINERS                                  |   9 +
 certs/system_keyring.c                       |   1 +
 crypto/asymmetric_keys/Makefile              |   4 +-
 crypto/asymmetric_keys/pkcs7_aa.asn1         |  18 +
 crypto/asymmetric_keys/pkcs7_key_type.c      |  44 ++-
 crypto/asymmetric_keys/pkcs7_parser.c        |  81 ++++
 crypto/asymmetric_keys/pkcs7_parser.h        |   1 +
 crypto/asymmetric_keys/pkcs7_trust.c         |   1 +
 include/crypto/pkcs7.h                       |   4 +
 include/linux/lsm_hook_defs.h                |   5 +
 include/linux/oid_registry.h                 |   3 +
 include/linux/security.h                     |  28 ++
 include/uapi/linux/lsm.h                     |   1 +
 scripts/Makefile                             |   1 +
 scripts/hornet/Makefile                      |   5 +
 scripts/hornet/extract-insn.sh               |  27 ++
 scripts/hornet/extract-map.sh                |  27 ++
 scripts/hornet/extract-skel.sh               |  27 ++
 scripts/hornet/gen_sig.c                     | 392 +++++++++++++++++++
 scripts/hornet/write-sig.sh                  |  27 ++
 security/Kconfig                             |   3 +-
 security/Makefile                            |   1 +
 security/hornet/Kconfig                      |  11 +
 security/hornet/Makefile                     |   7 +
 security/hornet/hornet.asn1                  |  13 +
 security/hornet/hornet_lsm.c                 | 346 ++++++++++++++++
 security/ipe/Kconfig                         |  14 +
 security/ipe/audit.c                         |  15 +
 security/ipe/eval.c                          |  73 +++-
 security/ipe/eval.h                          |   5 +
 security/ipe/hooks.c                         |  37 ++
 security/ipe/hooks.h                         |  11 +
 security/ipe/ipe.c                           |   3 +
 security/ipe/policy.h                        |  14 +
 security/ipe/policy_parser.c                 |  27 ++
 security/security.c                          |  75 +++-
 tools/testing/selftests/Makefile             |   1 +
 tools/testing/selftests/hornet/Makefile      |  63 +++
 tools/testing/selftests/hornet/loader.c      |  21 +
 tools/testing/selftests/hornet/trivial.bpf.c |  33 ++
 44 files changed, 1994 insertions(+), 8 deletions(-)
 create mode 100644 Documentation/admin-guide/LSM/Hornet.rst
 create mode 100644 crypto/asymmetric_keys/pkcs7_aa.asn1
 create mode 100644 scripts/hornet/Makefile
 create mode 100755 scripts/hornet/extract-insn.sh
 create mode 100755 scripts/hornet/extract-map.sh
 create mode 100755 scripts/hornet/extract-skel.sh
 create mode 100644 scripts/hornet/gen_sig.c
 create mode 100755 scripts/hornet/write-sig.sh
 create mode 100644 security/hornet/Kconfig
 create mode 100644 security/hornet/Makefile
 create mode 100644 security/hornet/hornet.asn1
 create mode 100644 security/hornet/hornet_lsm.c
 create mode 100644 tools/testing/selftests/hornet/Makefile
 create mode 100644 tools/testing/selftests/hornet/loader.c
 create mode 100644 tools/testing/selftests/hornet/trivial.bpf.c
Signature · 1 line
2.53.0
#1 lore raw

PATCH v5 1/10 crypto: pkcs7: add flag for validated trust on a signed info block

From: James Bottomley <[email protected]>

Allow consumers of struct pkcs7_message to tell if any of the sinfo fields has passed a trust validation. Note that this does not happen in parsing, pkcs7_validate_trust() must be explicitly called or called via validate_pkcs7_trust(). Since the way to get this trusted pkcs7 object is via verify_pkcs7_message_sig, export that so modules can use it.

---
 certs/system_keyring.c                | 1 +
 crypto/asymmetric_keys/pkcs7_parser.h | 1 +
 crypto/asymmetric_keys/pkcs7_trust.c  | 1 +
 3 files changed, 3 insertions(+)
certs/system_keyring.c +1 -0
index e0761436ec7f4..9bda49295bd02 100644
@@ -380,6 +380,7 @@ int verify_pkcs7_message_sig(const void *data, size_t len,
 	pr_devel("<==%s() = %d\n", __func__, ret);
 	return ret;
 }
+EXPORT_SYMBOL(verify_pkcs7_message_sig);
 
 /**
  * verify_pkcs7_signature - Verify a PKCS#7-based signature on system data.
crypto/asymmetric_keys/pkcs7_parser.h +1 -0
index 6ef9f335bb17f..203062a33def6 100644
@@ -20,6 +20,7 @@ struct pkcs7_signed_info {
 	unsigned	index;
 	bool		unsupported_crypto;	/* T if not usable due to missing crypto */
 	bool		blacklisted;
+	bool		verified; /* T if this signer has validated trust */
 
 	/* Message digest - the digest of the Content Data (or NULL) */
 	const void	*msgdigest;
crypto/asymmetric_keys/pkcs7_trust.c +1 -0
index 9a87c34ed1733..78ebfb6373b61 100644
@@ -127,6 +127,7 @@ static int pkcs7_validate_trust_one(struct pkcs7_message *pkcs7,
 		for (p = sinfo->signer; p != x509; p = p->signer)
 			p->verified = true;
 	}
+	sinfo->verified = true;
 	kleave(" = 0");
 	return 0;
 }
Signature · 1 line
2.53.0
#2 lore raw

Re: [PATCH v5 01/10] crypto: pkcs7: add flag for validated trust on a signed info block

Sashiko AI review found 1 potential issue(s): - [Low] `verify_pkcs7_message_sig` is exported using `EXPORT_SYMBOL` but requires a GPL-only structure, creating an inconsistent API boundary. --

Sashiko AI review found 1 potential issue(s):
- [Low] verify_pkcs7_message_sig is exported using EXPORT_SYMBOL but requires a GPL-only structure, creating an inconsistent API boundary. --

commit 6419b567f8141f60cbee1c62c6b8f2a8760edbda
Author: James Bottomley <[email protected]>

crypto: pkcs7: add flag for validated trust on a signed info block

This commit allows consumers to check if a PKCS#7 signed info block has passed trust validation by adding a verified flag. It also exports verify_pkcs7_message_sig so other modules can obtain a trusted PKCS#7 object.

certs/system_keyring.c int verify_pkcs7_message_sig(const void *data, size_t len, · line 383

Quoted 9 lines
certs/system_keyring.c +1 -0
index e0761436ec7f4..9bda49295bd02 100644
@@ -380,6 +380,7 @@ int verify_pkcs7_message_sig(const void *data, size_t len,
 	pr_devel("<==%s() = %d\n", __func__, ret);
 	return ret;
 }
+EXPORT_SYMBOL(verify_pkcs7_message_sig);

Is this intended to be EXPORT_SYMBOL instead of EXPORT_SYMBOL_GPL?

The function requires a parsed struct pkcs7_message as an argument, but the only function that parses and allocates this structure, pkcs7_parse_message(), is exported as EXPORT_SYMBOL_GPL.

This seems to create an inconsistent API boundary where non-GPL modules are permitted to use this function but cannot legitimately obtain the necessary input object to do so. Other related APIs in the PKCS#7 and keyring verification subsystems also use EXPORT_SYMBOL_GPL.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#3 lore raw

PATCH v5 2/10 crypto: pkcs7: add ability to extract signed attributes by OID

In reply to Blaise Boscaccy

From: James Bottomley <[email protected]>

Signers may add any information they like in signed attributes and sometimes this information turns out to be relevant to specific signing cases, so add an api pkcs7_get_authattr() to extract the value of an authenticated attribute by specific OID. The current implementation is designed for the single signer use case and simply terminates the search when it finds the relevant OID.

---
 crypto/asymmetric_keys/Makefile       |  4 +-
 crypto/asymmetric_keys/pkcs7_aa.asn1  | 18 ++++++
 crypto/asymmetric_keys/pkcs7_parser.c | 81 +++++++++++++++++++++++++++
 include/crypto/pkcs7.h                |  4 ++
 4 files changed, 106 insertions(+), 1 deletion(-)
 create mode 100644 crypto/asymmetric_keys/pkcs7_aa.asn1
crypto/asymmetric_keys/Makefile +3 -1
index bc65d3b98dcbf..f99b7169ae7cd 100644
@@ -53,12 +53,14 @@ clean-files	+= pkcs8.asn1.c pkcs8.asn1.h
 obj-$(CONFIG_PKCS7_MESSAGE_PARSER) += pkcs7_message.o
 pkcs7_message-y := \
 	pkcs7.asn1.o \
+	pkcs7_aa.asn1.o \
 	pkcs7_parser.o \
 	pkcs7_trust.o \
 	pkcs7_verify.o
 
-$(obj)/pkcs7_parser.o: $(obj)/pkcs7.asn1.h
+$(obj)/pkcs7_parser.o: $(obj)/pkcs7.asn1.h $(obj)/pkcs7_aa.asn1.h
 $(obj)/pkcs7.asn1.o: $(obj)/pkcs7.asn1.c $(obj)/pkcs7.asn1.h
+$(obj)/pkcs7_aa.asn1.o: $(obj)/pkcs7_aa.asn1.c $(obj)/pkcs7_aa.asn1.h
 
 #
 # PKCS#7 parser testing key
crypto/asymmetric_keys/pkcs7_aa.asn1 new file +18 -0
new file mode 100644
index 0000000000000..7a8857bdf56e1
@@ -0,0 +1,18 @@
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Copyright (C) 2009 IETF Trust and the persons identified as authors
+-- of the code
+--
+-- https://www.rfc-editor.org/rfc/rfc5652#section-3
+
+AA ::= 	CHOICE {
+	aaSet		[0] IMPLICIT AASet,
+	aaSequence	[2] EXPLICIT SEQUENCE OF AuthenticatedAttribute
+}
+
+AASet ::= SET OF AuthenticatedAttribute
+
+AuthenticatedAttribute ::= SEQUENCE {
+	type	OBJECT IDENTIFIER ({ pkcs7_aa_note_OID }),
+	values	SET OF ANY ({ pkcs7_aa_note_attr })
+}
crypto/asymmetric_keys/pkcs7_parser.c +81 -0

93 lines not on this page · raw message

include/crypto/pkcs7.h +4 -0
index 38ec7f5f90411..bd83202cd805c 100644
@@ -25,6 +25,10 @@ extern void pkcs7_free_message(struct pkcs7_message *pkcs7);
 extern int pkcs7_get_content_data(const struct pkcs7_message *pkcs7,
 				  const void **_data, size_t *_datalen,
 				  size_t *_headerlen);
+extern int pkcs7_get_authattr(const struct pkcs7_message *pkcs7,
+			      enum OID oid,
+			      const void **_data, size_t *_len);
+
 
 /*
  * pkcs7_trust.c
Signature · 1 line
2.53.0
#4 lore raw

Re: [PATCH v5 02/10] crypto: pkcs7: add ability to extract signed attributes by OID

Sashiko AI review found 4 potential issue(s): - [High] API Flaw / Data Corruption with Constructed Types (`SET OF ANY` behavior) - [High] Early Loop Termination on Missing Authenticated Attributes…

Sashiko AI review found 4 potential issue(s):
- [High] API Flaw / Data Corruption with Constructed Types (SET OF ANY behavior)
- [High] Early Loop Termination on Missing Authenticated Attributes
- [High] Multi-value Overwrite / Parser Differential
- [Medium] Genuine ASN.1 Decoding Errors are Masked
--

commit 1ec29c2985cc84f3d3e1912ff4626eaa51d31e0c
Author: James Bottomley <[email protected]>

crypto: pkcs7: add ability to extract signed attributes by OID

This patch adds an API to extract the value of an authenticated attribute by a specific OID. It introduces a new ASN.1 grammar and decoder to parse the PKCS#7 signed attributes.

Quoted 4 lines
crypto/asymmetric_keys/pkcs7_parser.c +0 -0
index 6e3ffdac83ace..d467866f7d930 100644

[ ... ]

Quoted 23 lines
crypto/asymmetric_keys/pkcs7_parser.c +19 -0
@@ -211,6 +212,86 @@ int pkcs7_get_content_data(const struct pkcs7_message *pkcs7,
 }
 EXPORT_SYMBOL_GPL(pkcs7_get_content_data);
 
+struct pkcs7_aa_context {
+	bool found;
+	enum OID oid_to_find;
+	const void *data;
+	size_t len;
+};
+
+int pkcs7_aa_note_OID(void *context, size_t hdrlen,
+		      unsigned char tag,
+		      const void *value, size_t vlen)
+{
+	struct pkcs7_aa_context *ctx = context;
+	enum OID oid = look_up_OID(value, vlen);
+
+	ctx->found = (oid == ctx->oid_to_find);
+
+	return 0;
+}
+

crypto/asymmetric_keys/pkcs7_parser.c int pkcs7_get_content_data(const struct pkcs7_message *pkcs7, · line 243

Quoted 10 lines
crypto/asymmetric_keys/pkcs7_parser.c +10 -0
+int pkcs7_aa_note_attr(void *context, size_t hdrlen,
+		       unsigned char tag,
+		       const void *value, size_t vlen)
+{
+	struct pkcs7_aa_context *ctx = context;
+
+	if (ctx->found) {
+		ctx->data = value;
+		ctx->len = vlen;
+	}

Since the ASN.1 SET OF ANY grammar allows constructed types, does asn1_ber_decoder() descend into the constructed type and repeatedly call this action for each nested primitive? If so, this would overwrite ctx->data with the last inner leaf element rather than capturing the outer bounds.

Also, if multiple attributes with the same OID exist, wouldn't this overwrite ctx->data with the last matching attribute instead of returning the first one as the documentation states?

Quoted 27 lines
crypto/asymmetric_keys/pkcs7_parser.c +27 -0
+
+	return 0;
+}
+
+/**
+ * pkcs7_get_authattr - get authenticated attribute by OID
+ * @pkcs7: The preparsed PKCS#7 message
+ * @oid: the enum value of the OID to find
+ * @_data: Place to return a pointer to the attribute value
+ * @_len: length of the attribute value
+ *
+ * Searches the authenticated attributes until one is found with a
+ * matching OID.  Note that because the attributes are per signer
+ * there could be multiple signers with different values, but this
+ * routine will simply return the first one in parse order.
+ *
+ * Returns -ENODATA if the attribute can't be found
+ */
+int pkcs7_get_authattr(const struct pkcs7_message *pkcs7,
+		       enum OID oid,
+		       const void **_data, size_t *_len)
+{
+	struct pkcs7_signed_info *sinfo = pkcs7->signed_infos;
+	struct pkcs7_aa_context ctx;
+
+	ctx.data = NULL;
+	ctx.oid_to_find = oid;

crypto/asymmetric_keys/pkcs7_parser.c int pkcs7_get_content_data(const struct pkcs7_message *pkcs7, · line 280

Quoted 10 lines
crypto/asymmetric_keys/pkcs7_parser.c +10 -0
+
+	for (; sinfo; sinfo = sinfo->next) {
+		int ret;
+
+		/* only extract OIDs from validated signers */
+		if (!sinfo->verified)
+			continue;
+
+		ret = asn1_ber_decoder(&pkcs7_aa_decoder, &ctx,
+				       sinfo->authattrs, sinfo->authattrs_len);

If a verified signer omits authenticated attributes, sinfo->authattrs is NULL and sinfo->authattrs_len is 0. Does passing a 0-length buffer to asn1_ber_decoder() cause it to return an error?

If so, the loop will break immediately below, meaning we could miss attributes from subsequent valid signers in the message. Should there be an explicit continue if !sinfo->authattrs?

crypto/asymmetric_keys/pkcs7_parser.c int pkcs7_get_content_data(const struct pkcs7_message *pkcs7, · line 282

Quoted 2 lines
crypto/asymmetric_keys/pkcs7_parser.c +2 -0
+		if (ret < 0 || ctx.data != NULL)
+			break;

If asn1_ber_decoder() returns an error due to the constructed type misalignment, does checking ctx.data != NULL here mask that error and allow the function to successfully return a corrupted inner pointer?

crypto/asymmetric_keys/pkcs7_parser.c int pkcs7_get_content_data(const struct pkcs7_message *pkcs7, · line 286

Quoted 4 lines
crypto/asymmetric_keys/pkcs7_parser.c +4 -0
+	}
+
+	if (!ctx.data)
+		return -ENODATA;

If asn1_ber_decoder() fails with a genuine structural error like -EBADMSG, the loop breaks but we unconditionally return -ENODATA here. Does this incorrectly mask malformed data errors from the caller?

Quoted 7 lines
crypto/asymmetric_keys/pkcs7_parser.c +7 -0
+
+	*_data = ctx.data;
+	*_len = ctx.len;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(pkcs7_get_authattr);
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#5 lore raw

PATCH v5 3/10 crypto: pkcs7: add tests for pkcs7_get_authattr

In reply to Blaise Boscaccy

From: James Bottomley <[email protected]>

Add example code to the test module pkcs7_key_type.c that verifies a message and then pulls out a known authenticated attribute.

---
 crypto/asymmetric_keys/pkcs7_key_type.c | 44 ++++++++++++++++++++++++-
 1 file changed, 43 insertions(+), 1 deletion(-)
crypto/asymmetric_keys/pkcs7_key_type.c +43 -1
index b930d3bbf1af5..e0b1ce0202f6d 100644
@@ -12,6 +12,7 @@
 #include <linux/verification.h>
 #include <linux/key-type.h>
 #include <keys/user-type.h>
+#include <crypto/pkcs7.h>
 
 MODULE_LICENSE("GPL");
 MODULE_DESCRIPTION("PKCS#7 testing key type");
@@ -51,16 +52,57 @@ static int pkcs7_view_content(void *ctx, const void *data, size_t len,
 static int pkcs7_preparse(struct key_preparsed_payload *prep)
 {
 	enum key_being_used_for usage = pkcs7_usage;
+	int ret;
+	struct pkcs7_message *pkcs7;
+	const void *data;
+	size_t len;
 
 	if (usage >= NR__KEY_BEING_USED_FOR) {
 		pr_err("Invalid usage type %d\n", usage);
 		return -EINVAL;
 	}
 
-	return verify_pkcs7_signature(NULL, 0,
+	ret = verify_pkcs7_signature(NULL, 0,
 				      prep->data, prep->datalen,
 				      VERIFY_USE_SECONDARY_KEYRING, usage,
 				      pkcs7_view_content, prep);
+	if (ret)
+		return ret;
+
+	pkcs7 = pkcs7_parse_message(prep->data, prep->datalen);
+	if (IS_ERR(pkcs7)) {
+		pr_err("pkcs7 parse error\n");
+		return PTR_ERR(pkcs7);
+	}
+
+	/*
+	 * the parsed message has no trusted signer, so nothing should
+	 * be returned here
+	 */
+	ret = pkcs7_get_authattr(pkcs7, OID_messageDigest, &data, &len);
+	if (ret == 0) {
+		pr_err("OID returned when no trust in signer\n");
+		goto out;
+	}
+	/* add trust and check again */
+	ret = verify_pkcs7_message_sig(NULL, 0, pkcs7,
+				       VERIFY_USE_SECONDARY_KEYRING, usage,
+				       NULL, NULL);
+	if (ret) {
+		pr_err("verify_pkcs7_message_sig failed!!\n");
+		goto out;
+	}
+	/* now we should find the OID */
+	ret = pkcs7_get_authattr(pkcs7, OID_messageDigest, &data, &len);
+	if (ret) {
+		pr_err("Failed to get message digest\n");
+		goto out;
+	}
+	pr_info("Correctly Got message hash, size=%zu\n", len);
+
+ out:
+	pkcs7_free_message(pkcs7);
+	return 0;
 }
 
 /*
Signature · 1 line
2.53.0
#6 lore raw

Re: [PATCH v5 03/10] crypto: pkcs7: add tests for pkcs7_get_authattr

Sashiko AI review found 1 potential issue(s): - [Medium] Test failures are silently masked by unconditionally returning 0. --

Sashiko AI review found 1 potential issue(s):
- [Medium] Test failures are silently masked by unconditionally returning 0. --

commit 11447b57d9861b91fcbcb5b89dcfd93489f5f252
Author: James Bottomley <[email protected]>

crypto: pkcs7: add tests for pkcs7_get_authattr

This commit adds example code to the pkcs7_key_type test module to verify a message and extract a known authenticated attribute.

Quoted 4 lines
crypto/asymmetric_keys/pkcs7_key_type.c +0 -0
index b930d3bbf1af5..e0b1ce0202f6d 100644

[ ... ]

Quoted 3 lines
crypto/asymmetric_keys/pkcs7_key_type.c +0 -0
@@ -51,16 +52,57 @@ static int pkcs7_view_content(void *ctx, const void *data, size_t len,
 static int pkcs7_preparse(struct key_preparsed_payload *prep)
 {

[ ... ]

crypto/asymmetric_keys/pkcs7_key_type.c static int pkcs7_view_content(void *ctx, const void *data, size_t len, · line 85

Quoted 8 lines
crypto/asymmetric_keys/pkcs7_key_type.c +8 -0
+	/*
+	 * the parsed message has no trusted signer, so nothing should
+	 * be returned here
+	 */
+	ret = pkcs7_get_authattr(pkcs7, OID_messageDigest, &data, &len);
+	if (ret == 0) {
+		pr_err("OID returned when no trust in signer\n");
+		goto out;

Should a negative error code be assigned to ret here before jumping to out?

Quoted 10 lines
crypto/asymmetric_keys/pkcs7_key_type.c +10 -0
+	}
+	/* add trust and check again */
+	ret = verify_pkcs7_message_sig(NULL, 0, pkcs7,
+				       VERIFY_USE_SECONDARY_KEYRING, usage,
+				       NULL, NULL);
+	if (ret) {
+		pr_err("verify_pkcs7_message_sig failed!!\n");
+		goto out;
+	}
+	/* now we should find the OID */

crypto/asymmetric_keys/pkcs7_key_type.c static int pkcs7_view_content(void *ctx, const void *data, size_t len, · line 105

Quoted 10 lines
crypto/asymmetric_keys/pkcs7_key_type.c +10 -0
+	ret = pkcs7_get_authattr(pkcs7, OID_messageDigest, &data, &len);
+	if (ret) {
+		pr_err("Failed to get message digest\n");
+		goto out;
+	}
+	pr_info("Correctly Got message hash, size=%zu\n", len);
+
+ out:
+	pkcs7_free_message(pkcs7);
+	return 0;

Does this return 0 unconditionally mask the test failures?

If any of the checks above fail and jump to the out label, the function returns 0, effectively discarding the error state. As a result, the key instantiation will always succeed from the perspective of user space, and test frameworks might incorrectly assume the test passed.

Should this return ret instead?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#7 lore raw

PATCH v5 4/10 lsm: framework for BPF integrity verification

In reply to Blaise Boscaccy

From: Paul Moore <[email protected]>

Add a new LSM hook and two new LSM hook callbacks to support LSMs that perform integrity verification, e.g. digital signature verification, of BPF programs.

While the BPF subsystem does implement a signature verification scheme, it does not satisfy a number of existing requirements, adding support for BPF program integrity verification to the LSM framework allows administrators to select additional integrity verification mechanisms to meet these needs while also providing a mechanism for future expansion. Additional on why this is necessary can be found at the lore archive link below:

https://lore.kernel.org/linux-security-module/CAHC9VhTQ_DR=ANzoDBjcCtrimV7XcCZVUsANPt=TjcvM4d-vjg@mail.gmail.com/

The LSM-based BPF integrity verification mechanism works within the existing security_bpf_prog_load() hook called by the BPF subsystem. It adds an additional dedicated integrity callback and a new LSM hook/callback to be called from within LSMs implementing integrity verification.

The first new callback, bpf_prog_load_integrity(), located within the security_bpf_prog_load() hook, is necessary to ensure that the integrity verification callbacks are executed before any of the existing LSMs are executed via the bpf_prog_load() callback. Reusing the existing bpf_prog_load() callback for integrity verification could result in LSMs not having access to the integrity verification results when asked to authorize the BPF program load in the bpf_prog_load() callback.

The new LSM hook, security_bpf_prog_load_post_integrity(), is intended to be called from within LSMs performing BPF program integrity verification. It is used to report the verdict of the integrity verification to other LSMs enforcing access control policy on BPF program loads. LSMs enforcing such access controls should register a bpf_prog_load_post_integrity() callback to receive integrity verdicts.

More information on these new callbacks and hook can be found in the code comments in this patch.

---
 include/linux/lsm_hook_defs.h |  5 +++
 include/linux/security.h      | 25 ++++++++++++
 security/security.c           | 75 +++++++++++++++++++++++++++++++++--
 3 files changed, 102 insertions(+), 3 deletions(-)
include/linux/lsm_hook_defs.h +5 -0
index 8c42b4bde09c0..4971d3c36d5b4 100644
@@ -434,6 +434,11 @@ LSM_HOOK(int, 0, bpf_prog, struct bpf_prog *prog)
 LSM_HOOK(int, 0, bpf_map_create, struct bpf_map *map, union bpf_attr *attr,
 	 struct bpf_token *token, bool kernel)
 LSM_HOOK(void, LSM_RET_VOID, bpf_map_free, struct bpf_map *map)
+LSM_HOOK(int, 0, bpf_prog_load_post_integrity, struct bpf_prog *prog,
+	 union bpf_attr *attr, struct bpf_token *token, bool kernel,
+	 const struct lsm_id *lsmid, enum lsm_integrity_verdict verdict)
+LSM_HOOK(int, 0, bpf_prog_load_integrity, struct bpf_prog *prog,
+	 union bpf_attr *attr, struct bpf_token *token, bool kernel)
 LSM_HOOK(int, 0, bpf_prog_load, struct bpf_prog *prog, union bpf_attr *attr,
 	 struct bpf_token *token, bool kernel)
 LSM_HOOK(void, LSM_RET_VOID, bpf_prog_free, struct bpf_prog *prog)
include/linux/security.h +25 -0
index ee88dd2d2d1f7..b3fd04baa78d0 100644
@@ -67,6 +67,7 @@ enum fs_value_type;
 struct watch;
 struct watch_notification;
 struct lsm_ctx;
+struct lsm_id;
 
 /* Default (no) options for the capable function */
 #define CAP_OPT_NONE 0x0
@@ -100,6 +101,14 @@ enum lsm_integrity_type {
 	LSM_INT_FSVERITY_BUILTINSIG_VALID,
 };
 
+enum lsm_integrity_verdict {
+	LSM_INT_VERDICT_NONE = 0,
+	LSM_INT_VERDICT_OK,
+	LSM_INT_VERDICT_UNSIGNED,
+	LSM_INT_VERDICT_PARTIALSIG,
+	LSM_INT_VERDICT_BADSIG,
+};
+
 /*
  * These are reasons that can be passed to the security_locked_down()
  * LSM hook. Lockdown reasons that protect kernel integrity (ie, the
@@ -2270,6 +2279,12 @@ extern int security_bpf_prog(struct bpf_prog *prog);
 extern int security_bpf_map_create(struct bpf_map *map, union bpf_attr *attr,
 				   struct bpf_token *token, bool kernel);
 extern void security_bpf_map_free(struct bpf_map *map);
+extern int security_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+					union bpf_attr *attr,
+					struct bpf_token *token,
+					bool kernel,
+					const struct lsm_id *lsmid,
+					enum lsm_integrity_verdict verdict);
 extern int security_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr,
 				  struct bpf_token *token, bool kernel);
 extern void security_bpf_prog_free(struct bpf_prog *prog);
@@ -2304,6 +2319,16 @@ static inline int security_bpf_map_create(struct bpf_map *map, union bpf_attr *a
 static inline void security_bpf_map_free(struct bpf_map *map)
 { }
 
+static inline int security_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+					  union bpf_attr *attr,
+					  struct bpf_token *token,
+					  bool kernel,
+					  const struct lsm_id *lsmid,
+					  enum lsm_integrity_verdict verdict)
+{
+	return 0;
+}
+
 static inline int security_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr,
 					 struct bpf_token *token, bool kernel)
 {
security/security.c +72 -3

95 lines not on this page · raw message

Signature · 1 line
2.53.0
#8 lore raw

PATCH v5 5/10 lsm: security: Add additional enum values for bpf integrity checks

In reply to Blaise Boscaccy

First add a generic LSM_INT_VERDICT_FAULT value to indicate a system failure during checking. Second, add a LSM_INT_VERDICT_UNKNOWNKEY to signal that the payload was signed with a key other than one that exists in the secondary keyring. And finally add an LSM_INT_VERDICT_UNEXPECTED enum value to indicate that a unexpected hash value was encountered at some stage of verification.

---
 include/linux/security.h | 3 +++
 1 file changed, 3 insertions(+)
include/linux/security.h +3 -0
index b3fd04baa78d0..4b4b8808f67de 100644
@@ -106,6 +106,9 @@ enum lsm_integrity_verdict {
 	LSM_INT_VERDICT_OK,
 	LSM_INT_VERDICT_UNSIGNED,
 	LSM_INT_VERDICT_PARTIALSIG,
+	LSM_INT_VERDICT_UNKNOWNKEY,
+	LSM_INT_VERDICT_UNEXPECTED,
+	LSM_INT_VERDICT_FAULT,
 	LSM_INT_VERDICT_BADSIG,
 };
 
Signature · 1 line
2.53.0
author NAK
#9 lore raw

PATCH v5 6/10 security: Hornet LSM

In reply to Blaise Boscaccy

This adds the Hornet Linux Security Module which provides enhanced signature verification and data validation for eBPF programs. This allows users to continue to maintain an invariant that all code running inside of the kernel has actually been signed and verified, by the kernel.

This effort builds upon the currently excepted upstream solution. It further hardens it by providing deterministic, in-kernel checking of map hashes to solidify auditing along with preventing TOCTOU attacks against lskel map hashes.

Target map hashes are passed in via PKCS#7 signed attributes. Hornet determines the extent which the eBFP program is signed and defers to other LSMs for policy decisions.

---
 Documentation/admin-guide/LSM/Hornet.rst | 321 +++++++++++++++++++++
 Documentation/admin-guide/LSM/index.rst  |   1 +
 MAINTAINERS                              |   9 +
 include/linux/oid_registry.h             |   3 +
 include/uapi/linux/lsm.h                 |   1 +
 security/Kconfig                         |   3 +-
 security/Makefile                        |   1 +
 security/hornet/Kconfig                  |  11 +
 security/hornet/Makefile                 |   7 +
 security/hornet/hornet.asn1              |  13 +
 security/hornet/hornet_lsm.c             | 346 +++++++++++++++++++++++
 11 files changed, 715 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/admin-guide/LSM/Hornet.rst
 create mode 100644 security/hornet/Kconfig
 create mode 100644 security/hornet/Makefile
 create mode 100644 security/hornet/hornet.asn1
 create mode 100644 security/hornet/hornet_lsm.c
Documentation/admin-guide/LSM/Hornet.rst new file +321 -0
new file mode 100644
index 0000000000000..af5e9cd9d83a8
@@ -0,0 +1,321 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======
+Hornet
+======
+
+Hornet is a Linux Security Module that provides extensible signature
+verification for eBPF programs. This is selectable at build-time with
+``CONFIG_SECURITY_HORNET``.
+
+Overview
+========
+
+Hornet addresses concerns from users who require strict audit trails and
+verification guarantees for eBPF programs, especially in
+security-sensitive environments. Many production systems need assurance
+that only authorized, unmodified eBPF programs are loaded into the
+kernel. Hornet provides this assurance through cryptographic signature
+verification.
+
+When an eBPF program is loaded via the ``bpf()`` syscall, Hornet
+verifies a PKCS#7 signature attached to the program instructions. The
+signature is checked against the kernel's secondary keyring using the
+existing kernel cryptographic infrastructure. In addition to signing the
+program bytecode, Hornet supports signing SHA-256 hashes of associated
+BPF maps, enabling integrity verification of map contents at load time
+and at runtime.
+
+After verification, Hornet classifies the program into one of the
+following integrity states and passes the result to a downstream LSM hook
+(``bpf_prog_load_post_integrity``), allowing other security modules to
+make policy decisions based on the verification outcome:
+
+``LSM_INT_VERDICT_OK``
+  The program signature and all map hashes verified successfully.
+
+``LSM_INT_VERDICT_UNSIGNED``
+  No signature was provided with the program.
+
+``LSM_INT_VERDICT_PARTIALSIG``
+  The program signature verified, but the signature did not contain
+  hornet map hash data.
+
+``LSM_INT_VERDICT_UNKNOWNKEY``
+  The signing certificate is not trusted in the secondary keyring,
+
+``LSM_INT_VERDICT_FAULT``
+  A system error occured during verification.
+
+``LSM_INT_VERDICT_UNEXPECTED``
+  An unexpected map hash value was encountered.
+
+``LSM_INT_VERDICT_BADSIG``
+  The signature or a map hash failed verification.
+
+Hornet itself does not enforce a policy on whether unsigned or partially
+signed programs should be rejected. It delegates that decision to
+downstream LSMs via the ``bpf_prog_load_post_integrity`` hook, making it
+a composable building block in a larger security architecture.
+
+Use Cases
+=========
+
+- **Locked-down production environments**: Ensure only eBPF programs
+  signed by a trusted authority can be loaded, preventing unauthorized
+  or tampered programs from running in the kernel.
+
+- **Audit and compliance**: Provide cryptographic evidence that loaded
+  eBPF programs match their expected build artifacts, supporting
+  compliance requirements in regulated industries.
+
+- **Supply chain integrity**: Verify that eBPF programs and their
+  associated map data have not been modified since they were built and
+  signed, protecting against supply chain attacks.
+
+Threat Model
+============
+
+Hornet protects against the following threats:
+
+- **Unauthorized eBPF program loading**: Programs that have not been
+  signed by a trusted key will be reported as unsigned or badly signed.
+
+- **Tampering with program instructions**: Any modification to the eBPF
+  bytecode after signing will cause signature verification to fail.
+
+- **Tampering with map data**: When map hashes are included in the
+  signature, Hornet verifies that frozen BPF maps match their expected
+  SHA-256 hashes at load time. Maps are also re-verified before program
+  execution via ``BPF_PROG_RUN``.
+
+Hornet does **not** protect against:
+
+- Compromise of the signing key itself.
+- Attacks that occur after a program has been loaded and verified.
+- Programs loaded by the kernel itself (kernel-internal loads bypass
+  the ``BPF_PROG_RUN`` map check).
+
+Known Limitations
+=================
+
+- Hornet requires programs to use :doc:`light skeletons
+  </bpf/libbpf/libbpf_naming_convention>` (lskels) for the signing
+  workflow, as the tooling operates on lskel-generated headers.
+
+- A maximum of 64 maps per program can be tracked for hash
+  verification.
+
+- Map hash verification requires the maps to be frozen before loading.
+  Maps that are not frozen at load time will cause verification to fail
+  when their hashes are included in the signature.
+
+- Hornet relies on the kernel's secondary keyring
+  (``VERIFY_USE_SECONDARY_KEYRING``) for certificate trust. Keys must
+  be provisioned into this keyring before programs can be verified.
+
+- The only hashing algorithm available is SHA256 due to it be hardcoded
+  in the bpf subsystem.
+
+Configuration
+=============
+
+Build Configuration
+-------------------
+
+Enable Hornet by setting the following kernel configuration option::
+
+  CONFIG_SECURITY_HORNET=y
+
+This option is found under :menuselection:`Security options --> Hornet
+support` and depends on ``CONFIG_SECURITY``.
+
+When enabled, Hornet is included in the default LSM initialization order
+and will appear in ``/sys/kernel/security/lsm``.
+
+Architecture
+============
+
+Signature Verification Flow
+---------------------------
+
+The following describes what happens when a userspace program calls
+``bpf(BPF_PROG_LOAD, ...)`` with a signature attached:
+
+1. The ``bpf_prog_load_integrity`` LSM hook is invoked.
+
+2. Hornet reads the signature from the userspace buffer specified by
+   ``attr->signature`` (with length ``attr->signature_size``).
+
+3. The PKCS#7 signature is verified against the program instructions
+   using ``verify_pkcs7_signature()`` with the kernel's secondary
+   keyring.
+
+4. The PKCS#7 message is parsed and its trust chain is validated via
+   ``validate_pkcs7_trust()``.
+
+5. Hornet extracts the authenticated attribute identified by
+   ``OID_hornet_data`` (OID ``2.25.316487325684022475439036912669789383960``)
+   from the PKCS#7 message. This attribute contains an ASN.1-encoded set
+   of map index/hash pairs.
+
+6. For each map hash entry, Hornet retrieves the corresponding BPF map
+   via its file descriptor, confirms it is frozen, computes its SHA-256
+   hash, and compares it against the signed hash.
+
+7. The resulting integrity verdict is passed to the
+   ``bpf_prog_load_post_integrity`` hook so that downstream LSMs can
+   enforce policy.
+
+Runtime Map Verification
+------------------------
+
+When ``bpf(BPF_PROG_RUN, ...)`` is called from userspace, Hornet
+re-verifies the hashes of all maps associated with the program. This
+ensures that map contents have not been modified between program load
+and execution. If any map hash no longer matches, the ``BPF_PROG_RUN``
+command is denied.
+
+Userspace Interface
+-------------------
+
+Signatures are passed to the kernel through fields in ``union bpf_attr``
+when using the ``BPF_PROG_LOAD`` command:
+
+``signature``
+  A pointer to a userspace buffer containing the PKCS#7 signature.
+
+``signature_size``
+  The size of the signature buffer in bytes.
+
+ASN.1 Schema
+------------
+
+Map hashes are encoded as a signed attribute in the PKCS#7 message using
+the following ASN.1 schema::
+
+  HornetData ::= SET OF Map
+
+  Map ::= SEQUENCE {
+      index   INTEGER,
+      sha     OCTET STRING
+  }
+
+Each ``Map`` entry contains the index of the map in the program's
+``fd_array`` and its expected SHA-256 hash. A zero-length ``sha`` field
+indicates that the map at that index should be skipped during
+verification.
+
+Tooling
+=======
+
+Helper scripts and a signature generation tool are provided in
+``scripts/hornet/`` to support the development of signed eBPF light
+skeletons.
+
+gen_sig
+-------
+
+``gen_sig`` is a C program (using OpenSSL) that creates a PKCS#7
+signature over eBPF program instructions and optionally includes
+SHA-256 hashes of BPF maps as signed attributes.
+
+Usage::
+
+  gen_sig --data <instructions.bin> \
+          --cert <signer.crt> \
+          --key <signer.key> \
+          [--pass <passphrase>] \
+          --out <signature.p7b> \
+          [--add <mapfile.bin>:<index> ...]
+
+``--data``
+  Path to the binary file containing eBPF program instructions to sign.
+
+``--cert``
+  Path to the signing certificate (PEM or DER format).
+
+``--key``
+  Path to the private key (PEM or DER format).
+
+``--pass``
+  Optional passphrase for the private key.
+
+``--out``
+  Path to write the output PKCS#7 signature.
+
+``--add``
+  Attach a map hash as a signed attribute. The argument is a path to a
+  binary map file followed by a colon and the map's index in the
+  ``fd_array``. This option may be specified multiple times.
+
+extract-skel.sh
+---------------
+
+Extracts a named field from an autogenerated eBPF lskel header file.
+Used internally by other helper scripts.
+
+extract-insn.sh
+---------------
+
+Extracts the eBPF program instructions (``opts_insn``) from an lskel
+header into a binary file suitable for signing with ``gen_sig``.
+
+extract-map.sh
+--------------
+
+Extracts the map data (``opts_data``) from an lskel header into a
+binary file suitable for hashing with ``gen_sig``.
+
+write-sig.sh
+------------
+
+Replaces the signature data in an lskel header with a new signature
+from a binary file. This is used to embed a freshly generated signature
+back into the header after signing.
+
+Signing Workflow
+================
+
+A typical workflow for building and signing an eBPF light skeleton is:
+
+1. **Compile the eBPF program**::
+
+     clang -O2 -target bpf -c program.bpf.c -o program.bpf.o
+
+2. **Generate the light skeleton header** using ``bpftool``::
+
+     bpftool gen skeleton -S program.bpf.o > loader.h
+
+3. **Extract instructions and map data** from the generated header::
+
+     scripts/hornet/extract-insn.sh loader.h > insn.bin
+     scripts/hornet/extract-map.sh loader.h > map.bin
+
+4. **Generate the signature** with ``gen_sig``::
+
+     scripts/hornet/gen_sig \
+       --key signing_key.pem \
+       --cert signing_key.x509 \
+       --data insn.bin \
+       --add map.bin:0 \
+       --out sig.bin
+
+5. **Embed the signature** back into the header::
+
+     scripts/hornet/write-sig.sh loader.h sig.bin > signed_loader.h
+
+6. **Build the loader program** using the signed header::
+
+     cc -o loader loader.c -lbpf
+
+The resulting loader program will pass the embedded signature to the
+kernel when loading the eBPF program, enabling Hornet to verify it.
+
+Testing
+=======
+
+Self-tests are provided in ``tools/testing/selftests/hornet/``. The test
+suite builds a minimal eBPF program (``trivial.bpf.c``), signs it using
+the workflow described above, and verifies that the signed program loads
+successfully.
Documentation/admin-guide/LSM/index.rst +1 -0
index b44ef68f6e4da..57f6e9fbe5fd1 100644
@@ -49,3 +49,4 @@ subdirectories.
    SafeSetID
    ipe
    landlock
+   Hornet
MAINTAINERS +9 -0
index d1cc0e12fe1f0..0942f5453c04d 100644
@@ -11692,6 +11692,15 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/iio/pressure/honeywell,mprls0025pa.yaml
 F:	drivers/iio/pressure/mprls0025pa*
 
+HORNET SECURITY MODULE
+M:	Blaise Boscaccy <[email protected]>
+L:	[email protected]
+S:	Supported
+T:	git https://github.com/blaiseboscaccy/hornet.git
+F:	Documentation/admin-guide/LSM/Hornet.rst
+F:	scripts/hornet/
+F:	security/hornet/
+
 HP BIOSCFG DRIVER
 M:	Jorge Lopez <[email protected]>
 L:	[email protected]
include/linux/oid_registry.h +3 -0
index ebce402854de4..bf852715aaea4 100644
@@ -150,6 +150,9 @@ enum OID {
 	OID_id_ml_dsa_65,			/* 2.16.840.1.101.3.4.3.18 */
 	OID_id_ml_dsa_87,			/* 2.16.840.1.101.3.4.3.19 */
 
+	/* Hornet LSM */
+	OID_hornet_data,	  /* 2.25.316487325684022475439036912669789383960 */
+
 	OID__NR
 };
 
include/uapi/linux/lsm.h +1 -0
index 938593dfd5daf..2ff9bcdd551e2 100644
@@ -65,6 +65,7 @@ struct lsm_ctx {
 #define LSM_ID_IMA		111
 #define LSM_ID_EVM		112
 #define LSM_ID_IPE		113
+#define LSM_ID_HORNET		114
 
 /*
  * LSM_ATTR_XXX definitions identify different LSM attributes
security/Kconfig +2 -1
index 6a4393fce9a17..283c4a1032094 100644
@@ -230,6 +230,7 @@ source "security/safesetid/Kconfig"
 source "security/lockdown/Kconfig"
 source "security/landlock/Kconfig"
 source "security/ipe/Kconfig"
+source "security/hornet/Kconfig"
 
 source "security/integrity/Kconfig"
 
@@ -274,7 +275,7 @@ config LSM
 	default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,ipe,bpf" if DEFAULT_SECURITY_APPARMOR
 	default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,ipe,bpf" if DEFAULT_SECURITY_TOMOYO
 	default "landlock,lockdown,yama,loadpin,safesetid,ipe,bpf" if DEFAULT_SECURITY_DAC
-	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf"
+	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,hornet,bpf"
 	help
 	  A comma-separated list of LSMs, in initialization order.
 	  Any LSMs left off this list, except for those with order
security/Makefile +1 -0
index 4601230ba442a..b68cb56e419bc 100644
@@ -26,6 +26,7 @@ obj-$(CONFIG_CGROUPS)			+= device_cgroup.o
 obj-$(CONFIG_BPF_LSM)			+= bpf/
 obj-$(CONFIG_SECURITY_LANDLOCK)		+= landlock/
 obj-$(CONFIG_SECURITY_IPE)		+= ipe/
+obj-$(CONFIG_SECURITY_HORNET)		+= hornet/
 
 # Object integrity file lists
 obj-$(CONFIG_INTEGRITY)			+= integrity/
security/hornet/Kconfig new file +11 -0
new file mode 100644
index 0000000000000..19406aa237ac6
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config SECURITY_HORNET
+	bool "Hornet support"
+	depends on SECURITY
+	default n
+	help
+	  This selects Hornet.
+	  Further information can be found in
+	  Documentation/admin-guide/LSM/Hornet.rst.
+
+	  If you are unsure how to answer this question, answer N.
security/hornet/Makefile new file +7 -0
new file mode 100644
index 0000000000000..26b6f954f762e
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_SECURITY_HORNET) := hornet.o
+
+hornet-y := hornet.asn1.o \
+	hornet_lsm.o \
+
+$(obj)/hornet.asn1.o: $(obj)/hornet.asn1.c $(obj)/hornet.asn1.h
security/hornet/hornet.asn1 new file +13 -0
new file mode 100644
index 0000000000000..c8d47b16b65d7
@@ -0,0 +1,13 @@
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Copyright (C) 2009 IETF Trust and the persons identified as authors
+-- of the code
+--
+-- https://www.rfc-editor.org/rfc/rfc5652#section-3
+
+HornetData ::= SET OF Map
+
+Map ::= SEQUENCE {
+	index			INTEGER ({ hornet_map_index }),
+	sha			OCTET STRING ({ hornet_map_hash })
+} ({ hornet_next_map })
security/hornet/hornet_lsm.c new file +346 -0

346 lines not on this page · raw message

Signature · 1 line
2.53.0
maintainer
#10 lore raw

Re: [PATCH v5 06/10] security: Hornet LSM

Quoted 18 lines · Blaise Boscaccy

This adds the Hornet Linux Security Module which provides enhanced signature verification and data validation for eBPF programs. This allows users to continue to maintain an invariant that all code running inside of the kernel has actually been signed and verified, by the kernel.

This effort builds upon the currently excepted upstream solution. It further hardens it by providing deterministic, in-kernel checking of map hashes to solidify auditing along with preventing TOCTOU attacks against lskel map hashes.

Target map hashes are passed in via PKCS#7 signed attributes. Hornet determines the extent which the eBFP program is signed and defers to other LSMs for policy decisions.

...

security/hornet/Kconfig line 4

Quoted 10 lines
security/hornet/Kconfig new file +4 -0
new file mode 100644
index 0000000000000..19406aa237ac6
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config SECURITY_HORNET
+       bool "Hornet support"
+       depends on SECURITY

The dependency doesn't seem to be complete, for example, PKCS7_MESSAGE_PARSER is not selected.

Quoted 26 lines
security/hornet/Kconfig new file +7 -0
+       default n
+       help
+         This selects Hornet.
+         Further information can be found in
+         Documentation/admin-guide/LSM/Hornet.rst.
+
+         If you are unsure how to answer this question, answer N.
security/hornet/Makefile new file +7 -0
new file mode 100644
index 0000000000000..26b6f954f762e
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_SECURITY_HORNET) := hornet.o
+
+hornet-y := hornet.asn1.o \
+       hornet_lsm.o \
+
+$(obj)/hornet.asn1.o: $(obj)/hornet.asn1.c $(obj)/hornet.asn1.h
security/hornet/hornet.asn1 new file +0 -0
new file mode 100644
index 0000000000000..c8d47b16b65d7

security/hornet/hornet.asn1 line 4

Quoted 4 lines
security/hornet/hornet.asn1 new file +4 -0
@@ -0,0 +1,13 @@
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Copyright (C) 2009 IETF Trust and the persons identified as authors
+-- of the code

I'm not a lawyer, but since this is a new AA which is not in the RFC, should the copyright belong to IETF?

Quoted 52 lines
security/hornet/hornet.asn1 new file +9 -0
+--
+-- https://www.rfc-editor.org/rfc/rfc5652#section-3
+
+HornetData ::= SET OF Map
+
+Map ::= SEQUENCE {
+       index                   INTEGER ({ hornet_map_index }),
+       sha                     OCTET STRING ({ hornet_map_hash })
+} ({ hornet_next_map })
security/hornet/hornet_lsm.c new file +37 -0
new file mode 100644
index 0000000000000..f7d62fe6229c9
@@ -0,0 +1,346 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hornet Linux Security Module
+ *
+ * Author: Blaise Boscaccy <[email protected]>
+ *
+ * Copyright (C) 2026 Microsoft Corporation
+ */
+
+#include <linux/lsm_hooks.h>
+#include <uapi/linux/lsm.h>
+#include <linux/bpf.h>
+#include <linux/verification.h>
+#include <crypto/public_key.h>
+#include <linux/module_signature.h>
+#include <crypto/pkcs7.h>
+#include <linux/sort.h>
+#include <linux/asn1_decoder.h>
+#include <linux/oid_registry.h>
+#include "hornet.asn1.h"
+
+#define MAX_USED_MAPS 64
+
+struct hornet_maps {
+       bpfptr_t fd_array;
+};
+
+/* The only hashing algorithm available is SHA256 due to it be hardcoded
+   in the bpf subsystem. */
+
+struct hornet_parse_context {
+       int indexes[MAX_USED_MAPS];
+       bool skips[MAX_USED_MAPS];
+       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
+       int hash_count;
+};
+

security/hornet/hornet_lsm.c line 47

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+struct hornet_prog_security_struct {
+       bool checked[MAX_USED_MAPS];
+       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
+};
+
+struct hornet_map_security_struct {
+       bool checked;
+       int index;
+};
+

Can maps be shared between programs? If so, since the LSM blob is per-map, a second program's load will overwrite map_security->index set by the first. A later run of the first program would then fail to find its own map.

Quoted 142 lines
security/hornet/hornet_lsm.c new file +142 -0
+struct lsm_blob_sizes hornet_blob_sizes __ro_after_init = {
+       .lbs_bpf_map = sizeof(struct hornet_map_security_struct),
+       .lbs_bpf_prog = sizeof(struct hornet_prog_security_struct),
+};
+
+static inline struct hornet_prog_security_struct *
+hornet_bpf_prog_security(struct bpf_prog *prog)
+{
+       return prog->aux->security + hornet_blob_sizes.lbs_bpf_prog;
+}
+
+static inline struct hornet_map_security_struct *
+hornet_bpf_map_security(struct bpf_map *map)
+{
+       return map->security + hornet_blob_sizes.lbs_bpf_map;
+}
+
+static int hornet_verify_hashes(struct hornet_maps *maps,
+                               struct hornet_parse_context *ctx,
+                               struct bpf_prog *prog)
+{
+       int map_fd;
+       u32 i;
+       struct bpf_map *map;
+       int err = 0;
+       unsigned char hash[SHA256_DIGEST_SIZE];
+       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
+       struct hornet_map_security_struct *map_security;
+
+       for (i = 0; i < ctx->hash_count; i++) {
+               if (ctx->skips[i])
+                       continue;
+
+               err = copy_from_bpfptr_offset(&map_fd, maps->fd_array,
+                                             ctx->indexes[i] * sizeof(map_fd),
+                                             sizeof(map_fd));
+               if (err < 0)
+                       return LSM_INT_VERDICT_FAULT;
+
+               CLASS(fd, f)(map_fd);
+               if (fd_empty(f))
+                       return LSM_INT_VERDICT_FAULT;
+               if (unlikely(fd_file(f)->f_op != &bpf_map_fops))
+                       return LSM_INT_VERDICT_FAULT;
+
+               map = fd_file(f)->private_data;
+               if (!map->frozen)
+                       return LSM_INT_VERDICT_FAULT;
+
+               map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);
+
+               err = memcmp(hash, &ctx->hashes[i * SHA256_DIGEST_SIZE],
+                             SHA256_DIGEST_SIZE);
+               if (err)
+                       return LSM_INT_VERDICT_UNEXPECTED;
+
+               security->checked[i] = true;
+               memcpy(&security->hashes[i * SHA256_DIGEST_SIZE], hash, SHA256_DIGEST_SIZE);
+               map_security = hornet_bpf_map_security(map);
+               map_security->checked = true;
+               map_security->index = i;
+       }
+       return LSM_INT_VERDICT_OK;
+}
+
+int hornet_next_map(void *context, size_t hdrlen,
+                    unsigned char tag,
+                    const void *value, size_t vlen)
+{
+       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+       if (++ctx->hash_count >= MAX_USED_MAPS)
+               return -EINVAL;
+       return 0;
+}
+
+int hornet_map_index(void *context, size_t hdrlen,
+                    unsigned char tag,
+                    const void *value, size_t vlen)
+{
+       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+       if (vlen > 1)
+               return -EINVAL;
+
+       ctx->indexes[ctx->hash_count] = *(u8 *)value;
+       return 0;
+}
+
+int hornet_map_hash(void *context, size_t hdrlen,
+                   unsigned char tag,
+                   const void *value, size_t vlen)
+
+{
+       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+       if (vlen != SHA256_DIGEST_SIZE && vlen != 0)
+               return -EINVAL;
+
+       if (vlen) {
+               ctx->skips[ctx->hash_count] = false;
+               memcpy(&ctx->hashes[ctx->hash_count * SHA256_DIGEST_SIZE], value, vlen);
+       } else
+               ctx->skips[ctx->hash_count] = true;
+
+       return 0;
+}
+
+static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
+                               struct bpf_token *token, bool is_kernel,
+                               enum lsm_integrity_verdict *verdict)
+{
+       struct hornet_maps maps = {0};
+       bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
+       struct pkcs7_message *msg;
+       struct hornet_parse_context *ctx;
+       void *sig;
+       int err;
+       const void *authattrs;
+       size_t authattrs_len;
+       struct key *key;
+
+       if (!attr->signature) {
+               *verdict = LSM_INT_VERDICT_UNSIGNED;
+               return 0;
+       }
+
+       ctx = kzalloc(sizeof(struct hornet_parse_context), GFP_KERNEL);
+       if (!ctx)
+               return -ENOMEM;
+
+       maps.fd_array = make_bpfptr(attr->fd_array, is_kernel);
+       sig = kzalloc(attr->signature_size, GFP_KERNEL);
+       if (!sig) {
+               err = -ENOMEM;
+               goto out;
+       }
+       err = copy_from_bpfptr(sig, usig, attr->signature_size);
+       if (err != 0)
+               goto cleanup_sig;
+
+       msg = pkcs7_parse_message(sig, attr->signature_size);

security/hornet/hornet_lsm.c line 199

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+       if (IS_ERR(msg)) {
+               *verdict = LSM_INT_VERDICT_BADSIG;
+               err = 0;
+               goto cleanup_sig;
+       }
+
+       if (system_keyring_id_check(attr->keyring_id) == 0)
+               key = (struct key*)(unsigned long)attr->keyring_id;
+       else
+               key = key_ref_to_ptr(lookup_user_key(attr->keyring_id, 0, KEY_DEFER_PERM_CHECK));

You might need to key_put the user key.

Quoted 99 lines
security/hornet/hornet_lsm.c new file +99 -0
+
+       if (verify_pkcs7_message_sig(prog->insnsi, prog->len * sizeof(struct bpf_insn), msg,
+                                    key,
+                                    VERIFYING_BPF_SIGNATURE,
+                                    NULL, NULL)) {
+               *verdict = LSM_INT_VERDICT_UNKNOWNKEY;
+               err = 0;
+               goto cleanup_msg;
+       }
+
+       if (pkcs7_get_authattr(msg, OID_hornet_data,
+                              &authattrs, &authattrs_len) == -ENODATA) {
+               *verdict = LSM_INT_VERDICT_PARTIALSIG;
+               err = 0;
+               goto cleanup_msg;
+       }
+
+       err = asn1_ber_decoder(&hornet_decoder, ctx, authattrs, authattrs_len);
+       if (err < 0 || authattrs == NULL) {
+               *verdict = LSM_INT_VERDICT_BADSIG;
+               err = 0;
+               goto cleanup_msg;
+       }
+
+       err = hornet_verify_hashes(&maps, ctx, prog);
+       if (err == 0)
+               *verdict = LSM_INT_VERDICT_OK;
+       else
+               *verdict = err;
+
+cleanup_msg:
+       pkcs7_free_message(msg);
+cleanup_sig:
+       kfree(sig);
+out:
+       kfree(ctx);
+       return err;
+}
+
+static const struct lsm_id hornet_lsmid = {
+       .name = "hornet",
+       .id = LSM_ID_HORNET,
+};
+
+static int hornet_bpf_prog_load_integrity(struct bpf_prog *prog, union bpf_attr *attr,
+                                         struct bpf_token *token, bool is_kernel)
+{
+       enum lsm_integrity_verdict verdict;
+       int result = hornet_check_program(prog, attr, token, is_kernel, &verdict);
+
+       if (result < 0)
+               return result;
+
+       return security_bpf_prog_load_post_integrity(prog, attr, token, is_kernel,
+                                                    &hornet_lsmid, verdict);
+}
+
+static int hornet_verify_map(struct bpf_prog *prog, int index)
+{
+       unsigned char hash[SHA256_DIGEST_SIZE];
+       int i;
+       struct bpf_map *map;
+       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
+       struct hornet_map_security_struct *map_security;
+
+       if (!security->checked[index])
+               return 0;
+
+       for (i = 0; i < prog->aux->used_map_cnt; i++) {
+               map = prog->aux->used_maps[i];
+               map_security = hornet_bpf_map_security(map);
+               if (map_security->index != index)
+                       continue;
+
+               if (!map->frozen)
+                       return -EPERM;
+
+               map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);
+               if (memcmp(hash, &security->hashes[index * SHA256_DIGEST_SIZE],
+                          SHA256_DIGEST_SIZE) != 0)
+                       return -EPERM;
+               else
+                       return 0;
+       }
+       return -EINVAL;
+}
+
+static int hornet_check_prog_maps(u32 ufd)
+{
+       CLASS(fd, f)(ufd);
+       struct bpf_prog *prog;
+       int i, result = 0;
+
+       if (fd_empty(f))
+               return -EBADF;
+       if (fd_file(f)->f_op != &bpf_prog_fops)
+               return -EINVAL;
+
+       prog = fd_file(f)->private_data;

security/hornet/hornet_lsm.c line 308

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+
+       mutex_lock(&prog->aux->used_maps_mutex);
+       if (!prog->aux->used_map_cnt)
+               goto out;
+
+       for (i = 0; i < prog->aux->used_map_cnt; i++) {
+               result = hornet_verify_map(prog, i);
+               if (result)
+                       goto out;
+       }

This loop is kind of confusing for me, I guess it's just iterating through the maps the program currently in use. I feel the nested loop is unnecessary and the hash could be saved into the map's blob instead. Please correct me if I'm wrong here.

-Fan

Quoted 41 lines
security/hornet/hornet_lsm.c new file +38 -1
+out:
+       mutex_unlock(&prog->aux->used_maps_mutex);
+       return result;
+}
+
+static int hornet_bpf(int cmd, union bpf_attr *attr, unsigned int size, bool kernel)
+{
+       /* in horent_bpf(), anything that had originated from kernel space we assume
+          has already been checked, in some form or another, so we don't bother
+          checking the intergity of any maps. In hornet_bpf_prog_load_integrity(),
+          hornet doesn't make any opinion on that and delegates that to the downstream
+          policy enforcement. */
+
+       if (cmd != BPF_PROG_RUN)
+               return 0;
+       if (kernel)
+               return 0;
+
+       return hornet_check_prog_maps(attr->test.prog_fd);
+}
+
+static struct security_hook_list hornet_hooks[] __ro_after_init = {
+       LSM_HOOK_INIT(bpf_prog_load_integrity, hornet_bpf_prog_load_integrity),
+       LSM_HOOK_INIT(bpf, hornet_bpf),
+};
+
+static int __init hornet_init(void)
+{
+       pr_info("Hornet: eBPF signature verification enabled\n");
+       security_add_hooks(hornet_hooks, ARRAY_SIZE(hornet_hooks), &hornet_lsmid);
+       return 0;
+}
+
+DEFINE_LSM(hornet) = {
+       .id = &hornet_lsmid,
+       .blobs = &hornet_blob_sizes,
+       .init = hornet_init,
+};
--

2.53.0

#11 lore raw

Re: [PATCH v5 06/10] security: Hornet LSM

Quoted 62 lines · Fan Wu
Quoted 18 lines · Blaise Boscaccy

This adds the Hornet Linux Security Module which provides enhanced signature verification and data validation for eBPF programs. This allows users to continue to maintain an invariant that all code running inside of the kernel has actually been signed and verified, by the kernel.

This effort builds upon the currently excepted upstream solution. It further hardens it by providing deterministic, in-kernel checking of map hashes to solidify auditing along with preventing TOCTOU attacks against lskel map hashes.

Target map hashes are passed in via PKCS#7 signed attributes. Hornet determines the extent which the eBFP program is signed and defers to other LSMs for policy decisions.

...

security/hornet/Kconfig line 4

Quoted 10 lines
security/hornet/Kconfig new file +4 -0
new file mode 100644
index 0000000000000..19406aa237ac6
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config SECURITY_HORNET
+       bool "Hornet support"
+       depends on SECURITY

The dependency doesn't seem to be complete, for example, PKCS7_MESSAGE_PARSER is not selected.

Quoted 25 lines
security/hornet/Kconfig new file +7 -0
+       default n
+       help
+         This selects Hornet.
+         Further information can be found in
+         Documentation/admin-guide/LSM/Hornet.rst.
+
+         If you are unsure how to answer this question, answer N.
security/hornet/Makefile new file +7 -0
new file mode 100644
index 0000000000000..26b6f954f762e
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_SECURITY_HORNET) := hornet.o
+
+hornet-y := hornet.asn1.o \
+       hornet_lsm.o \
+
+$(obj)/hornet.asn1.o: $(obj)/hornet.asn1.c $(obj)/hornet.asn1.h
security/hornet/hornet.asn1 new file +0 -0
new file mode 100644
index 0000000000000..c8d47b16b65d7
Quoted 9 lines

security/hornet/hornet.asn1 line 4

Quoted 5 lines
security/hornet/hornet.asn1 new file +4 -0
@@ -0,0 +1,13 @@
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Copyright (C) 2009 IETF Trust and the persons identified as authors
+-- of the code

I'm not a lawyer, but since this is a new AA which is not in the RFC, should the copyright belong to IETF?

gah thanks.

Quoted 59 lines
Quoted 59 lines
security/hornet/hornet.asn1 new file +9 -0
+--
+-- https://www.rfc-editor.org/rfc/rfc5652#section-3
+
+HornetData ::= SET OF Map
+
+Map ::= SEQUENCE {
+       index                   INTEGER ({ hornet_map_index }),
+       sha                     OCTET STRING ({ hornet_map_hash })
+} ({ hornet_next_map })
security/hornet/hornet_lsm.c new file +44 -0
new file mode 100644
index 0000000000000..f7d62fe6229c9
@@ -0,0 +1,346 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hornet Linux Security Module
+ *
+ * Author: Blaise Boscaccy <[email protected]>
+ *
+ * Copyright (C) 2026 Microsoft Corporation
+ */
+
+#include <linux/lsm_hooks.h>
+#include <uapi/linux/lsm.h>
+#include <linux/bpf.h>
+#include <linux/verification.h>
+#include <crypto/public_key.h>
+#include <linux/module_signature.h>
+#include <crypto/pkcs7.h>
+#include <linux/sort.h>
+#include <linux/asn1_decoder.h>
+#include <linux/oid_registry.h>
+#include "hornet.asn1.h"
+
+#define MAX_USED_MAPS 64
+
+struct hornet_maps {
+       bpfptr_t fd_array;
+};
+
+/* The only hashing algorithm available is SHA256 due to it be hardcoded
+   in the bpf subsystem. */
+
+struct hornet_parse_context {
+       int indexes[MAX_USED_MAPS];
+       bool skips[MAX_USED_MAPS];
+       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
+       int hash_count;
+};
+
+struct hornet_prog_security_struct {
+       bool checked[MAX_USED_MAPS];
+       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
+};
+
+struct hornet_map_security_struct {
+       bool checked;
Quoted 9 lines

security/hornet/hornet_lsm.c line 47

Quoted 3 lines
security/hornet/hornet_lsm.c new file +3 -0
+       int index;
+};
+

Can maps be shared between programs? If so, since the LSM blob is per-map, a second program's load will overwrite map_security->index set by the first. A later run of the first program would then fail to find its own map.

Yes, it can. We can support that.

Quoted 261 lines
Quoted 142 lines
security/hornet/hornet_lsm.c new file +142 -0
+struct lsm_blob_sizes hornet_blob_sizes __ro_after_init = {
+       .lbs_bpf_map = sizeof(struct hornet_map_security_struct),
+       .lbs_bpf_prog = sizeof(struct hornet_prog_security_struct),
+};
+
+static inline struct hornet_prog_security_struct *
+hornet_bpf_prog_security(struct bpf_prog *prog)
+{
+       return prog->aux->security + hornet_blob_sizes.lbs_bpf_prog;
+}
+
+static inline struct hornet_map_security_struct *
+hornet_bpf_map_security(struct bpf_map *map)
+{
+       return map->security + hornet_blob_sizes.lbs_bpf_map;
+}
+
+static int hornet_verify_hashes(struct hornet_maps *maps,
+                               struct hornet_parse_context *ctx,
+                               struct bpf_prog *prog)
+{
+       int map_fd;
+       u32 i;
+       struct bpf_map *map;
+       int err = 0;
+       unsigned char hash[SHA256_DIGEST_SIZE];
+       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
+       struct hornet_map_security_struct *map_security;
+
+       for (i = 0; i < ctx->hash_count; i++) {
+               if (ctx->skips[i])
+                       continue;
+
+               err = copy_from_bpfptr_offset(&map_fd, maps->fd_array,
+                                             ctx->indexes[i] * sizeof(map_fd),
+                                             sizeof(map_fd));
+               if (err < 0)
+                       return LSM_INT_VERDICT_FAULT;
+
+               CLASS(fd, f)(map_fd);
+               if (fd_empty(f))
+                       return LSM_INT_VERDICT_FAULT;
+               if (unlikely(fd_file(f)->f_op != &bpf_map_fops))
+                       return LSM_INT_VERDICT_FAULT;
+
+               map = fd_file(f)->private_data;
+               if (!map->frozen)
+                       return LSM_INT_VERDICT_FAULT;
+
+               map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);
+
+               err = memcmp(hash, &ctx->hashes[i * SHA256_DIGEST_SIZE],
+                             SHA256_DIGEST_SIZE);
+               if (err)
+                       return LSM_INT_VERDICT_UNEXPECTED;
+
+               security->checked[i] = true;
+               memcpy(&security->hashes[i * SHA256_DIGEST_SIZE], hash, SHA256_DIGEST_SIZE);
+               map_security = hornet_bpf_map_security(map);
+               map_security->checked = true;
+               map_security->index = i;
+       }
+       return LSM_INT_VERDICT_OK;
+}
+
+int hornet_next_map(void *context, size_t hdrlen,
+                    unsigned char tag,
+                    const void *value, size_t vlen)
+{
+       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+       if (++ctx->hash_count >= MAX_USED_MAPS)
+               return -EINVAL;
+       return 0;
+}
+
+int hornet_map_index(void *context, size_t hdrlen,
+                    unsigned char tag,
+                    const void *value, size_t vlen)
+{
+       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+       if (vlen > 1)
+               return -EINVAL;
+
+       ctx->indexes[ctx->hash_count] = *(u8 *)value;
+       return 0;
+}
+
+int hornet_map_hash(void *context, size_t hdrlen,
+                   unsigned char tag,
+                   const void *value, size_t vlen)
+
+{
+       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+       if (vlen != SHA256_DIGEST_SIZE && vlen != 0)
+               return -EINVAL;
+
+       if (vlen) {
+               ctx->skips[ctx->hash_count] = false;
+               memcpy(&ctx->hashes[ctx->hash_count * SHA256_DIGEST_SIZE], value, vlen);
+       } else
+               ctx->skips[ctx->hash_count] = true;
+
+       return 0;
+}
+
+static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
+                               struct bpf_token *token, bool is_kernel,
+                               enum lsm_integrity_verdict *verdict)
+{
+       struct hornet_maps maps = {0};
+       bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
+       struct pkcs7_message *msg;
+       struct hornet_parse_context *ctx;
+       void *sig;
+       int err;
+       const void *authattrs;
+       size_t authattrs_len;
+       struct key *key;
+
+       if (!attr->signature) {
+               *verdict = LSM_INT_VERDICT_UNSIGNED;
+               return 0;
+       }
+
+       ctx = kzalloc(sizeof(struct hornet_parse_context), GFP_KERNEL);
+       if (!ctx)
+               return -ENOMEM;
+
+       maps.fd_array = make_bpfptr(attr->fd_array, is_kernel);
+       sig = kzalloc(attr->signature_size, GFP_KERNEL);
+       if (!sig) {
+               err = -ENOMEM;
+               goto out;
+       }
+       err = copy_from_bpfptr(sig, usig, attr->signature_size);
+       if (err != 0)
+               goto cleanup_sig;
+
+       msg = pkcs7_parse_message(sig, attr->signature_size);

security/hornet/hornet_lsm.c line 199

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+       if (IS_ERR(msg)) {
+               *verdict = LSM_INT_VERDICT_BADSIG;
+               err = 0;
+               goto cleanup_sig;
+       }
+
+       if (system_keyring_id_check(attr->keyring_id) == 0)
+               key = (struct key*)(unsigned long)attr->keyring_id;
+       else
+               key = key_ref_to_ptr(lookup_user_key(attr->keyring_id, 0, KEY_DEFER_PERM_CHECK));

You might need to key_put the user key.

Quoted 106 lines
security/hornet/hornet_lsm.c new file +106 -0
+
+       if (verify_pkcs7_message_sig(prog->insnsi, prog->len * sizeof(struct bpf_insn), msg,
+                                    key,
+                                    VERIFYING_BPF_SIGNATURE,
+                                    NULL, NULL)) {
+               *verdict = LSM_INT_VERDICT_UNKNOWNKEY;
+               err = 0;
+               goto cleanup_msg;
+       }
+
+       if (pkcs7_get_authattr(msg, OID_hornet_data,
+                              &authattrs, &authattrs_len) == -ENODATA) {
+               *verdict = LSM_INT_VERDICT_PARTIALSIG;
+               err = 0;
+               goto cleanup_msg;
+       }
+
+       err = asn1_ber_decoder(&hornet_decoder, ctx, authattrs, authattrs_len);
+       if (err < 0 || authattrs == NULL) {
+               *verdict = LSM_INT_VERDICT_BADSIG;
+               err = 0;
+               goto cleanup_msg;
+       }
+
+       err = hornet_verify_hashes(&maps, ctx, prog);
+       if (err == 0)
+               *verdict = LSM_INT_VERDICT_OK;
+       else
+               *verdict = err;
+
+cleanup_msg:
+       pkcs7_free_message(msg);
+cleanup_sig:
+       kfree(sig);
+out:
+       kfree(ctx);
+       return err;
+}
+
+static const struct lsm_id hornet_lsmid = {
+       .name = "hornet",
+       .id = LSM_ID_HORNET,
+};
+
+static int hornet_bpf_prog_load_integrity(struct bpf_prog *prog, union bpf_attr *attr,
+                                         struct bpf_token *token, bool is_kernel)
+{
+       enum lsm_integrity_verdict verdict;
+       int result = hornet_check_program(prog, attr, token, is_kernel, &verdict);
+
+       if (result < 0)
+               return result;
+
+       return security_bpf_prog_load_post_integrity(prog, attr, token, is_kernel,
+                                                    &hornet_lsmid, verdict);
+}
+
+static int hornet_verify_map(struct bpf_prog *prog, int index)
+{
+       unsigned char hash[SHA256_DIGEST_SIZE];
+       int i;
+       struct bpf_map *map;
+       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
+       struct hornet_map_security_struct *map_security;
+
+       if (!security->checked[index])
+               return 0;
+
+       for (i = 0; i < prog->aux->used_map_cnt; i++) {
+               map = prog->aux->used_maps[i];
+               map_security = hornet_bpf_map_security(map);
+               if (map_security->index != index)
+                       continue;
+
+               if (!map->frozen)
+                       return -EPERM;
+
+               map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);
+               if (memcmp(hash, &security->hashes[index * SHA256_DIGEST_SIZE],
+                          SHA256_DIGEST_SIZE) != 0)
+                       return -EPERM;
+               else
+                       return 0;
+       }
+       return -EINVAL;
+}
+
+static int hornet_check_prog_maps(u32 ufd)
+{
+       CLASS(fd, f)(ufd);
+       struct bpf_prog *prog;
+       int i, result = 0;
+
+       if (fd_empty(f))
+               return -EBADF;
+       if (fd_file(f)->f_op != &bpf_prog_fops)
+               return -EINVAL;
+
+       prog = fd_file(f)->private_data;
+
+       mutex_lock(&prog->aux->used_maps_mutex);
+       if (!prog->aux->used_map_cnt)
+               goto out;
+
+       for (i = 0; i < prog->aux->used_map_cnt; i++) {
+               result = hornet_verify_map(prog, i);
Quoted 9 lines

security/hornet/hornet_lsm.c line 308

Quoted 3 lines
security/hornet/hornet_lsm.c new file +3 -0
+               if (result)
+                       goto out;
+       }

This loop is kind of confusing for me, I guess it's just iterating through the maps the program currently in use. I feel the nested loop is unnecessary and the hash could be saved into the map's blob instead. Please correct me if I'm wrong here.

It turns out that we can. I'll send out the simplified version in v6.

Quoted 43 lines

-Fan

Quoted 41 lines
security/hornet/hornet_lsm.c new file +38 -1
+out:
+       mutex_unlock(&prog->aux->used_maps_mutex);
+       return result;
+}
+
+static int hornet_bpf(int cmd, union bpf_attr *attr, unsigned int size, bool kernel)
+{
+       /* in horent_bpf(), anything that had originated from kernel space we assume
+          has already been checked, in some form or another, so we don't bother
+          checking the intergity of any maps. In hornet_bpf_prog_load_integrity(),
+          hornet doesn't make any opinion on that and delegates that to the downstream
+          policy enforcement. */
+
+       if (cmd != BPF_PROG_RUN)
+               return 0;
+       if (kernel)
+               return 0;
+
+       return hornet_check_prog_maps(attr->test.prog_fd);
+}
+
+static struct security_hook_list hornet_hooks[] __ro_after_init = {
+       LSM_HOOK_INIT(bpf_prog_load_integrity, hornet_bpf_prog_load_integrity),
+       LSM_HOOK_INIT(bpf, hornet_bpf),
+};
+
+static int __init hornet_init(void)
+{
+       pr_info("Hornet: eBPF signature verification enabled\n");
+       security_add_hooks(hornet_hooks, ARRAY_SIZE(hornet_hooks), &hornet_lsmid);
+       return 0;
+}
+
+DEFINE_LSM(hornet) = {
+       .id = &hornet_lsmid,
+       .blobs = &hornet_blob_sizes,
+       .init = hornet_init,
+};
--

2.53.0

#12 lore raw

Re: [PATCH v5 06/10] security: Hornet LSM

In reply to Blaise Boscaccy
Sashiko AI review found 10 potential issue(s): - [High] Invalid Pointer Dereference due to missing IS_ERR check on lookup_user_key(). - [High] Resource Leak due to missing key_put() on user keyring…

Sashiko AI review found 10 potential issue(s):
- [High] Invalid Pointer Dereference due to missing IS_ERR check on lookup_user_key().
- [High] Resource Leak due to missing key_put() on user keyring reference.
- [High] NULL Pointer Dereference when verifying map types that do not implement map_get_hash.
- [High] Security Bypass due to uninitialized verdict variable on copy_from_bpfptr() failure.
- [High] Logic Flaw allowing verification of arbitrary file descriptors on copy_from_bpfptr_offset() failure.
- [High] State Corruption and Data Race in globally shared map_security object.
- [High] Logic Error bypassing runtime map verification due to incorrect loop index.
- [High] False Positives and Logic Error due to matching unverified maps to signature index 0.
- [Medium] Out-of-bounds Read in hornet_map_index() due to missing length check.
- [Medium] Off-by-One Error limits tracked maps to 63 instead of the intended 64.
--

commit 59054f5223757f6e86fcfcc7b0a3aa6b97918bab
Author: Blaise Boscaccy <[email protected]>

security: Hornet LSM

This commit adds the Hornet Linux Security Module. It verifies PKCS#7 signatures attached to eBPF programs and checks SHA-256 hashes of BPF maps at load and run time.

Quoted 5 lines
security/hornet/hornet_lsm.c new file +0 -0
new file mode 100644
index 0000000000000..f7d62fe6229c9

[ ... ]

Quoted 4 lines
security/hornet/hornet_lsm.c new file +4 -0
+static int hornet_verify_hashes(struct hornet_maps *maps,
+				struct hornet_parse_context *ctx,
+				struct bpf_prog *prog)
+{

[ ... ]

security/hornet/hornet_lsm.c line 84

Quoted 8 lines
security/hornet/hornet_lsm.c new file +8 -0
+	for (i = 0; i < ctx->hash_count; i++) {
+		if (ctx->skips[i])
+			continue;
+
+		err = copy_from_bpfptr_offset(&map_fd, maps->fd_array,
+					      ctx->indexes[i] * sizeof(map_fd),
+					      sizeof(map_fd));
+		if (err < 0)

Does this correctly handle copy_from_bpfptr_offset() failures?

The copy_from_bpfptr_offset() function returns the positive number of uncopied bytes on failure, so err < 0 will not catch the error.

Since the destination buffer is zero-filled on failure, could this allow inspecting file descriptor 0 instead of the intended map?

security/hornet/hornet_lsm.c line 97

Quoted 13 lines
security/hornet/hornet_lsm.c new file +13 -0
+			return LSM_INT_VERDICT_FAULT;
+
+		CLASS(fd, f)(map_fd);
+		if (fd_empty(f))
+			return LSM_INT_VERDICT_FAULT;
+		if (unlikely(fd_file(f)->f_op != &bpf_map_fops))
+			return LSM_INT_VERDICT_FAULT;
+
+		map = fd_file(f)->private_data;
+		if (!map->frozen)
+			return LSM_INT_VERDICT_FAULT;
+
+		map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);

Are we sure map_get_hash is implemented for all map types being checked?

Many standard BPF maps (such as hash maps) do not implement this callback. Could calling it unconditionally trigger a NULL pointer dereference if an unsupported map type is encountered?

security/hornet/hornet_lsm.c line 108

Quoted 11 lines
security/hornet/hornet_lsm.c new file +11 -0
+
+		err = memcmp(hash, &ctx->hashes[i * SHA256_DIGEST_SIZE],
+			      SHA256_DIGEST_SIZE);
+		if (err)
+			return LSM_INT_VERDICT_UNEXPECTED;
+
+		security->checked[i] = true;
+		memcpy(&security->hashes[i * SHA256_DIGEST_SIZE], hash, SHA256_DIGEST_SIZE);
+		map_security = hornet_bpf_map_security(map);
+		map_security->checked = true;
+		map_security->index = i;

Is it safe to write the program-specific signature index into the globally shared map_security object?

If multiple programs are concurrently loaded and attach the same map at different signature indices, this could result in a data race. When the first program runs, hornet_verify_map() expects the original index, which might cause legitimate executions to fail.

security/hornet/hornet_lsm.c line 119

Quoted 11 lines
security/hornet/hornet_lsm.c new file +11 -0
+	}
+	return LSM_INT_VERDICT_OK;
+}
+
+int hornet_next_map(void *context, size_t hdrlen,
+		     unsigned char tag,
+		     const void *value, size_t vlen)
+{
+	struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+	if (++ctx->hash_count >= MAX_USED_MAPS)

Should this condition be > instead of >=?

Since MAX_USED_MAPS is 64, returning an error when the count reaches 64 artificially restricts the maximum number of tracked maps to 63.

Quoted 4 lines
security/hornet/hornet_lsm.c new file +4 -0
+		return -EINVAL;
+	return 0;
+}
+

security/hornet/hornet_lsm.c line 133

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+int hornet_map_index(void *context, size_t hdrlen,
+		     unsigned char tag,
+		     const void *value, size_t vlen)
+{
+	struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
+
+	if (vlen > 1)
+		return -EINVAL;
+
+	ctx->indexes[ctx->hash_count] = *(u8 *)value;

Is it possible for vlen to be 0 here?

If a crafted ASN.1 signature provides a zero-length integer, this might result in an out-of-bounds read when accessing *(u8 *)value.

Quoted 2 lines
security/hornet/hornet_lsm.c new file +2 -0
+	return 0;
+}

[ ... ]

Quoted 4 lines
security/hornet/hornet_lsm.c new file +4 -0
+static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
+				struct bpf_token *token, bool is_kernel,
+				enum lsm_integrity_verdict *verdict)
+{

[ ... ]

security/hornet/hornet_lsm.c line 187

Quoted 3 lines
security/hornet/hornet_lsm.c new file +3 -0
+	err = copy_from_bpfptr(sig, usig, attr->signature_size);
+	if (err != 0)
+		goto cleanup_sig;

Does this properly handle a copy_from_bpfptr() failure?

The copy_from_bpfptr() function returns the positive number of uncopied bytes on failure, which is then propagated and returned by hornet_check_program().

In hornet_bpf_prog_load_integrity(), the failure check is if (result < 0). That check will pass on a positive return value, which could lead to security_bpf_prog_load_post_integrity() being called with an uninitialized verdict variable.

Quoted 5 lines
security/hornet/hornet_lsm.c new file +5 -0
+
+	msg = pkcs7_parse_message(sig, attr->signature_size);
+	if (IS_ERR(msg)) {
+		*verdict = LSM_INT_VERDICT_BADSIG;
+		err = 0;

security/hornet/hornet_lsm.c line 202

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+		goto cleanup_sig;
+	}
+
+	if (system_keyring_id_check(attr->keyring_id) == 0)
+		key = (struct key*)(unsigned long)attr->keyring_id;
+	else
+		key = key_ref_to_ptr(lookup_user_key(attr->keyring_id, 0, KEY_DEFER_PERM_CHECK));
+
+	if (verify_pkcs7_message_sig(prog->insnsi, prog->len * sizeof(struct bpf_insn), msg,
+				     key,

Can lookup_user_key() return an error pointer here?

If lookup_user_key() fails (for instance, due to an invalid keyring ID), key_ref_to_ptr() masks the reference but does not check for IS_ERR(). This could lead to an invalid pointer dereference when key is evaluated later.

Additionally, lookup_user_key() increments the reference count of the returned key. Is there a missing key_put() to release this reference? It appears the reference is never dropped on either the success or error paths, which could cause a memory leak.

[ ... ]

Quoted 5 lines
security/hornet/hornet_lsm.c new file +5 -0
+static int hornet_verify_map(struct bpf_prog *prog, int index)
+{
+	unsigned char hash[SHA256_DIGEST_SIZE];
+	int i;
+	struct bpf_map *map;

security/hornet/hornet_lsm.c line 271

Quoted 10 lines
security/hornet/hornet_lsm.c new file +10 -0
+	struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
+	struct hornet_map_security_struct *map_security;
+
+	if (!security->checked[index])
+		return 0;
+
+	for (i = 0; i < prog->aux->used_map_cnt; i++) {
+		map = prog->aux->used_maps[i];
+		map_security = hornet_bpf_map_security(map);
+		if (map_security->index != index)

If a map has not been verified, its map_security structure is zero-initialized, making map_security->index evaluate to 0.

When verifying signature index 0, could this wrongly match an unverified map and attempt to compute its hash, leading to false positives?

security/hornet/hornet_lsm.c line 277

Quoted 6 lines
security/hornet/hornet_lsm.c new file +6 -0
+			continue;
+
+		if (!map->frozen)
+			return -EPERM;
+
+		map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);

Similar to hornet_verify_hashes(), does this safely handle map types that do not implement map_get_hash?

Calling it unconditionally on unsupported maps might trigger a NULL pointer dereference.

Quoted 11 lines
security/hornet/hornet_lsm.c new file +11 -0
+		if (memcmp(hash, &security->hashes[index * SHA256_DIGEST_SIZE],
+			   SHA256_DIGEST_SIZE) != 0)
+			return -EPERM;
+		else
+			return 0;
+	}
+	return -EINVAL;
+}
+
+static int hornet_check_prog_maps(u32 ufd)
+{

[ ... ]

security/hornet/hornet_lsm.c

Quoted 2 lines
security/hornet/hornet_lsm.c new file +2 -0
+	for (i = 0; i < prog->aux->used_map_cnt; i++) {
+		result = hornet_verify_map(prog, i);

Is i the correct value to pass as the signature index here?

The loop counter i iterates up to used_map_cnt. If a program uses maps assigned to higher signature indices (e.g., indices 5 and 6) but only uses 2 maps overall, it appears this loop will only check indices 0 and 1, skipping verification for the expected indices.

Quoted 7 lines
security/hornet/hornet_lsm.c new file +7 -0
+		if (result)
+			goto out;
+	}
+out:
+	mutex_unlock(&prog->aux->used_maps_mutex);
+	return result;
+}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
maintainer
#13 lore raw

Re: [PATCH v5 6/10] security: Hornet LSM

In reply to Blaise Boscaccy
Quoted 28 lines · On Apr 20, 2026 Blaise Boscaccy <[email protected]> wrote:

This adds the Hornet Linux Security Module which provides enhanced signature verification and data validation for eBPF programs. This allows users to continue to maintain an invariant that all code running inside of the kernel has actually been signed and verified, by the kernel.

This effort builds upon the currently excepted upstream solution. It further hardens it by providing deterministic, in-kernel checking of map hashes to solidify auditing along with preventing TOCTOU attacks against lskel map hashes.

Target map hashes are passed in via PKCS#7 signed attributes. Hornet determines the extent which the eBFP program is signed and defers to other LSMs for policy decisions.

---
 Documentation/admin-guide/LSM/Hornet.rst | 321 +++++++++++++++++++++
 Documentation/admin-guide/LSM/index.rst  |   1 +
 MAINTAINERS                              |   9 +
 include/linux/oid_registry.h             |   3 +
 include/uapi/linux/lsm.h                 |   1 +
 security/Kconfig                         |   3 +-
 security/Makefile                        |   1 +
 security/hornet/Kconfig                  |  11 +
 security/hornet/Makefile                 |   7 +
Quoted 8 lines
 security/hornet/hornet.asn1              |  13 +
 security/hornet/hornet_lsm.c             | 346 +++++++++++++++++++++++
 11 files changed, 715 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/admin-guide/LSM/Hornet.rst
 create mode 100644 security/hornet/Kconfig
 create mode 100644 security/hornet/Makefile
 create mode 100644 security/hornet/hornet.asn1
 create mode 100644 security/hornet/hornet_lsm.c

While I think this is looking pretty reasonable, I think Fan had some feedback which merits a reply. I also spotted some references to the secondary keyring in the docs which need to be updated (below).

Quoted 19 lines
Documentation/admin-guide/LSM/Hornet.rst new file +13 -0
new file mode 100644
index 0000000000000..af5e9cd9d83a8
@@ -0,0 +1,321 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======
+Hornet
+======
+
+Hornet is a Linux Security Module that provides extensible signature
+verification for eBPF programs. This is selectable at build-time with
+``CONFIG_SECURITY_HORNET``.
+
+Overview
+========
+

Documentation/admin-guide/LSM/Hornet.rst line 23

Quoted 10 lines
Documentation/admin-guide/LSM/Hornet.rst new file +10 -0
+Hornet addresses concerns from users who require strict audit trails and
+verification guarantees for eBPF programs, especially in
+security-sensitive environments. Many production systems need assurance
+that only authorized, unmodified eBPF programs are loaded into the
+kernel. Hornet provides this assurance through cryptographic signature
+verification.
+
+When an eBPF program is loaded via the ``bpf()`` syscall, Hornet
+verifies a PKCS#7 signature attached to the program instructions. The
+signature is checked against the kernel's secondary keyring using the

This version now supports using the keyring specified in the bpf_attr union (presumably for maximum compatibility with KP's signature scheme), which is good, but the docs need to be updated.

See below, but I would probably make a note that LSMs providing enforcement of BPF signatures will likely want to check what keyring was used to verify the signature, e.g. a trusted keyring vs a user supplied keyring.

Quoted 12 lines
Documentation/admin-guide/LSM/Hornet.rst new file +12 -0
+existing kernel cryptographic infrastructure. In addition to signing the
+program bytecode, Hornet supports signing SHA-256 hashes of associated
+BPF maps, enabling integrity verification of map contents at load time
+and at runtime.
+
+After verification, Hornet classifies the program into one of the
+following integrity states and passes the result to a downstream LSM hook
+(``bpf_prog_load_post_integrity``), allowing other security modules to
+make policy decisions based on the verification outcome:
+
+``LSM_INT_VERDICT_OK``
+  The program signature and all map hashes verified successfully.

Documentation/admin-guide/LSM/Hornet.rst line 45

Quoted 10 lines
Documentation/admin-guide/LSM/Hornet.rst new file +10 -0
+
+``LSM_INT_VERDICT_UNSIGNED``
+  No signature was provided with the program.
+
+``LSM_INT_VERDICT_PARTIALSIG``
+  The program signature verified, but the signature did not contain
+  hornet map hash data.
+
+``LSM_INT_VERDICT_UNKNOWNKEY``
+  The signing certificate is not trusted in the secondary keyring,

Another secondary keyring mention.

Documentation/admin-guide/LSM/Hornet.rst line 59

Quoted 13 lines
Documentation/admin-guide/LSM/Hornet.rst new file +13 -0
+``LSM_INT_VERDICT_FAULT``
+  A system error occured during verification.
+
+``LSM_INT_VERDICT_UNEXPECTED``
+  An unexpected map hash value was encountered.
+
+``LSM_INT_VERDICT_BADSIG``
+  The signature or a map hash failed verification.
+
+Hornet itself does not enforce a policy on whether unsigned or partially
+signed programs should be rejected. It delegates that decision to
+downstream LSMs via the ``bpf_prog_load_post_integrity`` hook, making it
+a composable building block in a larger security architecture.

This might be a good place to document that in addition to the verdicts described above, LSMs providing enforcement should also consider the keyring used for verification.

Quoted 7 lines
Documentation/admin-guide/LSM/Hornet.rst new file +7 -0
+Known Limitations
+=================
+
+- Hornet requires programs to use :doc:`light skeletons
+  </bpf/libbpf/libbpf_naming_convention>` (lskels) for the signing
+  workflow, as the tooling operates on lskel-generated headers.
+

Documentation/admin-guide/LSM/Hornet.rst line 115

Quoted 10 lines
Documentation/admin-guide/LSM/Hornet.rst new file +10 -0
+- A maximum of 64 maps per program can be tracked for hash
+  verification.
+
+- Map hash verification requires the maps to be frozen before loading.
+  Maps that are not frozen at load time will cause verification to fail
+  when their hashes are included in the signature.
+
+- Hornet relies on the kernel's secondary keyring
+  (``VERIFY_USE_SECONDARY_KEYRING``) for certificate trust. Keys must
+  be provisioned into this keyring before programs can be verified.

... another spot.

Quoted 2 lines
Documentation/admin-guide/LSM/Hornet.rst new file +2 -0
+- The only hashing algorithm available is SHA256 due to it be hardcoded
+  in the bpf subsystem.

...

Quoted 4 lines
Documentation/admin-guide/LSM/Hornet.rst new file +4 -0
+Signature Verification Flow
+---------------------------
+
+The following describes what happens when a userspace program calls

Documentation/admin-guide/LSM/Hornet.rst line 152

Quoted 10 lines
Documentation/admin-guide/LSM/Hornet.rst new file +10 -0
+``bpf(BPF_PROG_LOAD, ...)`` with a signature attached:
+
+1. The ``bpf_prog_load_integrity`` LSM hook is invoked.
+
+2. Hornet reads the signature from the userspace buffer specified by
+   ``attr->signature`` (with length ``attr->signature_size``).
+
+3. The PKCS#7 signature is verified against the program instructions
+   using ``verify_pkcs7_signature()`` with the kernel's secondary
+   keyring.

I believe this is the last mention of the secondary keyring.

Quoted 5 lines

+4. The PKCS#7 message is parsed and its trust chain is validated via

Documentation/admin-guide/LSM/Hornet.rst new file +4 -0
+   ``validate_pkcs7_trust()``.
+
+5. Hornet extracts the authenticated attribute identified by
+   ``OID_hornet_data`` (OID ``2.25.316487325684022475439036912669789383960``)

Documentation/admin-guide/LSM/Hornet.rst line 168

Quoted 10 lines
Documentation/admin-guide/LSM/Hornet.rst new file +10 -0
+   from the PKCS#7 message. This attribute contains an ASN.1-encoded set
+   of map index/hash pairs.
+
+6. For each map hash entry, Hornet retrieves the corresponding BPF map
+   via its file descriptor, confirms it is frozen, computes its SHA-256
+   hash, and compares it against the signed hash.
+
+7. The resulting integrity verdict is passed to the
+   ``bpf_prog_load_post_integrity`` hook so that downstream LSMs can
+   enforce policy.

--
paul-moore.com

#14 lore raw

PATCH v5 7/10 hornet: Introduce gen_sig

In reply to Blaise Boscaccy

This introduces the gen_sig tool. It creates a pkcs#7 signature of a data payload. Additionally it appends a signed attribute containing a set of hashes.

Typical usage is to provide a payload containing the light skeleton ebpf syscall program binary and it's associated maps, which can be extracted from the auto-generated skeleton header.

---
 scripts/Makefile            |   1 +
 scripts/hornet/Makefile     |   5 +
 scripts/hornet/gen_sig.c    | 392 ++++++++++++++++++++++++++++++++++++
 scripts/hornet/write-sig.sh |  27 +++
 4 files changed, 425 insertions(+)
 create mode 100644 scripts/hornet/Makefile
 create mode 100644 scripts/hornet/gen_sig.c
 create mode 100755 scripts/hornet/write-sig.sh
scripts/Makefile +1 -0
index 0941e5ce7b575..dea8ab91bbe4e 100644
@@ -63,6 +63,7 @@ subdir-$(CONFIG_GENKSYMS) += genksyms
 subdir-$(CONFIG_GENDWARFKSYMS) += gendwarfksyms
 subdir-$(CONFIG_SECURITY_SELINUX) += selinux
 subdir-$(CONFIG_SECURITY_IPE) += ipe
+subdir-$(CONFIG_SECURITY_HORNET) += hornet
 
 # Let clean descend into subdirs
 subdir-	+= basic dtc gdb kconfig mod
scripts/hornet/Makefile new file +5 -0
new file mode 100644
index 0000000000000..3ee41e5e9a9ff
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+hostprogs-always-y	:= gen_sig
+
+HOSTCFLAGS_gen_sig.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null)
+HOSTLDLIBS_gen_sig = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto)
scripts/hornet/gen_sig.c new file +392 -0

392 lines not on this page · raw message

scripts/hornet/write-sig.sh new file +27 -0
new file mode 100755
index 0000000000000..7eaabe3bab9aa
@@ -0,0 +1,27 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2025 Microsoft Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+
+function usage() {
+    echo "Sample for rewriting an autogenerated eBPF lskel headers"
+    echo "with a new signature"
+    echo ""
+    echo "USAGE: header_file sig"
+    exit
+}
+
+ARGC=$#
+
+EXPECTED_ARGS=2
+
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    SIG=$(xxd -p $2 | tr -d '\n' | sed 's/\(..\)/\\\\x\1/g')
+    sed '/const char opts_sig/,/;/c\\tstatic const char opts_sig[] __attribute__((__aligned__(8))) = "\\\n'"$(printf '%s\n' "$SIG")"'\";' $1
+fi
Signature · 1 line
2.53.0
maintainer
#15 lore raw

Re: [PATCH v5 07/10] hornet: Introduce gen_sig

Quoted 364 lines · Blaise Boscaccy

This introduces the gen_sig tool. It creates a pkcs#7 signature of a data payload. Additionally it appends a signed attribute containing a set of hashes.

Typical usage is to provide a payload containing the light skeleton ebpf syscall program binary and it's associated maps, which can be extracted from the auto-generated skeleton header.

---
 scripts/Makefile            |   1 +
 scripts/hornet/Makefile     |   5 +
 scripts/hornet/gen_sig.c    | 392 ++++++++++++++++++++++++++++++++++++
 scripts/hornet/write-sig.sh |  27 +++
 4 files changed, 425 insertions(+)
 create mode 100644 scripts/hornet/Makefile
 create mode 100644 scripts/hornet/gen_sig.c
 create mode 100755 scripts/hornet/write-sig.sh
scripts/Makefile +1 -0
index 0941e5ce7b575..dea8ab91bbe4e 100644
@@ -63,6 +63,7 @@ subdir-$(CONFIG_GENKSYMS) += genksyms
 subdir-$(CONFIG_GENDWARFKSYMS) += gendwarfksyms
 subdir-$(CONFIG_SECURITY_SELINUX) += selinux
 subdir-$(CONFIG_SECURITY_IPE) += ipe
+subdir-$(CONFIG_SECURITY_HORNET) += hornet
 
 # Let clean descend into subdirs
 subdir-        += basic dtc gdb kconfig mod
scripts/hornet/Makefile new file +5 -0
new file mode 100644
index 0000000000000..3ee41e5e9a9ff
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+hostprogs-always-y     := gen_sig
+
+HOSTCFLAGS_gen_sig.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null)
+HOSTLDLIBS_gen_sig = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto)
scripts/hornet/gen_sig.c new file +315 -0
new file mode 100644
index 0000000000000..f966516ebc99b
@@ -0,0 +1,392 @@
+/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+ *
+ * Generate a signature for an eBPF program along with appending
+ * map hashes as signed attributes
+ *
+ * Copyright © 2025      Microsoft Corporation.
+ *
+ * Authors: Blaise Boscaccy <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the licence, or (at your option) any later version.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <err.h>
+#include <getopt.h>
+
+#include <openssl/cms.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pkcs7.h>
+#include <openssl/x509.h>
+#include <openssl/pem.h>
+#include <openssl/objects.h>
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/opensslv.h>
+#include <openssl/bio.h>
+#include <openssl/stack.h>
+
+#if OPENSSL_VERSION_MAJOR >= 3
+# define USE_PKCS11_PROVIDER
+# include <openssl/provider.h>
+# include <openssl/store.h>
+#else
+# if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0)
+#  define USE_PKCS11_ENGINE
+#  include <openssl/engine.h>
+# endif
+#endif
+#include "../ssl-common.h"
+
+#define SHA256_LEN 32
+#define BUF_SIZE   (1 << 15) // 32 KiB
+#define MAX_HASHES 64
+
+struct hash_spec {
+       char *file;
+       int index;
+};
+
+typedef struct {
+       ASN1_INTEGER *index;
+       ASN1_OCTET_STRING *hash;
+
+} HORNET_MAP;
+
+DECLARE_ASN1_FUNCTIONS(HORNET_MAP)
+ASN1_SEQUENCE(HORNET_MAP) = {
+       ASN1_SIMPLE(HORNET_MAP, index, ASN1_INTEGER),
+       ASN1_SIMPLE(HORNET_MAP, hash, ASN1_OCTET_STRING)
+} ASN1_SEQUENCE_END(HORNET_MAP);
+
+IMPLEMENT_ASN1_FUNCTIONS(HORNET_MAP)
+
+DEFINE_STACK_OF(HORNET_MAP)
+
+typedef struct {
+       STACK_OF(HORNET_MAP) * maps;
+} MAP_SET;
+
+DECLARE_ASN1_FUNCTIONS(MAP_SET)
+ASN1_SEQUENCE(MAP_SET) = {
+       ASN1_SET_OF(MAP_SET, maps, HORNET_MAP)
+} ASN1_SEQUENCE_END(MAP_SET);
+
+IMPLEMENT_ASN1_FUNCTIONS(MAP_SET)
+
+#define DIE(...) do { fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); \
+               exit(EXIT_FAILURE); } while (0)
+
+static BIO *bio_open_wr(const char *path)
+{
+       BIO *b = BIO_new_file(path, "wb");
+
+       if (!b) {
+               perror(path);
+               ERR_print_errors_fp(stderr);
+               exit(EXIT_FAILURE);
+       }
+       return b;
+}
+
+static void usage(const char *prog)
+{
+       fprintf(stderr,
+               "Usage:\n"
+               "  %s --data content.bin --cert signer.crt --key signer.key [-pass pass]\n"
+               "     --out newsig.p7b \n"
+               "     --add FILE:index [--add FILE:index ...]\n",
+               prog);
+}
+
+static const char *key_pass;
+
+static int pem_pw_cb(char *buf, int len, int w, void *v)
+{
+       int pwlen;
+
+       if (!key_pass)
+               return -1;
+
+       pwlen = strlen(key_pass);
+       if (pwlen >= len)
+               return -1;
+
+       strcpy(buf, key_pass);
+
+       key_pass = NULL;
+
+       return pwlen;
+}
+
+static EVP_PKEY *read_private_key(const char *private_key_name)
+{
+       EVP_PKEY *private_key;
+       BIO *b;
+
+       b = BIO_new_file(private_key_name, "rb");
+       ERR(!b, "%s", private_key_name);
+       private_key = PEM_read_bio_PrivateKey(b, NULL, pem_pw_cb,
+                                             NULL);
+       ERR(!private_key, "%s", private_key_name);
+       BIO_free(b);
+
+       return private_key;
+}
+
+static X509 *read_x509(const char *x509_name)
+{
+       unsigned char buf[2];
+       X509 *x509;
+       BIO *b;
+       int n;
+
+       b = BIO_new_file(x509_name, "rb");
+       ERR(!b, "%s", x509_name);
+
+       /* Look at the first two bytes of the file to determine the encoding */
+       n = BIO_read(b, buf, 2);
+       if (n != 2) {
+               if (BIO_should_retry(b)) {
+                       fprintf(stderr, "%s: Read wanted retry\n", x509_name);
+                       exit(1);
+               }
+               if (n >= 0) {
+                       fprintf(stderr, "%s: Short read\n", x509_name);
+                       exit(1);
+               }
+               ERR(1, "%s", x509_name);
+       }
+
+       ERR(BIO_reset(b) != 0, "%s", x509_name);
+
+       if (buf[0] == 0x30 && buf[1] >= 0x81 && buf[1] <= 0x84)
+               /* Assume raw DER encoded X.509 */
+               x509 = d2i_X509_bio(b, NULL);
+       else
+               /* Assume PEM encoded X.509 */
+               x509 = PEM_read_bio_X509(b, NULL, NULL, NULL);
+
+       BIO_free(b);
+       ERR(!x509, "%s", x509_name);
+
+       return x509;
+}
+
+static int sha256(const char *path, unsigned char out[SHA256_LEN], unsigned int *out_len)
+{
+       FILE *f;
+       int rc;
+       EVP_MD_CTX *ctx;
+       unsigned char buf[BUF_SIZE];
+       size_t n;
+       unsigned int mdlen = 0;
+
+       if (!path || !out)
+               return -1;
+
+       f = fopen(path, "rb");
+       if (!f) {
+               perror("fopen");
+               return -2;
+       }
+
+       ERR_load_crypto_strings();
+
+       rc = -3;
+       ctx = EVP_MD_CTX_new();
+       if (!ctx) {
+               rc = -4;
+               goto done;
+       }
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+       if (EVP_DigestInit_ex2(ctx, EVP_sha256(), NULL) != 1) {
+               rc = -5;
+               goto done;
+       }
+#else
+       if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) {
+               rc = -5;
+               goto done;
+       }
+#endif
+       while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
+               if (EVP_DigestUpdate(ctx, buf, n) != 1) {
+                       rc = -6;
+                       goto done;
+               }
+       }
+       if (ferror(f)) {
+               rc = -7;
+               goto done;
+       }
+
+       if (EVP_DigestFinal_ex(ctx, out, &mdlen) != 1) {
+               rc = -8;
+               goto done;
+       }
+       if (mdlen != SHA256_LEN) {
+               rc = -9;
+               goto done;
+       }
+
+       if (out_len)
+               *out_len = mdlen;
+       rc = 0;
+
+done:
+       EVP_MD_CTX_free(ctx);
+       fclose(f);
+       ERR_free_strings();
+       return rc;
+}
+
+static void add_hash(MAP_SET *set, unsigned char *buffer, int buffer_len, int index)
+{
+       HORNET_MAP *map = NULL;
+
+       map = HORNET_MAP_new();
+       ASN1_INTEGER_set(map->index, index);
+       ASN1_OCTET_STRING_set(map->hash, buffer, buffer_len);
+       sk_HORNET_MAP_push(set->maps, map);
+}
+
+int main(int argc, char **argv)
+{
+       const char *cert_path = NULL;
+       const char *key_path = NULL;
+       const char *data_path = NULL;
+       const char *out_path = NULL;
+
+       X509 *signer;
+       EVP_PKEY *pkey;
+       BIO *data_in;
+       CMS_ContentInfo *cms_out;
+       struct hash_spec hashes[MAX_HASHES];
+       int hash_count = 0;
+       int flags;
+       CMS_SignerInfo *si;
+       MAP_SET *set;
+       unsigned char hash_buffer[SHA256_LEN];
+       unsigned int hash_len;
+       ASN1_OBJECT *oid;
+       unsigned char *der = NULL;
+       int der_len;
+       int err;
+       BIO *b_out;
+       int i;
+       char opt;
+
+       const char *short_opts = "C:K:P:O:A:Sh";
+
+       static const struct option long_opts[] = {
+               {"cert", required_argument, 0, 'C'},
+               {"key",  required_argument, 0, 'K'},
+               {"pass",  required_argument, 0, 'P'},
+               {"out",  required_argument, 0, 'O'},
+               {"data",  required_argument, 0, 'D'},
+               {"add",  required_argument, 0, 'A'},
+               {"help",    no_argument,       0, 'h'},
+               {0, 0, 0, 0}
+       };
+
+       while ((opt = getopt_long_only(argc, argv, short_opts, long_opts, NULL)) != -1) {
+               switch (opt) {
+               case 'C':
+                       cert_path = optarg;
+                       break;
+               case 'K':
+                       key_path = optarg;
+                       break;
+               case 'P':
+                       key_pass = optarg;
+                       break;
+               case 'O':

scripts/hornet/gen_sig.c line 325

Quoted 10 lines
scripts/hornet/gen_sig.c new file +10 -0
+                       out_path = optarg;
+                       break;
+               case 'D':
+                       data_path = optarg;
+                       break;
+               case 'A':
+                       if (strchr(optarg, ':')) {
+                               hashes[hash_count].file = strsep(&optarg, ":");
+                               hashes[hash_count].index = atoi(optarg);
+                               hash_count++;

Should the hash_count be compared to MAX_HASHES here?

Quoted 28 lines
scripts/hornet/gen_sig.c new file +28 -0
+                       } else {
+                               usage(argv[0]);
+                               return EXIT_FAILURE;
+                       }
+               }
+       }
+
+       if (!cert_path || !key_path || !out_path || !data_path) {
+               usage(argv[0]);
+               return EXIT_FAILURE;
+       }
+
+       OpenSSL_add_all_algorithms();
+       ERR_load_crypto_strings();
+
+       signer = read_x509(cert_path);
+       ERR(!signer, "Load cert failed");
+
+       pkey = read_private_key(key_path);
+       ERR(!pkey, "Load key failed");
+
+       data_in = BIO_new_file(data_path, "rb");
+       ERR(!data_in, "Load data failed");
+
+       cms_out = CMS_sign(NULL, NULL, NULL, NULL,
+                          CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY | CMS_DETACHED);
+       ERR(!cms_out, "create cms failed");
+

scripts/hornet/gen_sig.c line 363

Quoted 10 lines
scripts/hornet/gen_sig.c new file +10 -0
+       flags = CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY | CMS_NOSMIMECAP | CMS_DETACHED;
+
+       si = CMS_add1_signer(cms_out, signer, pkey, EVP_sha256(), flags);
+       ERR(!si, "add signer failed");
+
+       set = MAP_SET_new();
+       set->maps = sk_HORNET_MAP_new_null();
+
+       for (i = 0; i < hash_count; i++) {
+               sha256(hashes[i].file, hash_buffer, &hash_len);

Can sha256 fail here? In that case the program should stop here and report errors.

-Fan

Quoted 65 lines
scripts/hornet/gen_sig.c new file +29 -0
+               add_hash(set, hash_buffer, hash_len, hashes[i].index);
+       }
+
+       oid = OBJ_txt2obj("2.25.316487325684022475439036912669789383960", 1);
+       if (!oid) {
+               ERR_print_errors_fp(stderr);
+               DIE("create oid failed");
+       }
+
+       der_len = ASN1_item_i2d((ASN1_VALUE *)set, &der, ASN1_ITEM_rptr(MAP_SET));
+       CMS_signed_add1_attr_by_OBJ(si, oid, V_ASN1_SEQUENCE, der, der_len);
+
+       err = CMS_final(cms_out, data_in, NULL, CMS_NOCERTS | CMS_BINARY);
+       ERR(!err, "cms final failed");
+
+       OPENSSL_free(der);
+       MAP_SET_free(set);
+
+       b_out = bio_open_wr(out_path);
+       ERR(!b_out, "opening output path failed");
+
+       i2d_CMS_bio_stream(b_out, cms_out, NULL, 0);
+
+       BIO_free(data_in);
+       BIO_free(b_out);
+       EVP_cleanup();
+       ERR_free_strings();
+       return 0;
+}
scripts/hornet/write-sig.sh new file +27 -1
new file mode 100755
index 0000000000000..7eaabe3bab9aa
@@ -0,0 +1,27 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2025 Microsoft Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+
+function usage() {
+    echo "Sample for rewriting an autogenerated eBPF lskel headers"
+    echo "with a new signature"
+    echo ""
+    echo "USAGE: header_file sig"
+    exit
+}
+
+ARGC=$#
+
+EXPECTED_ARGS=2
+
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    SIG=$(xxd -p $2 | tr -d '\n' | sed 's/\(..\)/\\\\x\1/g')
+    sed '/const char opts_sig/,/;/c\\tstatic const char opts_sig[] __attribute__((__aligned__(8))) = "\\\n'"$(printf '%s\n' "$SIG")"'\";' $1
+fi
--

2.53.0

#16 lore raw

Re: [PATCH v5 07/10] hornet: Introduce gen_sig

In reply to Blaise Boscaccy
Sashiko AI review found 5 potential issue(s): - [High] Infinite loop during argument parsing on architectures with unsigned char. - [Medium] Ignored return value of `sha256()` causes use of…

Sashiko AI review found 5 potential issue(s):
- [High] Infinite loop during argument parsing on architectures with unsigned char.
- [Medium] Ignored return value of sha256() causes use of uninitialized memory.
- [Medium] Stack buffer overflow when parsing '--add' arguments.
- [Medium] Missing NULL pointer checks for OpenSSL allocations.
- [Medium] Unchecked return value of ASN1_item_i2d().
--

commit 72bf19e54c723e2a7111cbc219bbeb6e98977318
Author: Blaise Boscaccy <[email protected]>

hornet: Introduce gen_sig

This commit introduces the gen_sig tool. It creates a pkcs#7 signature of a data payload and appends a signed attribute containing a set of hashes.

Quoted 4 lines
scripts/hornet/gen_sig.c new file +0 -0
index 0000000000000..f966516ebc99b

[ ... ]

scripts/hornet/gen_sig.c line 260

Quoted 6 lines
scripts/hornet/gen_sig.c new file +6 -0
+static void add_hash(MAP_SET *set, unsigned char *buffer, int buffer_len, int index)
+{
+	HORNET_MAP *map = NULL;
+
+	map = HORNET_MAP_new();
+	ASN1_INTEGER_set(map->index, index);

map is allocated via HORNET_MAP_new(), but the return value is not checked for NULL before dereferencing it in ASN1_INTEGER_set(). Should we handle potential allocation failures here?

[ ... ]

Quoted 2 lines
scripts/hornet/gen_sig.c new file +2 -0
+int main(int argc, char **argv)
+{

[ ... ]

Quoted 7 lines
scripts/hornet/gen_sig.c new file +7 -0
+	int i;
+	char opt;
+
+	const char *short_opts = "C:K:P:O:A:Sh";
+
+	static const struct option long_opts[] = {
+		{"cert", required_argument, 0, 'C'},

scripts/hornet/gen_sig.c line 304

Quoted 10 lines
scripts/hornet/gen_sig.c new file +10 -0
+		{"key",  required_argument, 0, 'K'},
+		{"pass",  required_argument, 0, 'P'},
+		{"out",  required_argument, 0, 'O'},
+		{"data",  required_argument, 0, 'D'},
+		{"add",  required_argument, 0, 'A'},
+		{"help",    no_argument,       0, 'h'},
+		{0, 0, 0, 0}
+	};
+
+	while ((opt = getopt_long_only(argc, argv, short_opts, long_opts, NULL)) != -1) {

getopt_long_only() returns an int. If it returns -1, and opt is a char, architectures where char is unsigned by default (like ARM or PowerPC) will evaluate opt as 255.

Since 255 != -1, will this cause an infinite loop during argument parsing on those platforms?

Quoted 4 lines
scripts/hornet/gen_sig.c new file +4 -0
+		switch (opt) {
+		case 'C':
+			cert_path = optarg;
+			break;

[ ... ]

scripts/hornet/gen_sig.c line 325

Quoted 5 lines
scripts/hornet/gen_sig.c new file +5 -0
+		case 'A':
+			if (strchr(optarg, ':')) {
+				hashes[hash_count].file = strsep(&optarg, ":");
+				hashes[hash_count].index = atoi(optarg);
+				hash_count++;

The hashes array is defined with a fixed size of MAX_HASHES. Does this code need to check if hash_count is less than MAX_HASHES before writing to the array to prevent a stack buffer overflow if too many arguments are provided?

[ ... ]

scripts/hornet/gen_sig.c line 360

Quoted 5 lines
scripts/hornet/gen_sig.c new file +5 -0
+	si = CMS_add1_signer(cms_out, signer, pkey, EVP_sha256(), flags);
+	ERR(!si, "add signer failed");
+
+	set = MAP_SET_new();
+	set->maps = sk_HORNET_MAP_new_null();

MAP_SET_new() and sk_HORNET_MAP_new_null() can return NULL if memory allocation fails. Since set->maps dereferences set immediately, can this result in a segmentation fault under memory pressure?

scripts/hornet/gen_sig.c line 365

Quoted 5 lines
scripts/hornet/gen_sig.c new file +5 -0
+
+	for (i = 0; i < hash_count; i++) {
+		sha256(hashes[i].file, hash_buffer, &hash_len);
+		add_hash(set, hash_buffer, hash_len, hashes[i].index);
+	}

The return value of sha256() is ignored here. If the file cannot be opened, sha256() will fail, leaving hash_buffer and hash_len uninitialized.

Will this lead to an out-of-bounds read of uninitialized stack memory when passed to add_hash()?

scripts/hornet/gen_sig.c line 374

Quoted 9 lines
scripts/hornet/gen_sig.c new file +9 -0
+
+	oid = OBJ_txt2obj("2.25.316487325684022475439036912669789383960", 1);
+	if (!oid) {
+		ERR_print_errors_fp(stderr);
+		DIE("create oid failed");
+	}
+
+	der_len = ASN1_item_i2d((ASN1_VALUE *)set, &der, ASN1_ITEM_rptr(MAP_SET));
+	CMS_signed_add1_attr_by_OBJ(si, oid, V_ASN1_SEQUENCE, der, der_len);

ASN1_item_i2d() returns a negative value on failure. Since der_len is passed directly to CMS_signed_add1_attr_by_OBJ() without checking for errors, could this cause issues by passing a negative length to the OpenSSL routine?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=7
#17 lore raw

PATCH v5 8/10 hornet: Add a light skeleton data extractor scripts

In reply to Blaise Boscaccy

These script eases light skeleton development against Hornet by generating a data payloads which can be used for signing a light skeleton binary using gen_sig.

---
 scripts/hornet/extract-insn.sh | 27 +++++++++++++++++++++++++++
 scripts/hornet/extract-map.sh  | 27 +++++++++++++++++++++++++++
 scripts/hornet/extract-skel.sh | 27 +++++++++++++++++++++++++++
 3 files changed, 81 insertions(+)
 create mode 100755 scripts/hornet/extract-insn.sh
 create mode 100755 scripts/hornet/extract-map.sh
 create mode 100755 scripts/hornet/extract-skel.sh
scripts/hornet/extract-insn.sh new file +27 -0
new file mode 100755
index 0000000000000..52338f057ff6b
@@ -0,0 +1,27 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2025 Microsoft Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+
+function usage() {
+    echo "Sample script for extracting instructions"
+    echo "autogenerated eBPF lskel headers"
+    echo ""
+    echo "USAGE: header_file"
+    exit
+}
+
+ARGC=$#
+
+EXPECTED_ARGS=1
+
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    printf $(gcc -E $1 | grep "opts_insn" | \
+		 awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g')
+fi
scripts/hornet/extract-map.sh new file +27 -0
new file mode 100755
index 0000000000000..c309f505c6238
@@ -0,0 +1,27 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2025 Microsoft Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+
+function usage() {
+    echo "Sample script for extracting instructions"
+    echo "autogenerated eBPF lskel headers"
+    echo ""
+    echo "USAGE: header_file"
+    exit
+}
+
+ARGC=$#
+
+EXPECTED_ARGS=1
+
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    printf $(gcc -E $1 | grep "opts_data" | \
+		 awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g')
+fi
scripts/hornet/extract-skel.sh new file +27 -0
new file mode 100755
index 0000000000000..6550a86b89917
@@ -0,0 +1,27 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2025 Microsoft Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+
+function usage() {
+    echo "Sample script for extracting instructions and map data out of"
+    echo "autogenerated eBPF lskel headers"
+    echo ""
+    echo "USAGE: header_file field"
+    exit
+}
+
+ARGC=$#
+
+EXPECTED_ARGS=2
+
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    printf $(gcc -E $1 | grep "static const char opts_$2" | \
+		 awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g')
+fi
Signature · 1 line
2.53.0
#18 lore raw

Re: [PATCH v5 08/10] hornet: Add a light skeleton data extractor scripts

Sashiko AI review found 4 potential issue(s): - [High] Payload data corruption due to grep matching multiple lines and printf format string reuse. - [Medium] Scripts fail to preprocess headers…

Sashiko AI review found 4 potential issue(s):
- [High] Payload data corruption due to grep matching multiple lines and printf format string reuse.
- [Medium] Scripts fail to preprocess headers because gcc is invoked without include paths.
- [Medium] Pipeline failure when processing BPF programs with empty map data.
- [Low] Copy-paste error in usage text and unsafe unquoted variable usage.
--

commit 5d4574520f6f666709ea15ecc132a023d9222886
Author: Blaise Boscaccy <[email protected]>

hornet: Add a light skeleton data extractor scripts

This commit introduces bash scripts to extract instructions and map data from autogenerated eBPF light skeleton headers. These payloads can be used for signing light skeleton binaries.

Quoted 5 lines
scripts/hornet/extract-insn.sh new file +0 -0
new file mode 100755
index 0000000000000..52338f057ff6b

[ ... ]

scripts/hornet/extract-insn.sh line 27

Quoted 6 lines
scripts/hornet/extract-insn.sh new file +6 -0
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    printf $(gcc -E $1 | grep "opts_insn" | \
+		 awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g')
+fi

Will this corrupt the binary payload if grep matches multiple lines?

Skeleton headers generated by bpftool typically contain additional references like opts.insns_sz = sizeof(opts_insn) - 1; and opts.insns = (void *)opts_insn;.

Because the $(...) command substitution is unquoted, Bash will split the output into multiple arguments. Since printf doesn't receive format specifiers, it will reuse the binary payload as the format string for each extra argument (such as sizeof, -, 1), repeating the payload multiple times in the output.

Also, because EXPECTED_ARGS=1 is strictly enforced, users cannot pass -I flags. Since the generated headers unconditionally include <bpf/bpf.h> and <bpf/skel_internal.h>, will gcc -E fail and terminate before emitting the target variables if those headers are not in the default system include path?

Could $1 be quoted as "$1" to prevent failures when the file path contains spaces?

Quoted 5 lines
scripts/hornet/extract-map.sh new file +0 -0
new file mode 100755
index 0000000000000..c309f505c6238

[ ... ]

scripts/hornet/extract-map.sh

Quoted 2 lines
scripts/hornet/extract-map.sh new file +2 -0
+function usage() {
+    echo "Sample script for extracting instructions"

This isn't a bug, but should this say "extracting map data" instead of "instructions" since this is the map extractor script?

Quoted 5 lines
scripts/hornet/extract-map.sh new file +5 -0
+    echo "autogenerated eBPF lskel headers"
+    echo ""
+    echo "USAGE: header_file"
+    exit
+}

[ ... ]

Quoted 5 lines
scripts/hornet/extract-skel.sh new file +0 -0
new file mode 100755
index 0000000000000..6550a86b89917

[ ... ]

scripts/hornet/extract-skel.sh line 27

Quoted 6 lines
scripts/hornet/extract-skel.sh new file +6 -0
+if [ $ARGC -ne $EXPECTED_ARGS ] ; then
+    usage
+else
+    printf $(gcc -E $1 | grep "static const char opts_$2" | \
+		 awk -F"=" '{print $2}' | sed 's/;\+$//' | sed 's/\"//g')
+fi

If a BPF program uses no maps, the extracted skeleton will declare static const char opts_data[] = "";. The awk and sed pipeline will strip the quotes, resulting in an empty command substitution.

Since calling printf without arguments triggers a shell usage error (bash: printf: usage: printf [-v var] format [arguments]) and returns a non-zero exit status, will this break automated pipelines for map-less BPF programs?

Additionally, since extract-skel.sh avoids the multiple-match issue by using a stricter grep pattern (grep "static const char opts_$2"), does this make extract-insn.sh and extract-map.sh redundant?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
#19 lore raw

PATCH v5 9/10 selftests/hornet: Add a selftest for the Hornet LSM

In reply to Blaise Boscaccy

This selftest contains a testcase that utilizes light skeleton eBPF loaders and exercises hornet's map validation.

---
 tools/testing/selftests/Makefile             |  1 +
 tools/testing/selftests/hornet/Makefile      | 63 ++++++++++++++++++++
 tools/testing/selftests/hornet/loader.c      | 21 +++++++
 tools/testing/selftests/hornet/trivial.bpf.c | 33 ++++++++++
 4 files changed, 118 insertions(+)
 create mode 100644 tools/testing/selftests/hornet/Makefile
 create mode 100644 tools/testing/selftests/hornet/loader.c
 create mode 100644 tools/testing/selftests/hornet/trivial.bpf.c
tools/testing/selftests/Makefile +1 -0
index 450f13ba4cca9..4e2d1cd88c825 100644
@@ -44,6 +44,7 @@ TARGETS += ftrace
 TARGETS += futex
 TARGETS += gpio
 TARGETS += hid
+TARGETS += hornet
 TARGETS += intel_pstate
 TARGETS += iommu
 TARGETS += ipc
tools/testing/selftests/hornet/Makefile new file +63 -0

63 lines not on this page · raw message

tools/testing/selftests/hornet/loader.c new file +21 -0
new file mode 100644
index 0000000000000..f27580c7262b3
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stddef.h>
+#include <sys/resource.h>
+#include <bpf/libbpf.h>
+#include <errno.h>
+#include  "signed_loader.h"
+
+int main(int argc, char **argv)
+{
+	struct trivial *skel;
+
+	skel = trivial__open_and_load();
+	if (!skel)
+		return -1;
+
+	trivial__destroy(skel);
+	return 0;
+}
tools/testing/selftests/hornet/trivial.bpf.c new file +33 -0
new file mode 100644
index 0000000000000..d38c5b53ff932
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+
+#include "vmlinux.h"
+
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
+
+int monitored_pid = 0;
+
+SEC("tracepoint/syscalls/sys_enter_unlinkat")
+int handle_enter_unlink(struct trace_event_raw_sys_enter *ctx)
+{
+	char filename[128] = { 0 };
+	struct task_struct *task;
+	unsigned long start_time = 0;
+	int pid = bpf_get_current_pid_tgid() >> 32;
+	char *pathname_ptr = (char *) BPF_CORE_READ(ctx, args[1]);
+
+	bpf_probe_read_str(filename, sizeof(filename), pathname_ptr);
+	task = (struct task_struct *)bpf_get_current_task();
+	start_time = BPF_CORE_READ(task, start_time);
+
+	bpf_printk("BPF triggered unlinkat by PID: %d, start_time %ld. pathname = %s",
+		   pid, start_time, filename);
+
+	if (monitored_pid == pid)
+		bpf_printk("target pid found");
+
+	return 0;
+}
Signature · 1 line
2.53.0
#20 lore raw

Re: [PATCH v5 09/10] selftests/hornet: Add a selftest for the Hornet LSM

Sashiko AI review found 7 potential issue(s): - [High] Use of deprecated `bpf_probe_read_str` to read a user-space pointer. - [High] Hardcoded dependency on the host's `/sys/kernel/btf/vmlinux`.…

Sashiko AI review found 7 potential issue(s):
- [High] Use of deprecated bpf_probe_read_str to read a user-space pointer.
- [High] Hardcoded dependency on the host's /sys/kernel/btf/vmlinux.
- [High] Custom Makefile rules are bypassed due to $(OUTPUT)/ prefix mismatch.
- [High] Linking against the host system's libbpf instead of the in-tree library.
- [High] Unconditional hardcoded dependency on kernel signing keys.
- [Medium] The selftest fails to verify Hornet's negative enforcement capabilities.
- [Medium] BPF selftest loader.c uses raw conditionals instead of ASSERT_*() macros. --

commit 162ecc15ea5091af09f42092fc7aa704f31709fa
Author: Blaise Boscaccy <[email protected]>

selftests/hornet: Add a selftest for the Hornet LSM

This selftest contains a testcase that utilizes light skeleton eBPF loaders and exercises hornet's map validation.

Quoted 5 lines
tools/testing/selftests/hornet/Makefile new file +0 -0
new file mode 100644
index 0000000000000..432bce59f54e7

[ ... ]

tools/testing/selftests/hornet/Makefile line 20

Quoted 3 lines
tools/testing/selftests/hornet/Makefile new file +3 -0
+TEST_GEN_PROGS := loader
+TEST_GEN_FILES := vmlinux.h loader.h trivial.bpf.o map.bin sig.bin insn.bin signed_loader.h
+$(TEST_GEN_PROGS): LDLIBS += -lbpf

Could this link against the host operating system's libbpf.so instead of the locally built in-tree library?

It looks like it lacks the -L$(BPFDIR) library search path. If the host machine doesn't have libbpf-dev installed, the build might fail, or it could link against an outdated version.

[ ... ]

tools/testing/selftests/hornet/Makefile line 31

Quoted 2 lines
tools/testing/selftests/hornet/Makefile new file +2 -0
+vmlinux.h:
+	$(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

Does this hardcode a dependency on the host's running kernel instead of the target kernel being compiled?

This would break cross-compilation and fail the build entirely on host machines without CONFIG_DEBUG_INFO_BTF enabled in their running kernel. Should it use the in-tree $(VMLINUX_BTF) provided by the build infrastructure?

[ ... ]

tools/testing/selftests/hornet/Makefile line 54

Quoted 3 lines
tools/testing/selftests/hornet/Makefile new file +3 -0
+sig.bin: insn.bin map.bin $(OUTPUT)/gen_sig
+	$(OUTPUT)/gen_sig --key $(CERTDIR)/signing_key.pem --cert $(CERTDIR)/signing_key.x509 \
+		--data insn.bin --add map.bin:0 --out sig.bin

Will this unconditionally fail if the kernel configuration lacks CONFIG_MODULE_SIG?

The Makefile hardcodes the use of signing_key.pem and signing_key.x509, which might not exist. Should the test gracefully detect the absence of the required keys and skip the build or execution instead of failing the entire pipeline?

tools/testing/selftests/hornet/Makefile line 60

Quoted 6 lines
tools/testing/selftests/hornet/Makefile new file +6 -0
+
+signed_loader.h: sig.bin
+	$(SCRIPTSDIR)/write-sig.sh loader.h sig.bin > $@
+
+loader: loader.c signed_loader.h
+	$(CC) $(CFLAGS) -I$(LIBDIR) -I$(APIDIR) $< -o $@ -lbpf

Could this custom rule be bypassed because it lacks the $(OUTPUT)/ prefix?

The include ../lib.mk transforms TEST_GEN_PROGS by prefixing it with $(OUTPUT)/. When make attempts to build $(OUTPUT)/loader, it might fall back to its default pattern rule.

This would omit the -I include paths, the -lbpf linker flag, and the dependency on signed_loader.h, leading to compilation failures.

Also, similar to the LDLIBS addition above, this uses -lbpf without specifying the in-tree path.

Quoted 11 lines
tools/testing/selftests/hornet/loader.c new file +5 -0
new file mode 100644
index 0000000000000..f27580c7262b3
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stddef.h>

tools/testing/selftests/hornet/loader.c line 15

Quoted 10 lines
tools/testing/selftests/hornet/loader.c new file +10 -0
+#include <sys/resource.h>
+#include <bpf/libbpf.h>
+#include <errno.h>
+#include  "signed_loader.h"
+
+int main(int argc, char **argv)
+{
+	struct trivial *skel;
+
+	skel = trivial__open_and_load();

The commit message states this "exercises hornet's map validation", but this solely loads a valid, signed skeleton.

Are there any negative tests to verify that the LSM actually blocks invalid loads? If the Hornet LSM fails open or is disabled, this test might falsely pass.

tools/testing/selftests/hornet/loader.c line 17

Quoted 2 lines
tools/testing/selftests/hornet/loader.c new file +2 -0
+	if (!skel)
+		return -1;

Should this use the modern ASSERT_*() macros like ASSERT_OK_PTR() instead of raw conditionals?

Using raw conditionals silently bypasses the standard testing reporting infrastructure.

Quoted 9 lines
tools/testing/selftests/hornet/loader.c new file +4 -0
+
+	trivial__destroy(skel);
+	return 0;
+}
tools/testing/selftests/hornet/trivial.bpf.c new file +0 -0
new file mode 100644
index 0000000000000..d38c5b53ff932

[ ... ]

tools/testing/selftests/hornet/trivial.bpf.c line 22

Quoted 10 lines
tools/testing/selftests/hornet/trivial.bpf.c new file +10 -0
+SEC("tracepoint/syscalls/sys_enter_unlinkat")
+int handle_enter_unlink(struct trace_event_raw_sys_enter *ctx)
+{
+	char filename[128] = { 0 };
+	struct task_struct *task;
+	unsigned long start_time = 0;
+	int pid = bpf_get_current_pid_tgid() >> 32;
+	char *pathname_ptr = (char *) BPF_CORE_READ(ctx, args[1]);
+
+	bpf_probe_read_str(filename, sizeof(filename), pathname_ptr);

Can this silently fail on architectures with strict user/kernel address space separation (like x86 with SMAP enabled)?

The sys_enter_unlinkat tracepoint receives pathname from user space in args[1], but bpf_probe_read_str() is deprecated and meant for kernel-space pointers. Should this use bpf_probe_read_user_str() instead?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=9
#21 lore raw

PATCH v5 10/10 ipe: Add BPF program load policy enforcement via Hornet integration

In reply to Blaise Boscaccy

Add support for the bpf_prog_load_post_integrity LSM hook, enabling IPE to make policy decisions about BPF program loading based on integrity verdicts provided by the Hornet LSM.

New policy operation:
op=BPF_PROG_LOAD - Matches BPF program load events

New policy properties:

  bpf_signature=NONE      - No Verdict
  bpf_signature=OK        - Program signature and map hashes verified
  bpf_signature=UNSIGNED  - No signature provided
  bpf_signature=PARTIALSIG - Signature OK but no map hash data
  bpf_signature=UNKNOWNKEY - Cert not trusted
  bpf_signature=UNEXPECTED - An unexpected hash value was encountered
  bpf_signature=FAULT 	   - System error during verification
  bpf_signature=BADSIG    - Signature or map hash verification failed
  bpf_keyring=BUILTIN     - Program was signed using a builtin keyring
  bpf_keyring=SECONDARY   - Program was signed using the secondary keyring
  bpf_keyring=PLATFORM    - Program was signed using the platform keyring
  bpf_kernel=TRUE         - Program originated from kernelspace
  bpf_kernel=FALSE        - Program originated from userspace

These properties map directly to the lsm_integrity_verdict enum values provided by the Hornet LSM through security_bpf_prog_load_post_integrity.

The feature is gated on CONFIG_IPE_PROP_BPF_SIGNATURE which depends on CONFIG_SECURITY_HORNET.

---
 Documentation/admin-guide/LSM/ipe.rst | 162 +++++++++++++++++++++++++-
 Documentation/security/ipe.rst        |  39 +++++++
 security/ipe/Kconfig                  |  14 +++
 security/ipe/audit.c                  |  15 +++
 security/ipe/eval.c                   |  73 +++++++++++-
 security/ipe/eval.h                   |   5 +
 security/ipe/hooks.c                  |  37 ++++++
 security/ipe/hooks.h                  |  11 ++
 security/ipe/ipe.c                    |   3 +
 security/ipe/policy.h                 |  14 +++
 security/ipe/policy_parser.c          |  27 +++++
 11 files changed, 398 insertions(+), 2 deletions(-)
Documentation/admin-guide/LSM/ipe.rst +161 -1
index a756d81585317..4dfbf0d325a8a 100644
@@ -559,7 +559,8 @@ policy. Two properties are built-into the policy parser: 'op' and 'action'.
 The other properties are used to restrict immutable security properties
 about the files being evaluated. Currently those properties are:
 '``boot_verified``', '``dmverity_signature``', '``dmverity_roothash``',
-'``fsverity_signature``', '``fsverity_digest``'. A description of all
+'``fsverity_signature``', '``fsverity_digest``', '``bpf_signature``',
+'``bpf_keyring``', '``bpf_kernel``'. A description of all
 properties supported by IPE are listed below:
 
 op
@@ -603,6 +604,14 @@ as the first token. IPE supports the following operations:
       Controls loading IMA certificates through the Kconfigs,
       ``CONFIG_IMA_X509_PATH`` and ``CONFIG_EVM_X509_PATH``.
 
+   ``BPF_PROG_LOAD``:
+
+      Pertains to BPF programs being loaded via the ``bpf()`` syscall.
+      This operation is used in conjunction with the ``bpf_signature``,
+      ``bpf_keyring``, and ``bpf_kernel`` properties to control BPF
+      program loading based on integrity verification provided by the
+      Hornet LSM.
+
 action
 ~~~~~~
 
@@ -713,6 +722,105 @@ fsverity_signature
 
       fsverity_signature=(TRUE|FALSE)
 
+bpf_signature
+~~~~~~~~~~~~~
+
+   This property can be utilized for authorization of BPF program loads based
+   on the integrity verdict provided by the Hornet LSM. When a BPF program is
+   loaded, Hornet performs cryptographic verification of the program's PKCS#7
+   signature (if present) and passes an integrity verdict to IPE via the
+   ``security_bpf_prog_load_post_integrity`` hook. IPE can then allow or deny
+   the load based on the verdict.
+
+   This property depends on ``SECURITY_HORNET`` and is controlled by the
+   ``IPE_PROP_BPF_SIGNATURE`` config option.
+   The format of this property is::
+
+      bpf_signature=(NONE|OK|UNSIGNED|PARTIALSIG|UNKNOWNKEY|UNEXPECTED|FAULT|BADSIG)
+
+   The possible values correspond to the integrity verdicts from Hornet:
+
+      ``NONE``
+
+         No integrity verdict was set (default/uninitialized).
+
+      ``OK``
+
+         The BPF program's signature and all map hashes were successfully
+         verified.
+
+      ``UNSIGNED``
+
+         No signature was provided with the BPF program.
+
+      ``PARTIALSIG``
+
+         The program signature was verified, but no authenticated map hash
+         data was present.
+
+      ``UNKNOWNKEY``
+
+         The signing certificate is not trusted by the specified keyring.
+
+      ``UNEXPECTED``
+
+         An unexpected map hash value was encountered during verification.
+
+      ``FAULT``
+
+         A system error occurred during signature verification.
+
+      ``BADSIG``
+
+         The signature or hash verification failed.
+
+bpf_keyring
+~~~~~~~~~~~~
+
+   This property can be utilized for authorization of BPF program loads based
+   on the keyring specified in the ``bpf_attr`` during the ``BPF_PROG_LOAD``
+   syscall. This allows policies to restrict which keyring must be used for
+   signature verification of BPF programs.
+
+   This property shares the ``IPE_PROP_BPF_SIGNATURE`` config option with
+   ``bpf_signature``.
+   The format of this property is::
+
+      bpf_keyring=(BUILTIN|SECONDARY|PLATFORM)
+
+   The possible values correspond to the system keyrings:
+
+      ``BUILTIN``
+
+         The builtin trusted keyring (``.builtin_trusted_keys``), which
+         contains keys embedded at kernel compile time.
+
+      ``SECONDARY``
+
+         The secondary trusted keyring (``.secondary_trusted_keys``), which
+         includes both builtin trusted keys and keys added at runtime.
+
+      ``PLATFORM``
+
+         The platform keyring (``.platform``), which contains keys provided
+         by the platform firmware (e.g. UEFI db keys).
+
+bpf_kernel
+~~~~~~~~~~
+
+   This property can be utilized for authorization of BPF program loads based
+   on whether the load originated from kernel space or user space. The BPF
+   light skeleton infrastructure performs a secondary kernel-originated program
+   load that will not carry a signature. This property allows policies to
+   permit such kernel-originated loads while still requiring signatures for
+   user-space loads.
+
+   This property shares the ``IPE_PROP_BPF_SIGNATURE`` config option with
+   ``bpf_signature``.
+   The format of this property is::
+
+      bpf_kernel=(TRUE|FALSE)
+
 Policy Examples
 ---------------
 
@@ -788,6 +896,58 @@ Allow execution of a specific fs-verity file
 
    op=EXECUTE fsverity_digest=sha256:fd88f2b8824e197f850bf4c5109bea5cf0ee38104f710843bb72da796ba5af9e action=ALLOW
 
+Allow only signed BPF programs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+   policy_name=Allow_Signed_BPF policy_version=0.0.0
+   DEFAULT action=ALLOW
+
+   DEFAULT op=BPF_PROG_LOAD action=DENY
+   op=BPF_PROG_LOAD bpf_kernel=TRUE action=ALLOW
+   op=BPF_PROG_LOAD bpf_signature=OK action=ALLOW
+
+This policy allows all other operations but restricts BPF program loading
+to only programs that either originate from kernel space (e.g. light skeleton
+reloads) or have a valid signature verified by the Hornet LSM. Unsigned or
+improperly signed BPF programs from user space will be denied.
+
+Allow signed BPF programs from a specific keyring
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+   policy_name=Allow_BPF_Builtin_Keyring policy_version=0.0.0
+   DEFAULT action=ALLOW
+
+   DEFAULT op=BPF_PROG_LOAD action=DENY
+   op=BPF_PROG_LOAD bpf_kernel=TRUE action=ALLOW
+   op=BPF_PROG_LOAD bpf_signature=OK bpf_keyring=BUILTIN action=ALLOW
+
+This policy further restricts BPF program loading to only accept programs
+whose signatures were verified using the builtin trusted keyring. Programs
+signed against the secondary or platform keyrings will be denied, providing
+tighter control over which signing keys are acceptable.
+
+Allow signed BPF programs with relaxed partial signatures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+   policy_name=Allow_BPF_Partial policy_version=0.0.0
+   DEFAULT action=ALLOW
+
+   DEFAULT op=BPF_PROG_LOAD action=DENY
+   op=BPF_PROG_LOAD bpf_kernel=TRUE action=ALLOW
+   op=BPF_PROG_LOAD bpf_signature=OK action=ALLOW
+   op=BPF_PROG_LOAD bpf_signature=PARTIALSIG action=ALLOW
+
+This policy allows BPF programs that have been fully verified (``OK``) as
+well as programs with a valid program signature but without authenticated
+map hash data (``PARTIALSIG``). This can be useful during development or
+for programs that do not use maps.
+
 Additional Information
 ----------------------
 
Documentation/security/ipe.rst +39 -0
index 4a7d953abcdc3..de8fcf1dc173d 100644
@@ -412,6 +412,44 @@ a standard securityfs policy tree::
 
 The policy is stored in the ``->i_private`` data of the MyPolicy inode.
 
+BPF/Hornet Integration
+~~~~~~~~~~~~~~~~~~~~~~
+
+IPE integrates with the Hornet LSM to enforce integrity policies on BPF
+program loading. Hornet performs cryptographic verification of BPF program
+signatures (PKCS#7 with authenticated attributes containing map hashes) and
+provides an integrity verdict to IPE via the
+``security_bpf_prog_load_post_integrity`` hook.
+
+The hook flow is:
+
+  1. User space invokes ``BPF_PROG_LOAD`` via the ``bpf()`` syscall.
+  2. Hornet's ``bpf_prog_load_integrity`` hook calls ``hornet_check_program()``
+     to verify the program's signature and map hashes.
+  3. Hornet calls ``security_bpf_prog_load_post_integrity()`` with the
+     resulting ``lsm_integrity_verdict``.
+  4. IPE evaluates the verdict against the active policy's ``BPF_PROG_LOAD``
+     rules and returns ``-EACCES`` if denied.
+
+Three properties are available for BPF policy rules:
+
+  - ``bpf_signature``: Matches against the integrity verdict (OK, UNSIGNED,
+    BADSIG, etc.)
+  - ``bpf_keyring``: Matches against the keyring specified in ``bpf_attr``
+    (BUILTIN, SECONDARY, PLATFORM)
+  - ``bpf_kernel``: Matches whether the load originated from kernel space
+    (TRUE/FALSE). This is important because the BPF light skeleton
+    infrastructure performs a secondary kernel-originated program load that
+    does not carry a signature.
+
+All three properties are gated on ``CONFIG_IPE_PROP_BPF_SIGNATURE`` which
+depends on ``CONFIG_SECURITY_HORNET``.
+
+The evaluation context (``struct ipe_eval_ctx``) carries three BPF-specific
+fields: ``bpf_verdict`` (the integrity verdict enum), ``bpf_keyring_id``
+(the ``s32`` keyring ID from ``bpf_attr``), and ``bpf_kernel`` (bool
+indicating kernel origin).
+
 Tests
 -----
 
@@ -439,6 +477,7 @@ IPE has KUnit Tests for the policy parser. Recommended kunitconfig::
   CONFIG_IPE_PROP_DM_VERITY_SIGNATURE=y
   CONFIG_IPE_PROP_FS_VERITY=y
   CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG=y
+  CONFIG_IPE_PROP_BPF_SIGNATURE=y
   CONFIG_SECURITY_IPE_KUNIT_TEST=y
 
 In addition, IPE has a python based integration
security/ipe/Kconfig +14 -0
index a110a6cd848b7..4c1d46847582b 100644
@@ -95,6 +95,20 @@ config IPE_PROP_FS_VERITY_BUILTIN_SIG
 
 	  if unsure, answer Y.
 
+config IPE_PROP_BPF_SIGNATURE
+	bool "Enable support for Hornet BPF program signature verification"
+	depends on SECURITY_HORNET
+	help
+	  This option enables the 'bpf_signature' and 'bpf_keyring'
+	  properties within IPE policies. The 'bpf_signature' property
+	  allows IPE to make policy decisions based on the integrity
+	  verdict provided by the Hornet LSM when a BPF program is loaded.
+	  Verdicts include OK, UNSIGNED, PARTIALSIG, BADSIG, and others.
+	  The 'bpf_keyring' property allows policies to match against the
+	  keyring specified in bpf_attr (BUILTIN, SECONDARY, PLATFORM).
+
+	  If unsure, answer Y.
+
 endmenu
 
 config SECURITY_IPE_KUNIT_TEST
security/ipe/audit.c +15 -0
index 3f0deeb549127..251c6ec2f8423 100644
@@ -41,6 +41,7 @@ static const char *const audit_op_names[__IPE_OP_MAX + 1] = {
 	"KEXEC_INITRAMFS",
 	"POLICY",
 	"X509_CERT",
+	"BPF_PROG_LOAD",
 	"UNKNOWN",
 };
 
@@ -51,6 +52,7 @@ static const char *const audit_hook_names[__IPE_HOOK_MAX] = {
 	"MPROTECT",
 	"KERNEL_READ",
 	"KERNEL_LOAD",
+	"BPF_PROG_LOAD",
 };
 
 static const char *const audit_prop_names[__IPE_PROP_MAX] = {
@@ -62,6 +64,19 @@ static const char *const audit_prop_names[__IPE_PROP_MAX] = {
 	"fsverity_digest=",
 	"fsverity_signature=FALSE",
 	"fsverity_signature=TRUE",
+	"bpf_signature=NONE",
+	"bpf_signature=OK",
+	"bpf_signature=UNSIGNED",
+	"bpf_signature=PARTIALSIG",
+	"bpf_signature=UNKNOWNKEY",
+	"bpf_signature=UNEXPECTED",
+	"bpf_signature=FAULT",
+	"bpf_signature=BADSIG",
+	"bpf_keyring=BUILTIN",
+	"bpf_keyring=SECONDARY",
+	"bpf_keyring=PLATFORM",
+	"bpf_kernel=FALSE",
+	"bpf_kernel=TRUE",
 };
 
 /**
security/ipe/eval.c +72 -1

92 lines not on this page · raw message

security/ipe/eval.h +5 -0
index fef65a36468cb..1578d83bafc10 100644
@@ -52,6 +52,11 @@ struct ipe_eval_ctx {
 #ifdef CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG
 	const struct ipe_inode *ipe_inode;
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+	enum lsm_integrity_verdict bpf_verdict;
+	s32 bpf_keyring_id;
+	bool bpf_kernel;
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
 };
 
 enum ipe_match {
security/ipe/hooks.c +37 -0
index 0ae54a880405a..03541e5bb7f60 100644
@@ -340,3 +340,40 @@ int ipe_inode_setintegrity(const struct inode *inode,
 	return -EINVAL;
 }
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+/**
+ * ipe_bpf_prog_load_post_integrity() - ipe security hook for BPF program load.
+ * @prog: Supplies the BPF program being loaded.
+ * @attr: Supplies the bpf syscall attributes.
+ * @token: Supplies the BPF token, if any.
+ * @kernel: Whether the call originated from the kernel.
+ * @lsmid: Supplies the LSM ID of the integrity provider.
+ * @verdict: Supplies the integrity verdict from the provider (e.g. Hornet).
+ *
+ * This LSM hook is called after an integrity verification LSM (such as Hornet)
+ * has evaluated a BPF program's cryptographic signature. IPE uses the verdict
+ * to make a policy-based allow/deny decision.
+ *
+ * Return:
+ * * %0		- Success
+ * * %-EACCES	- Did not pass IPE policy
+ */
+int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+				     union bpf_attr *attr,
+				     struct bpf_token *token,
+				     bool kernel,
+				     const struct lsm_id *lsmid,
+				     enum lsm_integrity_verdict verdict)
+{
+	struct ipe_eval_ctx ctx = IPE_EVAL_CTX_INIT;
+
+	ctx.op = IPE_OP_BPF_PROG_LOAD;
+	ctx.hook = IPE_HOOK_BPF_PROG_LOAD;
+	ctx.bpf_verdict = verdict;
+	ctx.bpf_keyring_id = attr->keyring_id;
+	ctx.bpf_kernel = kernel;
+
+	return ipe_evaluate_event(&ctx);
+}
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
security/ipe/hooks.h +11 -0
index 07db373327402..95b74f7899750 100644
@@ -10,6 +10,7 @@
 #include <linux/security.h>
 #include <linux/blk_types.h>
 #include <linux/fsverity.h>
+#include <linux/bpf.h>
 
 enum ipe_hook_type {
 	IPE_HOOK_BPRM_CHECK = 0,
@@ -18,6 +19,7 @@ enum ipe_hook_type {
 	IPE_HOOK_MPROTECT,
 	IPE_HOOK_KERNEL_READ,
 	IPE_HOOK_KERNEL_LOAD,
+	IPE_HOOK_BPF_PROG_LOAD,
 	__IPE_HOOK_MAX
 };
 
@@ -52,4 +54,13 @@ int ipe_inode_setintegrity(const struct inode *inode, enum lsm_integrity_type ty
 			   const void *value, size_t size);
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
 
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+				     union bpf_attr *attr,
+				     struct bpf_token *token,
+				     bool kernel,
+				     const struct lsm_id *lsmid,
+				     enum lsm_integrity_verdict verdict);
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
+
 #endif /* _IPE_HOOKS_H */
security/ipe/ipe.c +3 -0
index 495bb765de1b8..6502d4ddc641c 100644
@@ -60,6 +60,9 @@ static struct security_hook_list ipe_hooks[] __ro_after_init = {
 #ifdef CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG
 	LSM_HOOK_INIT(inode_setintegrity, ipe_inode_setintegrity),
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+	LSM_HOOK_INIT(bpf_prog_load_post_integrity, ipe_bpf_prog_load_post_integrity),
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
 };
 
 /**
security/ipe/policy.h +14 -0
index 5bfbdbddeef86..748bea92beb19 100644
@@ -17,6 +17,7 @@ enum ipe_op_type {
 	IPE_OP_KEXEC_INITRAMFS,
 	IPE_OP_POLICY,
 	IPE_OP_X509,
+	IPE_OP_BPF_PROG_LOAD,
 	__IPE_OP_MAX,
 };
 
@@ -39,6 +40,19 @@ enum ipe_prop_type {
 	IPE_PROP_FSV_DIGEST,
 	IPE_PROP_FSV_SIG_FALSE,
 	IPE_PROP_FSV_SIG_TRUE,
+	IPE_PROP_BPF_SIG_NONE,
+	IPE_PROP_BPF_SIG_OK,
+	IPE_PROP_BPF_SIG_UNSIGNED,
+	IPE_PROP_BPF_SIG_PARTIALSIG,
+	IPE_PROP_BPF_SIG_UNKNOWNKEY,
+	IPE_PROP_BPF_SIG_UNEXPECTED,
+	IPE_PROP_BPF_SIG_FAULT,
+	IPE_PROP_BPF_SIG_BADSIG,
+	IPE_PROP_BPF_KEYRING_BUILTIN,
+	IPE_PROP_BPF_KEYRING_SECONDARY,
+	IPE_PROP_BPF_KEYRING_PLATFORM,
+	IPE_PROP_BPF_KERNEL_FALSE,
+	IPE_PROP_BPF_KERNEL_TRUE,
 	__IPE_PROP_MAX
 };
 
security/ipe/policy_parser.c +27 -0
index 6fa5bebf84714..71f63de56616b 100644
@@ -237,6 +237,7 @@ static const match_table_t operation_tokens = {
 	{IPE_OP_KEXEC_INITRAMFS,	"op=KEXEC_INITRAMFS"},
 	{IPE_OP_POLICY,			"op=POLICY"},
 	{IPE_OP_X509,			"op=X509_CERT"},
+	{IPE_OP_BPF_PROG_LOAD,		"op=BPF_PROG_LOAD"},
 	{IPE_OP_INVALID,		NULL}
 };
 
@@ -281,6 +282,19 @@ static const match_table_t property_tokens = {
 	{IPE_PROP_FSV_DIGEST,		"fsverity_digest=%s"},
 	{IPE_PROP_FSV_SIG_FALSE,	"fsverity_signature=FALSE"},
 	{IPE_PROP_FSV_SIG_TRUE,		"fsverity_signature=TRUE"},
+	{IPE_PROP_BPF_SIG_NONE,		"bpf_signature=NONE"},
+	{IPE_PROP_BPF_SIG_OK,		"bpf_signature=OK"},
+	{IPE_PROP_BPF_SIG_UNSIGNED,	"bpf_signature=UNSIGNED"},
+	{IPE_PROP_BPF_SIG_PARTIALSIG,	"bpf_signature=PARTIALSIG"},
+	{IPE_PROP_BPF_SIG_UNKNOWNKEY,	"bpf_signature=UNKNOWNKEY"},
+	{IPE_PROP_BPF_SIG_UNEXPECTED,	"bpf_signature=UNEXPECTED"},
+	{IPE_PROP_BPF_SIG_FAULT,	"bpf_signature=FAULT"},
+	{IPE_PROP_BPF_SIG_BADSIG,	"bpf_signature=BADSIG"},
+	{IPE_PROP_BPF_KEYRING_BUILTIN,	"bpf_keyring=BUILTIN"},
+	{IPE_PROP_BPF_KEYRING_SECONDARY,	"bpf_keyring=SECONDARY"},
+	{IPE_PROP_BPF_KEYRING_PLATFORM,	"bpf_keyring=PLATFORM"},
+	{IPE_PROP_BPF_KERNEL_FALSE,	"bpf_kernel=FALSE"},
+	{IPE_PROP_BPF_KERNEL_TRUE,	"bpf_kernel=TRUE"},
 	{IPE_PROP_INVALID,		NULL}
 };
 
@@ -331,6 +345,19 @@ static int parse_property(char *t, struct ipe_rule *r)
 	case IPE_PROP_DMV_SIG_TRUE:
 	case IPE_PROP_FSV_SIG_FALSE:
 	case IPE_PROP_FSV_SIG_TRUE:
+	case IPE_PROP_BPF_SIG_NONE:
+	case IPE_PROP_BPF_SIG_OK:
+	case IPE_PROP_BPF_SIG_UNSIGNED:
+	case IPE_PROP_BPF_SIG_PARTIALSIG:
+	case IPE_PROP_BPF_SIG_UNKNOWNKEY:
+	case IPE_PROP_BPF_SIG_UNEXPECTED:
+	case IPE_PROP_BPF_SIG_FAULT:
+	case IPE_PROP_BPF_SIG_BADSIG:
+	case IPE_PROP_BPF_KEYRING_BUILTIN:
+	case IPE_PROP_BPF_KEYRING_SECONDARY:
+	case IPE_PROP_BPF_KEYRING_PLATFORM:
+	case IPE_PROP_BPF_KERNEL_FALSE:
+	case IPE_PROP_BPF_KERNEL_TRUE:
 		p->type = token;
 		break;
 	default:
Signature · 1 line
2.53.0
maintainer
#22 lore raw

Re: [PATCH v5 10/10] ipe: Add BPF program load policy enforcement via Hornet integration

Quoted 31 lines · Blaise Boscaccy

Add support for the bpf_prog_load_post_integrity LSM hook, enabling IPE to make policy decisions about BPF program loading based on integrity verdicts provided by the Hornet LSM.

New policy operation:
op=BPF_PROG_LOAD - Matches BPF program load events

New policy properties:

  bpf_signature=NONE      - No Verdict
  bpf_signature=OK        - Program signature and map hashes verified
  bpf_signature=UNSIGNED  - No signature provided
  bpf_signature=PARTIALSIG - Signature OK but no map hash data
  bpf_signature=UNKNOWNKEY - Cert not trusted
  bpf_signature=UNEXPECTED - An unexpected hash value was encountered
  bpf_signature=FAULT      - System error during verification
  bpf_signature=BADSIG    - Signature or map hash verification failed
  bpf_keyring=BUILTIN     - Program was signed using a builtin keyring
  bpf_keyring=SECONDARY   - Program was signed using the secondary keyring
  bpf_keyring=PLATFORM    - Program was signed using the platform keyring
  bpf_kernel=TRUE         - Program originated from kernelspace
  bpf_kernel=FALSE        - Program originated from userspace

These properties map directly to the lsm_integrity_verdict enum values provided by the Hornet LSM through security_bpf_prog_load_post_integrity.

The feature is gated on CONFIG_IPE_PROP_BPF_SIGNATURE which depends on CONFIG_SECURITY_HORNET.

---

...

Quoted 84 lines
security/ipe/hooks.c +37 -0
index 0ae54a880405a..03541e5bb7f60 100644
@@ -340,3 +340,40 @@ int ipe_inode_setintegrity(const struct inode *inode,
        return -EINVAL;
 }
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+/**
+ * ipe_bpf_prog_load_post_integrity() - ipe security hook for BPF program load.
+ * @prog: Supplies the BPF program being loaded.
+ * @attr: Supplies the bpf syscall attributes.
+ * @token: Supplies the BPF token, if any.
+ * @kernel: Whether the call originated from the kernel.
+ * @lsmid: Supplies the LSM ID of the integrity provider.
+ * @verdict: Supplies the integrity verdict from the provider (e.g. Hornet).
+ *
+ * This LSM hook is called after an integrity verification LSM (such as Hornet)
+ * has evaluated a BPF program's cryptographic signature. IPE uses the verdict
+ * to make a policy-based allow/deny decision.
+ *
+ * Return:
+ * * %0                - Success
+ * * %-EACCES  - Did not pass IPE policy
+ */
+int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+                                    union bpf_attr *attr,
+                                    struct bpf_token *token,
+                                    bool kernel,
+                                    const struct lsm_id *lsmid,
+                                    enum lsm_integrity_verdict verdict)
+{
+       struct ipe_eval_ctx ctx = IPE_EVAL_CTX_INIT;
+
+       ctx.op = IPE_OP_BPF_PROG_LOAD;
+       ctx.hook = IPE_HOOK_BPF_PROG_LOAD;
+       ctx.bpf_verdict = verdict;
+       ctx.bpf_keyring_id = attr->keyring_id;
+       ctx.bpf_kernel = kernel;
+
+       return ipe_evaluate_event(&ctx);
+}
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
security/ipe/hooks.h +11 -0
index 07db373327402..95b74f7899750 100644
@@ -10,6 +10,7 @@
 #include <linux/security.h>
 #include <linux/blk_types.h>
 #include <linux/fsverity.h>
+#include <linux/bpf.h>
 
 enum ipe_hook_type {
        IPE_HOOK_BPRM_CHECK = 0,
@@ -18,6 +19,7 @@ enum ipe_hook_type {
        IPE_HOOK_MPROTECT,
        IPE_HOOK_KERNEL_READ,
        IPE_HOOK_KERNEL_LOAD,
+       IPE_HOOK_BPF_PROG_LOAD,
        __IPE_HOOK_MAX
 };
 
@@ -52,4 +54,13 @@ int ipe_inode_setintegrity(const struct inode *inode, enum lsm_integrity_type ty
                           const void *value, size_t size);
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
 
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+                                    union bpf_attr *attr,
+                                    struct bpf_token *token,
+                                    bool kernel,
+                                    const struct lsm_id *lsmid,
+                                    enum lsm_integrity_verdict verdict);
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
+
 #endif /* _IPE_HOOKS_H */
security/ipe/ipe.c +0 -0
index 495bb765de1b8..6502d4ddc641c 100644

security/ipe/ipe.c static struct security_hook_list ipe_hooks[] __ro_after_init = { · line 67

Quoted 8 lines
security/ipe/ipe.c +3 -0
@@ -60,6 +60,9 @@ static struct security_hook_list ipe_hooks[] __ro_after_init = {
 #ifdef CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG
        LSM_HOOK_INIT(inode_setintegrity, ipe_inode_setintegrity),
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+       LSM_HOOK_INIT(bpf_prog_load_post_integrity, ipe_bpf_prog_load_post_integrity),
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
 };
 

IPE only registers bpf_prog_load_post_integrity(), which fires only when Hornet calls security_bpf_prog_load_post_integrity. If Hornet isn't enabled via lsm=, this hook never triggers, and an IPE policy like "DEFAULT op=BPF_PROG_LOAD action=DENY" silently allows every load.

I think IPE should register bpf_prog_load() as well, post_integrity() should only store the verdict to a per-prog blob, and bpf_prog_load reads it and evaluates policy.

-Fan

#23 lore raw

Re: [PATCH v5 10/10] ipe: Add BPF program load policy enforcement via Hornet integration

Quoted 135 lines · Fan Wu
Quoted 31 lines · Blaise Boscaccy

Add support for the bpf_prog_load_post_integrity LSM hook, enabling IPE to make policy decisions about BPF program loading based on integrity verdicts provided by the Hornet LSM.

New policy operation:
op=BPF_PROG_LOAD - Matches BPF program load events

New policy properties:

  bpf_signature=NONE      - No Verdict
  bpf_signature=OK        - Program signature and map hashes verified
  bpf_signature=UNSIGNED  - No signature provided
  bpf_signature=PARTIALSIG - Signature OK but no map hash data
  bpf_signature=UNKNOWNKEY - Cert not trusted
  bpf_signature=UNEXPECTED - An unexpected hash value was encountered
  bpf_signature=FAULT      - System error during verification
  bpf_signature=BADSIG    - Signature or map hash verification failed
  bpf_keyring=BUILTIN     - Program was signed using a builtin keyring
  bpf_keyring=SECONDARY   - Program was signed using the secondary keyring
  bpf_keyring=PLATFORM    - Program was signed using the platform keyring
  bpf_kernel=TRUE         - Program originated from kernelspace
  bpf_kernel=FALSE        - Program originated from userspace

These properties map directly to the lsm_integrity_verdict enum values provided by the Hornet LSM through security_bpf_prog_load_post_integrity.

The feature is gated on CONFIG_IPE_PROP_BPF_SIGNATURE which depends on CONFIG_SECURITY_HORNET.

---

...

Quoted 84 lines
security/ipe/hooks.c +37 -0
index 0ae54a880405a..03541e5bb7f60 100644
@@ -340,3 +340,40 @@ int ipe_inode_setintegrity(const struct inode *inode,
        return -EINVAL;
 }
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+/**
+ * ipe_bpf_prog_load_post_integrity() - ipe security hook for BPF program load.
+ * @prog: Supplies the BPF program being loaded.
+ * @attr: Supplies the bpf syscall attributes.
+ * @token: Supplies the BPF token, if any.
+ * @kernel: Whether the call originated from the kernel.
+ * @lsmid: Supplies the LSM ID of the integrity provider.
+ * @verdict: Supplies the integrity verdict from the provider (e.g. Hornet).
+ *
+ * This LSM hook is called after an integrity verification LSM (such as Hornet)
+ * has evaluated a BPF program's cryptographic signature. IPE uses the verdict
+ * to make a policy-based allow/deny decision.
+ *
+ * Return:
+ * * %0                - Success
+ * * %-EACCES  - Did not pass IPE policy
+ */
+int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+                                    union bpf_attr *attr,
+                                    struct bpf_token *token,
+                                    bool kernel,
+                                    const struct lsm_id *lsmid,
+                                    enum lsm_integrity_verdict verdict)
+{
+       struct ipe_eval_ctx ctx = IPE_EVAL_CTX_INIT;
+
+       ctx.op = IPE_OP_BPF_PROG_LOAD;
+       ctx.hook = IPE_HOOK_BPF_PROG_LOAD;
+       ctx.bpf_verdict = verdict;
+       ctx.bpf_keyring_id = attr->keyring_id;
+       ctx.bpf_kernel = kernel;
+
+       return ipe_evaluate_event(&ctx);
+}
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
security/ipe/hooks.h +11 -0
index 07db373327402..95b74f7899750 100644
@@ -10,6 +10,7 @@
 #include <linux/security.h>
 #include <linux/blk_types.h>
 #include <linux/fsverity.h>
+#include <linux/bpf.h>
 
 enum ipe_hook_type {
        IPE_HOOK_BPRM_CHECK = 0,
@@ -18,6 +19,7 @@ enum ipe_hook_type {
        IPE_HOOK_MPROTECT,
        IPE_HOOK_KERNEL_READ,
        IPE_HOOK_KERNEL_LOAD,
+       IPE_HOOK_BPF_PROG_LOAD,
        __IPE_HOOK_MAX
 };
 
@@ -52,4 +54,13 @@ int ipe_inode_setintegrity(const struct inode *inode, enum lsm_integrity_type ty
                           const void *value, size_t size);
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
 
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
+                                    union bpf_attr *attr,
+                                    struct bpf_token *token,
+                                    bool kernel,
+                                    const struct lsm_id *lsmid,
+                                    enum lsm_integrity_verdict verdict);
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
+
 #endif /* _IPE_HOOKS_H */
security/ipe/ipe.c +0 -0
index 495bb765de1b8..6502d4ddc641c 100644

security/ipe/ipe.c static struct security_hook_list ipe_hooks[] __ro_after_init = { · line 67

Quoted 8 lines
security/ipe/ipe.c +3 -0
@@ -60,6 +60,9 @@ static struct security_hook_list ipe_hooks[] __ro_after_init = {
 #ifdef CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG
        LSM_HOOK_INIT(inode_setintegrity, ipe_inode_setintegrity),
 #endif /* CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG */
+#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
+       LSM_HOOK_INIT(bpf_prog_load_post_integrity, ipe_bpf_prog_load_post_integrity),
+#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
 };
 

IPE only registers bpf_prog_load_post_integrity(), which fires only when Hornet calls security_bpf_prog_load_post_integrity. If Hornet isn't enabled via lsm=, this hook never triggers, and an IPE policy like "DEFAULT op=BPF_PROG_LOAD action=DENY" silently allows every load.

Quoted 4 lines

I think IPE should register bpf_prog_load() as well, post_integrity() should only store the verdict to a per-prog blob, and bpf_prog_load reads it and evaluates policy.

Sounds reasonable to me.

Quoted 1 line

-Fan

#24 lore raw

Re: [PATCH v5 10/10] ipe: Add BPF program load policy enforcement via Hornet integration

In reply to Blaise Boscaccy
Sashiko AI review found 1 potential issue(s): - [Low] Compilation failure in `evaluate_property()` when `CONFIG_IPE_PROP_BPF_SIGNATURE` is disabled due to unconditional access to `ctx->bpf_kernel`. --

Sashiko AI review found 1 potential issue(s):
- [Low] Compilation failure in evaluate_property() when CONFIG_IPE_PROP_BPF_SIGNATURE is disabled due to unconditional access to ctx->bpf_kernel. --

commit f674a6cd60a4a820baf0e01920f42c00f9ad8225
Author: Blaise Boscaccy <[email protected]>

ipe: Add BPF program load policy enforcement via Hornet integration

Add support for the bpf_prog_load_post_integrity LSM hook, enabling IPE to make policy decisions about BPF program loading based on integrity verdicts provided by the Hornet LSM.

Quoted 4 lines
security/ipe/eval.c +0 -0
index 21439c5be3364..9a6d583fea125 100644

[ ... ]

Quoted 20 lines
security/ipe/eval.c +16 -0
@@ -297,6 +342,32 @@ static bool evaluate_property(const struct ipe_eval_ctx *const ctx,
 		return evaluate_fsv_sig_false(ctx);
 	case IPE_PROP_FSV_SIG_TRUE:
 		return evaluate_fsv_sig_true(ctx);
+	case IPE_PROP_BPF_SIG_NONE:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_NONE);
+	case IPE_PROP_BPF_SIG_OK:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_OK);
+	case IPE_PROP_BPF_SIG_UNSIGNED:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_UNSIGNED);
+	case IPE_PROP_BPF_SIG_PARTIALSIG:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_PARTIALSIG);
+	case IPE_PROP_BPF_SIG_UNKNOWNKEY:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_UNKNOWNKEY);
+	case IPE_PROP_BPF_SIG_UNEXPECTED:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_UNEXPECTED);
+	case IPE_PROP_BPF_SIG_FAULT:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_FAULT);
+	case IPE_PROP_BPF_SIG_BADSIG:
+		return evaluate_bpf_sig(ctx, LSM_INT_VERDICT_BADSIG);

security/ipe/eval.c static bool evaluate_property(const struct ipe_eval_ctx *const ctx, · line 370

Quoted 10 lines
security/ipe/eval.c +10 -0
+	case IPE_PROP_BPF_KEYRING_BUILTIN:
+		return evaluate_bpf_keyring(ctx, 0);
+	case IPE_PROP_BPF_KEYRING_SECONDARY:
+		return evaluate_bpf_keyring(ctx, (s32)(unsigned long)VERIFY_USE_SECONDARY_KEYRING);
+	case IPE_PROP_BPF_KEYRING_PLATFORM:
+		return evaluate_bpf_keyring(ctx, (s32)(unsigned long)VERIFY_USE_PLATFORM_KEYRING);
+	case IPE_PROP_BPF_KERNEL_FALSE:
+		return !ctx->bpf_kernel;
+	case IPE_PROP_BPF_KERNEL_TRUE:
+		return ctx->bpf_kernel;

Will this cause a build failure when CONFIG_IPE_PROP_BPF_SIGNATURE is disabled?

In security/ipe/eval.h, the bpf_kernel field in struct ipe_eval_ctx is protected by #ifdef CONFIG_IPE_PROP_BPF_SIGNATURE:

struct ipe_eval_ctx {
...

#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
	enum lsm_integrity_verdict bpf_verdict;
	s32 bpf_keyring_id;
	bool bpf_kernel;

#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
};

However, evaluate_property() unconditionally accesses ctx->bpf_kernel for IPE_PROP_BPF_KERNEL_FALSE and IPE_PROP_BPF_KERNEL_TRUE. If the kernel is configured without CONFIG_IPE_PROP_BPF_SIGNATURE (for example, if CONFIG_SECURITY_HORNET is disabled), it appears the build will fail due to the missing struct member.

Quoted 3 lines
security/ipe/eval.c +0 -0
 	default:
 		return false;
 	}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=10