plugins/nordic-hid: added initial version

The plugin is using Nordic Semiconductor HID config channel to perform
devices update directly atteched via USB and BLE.
Current implementation supports FW images compatible with the nRF Secure
Immutable Bootloader.
This version has been tested with nRF52840-DK board.

Signed-off-by: Denis Pynkin <denis.pynkin@collabora.com>
Signed-off-by: Ricardo Cañuelo <ricardo.canuelo@collabora.com>
Signed-off-by: Richard Hughes <richard@hughsie.com>
This commit is contained in:
Denis Pynkin 2021-11-10 16:54:56 +01:00 committed by Richard Hughes
parent c47446c411
commit 4fbe1c58c7
14 changed files with 1615 additions and 0 deletions

View File

@ -448,6 +448,7 @@ done
%endif
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_mtd.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nitrokey.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nordic_hid.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nvme.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_optionrom.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_parade_lspcon.so

View File

@ -41,6 +41,7 @@ subdir('modem-manager')
subdir('msr')
subdir('mtd')
subdir('nitrokey')
subdir('nordic-hid')
subdir('nvme')
subdir('optionrom')
subdir('parade-lspcon')

View File

@ -0,0 +1,45 @@
# Nordic Semiconductor HID
## Introduction
This plugin is able flash the firmware on:
* nRF52-Desktop: nrf52840dk development kit
The plugin is using Nordic Semiconductor
[HID config channel](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/applications/nrf_desktop/doc/config_channel.html)
to perform devices update.
## Firmware Format
The cabinet file contains ZIP archive prepared by Nordic Semiconductor.
This ZIP archive includes 2 signed image blobs for the target
device, one firmware blob per application slot, and the `manifest.json` file with the metadata description.
At the moment only [nRF Secure Immutable Bootloader](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/samples/bootloader/README.html#bootloader)
aka "B0" is supported and tested.
This plugin supports the following protocol ID:
* "Nordic HID Config Channel"
## GUID Generation
For GUID generation the standard HIDRAW DeviceInstanceId values are used
with the addition of the target board and bootloader name:
* `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_B0` -> 22952036-c346-5755-9646-7bf766b28922
* `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_MCUBOOT` -> 43b38427-fdf5-5400-a23c-f3eb7ea00e7c
## Update Behavior
The firmware is deployed when the device is in normal runtime mode, and the
device will reset when the new firmware has been written.
## Vendor ID Security
The vendor ID is set from the HID vendor ID, in this instance set
to `HIDRAW:0x1915`.
## External Interface Access
This plugin requires ioctl `HIDIOCSFEATURE` and `HIDIOCGFEATURE` access.

View File

@ -0,0 +1,186 @@
/*
* Copyright (C) 2021 Richard Hughes <richard@hughsie.com>
* Copyright (C) 2021 Denis Pynkin <denis.pynkin@collabora.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include "fu-nordic-hid-archive.h"
#include "fu-nordic-hid-firmware-b0.h"
/* current version format is 0 */
#define MAX_VERSION_FORMAT 0
struct _FuNordicHidArchive {
FuFirmwareClass parent_instance;
};
G_DEFINE_TYPE(FuNordicHidArchive, fu_nordic_hid_archive, FU_TYPE_FIRMWARE)
static gboolean
fu_nordic_hid_archive_parse(FuFirmware *firmware,
GBytes *fw,
guint64 addr_start,
guint64 addr_end,
FwupdInstallFlags flags,
GError **error)
{
JsonNode *json_root_node;
JsonObject *json_obj;
JsonArray *json_files;
guint manifest_ver;
guint files_cnt = 0;
GBytes *manifest = NULL;
g_autoptr(FuArchive) archive = NULL;
g_autoptr(JsonParser) parser = json_parser_new();
/* load archive */
archive = fu_archive_new(fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
if (archive == NULL)
return FALSE;
manifest = fu_archive_lookup_by_fn(archive, "manifest.json", error);
if (manifest == NULL)
return FALSE;
/* parse JSON */
if (!json_parser_load_from_data(parser,
(const gchar *)g_bytes_get_data(manifest, NULL),
(gssize)g_bytes_get_size(manifest),
error)) {
g_prefix_error(error, "manifest not in JSON format: ");
return FALSE;
}
json_root_node = json_parser_get_root(parser);
if (json_root_node == NULL || !JSON_NODE_HOLDS_OBJECT(json_root_node)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest invalid as has no root");
return FALSE;
}
json_obj = json_node_get_object(json_root_node);
if (!json_object_has_member(json_obj, "format-version")) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest has invalid format");
return FALSE;
}
manifest_ver = json_object_get_int_member(json_obj, "format-version");
if (manifest_ver > MAX_VERSION_FORMAT) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"unsupported manifest version");
return FALSE;
}
json_files = json_object_get_array_member(json_obj, "files");
if (json_files == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest invalid as has no 'files' array");
return FALSE;
}
files_cnt = json_array_get_length(json_files);
if (files_cnt == 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest invalid as contains no update images");
return FALSE;
}
for (guint i = 0; i < files_cnt; i++) {
const gchar *filename = NULL;
const gchar *bootloader_name = NULL;
guint image_addr = 0;
JsonObject *obj = json_array_get_object_element(json_files, i);
GBytes *blob = NULL;
FuFirmware *image = NULL;
g_autofree gchar *image_id = NULL;
g_auto(GStrv) board_split = NULL;
if (!json_object_has_member(obj, "file")) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest invalid as has no file name for the image");
return FALSE;
}
filename = json_object_get_string_member(obj, "file");
blob = fu_archive_lookup_by_fn(archive, filename, error);
if (blob == NULL)
return FALSE;
if (json_object_has_member(obj, "version_B0")) {
bootloader_name = "B0";
image = g_object_new(FU_TYPE_NORDIC_HID_FIRMWARE_B0, NULL);
} else if (json_object_has_member(obj, "version_MCUBOOT")) {
/* TODO: add MCUboot format */
bootloader_name = "MCUBOOT";
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"MCUboot bootloader is not supported");
return FALSE;
} else {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"only B0 and MCUboot bootloaders are supported");
return FALSE;
}
/* the "board" field contains board name before "_" symbol */
if (!json_object_has_member(obj, "board")) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest invalid as has no target board information");
return FALSE;
}
board_split = g_strsplit(json_object_get_string_member(obj, "board"), "_", -1);
if (board_split[0] == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"manifest invalid as has no target board information");
return FALSE;
}
/* images are listed in strict order: this is guaranteed by producer
* set the id format as <board>_<bl>_<bank>N, i.e "nrf52840dk_B0_bank0" */
image_id = g_strdup_printf("%s_%s_bank%01u", board_split[0], bootloader_name, i);
if (!fu_firmware_parse(image, blob, flags, error))
return FALSE;
fu_firmware_set_id(image, image_id);
fu_firmware_set_idx(image, i);
if (json_object_has_member(obj, "load_address")) {
image_addr = json_object_get_int_member(obj, "load_address");
fu_firmware_set_addr(image, image_addr);
}
fu_firmware_add_image(firmware, image);
}
/* success */
return TRUE;
}
static void
fu_nordic_hid_archive_init(FuNordicHidArchive *self)
{
}
static void
fu_nordic_hid_archive_class_init(FuNordicHidArchiveClass *klass)
{
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
klass_firmware->parse = fu_nordic_hid_archive_parse;
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (C) 2021 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include <fwupdplugin.h>
#define FU_TYPE_NORDIC_HID_ARCHIVE (fu_nordic_hid_archive_get_type())
G_DECLARE_FINAL_TYPE(FuNordicHidArchive,
fu_nordic_hid_archive,
FU,
NORDIC_HID_ARCHIVE,
FuArchiveFirmware)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
/*
* Copyright (C) 2021 Ricardo Cañuelo <ricardo.canuelo@collabora.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include <fwupdplugin.h>
#define FU_TYPE_NORDIC_HID_CFG_CHANNEL (fu_nordic_hid_cfg_channel_get_type())
G_DECLARE_FINAL_TYPE(FuNordicDeviceCfgChannel,
fu_nordic_hid_cfg_channel,
FU,
NORDIC_HID_CFG_CHANNEL,
FuUdevDevice)

View File

@ -0,0 +1,178 @@
/*
* Copyright (C) 2021 Denis Pynkin <denis.pynkin@collabora.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include "fu-nordic-hid-firmware-b0.h"
#define UPDATE_IMAGE_MAGIC_COMMON 0x281ee6de
#define UPDATE_IMAGE_MAGIC_FWINFO 0x8fcebb4c
#define UPDATE_IMAGE_MAGIC_NRF52 0x00003402
#define UPDATE_IMAGE_MAGIC_NRF53 0x00003502
struct _FuNordicHidFirmwareB0 {
FuIhexFirmwareClass parent_instance;
guint32 crc32;
};
G_DEFINE_TYPE(FuNordicHidFirmwareB0, fu_nordic_hid_firmware_b0, FU_TYPE_FIRMWARE)
static void
fu_nordic_hid_firmware_b0_export(FuFirmware *firmware,
FuFirmwareExportFlags flags,
XbBuilderNode *bn)
{
FuNordicHidFirmwareB0 *self = FU_NORDIC_HID_FIRMWARE_B0(firmware);
fu_xmlb_builder_insert_kx(bn, "crc32", self->crc32);
}
static GBytes *
fu_nordic_hid_firmware_b0_write(FuFirmware *firmware, GError **error)
{
g_autoptr(GByteArray) buf = g_byte_array_new();
g_autoptr(GBytes) blob = NULL;
fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_COMMON, G_LITTLE_ENDIAN);
fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_FWINFO, G_LITTLE_ENDIAN);
fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_NRF52, G_LITTLE_ENDIAN);
blob = fu_firmware_get_bytes(firmware, error);
if (blob == NULL)
return NULL;
fu_byte_array_append_bytes(buf, blob);
return g_byte_array_free_to_bytes(g_steal_pointer(&buf));
}
static gboolean
fu_nordic_hid_firmware_b0_read_fwinfo(guint8 const *buf, gsize bufsz, GError **error)
{
guint32 magic_common;
guint32 magic_fwinfo;
guint32 magic_compat;
guint32 offset;
guint32 hdr_offset[5] = {0x0000, 0x0200, 0x400, 0x800, 0x1000};
/* find correct offset to fwinfo */
for (guint32 i = 0; i < G_N_ELEMENTS(hdr_offset); i++) {
offset = hdr_offset[i];
if (!fu_common_read_uint32_safe(buf,
bufsz,
offset,
&magic_common,
G_LITTLE_ENDIAN,
error))
return FALSE;
if (!fu_common_read_uint32_safe(buf,
bufsz,
offset + 0x04,
&magic_fwinfo,
G_LITTLE_ENDIAN,
error))
return FALSE;
if (!fu_common_read_uint32_safe(buf,
bufsz,
offset + 0x08,
&magic_compat,
G_LITTLE_ENDIAN,
error))
return FALSE;
if (magic_common != UPDATE_IMAGE_MAGIC_COMMON ||
magic_fwinfo != UPDATE_IMAGE_MAGIC_FWINFO)
continue;
switch (magic_compat) {
case UPDATE_IMAGE_MAGIC_NRF52:
case UPDATE_IMAGE_MAGIC_NRF53:
return TRUE;
default:
break;
}
}
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"unable to validate the update binary");
return FALSE;
}
static guint32
fu_nordic_hid_firmware_b0_crc32(const guint8 *buf, gsize bufsz)
{
guint crc32 = 0x01;
/* maybe skipped "^" step in fu_common_crc32_full()?
* according https://github.com/madler/zlib/blob/master/crc32.c#L225 */
crc32 ^= 0xFFFFFFFFUL;
return fu_common_crc32_full(buf, bufsz, crc32, 0xEDB88320);
}
static gchar *
fu_nordic_hid_firmware_b0_get_checksum(FuFirmware *firmware,
GChecksumType csum_kind,
GError **error)
{
FuNordicHidFirmwareB0 *self = FU_NORDIC_HID_FIRMWARE_B0(firmware);
if (!fu_firmware_has_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"unable to calculate the checksum of the update binary");
return NULL;
}
return g_strdup_printf("%x", self->crc32);
}
static gboolean
fu_nordic_hid_firmware_b0_parse(FuFirmware *firmware,
GBytes *fw,
guint64 addr_start,
guint64 addr_end,
FwupdInstallFlags flags,
GError **error)
{
FuNordicHidFirmwareB0 *self = FU_NORDIC_HID_FIRMWARE_B0(firmware);
const guint8 *buf;
gsize bufsz = 0;
buf = g_bytes_get_data(fw, &bufsz);
if (buf == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"unable to get the image binary");
return FALSE;
}
if (!fu_nordic_hid_firmware_b0_read_fwinfo(buf, bufsz, error))
return FALSE;
self->crc32 = fu_nordic_hid_firmware_b0_crc32(buf, bufsz);
fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM);
/* do not strip the header */
fu_firmware_set_bytes(firmware, fw);
/* success */
return TRUE;
}
static void
fu_nordic_hid_firmware_b0_init(FuNordicHidFirmwareB0 *self)
{
}
static void
fu_nordic_hid_firmware_b0_class_init(FuNordicHidFirmwareB0Class *klass)
{
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
klass_firmware->get_checksum = fu_nordic_hid_firmware_b0_get_checksum;
klass_firmware->export = fu_nordic_hid_firmware_b0_export;
klass_firmware->parse = fu_nordic_hid_firmware_b0_parse;
klass_firmware->write = fu_nordic_hid_firmware_b0_write;
}
FuFirmware *
fu_nordic_hid_firmware_b0_new(void)
{
return FU_FIRMWARE(g_object_new(FU_TYPE_NORDIC_HID_FIRMWARE_B0, NULL));
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2021 Denis Pynkin <denis.pynkin@collabora.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include <fwupdplugin.h>
#define FU_TYPE_NORDIC_HID_FIRMWARE_B0 (fu_nordic_hid_firmware_b0_get_type())
G_DECLARE_FINAL_TYPE(FuNordicHidFirmwareB0,
fu_nordic_hid_firmware_b0,
FU,
NORDIC_HID_FIRMWARE_B0,
FuFirmware)
FuFirmware *
fu_nordic_hid_firmware_b0_new(void);

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2021 Ricardo Cañuelo <ricardo.canuelo@collabora.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include "fu-nordic-hid-archive.h"
#include "fu-nordic-hid-cfg-channel.h"
#include "fu-nordic-hid-firmware-b0.h"
static void
fu_plugin_nordic_hid_init(FuPlugin *plugin)
{
fu_plugin_add_udev_subsystem(plugin, "hidraw");
fu_plugin_add_device_gtype(plugin, FU_TYPE_NORDIC_HID_CFG_CHANNEL);
fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_ARCHIVE);
fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_FIRMWARE_B0);
}
void
fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs)
{
vfuncs->build_hash = FU_BUILD_HASH;
vfuncs->init = fu_plugin_nordic_hid_init;
}

View File

@ -0,0 +1,34 @@
if get_option('gudev') and get_option('gusb')
cargs = ['-DG_LOG_DOMAIN="FuPluginNordicHid"']
install_data([
'nordic-hid.quirk',
],
install_dir: join_paths(datadir, 'fwupd', 'quirks.d')
)
shared_module('fu_plugin_nordic_hid',
fu_hash,
sources : [
'fu-plugin-nordic-hid.c',
'fu-nordic-hid-cfg-channel.c',
'fu-nordic-hid-firmware-b0.c',
'fu-nordic-hid-archive.c',
],
include_directories : [
root_incdir,
fwupd_incdir,
fwupdplugin_incdir,
],
install : true,
install_dir: plugin_dir,
link_with : [
fwupd,
fwupdplugin,
],
c_args : cargs,
dependencies : [
plugin_deps,
],
)
endif

View File

@ -0,0 +1,9 @@
# Mouse nRF52 Desktop
[HIDRAW\VEN_1915&DEV_52DE]
Plugin = nordic_hid
GType = FuNordicHidCfgChannel
# Nordic Semiconductor ASA Dongle nRF52 Desktop
[HIDRAW\VEN_1915&DEV_52DC]
Plugin = nordic_hid
GType = FuNordicHidCfgChannel

Binary file not shown.

View File

@ -0,0 +1,3 @@
<firmware gtype="FuNordicHidFirmwareB0">
<data>aGVsbG8gd29ybGQ=</data> <!-- base64 -->
</firmware>