Embedded Rust Bluetooth on ESP: Secure BLE Server
Tags: rust, tutorial, esp32, programming
Published: 2024-04-15 20:25:39

> ***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.***
    

## **Introduction**

In this post, we're going to build on the [BLE Server post](https://apollolabsblog.hashnode.dev/embedded-rust-bluetooth-on-esp-ble-server) 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.](https://crates.io/crates/esp32-nimble/0.6.0)

#### If you find this post useful, and if Embedded Rust interests you, stay in the know by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com/subscribe %} Subscribe Now to The Embedded Rustacean{% endcta%} 

### **📚 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**](https://github.com/apollolabsdev/ESP32C3). 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**](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html)
    
    ![kit](https://cdn.hashnode.com/res/hashnode/image/upload/v1681796942083/da81fb7b-1f90-4593-a848-11c53a87821d.jpeg)
    
    #### **🔌 Connections**
    
    No connections are required for this example.
    

## **👨‍🎨 Software Design**

The steps in securing a channel were presented in [last week's post](https://apollolabsblog.hashnode.dev/embedded-rust-bluetooth-on-esp-secure-ble-client) 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](https://apollolabsblog.hashnode.dev/embedded-rust-bluetooth-on-esp-ble-server) 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.
    

```rust
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:

```rust
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:

```rust
// 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:

```rust
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:

```rust
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:

```rust
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.

```rust
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:

```rust
// 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:

```rust
// 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](https://apollolabsblog.hashnode.dev/embedded-rust-bluetooth-on-esp-ble-advertiser). 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.

```rust
// 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.

```rust
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.

```rust
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](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-mobile) mobile app or the [bluefruit connect](https://learn.adafruit.com/bluefruit-le-connect/ios-setup) 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**](https://github.com/apollolabsdev/ESP32C3) git repo.

```rust
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 👇.

#### If you found this post useful, and if Embedded Rust interests you, stay in the know by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com/subscribe %} Subscribe Now to The Embedded Rustacean{% endcta%}