-
Notifications
You must be signed in to change notification settings - Fork 5
Example: Support certificates #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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>; | ||
| }; | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,165 @@ | ||||||
| /* | ||||||
| * Copyright (c) 2025 Golioth, Inc. | ||||||
| * | ||||||
| * 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) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| { | ||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to update |
||||||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment.
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