From 5f7685163cbb019a3a09f80ed8592582cc7f8fcf Mon Sep 17 00:00:00 2001 From: Trond Snekvik Date: Mon, 21 Jul 2025 15:39:21 +0200 Subject: [PATCH] Example: Support certificates Adds support for certificates in the example. Certificates can be loaded by mcumgr, as in the certificate provisioning example in the SDK. Signed-off-by: Trond Snekvik --- examples/ble_gatt/CMakeLists.txt | 6 +- examples/ble_gatt/Kconfig | 10 +- examples/ble_gatt/README.md | 96 ++++++++++++++++ examples/ble_gatt/app.overlay | 19 ++++ examples/ble_gatt/prj.conf | 31 +++++- examples/ble_gatt/src/credentials.c | 165 ++++++++++++++++++++++++++++ examples/ble_gatt/src/credentials.h | 24 ++++ examples/ble_gatt/src/main.c | 29 ++++- west-ncs.yml | 1 + west.yml | 1 + 10 files changed, 365 insertions(+), 17 deletions(-) create mode 100644 examples/ble_gatt/README.md create mode 100644 examples/ble_gatt/app.overlay create mode 100644 examples/ble_gatt/src/credentials.c create mode 100644 examples/ble_gatt/src/credentials.h diff --git a/examples/ble_gatt/CMakeLists.txt b/examples/ble_gatt/CMakeLists.txt index dbd3266..f7eb923 100644 --- a/examples/ble_gatt/CMakeLists.txt +++ b/examples/ble_gatt/CMakeLists.txt @@ -7,5 +7,7 @@ cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(ble_gatt_example) -target_sources(app PRIVATE src/main.c) - +target_sources(app PRIVATE + src/main.c + src/credentials.c +) diff --git a/examples/ble_gatt/Kconfig b/examples/ble_gatt/Kconfig index 5636825..00180fe 100644 --- a/examples/ble_gatt/Kconfig +++ b/examples/ble_gatt/Kconfig @@ -4,15 +4,9 @@ source "$ZEPHYR_BASE/Kconfig" -config EXAMPLE_DEVICE_ID - string "Device ID" - help - The device ID to use for the device in pouches sent to the - Golioth cloud. - config EXAMPLE_SYNC_PERIOD_S int "Sync Period" default 20 help - The time, in seconds, after a sync to wait before requesting a sync from - a gateway. + The time, in seconds, after a sync to wait before requesting a sync from + a gateway. diff --git a/examples/ble_gatt/README.md b/examples/ble_gatt/README.md new file mode 100644 index 0000000..a7a19af --- /dev/null +++ b/examples/ble_gatt/README.md @@ -0,0 +1,96 @@ +# Pouch BLE GATT Example + +The Pouch BLE GATT example demonstrates how to create a Pouch application based +on the BLE GATT transport. + +## Building + +The example should be built with west: + +```bash +west build -b +``` + +The `` should be the Zephyr board ID of your board. The example is +primarily developed and tested on the `nrf52840dk/nrf52840` board, but any Zephyr +board with BLE, PSA, MbedTLS and LittleFS support should work. + +## Authentication + +The Pouch BLE GATT example requires a private key and certificate to authenticate +and encrypt the communication with the Golioth Cloud. + +Pouch devices use the same certificates and keys as devices that connect directly +to the Golioth Cloud. Please refer to [the official +documentation](https://docs.golioth.io/firmware/golioth-firmware-sdk/authentication/certificate-auth) +for generating and signing a valid private key and certificate. + +The example's credential management is implemented in src/credentials.c, and +expects to find the following files in its filesystem when booting up: + +- A DER encoded certificate at `/lfs1/credentials/crt.der` +- A DER encoded private key at `/lfs1/credentials/key.der` + +The `/lfs1/credentials/` directory gets created automatically when the device +boots for the first time. + + +> [!NOTE] +> The first time the application boots up after being erased, it has to format +> the file system, which generates the following warnings in the device log: +> +> ```log +> [00:00:00.392,486] littlefs: WEST_TOPDIR/deps/modules/fs/littlefs/lfs.c:1389: Corrupted dir pair at {0x0, 0x1} +> [00:00:00.392,517] littlefs: can't mount (LFS -84); formatting +> ``` +> +> These are expected, and can safely be ignored. + +## Provisioning + +The example is set up with +[MCUmgr](https://docs.zephyrproject.org/latest/services/device_mgmt/mcumgr.html) +support for transferring credentials into the device's built-in file system over +a serial connection. + +### Provisioning with MCUmgr: + +[The MCUmgr CLI](https://github.com/apache/mynewt-mcumgr) is available as a +command line tool built as a Go package. Follow the installation instructions in +the MCUmgr repository, then transfer your certificate and private key to the +device using the following commands: + +```bash +mcumgr --conntype serial --connstring $SERIAL_PORT fs upload $CERT_FILE /lfs1/credentials/crt.der +mcumgr --conntype serial --connstring $SERIAL_PORT fs upload $KEY_FILE /lfs1/credentials/key.der +``` + +where `$SERIAL_PORT` is the serial port for the device, like `/dev/ttyACM0` on +Linux, or `COM1` on Windows. + +`$CERT_FILE` and `$KEY_FILE` are the paths to the DER encoded certificate and +private key files, respectively. + +After both files have been transferred, restart the device to initialize Pouch +with the credentials. + +### Provisioning with SMP Manager: + +[SMP Manager](https://github.com/intercreate/smpmgr) is a Python based SMP client +that can be used as an alternative to MCUmgr. Follow the installation +instructions in the SMP Manager repository, then transfer your certificate and +private key to the device using the following commands: + +```bash +smpmgr --port $SERIAL_PORT --mtu 128 file upload $CERT_FILE /lfs1/credentials/crt.der +smpmgr --port $SERIAL_PORT --mtu 128 file upload $KEY_FILE /lfs1/credentials/key.der +``` + +where `$SERIAL_PORT` is the serial port for the device, like `/dev/ttyACM0` on +Linux, or `COM1` on Windows. + +`$CERT_FILE` and `$KEY_FILE` are the paths to the DER encoded certificate and +private key files, respectively. + +After both files have been transferred, restart the device to initialize Pouch +with the credentials. diff --git a/examples/ble_gatt/app.overlay b/examples/ble_gatt/app.overlay new file mode 100644 index 0000000..67577f5 --- /dev/null +++ b/examples/ble_gatt/app.overlay @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Golioth, Inc. + */ +/ { + fstab { + compatible = "zephyr,fstab"; + lfs1: lfs1 { + compatible = "zephyr,fstab,littlefs"; + mount-point = "/lfs1"; + partition = <&storage_partition>; + automount; + read-size = <16>; + prog-size = <16>; + cache-size = <64>; + lookahead-size = <32>; + block-cycles = <512>; + }; + }; +}; diff --git a/examples/ble_gatt/prj.conf b/examples/ble_gatt/prj.conf index 52a67f0..c6de728 100644 --- a/examples/ble_gatt/prj.conf +++ b/examples/ble_gatt/prj.conf @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 CONFIG_BT=y -CONFIG_LOG=y CONFIG_BT_SMP=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_DEVICE_NAME="Golioth" @@ -14,14 +13,42 @@ CONFIG_BT_BUF_ACL_RX_SIZE=128 CONFIG_BT_L2CAP_TX_MTU=128 CONFIG_BT_RX_STACK_SIZE=2048 +CONFIG_MAIN_STACK_SIZE=4096 + CONFIG_LOG=y CONFIG_ZCBOR=y CONFIG_SHELL=y CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_POUCH=y -CONFIG_POUCH_ENCRYPTION_NONE=y CONFIG_POUCH_TRANSPORT_BLE_GATT=y CONFIG_GOLIOTH=y CONFIG_GOLIOTH_SETTINGS=y + +# Enable flash operations +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +# Enable the LittleFS file system +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FILE_SYSTEM_SHELL=y + +CONFIG_MCUMGR=y + +# Enable console with mcumgr pass through +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y +CONFIG_UART_CONSOLE_MCUMGR=y + +# Enable the shell mcumgr transport. This allows SMP messages to pass +# through the shell to the mgmt subsystem. +CONFIG_CRC=y +CONFIG_MCUMGR_TRANSPORT_SHELL=y + +# Enable file system commands +CONFIG_MCUMGR_GRP_FS=y + +CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y +CONFIG_MCUMGR_GRP_FS_FILE_ACCESS_HOOK=y diff --git a/examples/ble_gatt/src/credentials.c b/examples/ble_gatt/src/credentials.c new file mode 100644 index 0000000..ac90165 --- /dev/null +++ b/examples/ble_gatt/src/credentials.c @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2025 Golioth, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(credentials, LOG_LEVEL_DBG); + +#define CERT_DIR "/lfs1/credentials/" +#define CERT_FILE CERT_DIR "crt.der" +#define KEY_FILE CERT_DIR "key.der" + +static int psa_rng_for_mbedtls(void *p_rng, unsigned char *output, size_t output_len) +{ + return psa_generate_random(output, output_len); +} + +/** Load the raw private key data into PSA */ +static psa_key_id_t import_raw_pk(const uint8_t *private_key, size_t size) +{ + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + int err = mbedtls_pk_parse_key(&pk, private_key, size, NULL, 0, psa_rng_for_mbedtls, NULL); + if (err) + { + LOG_ERR("Failed to parse key: %x", err); + return PSA_KEY_ID_NULL; + } + + psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT; + + psa_set_key_algorithm(&attrs, PSA_ALG_ECDH); + psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_DERIVE); + + psa_key_id_t key_id; + err = mbedtls_pk_import_into_psa(&pk, &attrs, &key_id); + if (err) + { + LOG_ERR("Failed to import private key: %x", err); + return PSA_KEY_ID_NULL; + } + + return key_id; +} + +static void ensure_credentials_dir(void) +{ + struct fs_dirent dirent; + + int err = fs_stat(CERT_DIR, &dirent); + if (err == -ENOENT) + { + err = fs_mkdir(CERT_DIR); + } + + if (err) + { + LOG_ERR("Failed to create credentials dir: %d", err); + } +} + +static ssize_t get_file_size(const char *path) +{ + struct fs_dirent dirent; + + int err = fs_stat(path, &dirent); + + if (err < 0) + { + return err; + } + if (dirent.type != FS_DIR_ENTRY_FILE) + { + return -EISDIR; + } + + return dirent.size; +} + +static ssize_t read_file(const char *path, uint8_t **out) +{ + // Ensure that the credentials directory exists, so the user doesn't have to + ensure_credentials_dir(); + + ssize_t size = get_file_size(path); + if (size <= 0) + { + return size; + } + + uint8_t *buf = malloc(size); + if (buf == NULL) + { + return -ENOMEM; + } + + struct fs_file_t file; + fs_file_t_init(&file); + + int err = fs_open(&file, path, FS_O_READ); + if (err) + { + LOG_ERR("Could not open %s, err: %d", path, err); + free(buf); + return err; + } + + size = fs_read(&file, buf, size); + if (size < 0) + { + LOG_ERR("Could not read %s, err: %d", path, size); + free(buf); + goto finish; + } + + LOG_INF("Read %d bytes from %s", size, path); + + *out = buf; + +finish: + fs_close(&file); + return size; +} + +psa_key_id_t load_private_key(void) +{ + uint8_t *buf; + ssize_t size = read_file(KEY_FILE, &buf); + if (size < 0) + { + return PSA_KEY_ID_NULL; + } + + psa_key_id_t key_id = import_raw_pk(buf, size); + free(buf); + return key_id; +} + +int load_certificate(struct pouch_cert *cert) +{ + uint8_t *buf; + ssize_t size = read_file(CERT_FILE, &buf); + if (size < 0) + { + return size; + } + + cert->der = buf; + cert->size = size; + + LOG_INF("Read certificate (%d bytes)", size); + + return 0; +} + +void free_certificate(struct pouch_cert *cert) +{ + free((void *) cert->der); +} diff --git a/examples/ble_gatt/src/credentials.h b/examples/ble_gatt/src/credentials.h new file mode 100644 index 0000000..581c554 --- /dev/null +++ b/examples/ble_gatt/src/credentials.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Golioth, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include +#include + +/** + * Import the device private key into PSA + * + * @returns The assigned PSA key ID for the device's private key, or @c PSA_KEY_ID_NULL if the + * private key couldn't be loaded. + */ +psa_key_id_t load_private_key(void); + +/** + * Load the device certificate. + */ +int load_certificate(struct pouch_cert *cert); + +/** Free the device certificate. */ +void free_certificate(struct pouch_cert *cert); diff --git a/examples/ble_gatt/src/main.c b/examples/ble_gatt/src/main.c index cdbb738..13b9aae 100644 --- a/examples/ble_gatt/src/main.c +++ b/examples/ble_gatt/src/main.c @@ -7,6 +7,8 @@ #include LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); +#include "credentials.h" + #include #include #include @@ -120,7 +122,7 @@ GOLIOTH_SETTINGS_HANDLER(LED, led_setting_cb, NULL); int main(void) { struct golioth_ble_gatt_peripheral *peripheral = - golioth_ble_gatt_peripheral_create(CONFIG_EXAMPLE_DEVICE_ID); + golioth_ble_gatt_peripheral_create(""); // TODO: This should no longer take the device ID if (NULL == peripheral) { LOG_ERR("Failed to create peripheral"); @@ -133,11 +135,23 @@ int main(void) return 0; } - LOG_DBG("Bluetooth initialized\n"); + LOG_DBG("Bluetooth initialized"); + + struct pouch_config config; + err = load_certificate(&config.certificate); + if (err) + { + LOG_ERR("Failed to load certificate (err %d)", err); + return 0; + } + + config.private_key = load_private_key(); + if (config.private_key == PSA_KEY_ID_NULL) + { + LOG_ERR("Failed to load private key"); + return 0; + } - struct pouch_config config = { - .device_id = CONFIG_EXAMPLE_DEVICE_ID, - }; err = pouch_init(&config); if (err) { @@ -145,6 +159,11 @@ int main(void) return 0; } + LOG_DBG("Pouch successfully initialized"); + + // Can safely free the raw certificate after pouch initialization: + free_certificate(&config.certificate); + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_2, ad, ARRAY_SIZE(ad), NULL, 0); if (err) { diff --git a/west-ncs.yml b/west-ncs.yml index 852a26c..1027ccf 100644 --- a/west-ncs.yml +++ b/west-ncs.yml @@ -17,4 +17,5 @@ manifest: - tfm-mcuboot - oberon-psa-crypto - trusted-firmware-m + - littlefs - zcbor diff --git a/west.yml b/west.yml index 39d26b0..cc27861 100644 --- a/west.yml +++ b/west.yml @@ -20,4 +20,5 @@ manifest: - segger - tfm-mcuboot - trusted-firmware-m + - littlefs - zcbor