Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,98 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# For a description of the syntax of this configuration file,
# see Documentation/kbuild/kconfig-language.rst.
#
menuconfig CZNIC_PLATFORMS
bool "Platform support for CZ.NIC's Turris hardware"
depends on ARCH_MVEBU || COMPILE_TEST
help
Say Y here to be able to choose driver support for CZ.NIC's Turris
devices. This option alone does not add any kernel code.
if CZNIC_PLATFORMS
config TURRIS_OMNIA_MCU
tristate "Turris Omnia MCU driver"
depends on MACH_ARMADA_38X || COMPILE_TEST
depends on I2C
help
Say Y here to add support for the features implemented by the
microcontroller on the CZ.NIC's Turris Omnia SOHO router.
This option only enables the core part of the driver. Specific
features can be enabled by subsequent config options.
To compile this driver as a module, choose M here; the module will be
called turris-omnia-mcu.
if TURRIS_OMNIA_MCU
config TURRIS_OMNIA_MCU_GPIO
bool "Turris Omnia MCU GPIOs"
default y
depends on GPIOLIB
depends on OF
select GPIOLIB_IRQCHIP
help
Say Y here to add support for controlling MCU GPIO pins and receiving
MCU interrupts on CZ.NIC's Turris Omnia.
This enables you to
- get front button press events (the front button can be configured
either to generate press events to the CPU or to change front LEDs
panel brightness),
- enable / disable USB port voltage regulators and to detect USB
overcurrent,
- detect MiniPCIe / mSATA card presence in MiniPCIe port 0,
- configure resets of various peripherals on board revisions 32+,
- enable / disable the VHV voltage regulator to the SOC in order to be
able to program SOC's OTP on board revisions 32+,
- get input from the LED output pins of the WAN ethernet PHY, LAN
switch and MiniPCIe ports.
config TURRIS_OMNIA_MCU_SYSOFF_WAKEUP
bool "Turris Omnia MCU system off and RTC wakeup"
default y
depends on RTC_CLASS
help
Say Y here to add support for CZ.NIC's Turris Omnia board poweroff
into true low power mode (with voltage regulators disabled) and the
ability to configure wake up from this mode (via rtcwake).
config TURRIS_OMNIA_MCU_WATCHDOG
bool "Turris Omnia MCU watchdog"
default y
depends on WATCHDOG
select WATCHDOG_CORE
help
Say Y here to add support for watchdog provided by CZ.NIC's Turris
Omnia MCU.
config TURRIS_OMNIA_MCU_TRNG
bool "Turris Omnia MCU true random number generator"
default y
depends on TURRIS_OMNIA_MCU_GPIO
depends on HW_RANDOM=y || HW_RANDOM=TURRIS_OMNIA_MCU
help
Say Y here to add support for the true random number generator
provided by CZ.NIC's Turris Omnia MCU.
config TURRIS_OMNIA_MCU_KEYCTL
bool "Turris Omnia MCU ECDSA message signing"
default y
depends on KEYS
depends on ASYMMETRIC_KEY_TYPE
depends on TURRIS_OMNIA_MCU_GPIO
select TURRIS_SIGNING_KEY
help
Say Y here to add support for ECDSA message signing with board private
key (if available on the MCU). This is exposed via the keyctl()
syscall.
endif # TURRIS_OMNIA_MCU
config TURRIS_SIGNING_KEY
tristate
depends on KEYS
depends on ASYMMETRIC_KEY_TYPE
endif # CZNIC_PLATFORMS
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_TURRIS_OMNIA_MCU) += turris-omnia-mcu.o
turris-omnia-mcu-y := turris-omnia-mcu-base.o
turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_GPIO) += turris-omnia-mcu-gpio.o
turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_KEYCTL) += turris-omnia-mcu-keyctl.o
turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP) += turris-omnia-mcu-sys-off-wakeup.o
turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_TRNG) += turris-omnia-mcu-trng.o
turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_WATCHDOG) += turris-omnia-mcu-watchdog.o
obj-$(CONFIG_TURRIS_SIGNING_KEY) += turris-signing-key.o
@@ -0,0 +1,419 @@
// SPDX-License-Identifier: GPL-2.0
/*
* CZ.NIC's Turris Omnia MCU driver
*
* 2024 by Marek Behún <kabel@kernel.org>
*/
#include <linux/array_size.h>
#include <linux/bits.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/hex.h>
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/turris-omnia-mcu-interface.h>
#include "turris-omnia-mcu.h"
#define OMNIA_FW_VERSION_LEN 20
#define OMNIA_FW_VERSION_HEX_LEN (2 * OMNIA_FW_VERSION_LEN + 1)
#define OMNIA_BOARD_INFO_LEN 16
int omnia_cmd_write_read(const struct i2c_client *client,
void *cmd, unsigned int cmd_len,
void *reply, unsigned int reply_len)
{
struct i2c_msg msgs[2];
int ret, num;
msgs[0].addr = client->addr;
msgs[0].flags = 0;
msgs[0].len = cmd_len;
msgs[0].buf = cmd;
num = 1;
if (reply_len) {
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD;
msgs[1].len = reply_len;
msgs[1].buf = reply;
num++;
}
ret = i2c_transfer(client->adapter, msgs, num);
if (ret < 0)
return ret;
if (ret != num)
return -EIO;
return 0;
}
EXPORT_SYMBOL_GPL(omnia_cmd_write_read);
static int omnia_get_version_hash(struct omnia_mcu *mcu, bool bootloader,
char version[static OMNIA_FW_VERSION_HEX_LEN])
{
u8 reply[OMNIA_FW_VERSION_LEN];
char *p;
int err;
err = omnia_cmd_read(mcu->client,
bootloader ? OMNIA_CMD_GET_FW_VERSION_BOOT
: OMNIA_CMD_GET_FW_VERSION_APP,
reply, sizeof(reply));
if (err)
return err;
p = bin2hex(version, reply, OMNIA_FW_VERSION_LEN);
*p = '\0';
return 0;
}
static ssize_t fw_version_hash_show(struct device *dev, char *buf,
bool bootloader)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
char version[OMNIA_FW_VERSION_HEX_LEN];
int err;
err = omnia_get_version_hash(mcu, bootloader, version);
if (err)
return err;
return sysfs_emit(buf, "%s\n", version);
}
static ssize_t fw_version_hash_application_show(struct device *dev,
struct device_attribute *a,
char *buf)
{
return fw_version_hash_show(dev, buf, false);
}
static DEVICE_ATTR_RO(fw_version_hash_application);
static ssize_t fw_version_hash_bootloader_show(struct device *dev,
struct device_attribute *a,
char *buf)
{
return fw_version_hash_show(dev, buf, true);
}
static DEVICE_ATTR_RO(fw_version_hash_bootloader);
static ssize_t fw_features_show(struct device *dev, struct device_attribute *a,
char *buf)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
return sysfs_emit(buf, "0x%x\n", mcu->features);
}
static DEVICE_ATTR_RO(fw_features);
static ssize_t mcu_type_show(struct device *dev, struct device_attribute *a,
char *buf)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
return sysfs_emit(buf, "%s\n", mcu->type);
}
static DEVICE_ATTR_RO(mcu_type);
static ssize_t reset_selector_show(struct device *dev,
struct device_attribute *a, char *buf)
{
u8 reply;
int err;
err = omnia_cmd_read_u8(to_i2c_client(dev), OMNIA_CMD_GET_RESET,
&reply);
if (err)
return err;
return sysfs_emit(buf, "%d\n", reply);
}
static DEVICE_ATTR_RO(reset_selector);
static ssize_t serial_number_show(struct device *dev,
struct device_attribute *a, char *buf)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
return sysfs_emit(buf, "%016llX\n", mcu->board_serial_number);
}
static DEVICE_ATTR_RO(serial_number);
static ssize_t first_mac_address_show(struct device *dev,
struct device_attribute *a, char *buf)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
return sysfs_emit(buf, "%pM\n", mcu->board_first_mac);
}
static DEVICE_ATTR_RO(first_mac_address);
static ssize_t board_revision_show(struct device *dev,
struct device_attribute *a, char *buf)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
return sysfs_emit(buf, "%u\n", mcu->board_revision);
}
static DEVICE_ATTR_RO(board_revision);
static struct attribute *omnia_mcu_base_attrs[] = {
&dev_attr_fw_version_hash_application.attr,
&dev_attr_fw_version_hash_bootloader.attr,
&dev_attr_fw_features.attr,
&dev_attr_mcu_type.attr,
&dev_attr_reset_selector.attr,
&dev_attr_serial_number.attr,
&dev_attr_first_mac_address.attr,
&dev_attr_board_revision.attr,
NULL
};
static umode_t omnia_mcu_base_attrs_visible(struct kobject *kobj,
struct attribute *a, int n)
{
struct device *dev = kobj_to_dev(kobj);
struct omnia_mcu *mcu = dev_get_drvdata(dev);
if ((a == &dev_attr_serial_number.attr ||
a == &dev_attr_first_mac_address.attr ||
a == &dev_attr_board_revision.attr) &&
!(mcu->features & OMNIA_FEAT_BOARD_INFO))
return 0;
return a->mode;
}
static const struct attribute_group omnia_mcu_base_group = {
.attrs = omnia_mcu_base_attrs,
.is_visible = omnia_mcu_base_attrs_visible,
};
static const struct attribute_group *omnia_mcu_groups[] = {
&omnia_mcu_base_group,
#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO
&omnia_mcu_gpio_group,
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP
&omnia_mcu_poweroff_group,
#endif
NULL
};
static void omnia_mcu_print_version_hash(struct omnia_mcu *mcu, bool bootloader)
{
const char *type = bootloader ? "bootloader" : "application";
struct device *dev = &mcu->client->dev;
char version[OMNIA_FW_VERSION_HEX_LEN];
int err;
err = omnia_get_version_hash(mcu, bootloader, version);
if (err) {
dev_err(dev, "Cannot read MCU %s firmware version: %d\n",
type, err);
return;
}
dev_info(dev, "MCU %s firmware version hash: %s\n", type, version);
}
static const char *omnia_status_to_mcu_type(u16 status)
{
switch (status & OMNIA_STS_MCU_TYPE_MASK) {
case OMNIA_STS_MCU_TYPE_STM32:
return "STM32";
case OMNIA_STS_MCU_TYPE_GD32:
return "GD32";
case OMNIA_STS_MCU_TYPE_MKL:
return "MKL";
default:
return "unknown";
}
}
static void omnia_info_missing_feature(struct device *dev, const char *feature)
{
dev_info(dev,
"Your board's MCU firmware does not support the %s feature.\n",
feature);
}
static int omnia_mcu_read_features(struct omnia_mcu *mcu)
{
static const struct {
u16 mask;
const char *name;
} features[] = {
#define _DEF_FEAT(_n, _m) { OMNIA_FEAT_ ## _n, _m }
_DEF_FEAT(EXT_CMDS, "extended control and status"),
_DEF_FEAT(WDT_PING, "watchdog pinging"),
_DEF_FEAT(LED_STATE_EXT_MASK, "peripheral LED pins reading"),
_DEF_FEAT(NEW_INT_API, "new interrupt API"),
_DEF_FEAT(POWEROFF_WAKEUP, "poweroff and wakeup"),
_DEF_FEAT(TRNG, "true random number generator"),
_DEF_FEAT(BRIGHTNESS_INT, "LED panel brightness change interrupt"),
_DEF_FEAT(LED_GAMMA_CORRECTION, "LED gamma correction"),
#undef _DEF_FEAT
};
struct i2c_client *client = mcu->client;
struct device *dev = &client->dev;
bool suggest_fw_upgrade = false;
u16 status;
int err;
/* status word holds MCU type, which we need below */
err = omnia_cmd_read_u16(client, OMNIA_CMD_GET_STATUS_WORD, &status);
if (err)
return err;
/*
* Check whether MCU firmware supports the OMNIA_CMD_GET_FEATURES
* command.
*/
if (status & OMNIA_STS_FEATURES_SUPPORTED) {
/* try read 32-bit features */
err = omnia_cmd_read_u32(client, OMNIA_CMD_GET_FEATURES,
&mcu->features);
if (err) {
/* try read 16-bit features */
u16 features16;
err = omnia_cmd_read_u16(client, OMNIA_CMD_GET_FEATURES,
&features16);
if (err)
return err;
mcu->features = features16;
} else {
if (mcu->features & OMNIA_FEAT_FROM_BIT_16_INVALID)
mcu->features &= GENMASK(15, 0);
}
} else {
dev_info(dev,
"Your board's MCU firmware does not support feature reading.\n");
suggest_fw_upgrade = true;
}
mcu->type = omnia_status_to_mcu_type(status);
dev_info(dev, "MCU type %s%s\n", mcu->type,
(mcu->features & OMNIA_FEAT_PERIPH_MCU) ?
", with peripheral resets wired" : "");
omnia_mcu_print_version_hash(mcu, true);
if (mcu->features & OMNIA_FEAT_BOOTLOADER)
dev_warn(dev,
"MCU is running bootloader firmware. Was firmware upgrade interrupted?\n");
else
omnia_mcu_print_version_hash(mcu, false);
for (unsigned int i = 0; i < ARRAY_SIZE(features); i++) {
if (mcu->features & features[i].mask)
continue;
omnia_info_missing_feature(dev, features[i].name);
suggest_fw_upgrade = true;
}
if (suggest_fw_upgrade)
dev_info(dev,
"Consider upgrading MCU firmware with the omnia-mcutool utility.\n");
return 0;
}
static int omnia_mcu_read_board_info(struct omnia_mcu *mcu)
{
u8 reply[1 + OMNIA_BOARD_INFO_LEN];
int err;
err = omnia_cmd_read(mcu->client, OMNIA_CMD_BOARD_INFO_GET, reply,
sizeof(reply));
if (err)
return err;
if (reply[0] != OMNIA_BOARD_INFO_LEN)
return -EIO;
mcu->board_serial_number = get_unaligned_le64(&reply[1]);
/* we can't use ether_addr_copy() because reply is not u16-aligned */
memcpy(mcu->board_first_mac, &reply[9], sizeof(mcu->board_first_mac));
mcu->board_revision = reply[15];
return 0;
}
static int omnia_mcu_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct omnia_mcu *mcu;
int err;
if (!client->irq)
return dev_err_probe(dev, -EINVAL, "IRQ resource not found\n");
mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
if (!mcu)
return -ENOMEM;
mcu->client = client;
i2c_set_clientdata(client, mcu);
err = omnia_mcu_read_features(mcu);
if (err)
return dev_err_probe(dev, err,
"Cannot determine MCU supported features\n");
if (mcu->features & OMNIA_FEAT_BOARD_INFO) {
err = omnia_mcu_read_board_info(mcu);
if (err)
return dev_err_probe(dev, err,
"Cannot read board info\n");
}
err = omnia_mcu_register_sys_off_and_wakeup(mcu);
if (err)
return err;
err = omnia_mcu_register_watchdog(mcu);
if (err)
return err;
err = omnia_mcu_register_gpiochip(mcu);
if (err)
return err;
err = omnia_mcu_register_keyctl(mcu);
if (err)
return err;
return omnia_mcu_register_trng(mcu);
}
static const struct of_device_id of_omnia_mcu_match[] = {
{ .compatible = "cznic,turris-omnia-mcu" },
{}
};
static struct i2c_driver omnia_mcu_driver = {
.probe = omnia_mcu_probe,
.driver = {
.name = "turris-omnia-mcu",
.of_match_table = of_omnia_mcu_match,
.dev_groups = omnia_mcu_groups,
},
};
module_i2c_driver(omnia_mcu_driver);
MODULE_AUTHOR("Marek Behun <kabel@kernel.org>");
MODULE_DESCRIPTION("CZ.NIC's Turris Omnia MCU");
MODULE_LICENSE("GPL");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: GPL-2.0
/*
* CZ.NIC's Turris Omnia MCU ECDSA message signing via keyctl
*
* 2025 by Marek Behún <kabel@kernel.org>
*/
#include <crypto/sha2.h>
#include <linux/cleanup.h>
#include <linux/completion.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/key.h>
#include <linux/mutex.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/turris-omnia-mcu-interface.h>
#include <linux/turris-signing-key.h>
#include "turris-omnia-mcu.h"
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);
}
return IRQ_HANDLED;
}
static int omnia_mcu_sign(const struct key *key, const void *msg,
void *signature)
{
struct omnia_mcu *mcu = dev_get_drvdata(turris_signing_key_get_dev(key));
u8 cmd[1 + SHA256_DIGEST_SIZE], reply;
int err;
scoped_guard(mutex, &mcu->sign_lock) {
if (mcu->sign_requested)
return -EBUSY;
cmd[0] = OMNIA_CMD_CRYPTO_SIGN_MESSAGE;
memcpy(&cmd[1], msg, 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))
return -EINTR;
guard(mutex)(&mcu->sign_lock);
if (mcu->sign_err)
return mcu->sign_err;
memcpy(signature, mcu->signature, OMNIA_MCU_CRYPTO_SIGNATURE_LEN);
/* forget the signature, for security */
memzero_explicit(mcu->signature, sizeof(mcu->signature));
return OMNIA_MCU_CRYPTO_SIGNATURE_LEN;
}
static const void *omnia_mcu_get_public_key(const struct key *key)
{
struct omnia_mcu *mcu = dev_get_drvdata(turris_signing_key_get_dev(key));
return mcu->board_public_key;
}
static const struct turris_signing_key_subtype omnia_signing_key_subtype = {
.key_size = 256,
.data_size = SHA256_DIGEST_SIZE,
.sig_size = OMNIA_MCU_CRYPTO_SIGNATURE_LEN,
.public_key_size = OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN,
.hash_algo = "sha256",
.get_public_key = omnia_mcu_get_public_key,
.sign = omnia_mcu_sign,
};
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;
}
int omnia_mcu_register_keyctl(struct omnia_mcu *mcu)
{
struct device *dev = &mcu->client->dev;
char desc[48];
int err;
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");
err = devm_mutex_init(dev, &mcu->sign_lock);
if (err)
return err;
init_completion(&mcu->msg_signed);
err = omnia_mcu_request_irq(mcu, OMNIA_INT_MESSAGE_SIGNED,
omnia_msg_signed_irq_handler,
"turris-omnia-mcu-keyctl");
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);
err = devm_turris_signing_key_create(dev, &omnia_signing_key_subtype,
desc);
if (err)
return dev_err_probe(dev, err, "Cannot create signing key\n");
return 0;
}
@@ -0,0 +1,260 @@
// SPDX-License-Identifier: GPL-2.0
/*
* CZ.NIC's Turris Omnia MCU system off and RTC wakeup driver
*
* This is not a true RTC driver (in the sense that it does not provide a
* real-time clock), rather the MCU implements a wakeup from powered off state
* at a specified time relative to MCU boot, and we expose this feature via RTC
* alarm, so that it can be used via the rtcwake command, which is the standard
* Linux command for this.
*
* 2024 by Marek Behún <kabel@kernel.org>
*/
#include <linux/crc32.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/i2c.h>
#include <linux/kstrtox.h>
#include <linux/reboot.h>
#include <linux/rtc.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/turris-omnia-mcu-interface.h>
#include "turris-omnia-mcu.h"
static int omnia_get_uptime_wakeup(const struct i2c_client *client, u32 *uptime,
u32 *wakeup)
{
__le32 reply[2];
int err;
err = omnia_cmd_read(client, OMNIA_CMD_GET_UPTIME_AND_WAKEUP, reply,
sizeof(reply));
if (err)
return err;
if (uptime)
*uptime = le32_to_cpu(reply[0]);
if (wakeup)
*wakeup = le32_to_cpu(reply[1]);
return 0;
}
static int omnia_read_time(struct device *dev, struct rtc_time *tm)
{
u32 uptime;
int err;
err = omnia_get_uptime_wakeup(to_i2c_client(dev), &uptime, NULL);
if (err)
return err;
rtc_time64_to_tm(uptime, tm);
return 0;
}
static int omnia_read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
struct i2c_client *client = to_i2c_client(dev);
struct omnia_mcu *mcu = i2c_get_clientdata(client);
u32 wakeup;
int err;
err = omnia_get_uptime_wakeup(client, NULL, &wakeup);
if (err)
return err;
alrm->enabled = !!wakeup;
rtc_time64_to_tm(wakeup ?: mcu->rtc_alarm, &alrm->time);
return 0;
}
static int omnia_set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
struct i2c_client *client = to_i2c_client(dev);
struct omnia_mcu *mcu = i2c_get_clientdata(client);
mcu->rtc_alarm = rtc_tm_to_time64(&alrm->time);
if (alrm->enabled)
return omnia_cmd_write_u32(client, OMNIA_CMD_SET_WAKEUP,
mcu->rtc_alarm);
return 0;
}
static int omnia_alarm_irq_enable(struct device *dev, unsigned int enabled)
{
struct i2c_client *client = to_i2c_client(dev);
struct omnia_mcu *mcu = i2c_get_clientdata(client);
return omnia_cmd_write_u32(client, OMNIA_CMD_SET_WAKEUP,
enabled ? mcu->rtc_alarm : 0);
}
static const struct rtc_class_ops omnia_rtc_ops = {
.read_time = omnia_read_time,
.read_alarm = omnia_read_alarm,
.set_alarm = omnia_set_alarm,
.alarm_irq_enable = omnia_alarm_irq_enable,
};
static int omnia_power_off(struct sys_off_data *data)
{
struct omnia_mcu *mcu = data->cb_data;
__be32 tmp;
u8 cmd[9];
u16 arg;
int err;
if (mcu->front_button_poweron)
arg = OMNIA_CMD_POWER_OFF_POWERON_BUTTON;
else
arg = 0;
cmd[0] = OMNIA_CMD_POWER_OFF;
put_unaligned_le16(OMNIA_CMD_POWER_OFF_MAGIC, &cmd[1]);
put_unaligned_le16(arg, &cmd[3]);
/*
* Although all values from and to MCU are passed in little-endian, the
* MCU's CRC unit uses big-endian CRC32 polynomial (0x04c11db7), so we
* need to use crc32_be() here.
*/
tmp = cpu_to_be32(get_unaligned_le32(&cmd[1]));
put_unaligned_le32(crc32_be(~0, (void *)&tmp, sizeof(tmp)), &cmd[5]);
err = omnia_cmd_write(mcu->client, cmd, sizeof(cmd));
if (err)
dev_err(&mcu->client->dev,
"Unable to send the poweroff command: %d\n", err);
return NOTIFY_DONE;
}
static int omnia_restart(struct sys_off_data *data)
{
struct omnia_mcu *mcu = data->cb_data;
u8 cmd[3];
int err;
cmd[0] = OMNIA_CMD_GENERAL_CONTROL;
if (reboot_mode == REBOOT_HARD)
cmd[1] = cmd[2] = OMNIA_CTL_HARD_RST;
else
cmd[1] = cmd[2] = OMNIA_CTL_LIGHT_RST;
err = omnia_cmd_write(mcu->client, cmd, sizeof(cmd));
if (err)
dev_err(&mcu->client->dev,
"Unable to send the restart command: %d\n", err);
/*
* MCU needs a little bit to process the I2C command, otherwise it will
* do a light reset based on SOC SYSRES_OUT pin.
*/
mdelay(1);
return NOTIFY_DONE;
}
static ssize_t front_button_poweron_show(struct device *dev,
struct device_attribute *a, char *buf)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
return sysfs_emit(buf, "%d\n", mcu->front_button_poweron);
}
static ssize_t front_button_poweron_store(struct device *dev,
struct device_attribute *a,
const char *buf, size_t count)
{
struct omnia_mcu *mcu = dev_get_drvdata(dev);
bool val;
int err;
err = kstrtobool(buf, &val);
if (err)
return err;
mcu->front_button_poweron = val;
return count;
}
static DEVICE_ATTR_RW(front_button_poweron);
static struct attribute *omnia_mcu_poweroff_attrs[] = {
&dev_attr_front_button_poweron.attr,
NULL
};
static umode_t poweroff_attrs_visible(struct kobject *kobj, struct attribute *a,
int n)
{
struct device *dev = kobj_to_dev(kobj);
struct omnia_mcu *mcu = dev_get_drvdata(dev);
if (mcu->features & OMNIA_FEAT_POWEROFF_WAKEUP)
return a->mode;
return 0;
}
const struct attribute_group omnia_mcu_poweroff_group = {
.attrs = omnia_mcu_poweroff_attrs,
.is_visible = poweroff_attrs_visible,
};
int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu)
{
struct device *dev = &mcu->client->dev;
int err;
/* MCU restart is always available */
err = devm_register_sys_off_handler(dev, SYS_OFF_MODE_RESTART,
SYS_OFF_PRIO_FIRMWARE,
omnia_restart, mcu);
if (err)
return dev_err_probe(dev, err,
"Cannot register system restart handler\n");
/*
* Poweroff and wakeup are available only if POWEROFF_WAKEUP feature is
* present.
*/
if (!(mcu->features & OMNIA_FEAT_POWEROFF_WAKEUP))
return 0;
err = devm_register_sys_off_handler(dev, SYS_OFF_MODE_POWER_OFF,
SYS_OFF_PRIO_FIRMWARE,
omnia_power_off, mcu);
if (err)
return dev_err_probe(dev, err,
"Cannot register system power off handler\n");
mcu->rtcdev = devm_rtc_allocate_device(dev);
if (IS_ERR(mcu->rtcdev))
return dev_err_probe(dev, PTR_ERR(mcu->rtcdev),
"Cannot allocate RTC device\n");
mcu->rtcdev->ops = &omnia_rtc_ops;
mcu->rtcdev->range_max = U32_MAX;
set_bit(RTC_FEATURE_ALARM_WAKEUP_ONLY, mcu->rtcdev->features);
err = devm_rtc_register_device(mcu->rtcdev);
if (err)
return dev_err_probe(dev, err, "Cannot register RTC device\n");
mcu->front_button_poweron = true;
return 0;
}
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-2.0
/*
* CZ.NIC's Turris Omnia MCU TRNG driver
*
* 2024 by Marek Behún <kabel@kernel.org>
*/
#include <linux/completion.h>
#include <linux/container_of.h>
#include <linux/errno.h>
#include <linux/hw_random.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/minmax.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/turris-omnia-mcu-interface.h>
#include "turris-omnia-mcu.h"
#define OMNIA_CMD_TRNG_MAX_ENTROPY_LEN 64
static irqreturn_t omnia_trng_irq_handler(int irq, void *dev_id)
{
struct omnia_mcu *mcu = dev_id;
complete(&mcu->trng_entropy_ready);
return IRQ_HANDLED;
}
static int omnia_trng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct omnia_mcu *mcu = container_of(rng, struct omnia_mcu, trng);
u8 reply[1 + OMNIA_CMD_TRNG_MAX_ENTROPY_LEN];
int err, bytes;
if (!wait && !completion_done(&mcu->trng_entropy_ready))
return 0;
do {
if (wait_for_completion_interruptible(&mcu->trng_entropy_ready))
return -ERESTARTSYS;
err = omnia_cmd_read(mcu->client,
OMNIA_CMD_TRNG_COLLECT_ENTROPY,
reply, sizeof(reply));
if (err)
return err;
bytes = min3(reply[0], max, OMNIA_CMD_TRNG_MAX_ENTROPY_LEN);
} while (wait && !bytes);
memcpy(data, &reply[1], bytes);
return bytes;
}
int omnia_mcu_register_trng(struct omnia_mcu *mcu)
{
struct device *dev = &mcu->client->dev;
u8 dummy;
int err;
if (!(mcu->features & OMNIA_FEAT_TRNG))
return 0;
/*
* If someone else cleared the TRNG interrupt but did not read the
* entropy, a new interrupt won't be generated, and entropy collection
* will be stuck. Ensure an interrupt will be generated by executing
* the collect entropy command (and discarding the result).
*/
err = omnia_cmd_read(mcu->client, OMNIA_CMD_TRNG_COLLECT_ENTROPY,
&dummy, 1);
if (err)
return err;
init_completion(&mcu->trng_entropy_ready);
err = omnia_mcu_request_irq(mcu, OMNIA_INT_TRNG, omnia_trng_irq_handler,
"turris-omnia-mcu-trng");
if (err)
return dev_err_probe(dev, err, "Cannot request TRNG IRQ\n");
mcu->trng.name = "turris-omnia-mcu-trng";
mcu->trng.read = omnia_trng_read;
err = devm_hwrng_register(dev, &mcu->trng);
if (err)
return dev_err_probe(dev, err, "Cannot register TRNG\n");
return 0;
}
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: GPL-2.0
/*
* CZ.NIC's Turris Omnia MCU watchdog driver
*
* 2024 by Marek Behún <kabel@kernel.org>
*/
#include <linux/bitops.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <linux/moduleparam.h>
#include <linux/types.h>
#include <linux/units.h>
#include <linux/watchdog.h>
#include <linux/turris-omnia-mcu-interface.h>
#include "turris-omnia-mcu.h"
#define WATCHDOG_TIMEOUT 120
static unsigned int timeout;
module_param(timeout, int, 0);
MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds");
static bool nowayout = WATCHDOG_NOWAYOUT;
module_param(nowayout, bool, 0);
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
__MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
static int omnia_wdt_start(struct watchdog_device *wdt)
{
struct omnia_mcu *mcu = watchdog_get_drvdata(wdt);
return omnia_cmd_write_u8(mcu->client, OMNIA_CMD_SET_WATCHDOG_STATE, 1);
}
static int omnia_wdt_stop(struct watchdog_device *wdt)
{
struct omnia_mcu *mcu = watchdog_get_drvdata(wdt);
return omnia_cmd_write_u8(mcu->client, OMNIA_CMD_SET_WATCHDOG_STATE, 0);
}
static int omnia_wdt_ping(struct watchdog_device *wdt)
{
struct omnia_mcu *mcu = watchdog_get_drvdata(wdt);
return omnia_cmd_write_u8(mcu->client, OMNIA_CMD_SET_WATCHDOG_STATE, 1);
}
static int omnia_wdt_set_timeout(struct watchdog_device *wdt,
unsigned int timeout)
{
struct omnia_mcu *mcu = watchdog_get_drvdata(wdt);
return omnia_cmd_write_u16(mcu->client, OMNIA_CMD_SET_WDT_TIMEOUT,
timeout * DECI);
}
static unsigned int omnia_wdt_get_timeleft(struct watchdog_device *wdt)
{
struct omnia_mcu *mcu = watchdog_get_drvdata(wdt);
u16 timeleft;
int err;
err = omnia_cmd_read_u16(mcu->client, OMNIA_CMD_GET_WDT_TIMELEFT,
&timeleft);
if (err) {
dev_err(&mcu->client->dev, "Cannot get watchdog timeleft: %d\n",
err);
return 0;
}
return timeleft / DECI;
}
static const struct watchdog_info omnia_wdt_info = {
.options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE,
.identity = "Turris Omnia MCU Watchdog",
};
static const struct watchdog_ops omnia_wdt_ops = {
.owner = THIS_MODULE,
.start = omnia_wdt_start,
.stop = omnia_wdt_stop,
.ping = omnia_wdt_ping,
.set_timeout = omnia_wdt_set_timeout,
.get_timeleft = omnia_wdt_get_timeleft,
};
int omnia_mcu_register_watchdog(struct omnia_mcu *mcu)
{
struct device *dev = &mcu->client->dev;
u8 state;
int err;
if (!(mcu->features & OMNIA_FEAT_WDT_PING))
return 0;
mcu->wdt.info = &omnia_wdt_info;
mcu->wdt.ops = &omnia_wdt_ops;
mcu->wdt.parent = dev;
mcu->wdt.min_timeout = 1;
mcu->wdt.max_timeout = 65535 / DECI;
mcu->wdt.timeout = WATCHDOG_TIMEOUT;
watchdog_init_timeout(&mcu->wdt, timeout, dev);
watchdog_set_drvdata(&mcu->wdt, mcu);
omnia_wdt_set_timeout(&mcu->wdt, mcu->wdt.timeout);
err = omnia_cmd_read_u8(mcu->client, OMNIA_CMD_GET_WATCHDOG_STATE,
&state);
if (err)
return dev_err_probe(dev, err,
"Cannot get MCU watchdog state\n");
if (state)
set_bit(WDOG_HW_RUNNING, &mcu->wdt.status);
watchdog_set_nowayout(&mcu->wdt, nowayout);
watchdog_stop_on_reboot(&mcu->wdt);
err = devm_watchdog_register_device(dev, &mcu->wdt);
if (err)
return dev_err_probe(dev, err,
"Cannot register MCU watchdog\n");
return 0;
}
@@ -0,0 +1,163 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* CZ.NIC's Turris Omnia MCU driver
*
* 2024 by Marek Behún <kabel@kernel.org>
*/
#ifndef __TURRIS_OMNIA_MCU_H
#define __TURRIS_OMNIA_MCU_H
#include <linux/completion.h>
#include <linux/gpio/driver.h>
#include <linux/hw_random.h>
#include <linux/if_ether.h>
#include <linux/interrupt.h>
#include <linux/mutex.h>
#include <linux/types.h>
#include <linux/watchdog.h>
#include <linux/workqueue.h>
enum {
OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN = 1 + 32,
OMNIA_MCU_CRYPTO_SIGNATURE_LEN = 64,
};
struct i2c_client;
struct rtc_device;
/**
* struct omnia_mcu - driver private data structure
* @client: I2C client
* @type: MCU type (STM32, GD32, MKL, or unknown)
* @features: bitmap of features supported by the MCU firmware
* @board_serial_number: board serial number, if stored in MCU
* @board_first_mac: board first MAC address, if stored in MCU
* @board_revision: board revision, if stored in MCU
* @gc: GPIO chip
* @lock: mutex to protect internal GPIO chip state
* @mask: bitmap of masked IRQs
* @rising: bitmap of rising edge IRQs
* @falling: bitmap of falling edge IRQs
* @both: bitmap of both edges IRQs
* @cached: bitmap of cached IRQ line values (when an IRQ line is configured for
* both edges, we cache the corresponding GPIO values in the IRQ
* handler)
* @is_cached: bitmap of which IRQ line values are cached
* @button_release_emul_work: front button release emulation work, used with old MCU firmware
* versions which did not send button release events, only button press
* events
* @last_status: cached value of the status word, to be compared with new value to
* determine which interrupt events occurred, used with old MCU
* firmware versions which only informed that the status word changed,
* but not which bits of the status word changed
* @button_pressed_emul: the front button is still emulated to be pressed
* @rtcdev: RTC device, does not actually count real-time, the device is only
* used for the RTC alarm mechanism, so that the board can be
* configured to wake up from poweroff state at a specific time
* @rtc_alarm: RTC alarm that was set for the board to wake up on, in MCU time
* (seconds since last MCU reset)
* @front_button_poweron: the front button should power on the device after it is powered off
* @wdt: watchdog driver structure
* @trng: RNG driver structure
* @trng_entropy_ready: RNG entropy ready completion
* @msg_signed: message signed completion
* @sign_lock: mutex to protect message signing state
* @sign_requested: flag indicating that message signing was requested but not completed
* @sign_err: message signing error number, filled in interrupt handler
* @signature: message signing signature, filled in interrupt handler
* @board_public_key: board public key, if stored in MCU
*/
struct omnia_mcu {
struct i2c_client *client;
const char *type;
u32 features;
u64 board_serial_number;
u8 board_first_mac[ETH_ALEN];
u8 board_revision;
#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO
struct gpio_chip gc;
struct mutex lock;
unsigned long mask, rising, falling, both, cached, is_cached;
struct delayed_work button_release_emul_work;
unsigned long last_status;
bool button_pressed_emul;
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP
struct rtc_device *rtcdev;
u32 rtc_alarm;
bool front_button_poweron;
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_WATCHDOG
struct watchdog_device wdt;
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_TRNG
struct hwrng trng;
struct completion trng_entropy_ready;
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_KEYCTL
struct completion msg_signed;
struct mutex sign_lock;
bool sign_requested;
int sign_err;
u8 signature[OMNIA_MCU_CRYPTO_SIGNATURE_LEN];
u8 board_public_key[OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN];
#endif
};
#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO
extern const struct attribute_group omnia_mcu_gpio_group;
int omnia_mcu_register_gpiochip(struct omnia_mcu *mcu);
int omnia_mcu_request_irq(struct omnia_mcu *mcu, u32 spec,
irq_handler_t thread_fn, const char *devname);
#else
static inline int omnia_mcu_register_gpiochip(struct omnia_mcu *mcu)
{
return 0;
}
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_KEYCTL
int omnia_mcu_register_keyctl(struct omnia_mcu *mcu);
#else
static inline int omnia_mcu_register_keyctl(struct omnia_mcu *mcu)
{
return 0;
}
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP
extern const struct attribute_group omnia_mcu_poweroff_group;
int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu);
#else
static inline int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu)
{
return 0;
}
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_TRNG
int omnia_mcu_register_trng(struct omnia_mcu *mcu);
#else
static inline int omnia_mcu_register_trng(struct omnia_mcu *mcu)
{
return 0;
}
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_WATCHDOG
int omnia_mcu_register_watchdog(struct omnia_mcu *mcu);
#else
static inline int omnia_mcu_register_watchdog(struct omnia_mcu *mcu)
{
return 0;
}
#endif
#endif /* __TURRIS_OMNIA_MCU_H */
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Some of CZ.NIC's Turris devices support signing messages with a per-device unique asymmetric
* cryptographic key that was burned into the device at manufacture.
*
* This helper module exposes this message signing ability via the keyctl() syscall. Upon load, it
* creates the `.turris-signing-keys` keyring. A device-specific driver then has to create a signing
* key by calling devm_turris_signing_key_create().
*
* 2025 by Marek Behún <kabel@kernel.org>
*/
#include <linux/device.h>
#include <linux/err.h>
#include <linux/key-type.h>
#include <linux/key.h>
#include <linux/keyctl.h>
#include <linux/module.h>
#include <linux/seq_file.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/turris-signing-key.h>
static int turris_signing_key_instantiate(struct key *key,
struct key_preparsed_payload *payload)
{
return 0;
}
static void turris_signing_key_describe(const struct key *key, struct seq_file *m)
{
const struct turris_signing_key_subtype *subtype = dereference_key_rcu(key);
if (!subtype)
return;
seq_printf(m, "%s: %*phN", key->description, subtype->public_key_size,
subtype->get_public_key(key));
}
static long turris_signing_key_read(const struct key *key, char *buffer, size_t buflen)
{
const struct turris_signing_key_subtype *subtype = dereference_key_rcu(key);
if (!subtype)
return -EIO;
if (buffer) {
if (buflen > subtype->public_key_size)
buflen = subtype->public_key_size;
memcpy(buffer, subtype->get_public_key(key), subtype->public_key_size);
}
return subtype->public_key_size;
}
static bool turris_signing_key_asym_valid_params(const struct turris_signing_key_subtype *subtype,
const struct kernel_pkey_params *params)
{
if (params->encoding && strcmp(params->encoding, "raw"))
return false;
if (params->hash_algo && strcmp(params->hash_algo, subtype->hash_algo))
return false;
return true;
}
static int turris_signing_key_asym_query(const struct kernel_pkey_params *params,
struct kernel_pkey_query *info)
{
const struct turris_signing_key_subtype *subtype = dereference_key_rcu(params->key);
if (!subtype)
return -EIO;
if (!turris_signing_key_asym_valid_params(subtype, params))
return -EINVAL;
info->supported_ops = KEYCTL_SUPPORTS_SIGN;
info->key_size = subtype->key_size;
info->max_data_size = subtype->data_size;
info->max_sig_size = subtype->sig_size;
info->max_enc_size = 0;
info->max_dec_size = 0;
return 0;
}
static int turris_signing_key_asym_eds_op(struct kernel_pkey_params *params,
const void *in, void *out)
{
const struct turris_signing_key_subtype *subtype = dereference_key_rcu(params->key);
int err;
if (!subtype)
return -EIO;
if (!turris_signing_key_asym_valid_params(subtype, params))
return -EINVAL;
if (params->op != kernel_pkey_sign)
return -EOPNOTSUPP;
if (params->in_len != subtype->data_size || params->out_len != subtype->sig_size)
return -EINVAL;
err = subtype->sign(params->key, in, out);
if (err)
return err;
return subtype->sig_size;
}
static struct key_type turris_signing_key_type = {
.name = "turris-signing-key",
.instantiate = turris_signing_key_instantiate,
.describe = turris_signing_key_describe,
.read = turris_signing_key_read,
.asym_query = turris_signing_key_asym_query,
.asym_eds_op = turris_signing_key_asym_eds_op,
};
static struct key *turris_signing_keyring;
static void turris_signing_key_release(void *key)
{
key_unlink(turris_signing_keyring, key);
key_put(key);
}
int
devm_turris_signing_key_create(struct device *dev, const struct turris_signing_key_subtype *subtype,
const char *desc)
{
struct key *key;
key_ref_t kref;
kref = key_create(make_key_ref(turris_signing_keyring, true),
turris_signing_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 PTR_ERR(kref);
key = key_ref_to_ptr(kref);
key->payload.data[1] = dev;
rcu_assign_keypointer(key, subtype);
return devm_add_action_or_reset(dev, turris_signing_key_release, key);
}
EXPORT_SYMBOL_GPL(devm_turris_signing_key_create);
static int turris_signing_key_init(void)
{
int err;
err = register_key_type(&turris_signing_key_type);
if (err)
return err;
turris_signing_keyring = keyring_alloc(".turris-signing-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(turris_signing_keyring)) {
pr_err("Cannot allocate Turris keyring\n");
unregister_key_type(&turris_signing_key_type);
return PTR_ERR(turris_signing_keyring);
}
return 0;
}
module_init(turris_signing_key_init);
static void turris_signing_key_exit(void)
{
key_put(turris_signing_keyring);
unregister_key_type(&turris_signing_key_type);
}
module_exit(turris_signing_key_exit);
MODULE_AUTHOR("Marek Behun <kabel@kernel.org>");
MODULE_DESCRIPTION("CZ.NIC's Turris signing key helper");
MODULE_LICENSE("GPL");