DEV Community

Cover image for Embedded Rust Bluetooth on ESP: Secure BLE Server
Omar Hiari
Omar Hiari

Posted on

Embedded Rust Bluetooth on ESP: Secure BLE Server

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

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

Subscribe Now to The Embedded Rustacean

📚 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

👨‍🎨 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 _;
Enter fullscreen mode Exit fullscreen mode

🎛 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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

🧪 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Subscribe Now to The Embedded Rustacean

Top comments (3)

Collapse
 
akostadinov profile image
Aleksandar Kostadinov

Hello, I read that hardcoding the passcode is insecure, because it can be cracked one bit at a time.

And reading the BT code spec I see:

The user inputs an identical Passkey into both devices. Alternately, the
Passkey may be generated and displayed on one device, and the user then
inputs it into the other (step 2). This short shared key will be the basis of the
mutual authentication of the devices. The Passkey should be generated
randomly during each pairing procedure and not be reused from a previous
procedure. Static Passkeys should not be used since they can compromise the
security of the link.

Probably related to the following instructions:

In Steps 3 to 8, each side commits to each bit of the Passkey, using a long nonce (128 bits), and sending the hash of the nonce, the bit of the Passkey, and both public keys to the other party. The parties then take turns revealing their commitments until the entire Passkey has been mutually disclosed. The first party to reveal a commitment for a given bit of the Passkey effectively reveals that bit of the Passkey in the process, but the other party then has to reveal the corresponding commitment to show the same bit value for that bit of the Passkey, or else the first party will then abort the protocol, after which no more bits of the Passkey are revealed.

So it doesn't seem much more secure than the "Just works" method.

Idk how to overcome this except for adding a keypad... is it possible to somehow ensure the physical security, that there are no other devices around? Like shielding the devices together?

Collapse
 
theembeddedrustacean profile image
Omar Hiari

I’m actually not sure. However yes, in the first phase of establishing a secure connection the device capabilities need to be identified. Having a static key although undesirable might be unavoidable if there isn’t a method for entry.

I think though in the latest version of the spec there is an option for out of bound security. It is the recommended approach but I’m not sure what it exactly entails.

Collapse
 
akostadinov profile image
Aleksandar Kostadinov

Out of bound I assume still one needs to interact with the devices somehow. But for my use case this would be still too expensive and moreover undesirable as the device is supposed not to be easily accessible. So I'm thinking about restricting wireless coverage distance and physical security during pairing. Afterwards that would not be a concern.