Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions examples/ble_gatt/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
10 changes: 2 additions & 8 deletions examples/ble_gatt/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
96 changes: 96 additions & 0 deletions examples/ble_gatt/README.md
Original file line number Diff line number Diff line change
@@ -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 <board>
```

The `<board>` 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] <err> littlefs: WEST_TOPDIR/deps/modules/fs/littlefs/lfs.c:1389: Corrupted dir pair at {0x0, 0x1}
> [00:00:00.392,517] <wrn> 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.
19 changes: 19 additions & 0 deletions examples/ble_gatt/app.overlay
Original file line number Diff line number Diff line change
@@ -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>;
};
};
};
31 changes: 29 additions & 2 deletions examples/ble_gatt/prj.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
165 changes: 165 additions & 0 deletions examples/ble_gatt/src/credentials.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright (c) 2025 Golioth, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add the SPDX identifier as well

*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <zephyr/fs/fs.h>
#include <pouch/pouch.h>
#include <mbedtls/pk.h>

#include <zephyr/logging/log.h>
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (err)
if (err != 0)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small, bikeshed-shaped hill to die on, but I actually don't want to do this change, and I'd rather type out a response than leave it unaddressed.

The overwhelming majority of error checks in this repo and the firmware SDK uses if (err), like Zephyr and Linux does. A similar ratio appears to be practiced for pointer checks, where we're using if (!ptr) more often than if (ptr == NULL), particularly after allocations.

I recognize that this is a rather enraging response to a trivial suggestion in a PR, but unless it is in the style guide, it's a personal preference, IMO. I prefer if (err) because it reads more like English and follows the same convention as Zephyr (and the Linux Kernel, which is what our style guide defers to). I'm aware of the CERT recommendation, but for the error and NULL checks, I think readability and sticking with a familiar convention is more important. The scenarios listed as potential regressions in the recommendation are arguably rather contrived and extremely unlikely to manifest when we have a sane error return value convention. I absolutely agree with explicit equality checks for non-standard values like enums though, and would definitely consider it problematic if we used an implicit comparison to check for the new GOLIOTH_SETTING_VALUE_TYPE_UNKNOWN, for example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough - it's not a hill I want to die on 🙂

{
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (err)
if (err != 0)

{
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);
}
Comment on lines +163 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to update size here and/or assign der to NULL? This usually helps with maintaining information whether data should be valid or not (either in runtime or during debugging).

24 changes: 24 additions & 0 deletions examples/ble_gatt/src/credentials.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Golioth, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing copyright/license header

#include <psa/crypto.h>
#include <pouch/types.h>

/**
* 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);
Loading