diff --git a/drivers/platform/cznic/Kconfig b/drivers/platform/cznic/Kconfig
index 2a5235cf6844..9bc0f976d13f 100644
--- a/drivers/platform/cznic/Kconfig
+++ b/drivers/platform/cznic/Kconfig
@@ -21,6 +21,8 @@ config TURRIS_OMNIA_MCU
 	select GPIOLIB
 	select GPIOLIB_IRQCHIP
 	select HW_RANDOM
+	select KEYS
+	select ASYMMETRIC_KEY_TYPE
 	select RTC_CLASS
 	select WATCHDOG_CORE
 	help
@@ -31,6 +33,8 @@ config TURRIS_OMNIA_MCU
 	    disabled) and the ability to configure wake up from this mode (via
 	    rtcwake)
 	  - true random number generator (if available on the MCU)
+	  - ECDSA message signing with board private key (if available on the
+	    MCU)
 	  - MCU watchdog
 	  - GPIO pins
 	    - to get front button press events (the front button can be
diff --git a/drivers/platform/cznic/Makefile b/drivers/platform/cznic/Makefile
index eae4c6b341ff..ad42cbbb4fb0 100644
--- a/drivers/platform/cznic/Makefile
+++ b/drivers/platform/cznic/Makefile
@@ -3,6 +3,7 @@
 obj-$(CONFIG_TURRIS_OMNIA_MCU)	+= turris-omnia-mcu.o
 turris-omnia-mcu-y		:= turris-omnia-mcu-base.o
 turris-omnia-mcu-y		+= turris-omnia-mcu-gpio.o
+turris-omnia-mcu-y		+= turris-omnia-mcu-keyctl.o
 turris-omnia-mcu-y		+= turris-omnia-mcu-sys-off-wakeup.o
 turris-omnia-mcu-y		+= turris-omnia-mcu-trng.o
 turris-omnia-mcu-y		+= turris-omnia-mcu-watchdog.o
diff --git a/drivers/platform/cznic/turris-omnia-mcu-base.c b/drivers/platform/cznic/turris-omnia-mcu-base.c
index c68a7a84a951..b2fb4fbb33e8 100644
--- a/drivers/platform/cznic/turris-omnia-mcu-base.c
+++ b/drivers/platform/cznic/turris-omnia-mcu-base.c
@@ -385,7 +385,11 @@ static int omnia_mcu_probe(struct i2c_client *client)
 	if (err)
 		return err;
 
-	return omnia_mcu_register_trng(mcu);
+	err = omnia_mcu_register_trng(mcu);
+	if (err)
+		return err;
+
+	return omnia_mcu_register_keyctl(mcu);
 }
 
 static const struct of_device_id of_omnia_mcu_match[] = {
@@ -401,7 +405,29 @@ static struct i2c_driver omnia_mcu_driver = {
 		.dev_groups = omnia_mcu_groups,
 	},
 };
-module_i2c_driver(omnia_mcu_driver);
+
+static int __init omnia_mcu_driver_init(void)
+{
+	int err;
+
+	err = omnia_mcu_keyctl_init();
+	if (err)
+		return err;
+
+	err = i2c_add_driver(&omnia_mcu_driver);
+	if (err)
+		omnia_mcu_keyctl_exit();
+
+	return err;
+}
+module_init(omnia_mcu_driver_init);
+
+static void __exit omnia_mcu_driver_exit(void)
+{
+	i2c_del_driver(&omnia_mcu_driver);
+	omnia_mcu_keyctl_exit();
+}
+module_exit(omnia_mcu_driver_exit);
 
 MODULE_AUTHOR("Marek Behun <kabel@kernel.org>");
 MODULE_DESCRIPTION("CZ.NIC's Turris Omnia MCU");
diff --git a/drivers/platform/cznic/turris-omnia-mcu-keyctl.c b/drivers/platform/cznic/turris-omnia-mcu-keyctl.c
new file mode 100644
index 000000000000..79c361c0618d
--- /dev/null
+++ b/drivers/platform/cznic/turris-omnia-mcu-keyctl.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * CZ.NIC's Turris Omnia MCU ECDSA message signing via keyctl
+ *
+ * 2024 by Marek Behún <kabel@kernel.org>
+ */
+
+#include <crypto/sha2.h>
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/gpio/driver.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/key-type.h>
+#include <linux/key.h>
+#include <linux/keyctl.h>
+#include <linux/mutex.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <linux/turris-omnia-mcu-interface.h>
+#include "turris-omnia-mcu.h"
+
+#define OMNIA_CMD_CRYPTO_SIGN_MESSAGE_LEN	32
+
+static irqreturn_t omnia_msg_signed_irq_handler(int irq, void *dev_id)
+{
+	u8 reply[1 + OMNIA_MCU_CRYPTO_SIGNATURE_LEN];
+	struct omnia_mcu *mcu = dev_id;
+	int err;
+
+	err = omnia_cmd_read(mcu->client, OMNIA_CMD_CRYPTO_COLLECT_SIGNATURE,
+			     reply, sizeof(reply));
+	if (!err && reply[0] != OMNIA_MCU_CRYPTO_SIGNATURE_LEN)
+		err = -EIO;
+
+	guard(mutex)(&mcu->sign_lock);
+
+	if (mcu->sign_requested) {
+		mcu->sign_err = err;
+		if (!err)
+			memcpy(mcu->signature, &reply[1],
+			       OMNIA_MCU_CRYPTO_SIGNATURE_LEN);
+		mcu->sign_requested = false;
+		complete(&mcu->msg_signed_completion);
+	}
+
+	return IRQ_HANDLED;
+}
+
+static int omnia_key_instantiate(struct key *, struct key_preparsed_payload *)
+{
+	return 0;
+}
+
+static void omnia_key_describe(const struct key *key, struct seq_file *m)
+{
+	struct omnia_mcu *mcu = dereference_key_rcu(key);
+
+	if (!mcu)
+		return;
+
+	seq_printf(m, "%s: %*phN", key->description,
+		   OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN, mcu->board_public_key);
+}
+
+static long omnia_key_read(const struct key *key, char *buffer, size_t buflen)
+{
+	struct omnia_mcu *mcu = dereference_key_rcu(key);
+
+	if (!mcu)
+		return -EIO;
+
+	if (buffer) {
+		if (buflen > OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN)
+			buflen = OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN;
+
+		memcpy(buffer, mcu->board_public_key, buflen);
+	}
+
+	return OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN;
+}
+
+static bool omnia_asym_valid_params(const struct kernel_pkey_params *params)
+{
+	if (params->encoding && strcmp(params->encoding, "raw"))
+		return false;
+
+	if (params->hash_algo && strcmp(params->hash_algo, "sha256"))
+		return false;
+
+	return true;
+}
+
+static int omnia_key_asym_query(const struct kernel_pkey_params *params,
+				struct kernel_pkey_query *info)
+{
+	struct omnia_mcu *mcu = dereference_key_rcu(params->key);
+
+	if (!mcu)
+		return -EIO;
+
+	if (!omnia_asym_valid_params(params))
+		return -EINVAL;
+
+	info->supported_ops = KEYCTL_SUPPORTS_SIGN;
+	info->key_size = 256;
+	info->max_data_size = SHA256_DIGEST_SIZE;
+	info->max_sig_size = OMNIA_MCU_CRYPTO_SIGNATURE_LEN;
+	info->max_enc_size = 0;
+	info->max_dec_size = 0;
+
+	return 0;
+}
+
+static int omnia_key_asym_eds_op(struct kernel_pkey_params *params,
+				 const void *in, void *out)
+{
+	struct omnia_mcu *mcu = dereference_key_rcu(params->key);
+	u8 cmd[1 + SHA256_DIGEST_SIZE], reply;
+	int err;
+
+	if (!mcu)
+		return -EIO;
+
+	if (!omnia_asym_valid_params(params))
+		return -EINVAL;
+
+	if (params->op != kernel_pkey_sign)
+		return -EOPNOTSUPP;
+
+	if (params->in_len != SHA256_DIGEST_SIZE ||
+	    params->out_len != OMNIA_MCU_CRYPTO_SIGNATURE_LEN)
+		return -EINVAL;
+
+	scoped_guard(mutex, &mcu->sign_lock) {
+		if (mcu->sign_requested)
+			return -EBUSY;
+
+		cmd[0] = OMNIA_CMD_CRYPTO_SIGN_MESSAGE;
+		memcpy(&cmd[1], in, SHA256_DIGEST_SIZE);
+
+		err = omnia_cmd_write_read(mcu->client, cmd, sizeof(cmd),
+					   &reply, 1);
+		if (err)
+			return err;
+
+		if (!reply)
+			return -EBUSY;
+
+		mcu->sign_requested = true;
+	}
+
+	if (wait_for_completion_interruptible(&mcu->msg_signed_completion))
+		return -EINTR;
+
+	guard(mutex)(&mcu->sign_lock);
+
+	if (mcu->sign_err)
+		return mcu->sign_err;
+
+	memcpy(out, mcu->signature, params->out_len);
+
+	/* forget the signature, for security */
+	memzero_explicit(mcu->signature, sizeof(mcu->signature));
+
+	return params->out_len;
+}
+
+static struct key_type omnia_mcu_key_type = {
+	.name		= "turris-omnia-mcu",
+	.instantiate	= omnia_key_instantiate,
+	.describe	= omnia_key_describe,
+	.read		= omnia_key_read,
+	.asym_query	= omnia_key_asym_query,
+	.asym_eds_op	= omnia_key_asym_eds_op,
+};
+
+static int omnia_mcu_read_public_key(struct omnia_mcu *mcu)
+{
+	u8 reply[1 + OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN];
+	int err;
+
+	err = omnia_cmd_read(mcu->client, OMNIA_CMD_CRYPTO_GET_PUBLIC_KEY,
+			     reply, sizeof(reply));
+	if (err)
+		return err;
+
+	if (reply[0] != OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN)
+		return -EIO;
+
+	memcpy(mcu->board_public_key, &reply[1], OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN);
+
+	return 0;
+}
+
+static struct key *omnia_keyring;
+
+static void omnia_release_key(void *key)
+{
+	key_unlink(omnia_keyring, key);
+	key_put(key);
+}
+
+int omnia_mcu_register_keyctl(struct omnia_mcu *mcu)
+{
+	struct device *dev = &mcu->client->dev;
+	struct key *key;
+	key_ref_t kref;
+	char desc[48];
+	int irq, err;
+	u8 irq_idx;
+
+	if (!(mcu->features & OMNIA_FEAT_CRYPTO))
+		return 0;
+
+	err = omnia_mcu_read_public_key(mcu);
+	if (err)
+		return dev_err_probe(dev, err, "Cannot read board public key\n");
+
+	irq_idx = omnia_int_to_gpio_idx[__bf_shf(OMNIA_INT_MESSAGE_SIGNED)];
+	irq = gpiod_to_irq(gpiochip_get_desc(&mcu->gc, irq_idx));
+	if (irq < 0)
+		return dev_err_probe(dev, irq,
+				     "Cannot get MESSAGE_SIGNED IRQ\n");
+
+	err = devm_mutex_init(dev, &mcu->sign_lock);
+	if (err)
+		return err;
+
+	init_completion(&mcu->msg_signed_completion);
+
+	err = devm_request_threaded_irq(dev, irq, NULL,
+					omnia_msg_signed_irq_handler,
+					IRQF_ONESHOT,
+					"turris-omnia-mcu-keyctl", mcu);
+	if (err)
+		return dev_err_probe(dev, err,
+				     "Cannot request MESSAGE_SIGNED IRQ\n");
+
+	sprintf(desc, "Turris Omnia SN %016llX MCU ECDSA key",
+		mcu->board_serial_number);
+
+	kref = key_create(make_key_ref(omnia_keyring, true),
+			  omnia_mcu_key_type.name, desc, NULL, 0,
+			  (KEY_POS_ALL & ~KEY_POS_SETATTR) |
+			  KEY_USR_VIEW | KEY_USR_READ | KEY_USR_SEARCH,
+			  KEY_ALLOC_BUILT_IN | KEY_ALLOC_SET_KEEP |
+			  KEY_ALLOC_NOT_IN_QUOTA);
+	if (IS_ERR(kref))
+		return dev_err_probe(dev, PTR_ERR(kref), "Cannot create key\n");
+
+	key = key_ref_to_ptr(kref);
+	rcu_assign_keypointer(key, mcu);
+
+	return devm_add_action_or_reset(dev, omnia_release_key, key);
+}
+
+int omnia_mcu_keyctl_init(void)
+{
+	int err;
+
+	err = register_key_type(&omnia_mcu_key_type);
+	if (err)
+		return err;
+
+	omnia_keyring = keyring_alloc(".turris-omnia-mcu-keys",
+				      GLOBAL_ROOT_UID, GLOBAL_ROOT_GID,
+				      current_cred(),
+				      (KEY_POS_ALL & ~KEY_POS_SETATTR) |
+				      KEY_USR_VIEW | KEY_USR_READ | KEY_USR_SEARCH,
+				      KEY_ALLOC_BUILT_IN | KEY_ALLOC_SET_KEEP |
+				      KEY_ALLOC_NOT_IN_QUOTA,
+				      NULL, NULL);
+	if (IS_ERR(omnia_keyring)) {
+		pr_err("Cannot allocate Turris Omnia MCU keyring\n");
+
+		unregister_key_type(&omnia_mcu_key_type);
+
+		return PTR_ERR(omnia_keyring);
+	}
+
+	return 0;
+}
+
+void omnia_mcu_keyctl_exit(void)
+{
+	key_put(omnia_keyring);
+	unregister_key_type(&omnia_mcu_key_type);
+}
diff --git a/drivers/platform/cznic/turris-omnia-mcu.h b/drivers/platform/cznic/turris-omnia-mcu.h
index 2ca56ae13aa9..3371e4f635cd 100644
--- a/drivers/platform/cznic/turris-omnia-mcu.h
+++ b/drivers/platform/cznic/turris-omnia-mcu.h
@@ -20,6 +20,9 @@
 #include <asm/byteorder.h>
 #include <asm/unaligned.h>
 
+#define OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN	(1 + 32)
+#define OMNIA_MCU_CRYPTO_SIGNATURE_LEN	64
+
 struct i2c_client;
 struct rtc_device;
 
@@ -32,6 +35,7 @@ struct omnia_mcu {
 	u64 board_serial_number;
 	u8 board_first_mac[ETH_ALEN];
 	u8 board_revision;
+	u8 board_public_key[OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN];
 
 	/* GPIO chip */
 	struct gpio_chip gc;
@@ -53,6 +57,13 @@ struct omnia_mcu {
 	/* true random number generator */
 	struct hwrng trng;
 	struct completion trng_entropy_ready;
+
+	/* MCU ECDSA message signing via keyctl */
+	struct completion msg_signed_completion;
+	struct mutex sign_lock;
+	bool sign_requested;
+	u8 signature[OMNIA_MCU_CRYPTO_SIGNATURE_LEN];
+	int sign_err;
 };
 
 int omnia_cmd_write_read(const struct i2c_client *client,
@@ -187,8 +198,12 @@ extern const struct attribute_group omnia_mcu_gpio_group;
 extern const struct attribute_group omnia_mcu_poweroff_group;
 
 int omnia_mcu_register_gpiochip(struct omnia_mcu *mcu);
+int omnia_mcu_register_keyctl(struct omnia_mcu *mcu);
 int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu);
 int omnia_mcu_register_trng(struct omnia_mcu *mcu);
 int omnia_mcu_register_watchdog(struct omnia_mcu *mcu);
 
+int omnia_mcu_keyctl_init(void);
+void omnia_mcu_keyctl_exit(void);
+
 #endif /* __TURRIS_OMNIA_MCU_H */
