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:
(Added Step) Configure device security capabilities and connection method for pairing
(Modified Step) Create Secure Service & Characteristic
Configure Advertising Data & Start Advertising
Establish a connection
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 π.
Top comments (3)
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:
Probably related to the following instructions:
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?
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.
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.