Embedded Rust Bluetooth on ESP: Secure BLE Server

Omar HiariOmar Hiari
9 min read

This post is the sixth of a multi-part series where I'm exploring the use of Bluetooth Low Energy along embedded Rust on the ESP32. Information in this post might rely on knowledge presented in past posts.

  1. Embedded Rust Bluetooth on ESP: BLE Scanner

  2. Embedded Rust Bluetooth on ESP: BLE Advertiser

  3. Embedded Rust Bluetooth on ESP: BLE Server

  4. Embedded Rust Bluetooth on ESP: BLE Client

  5. Embedded Rust Bluetooth on ESP: Secure BLE Client

Introduction

In this post, we're going to build on the BLE Server post to instead create a secure BLE server. We're going to create a peripheral device that will assume the role of a server upon connection establishment. Afterward, the connection will be secured. Similar to past posts, the code will be built using the esp32-nimble crate.

๐Ÿ“š Knowledge Pre-requisites

To understand the content of this post, you need the following:

  • Basic knowledge of coding in Rust.

  • Familiarity with standard library development in Rust with the ESP.

  • Basic knowledge of networking layering concepts/stacks (ex. OSI model).

  • Basic knowledge of Bluetooth.

๐Ÿ’พ Software Setup

All the code presented in this post is available on the apollolabs ESP32C3 git repo. Note that if the code on the git repo is slightly different then it means that it was modified to enhance the code quality or accommodate any HAL/Rust updates.

๐Ÿ›  Hardware Setup

Materials

  • ESP32-C3-DevKitM

    ๐Ÿ”Œ Connections

    No connections are required for this example.

๐Ÿ‘จโ€๐ŸŽจ Software Design

The steps in securing a channel were presented in last week's post where we secured the connection of a BLE Client. It was demonstrated how after a connection is established, it can be secured by encrypting the connection. The exact same idea applies whether the device is a server or a client. The terms pairing and bonding were also introduced. security information to quickly establish a secure connection.

We mentioned that after a connection is secured, we further have the option to secure particular attributes. This is captured through the attribute properties. The properties available related to security include a choice of authentication, authorization, or encryption. These all apply to both read and write operations. As such, a server may define permissions independently for each characteristic. The server may allow some characteristics to be accessed by any client, while limiting access to other characteristics to only authenticated or authorized clients.

Authentication guarantees that a message has originated from a trusted party while authorization is a confirmation by the user to continue with the procedure. Characteristics that require authentication cannot be accessed until the client has gone through an authenticated pairing method. Authorization, on the other hand, is typically handled by the application. The BLE stack would forward requests to the application to complete the process.

In this post, we'll be appending the code in the peripheral server post to make it's connection secure. As such, we'll be creating a secure peripheral server with one characteristic. In that context, the code will take the following steps:

  1. (Added Step) Configure device security capabilities and connection method for pairing

  2. (Modified Step) Create Secure Service & Characteristic

  3. Configure Advertising Data & Start Advertising

  4. Establish a connection

  5. Read a characteristic value every second

๐Ÿ‘จโ€๐Ÿ’ป Code Implementation

๐Ÿ“ฅ Crate Imports

In this implementation, the following crates are required:

  • The esp_idf_hal crate to import delays.

  • The esp_idf_sys crate since its needed.

  • The esp32_nimble crate for the BLE abstractions.

use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEDevice, NimbleProperties};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_sys as _;

๐ŸŽ› Initialization/Configuration Code

1๏ธโƒฃ Obtain a handle for the BLE device: Similar to the pattern we've seen in embedded Rust with peripherals, as part of the singleton design pattern, we first have to take ownership of the device peripherals. In this context, its the BLEDevice that we need to take ownership of. This is done using the take() associated method. Here I create a BLE device handler named ble_device as follows:

let ble_device = BLEDevice::take();

2๏ธโƒฃ Configure Device Security: In this step we need to configure the device I/O capabilities for pairing and set the authorization mode. The authorization mode would determine the pairing method and whether bonding is required or not. The authorization mode is captured through several flags mentioned earlier configurable through the esp32_nimble::enums::AuthReq enum. To match the client from last week, we will choose the passkey entry method.

To configure security parameters, we first need to call the security method on the BLEDevice instance. This would return a BLESecurity type that renders access to the security configuration methods. The authorization mode is configured through the set_auth method where we pass a AuthReq enum choice. AuthReq::all means setting all the flags and implies the passkey entry method and bonding. We also need to set the IO capabilities through the set_io_cap method. For that, we need to pass a SecurityIOCap enum option which we pass DisplayOnly. Here's the code:

// Configure Device Security
ble_device
    .security()
    .set_auth(AuthReq::all())
    .set_passkey(123456)
    .set_io_cap(SecurityIOCap::DisplayOnly)
    .resolve_rpa();

Note the resolve_rpa method utilized. RPA stands for resolvable private address. RPA is required to establish a higher level of security which generates what is referred to as a Long Term Key (LTK). Without going into too much detail, this would be required if you want to test with certain devices, especially iOS ones.

3๏ธโƒฃ Create an Advertiser Instance: After initializing the NimBLE stack we create an advertiser instance by calling get_advertising, this will create a &Mutex<BLEAdvertising> instance. Heres the code:

let ble_advertiser = ble_device.get_advertising();

4๏ธโƒฃ Obtain Handle for Server: We create a server instance by calling get_server, this will create a BLEServer instance. Heres the code:

let server = ble_device.get_server();

5๏ธโƒฃ Define Server Connect & Disconnect Behaviour: using the server instance there exists a on_connect method for BLEServer. on_connect has one argument which is a closure that passes a handle for a BLEServer and a BLEConnDesc that contains connection information. In the closure body, upon connect, we'll print the connection data to the console then update the connection parameters. To update connection parameters, the BLEServerupdate_conn_params method is used. Here's the code:

server.on_connect(|server, clntdesc| {
    // Print connected client data
    println!("{:?}", clntdesc);
    // Update connection parameters
    server
        .update_conn_params(clntdesc.conn_handle(), 24, 48, 0, 60)
        .unwrap();
});

Similar to on_connect theres an on_disconnect method. on_disconnect also has one argument which is a closure that passes a handle for a BLEConnDesc and a Result that contains the reason for disconnection. All were going to do is to print a message that our device disconnected.

server.on_disconnect(|_desc, _reason| {
    println!("Disconnected, back to advertising");
});

6๏ธโƒฃ Define Services & Characteristics: In this example, only one characteristic will be defined with a read and notify properties. However, every characteristic has to be associated with (listed under) a service. To create a service, within BLEServer there exists a create_service method that takes a single BleUuid argument. BleUuid is an enum representing a Bluetooth UUID. To make things easier, the nimble crate provides a uuid128 macro to parse a 128 UUID from string literals at compile time. We only need to pass a UUID string literal and the macro would take care of the rest. We create a service as follows:

// Create a service with custom UUID
let my_service = server.create_service(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1"));

Next, we need to create a characteristic. This is done using the BLEServicecreate_characteristic method which has two arguments; BlueUuid and NimbleProperties. NimbleProperties is a struct containing a collection of associated constants representing the different supported operations and required security levels. For this characteristic we will support encrypted READ through the NimbleProperties::READ_ENC property. create_characteristic will return a Arc<Mutex<BLECharacteristic>>. As such, we can set a starting value for the characteristic that we created using the BLECharacteristic set_value method. set_value takes a single &[u8] parameter. Here's the code:

// Create a characteristic to associate with created service
let my_service_characteristic = my_service.lock().create_characteristic(
    uuid128!("681285a6-247f-48c6-80ad-68c3dce18585"),
    NimbleProperties::READ | NimbleProperties::READ_ENC,
);

// Set a starting value for the characteristic
my_service_characteristic.lock().set_value(b"Start Value");

6๏ธโƒฃ Configure Advertiser Data: This step is similar to what was done in the BLE Advertiser post. A small difference is that here we are also attaching the service to the advertisement data. This is done using the add_service_uuid method. Note that the UUID being advertised is the same service UUID created earlier.

// Configure Advertiser Data
ble_advertiser
    .lock()
    .set_data(
        BLEAdvertisementData::new()
            .name("ESP32 Server")
            .add_service_uuid(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")),
    )
    .unwrap();

That's it for configuration!

๐Ÿ“ฑ Application Code

Start Advertising: Now we have to start the advertising process. This is done by calling the BLEAdvertising start method.

ble_advertiser.lock().start().unwrap();

Update Characteristic: All we have to do now is keep updating the characteristic by calling the set_value method again. Here, the my_service_characteristic value keeps getting updated every one every second by incrementing its contents.

let mut val = 0;

loop {
    FreeRtos::delay_ms(1000);
    my_service_characteristic.lock().set_value(&[val]).notify();
    val = val.wrapping_add(1);
}

๐Ÿงช Testing

In order to test this code, you can use nRF connect mobile app or the bluefruit connect mobile app. In either app you would be able to connect to the ESP and poll the server data.

๐Ÿ“ฑFull Application Code

Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the apollolabs ESP32C3 git repo.

use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEDevice, NimbleProperties};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_sys as _;

fn main() {
    esp_idf_sys::link_patches();

    // Take ownership of device
    let ble_device = BLEDevice::take();

    // Obtain handle for peripheral advertiser
    let ble_advertiser = ble_device.get_advertising();

    // Configure Device Security
    ble_device
        .security()
        .set_auth(AuthReq::all())
        .set_passkey(123456)
        .set_io_cap(SecurityIOCap::DisplayOnly)
        .resolve_rpa();

    // Obtain handle for server
    let server = ble_device.get_server();

    // Define server connect behaviour
    server.on_connect(|server, clntdesc| {
        // Print connected client data
        println!("{:?}", clntdesc);
        // Update connection parameters
        server
            .update_conn_params(clntdesc.conn_handle(), 24, 48, 0, 60)
            .unwrap();
    });

    // Define server disconnect behaviour
    server.on_disconnect(|_desc, _reason| {
        println!("Disconnected, back to advertising");
    });

    // Create a service with custom UUID
    let my_service = server.create_service(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1"));

    // Create a characteristic to associate with created service
    let my_service_characteristic = my_service.lock().create_characteristic(
        uuid128!("681285a6-247f-48c6-80ad-68c3dce18585"),
        NimbleProperties::READ | NimbleProperties::READ_ENC ,
    );

    // Modify characteristic value
    my_service_characteristic.lock().set_value(b"Start Value");

    // Configure Advertiser Data
    ble_advertiser
        .lock()
        .set_data(
            BLEAdvertisementData::new()
                .name("ESP32 Server")
                .add_service_uuid(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")),
        )
        .unwrap();

    // Start Advertising
    ble_advertiser.lock().start().unwrap();

    // (Optional) Print dump of local GATT table
    // server.ble_gatts_show_local();

    // Init a value to pass to characteristic
    let mut val = 0;

    loop {
        FreeRtos::delay_ms(1000);
        my_service_characteristic.lock().set_value(&[val]).notify();
        val = val.wrapping_add(1);
    }
}

Conclusion

This post introduced how to create a secure BLE server on the ESP32-C3 with Rust. This was by using the esp32-nimble crate in a standard library development environment using the esp-idf-hal . In this post, the ESP32-C3 was configured as a secure peripheral device advertising a service. The device also assumes a server role after a connection is established and secures the connection with a passkey. Have any questions? Share your thoughts in the comments below ๐Ÿ‘‡.

0
Subscribe to my newsletter

Read articles from Omar Hiari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Omar Hiari
Omar Hiari

I am an ๐Ÿ“Ÿ Embedded Engineer with years of experience in both industry ๐Ÿญ and academia ๐Ÿซ. Passionate Mentor ๐Ÿ‘จโ€๐Ÿ’ผ and Instructor ๐Ÿ‘จโ€๐Ÿซ. Aspiring Rustacean ๐Ÿฆ€