As we all know, the S in IoT stands for security. However, one reason IoT has a bad security track record is that it consumes a lot of resources and is not that easy to setup. One piece of this puzzle is having good client side libraries for doing TLS. Read on to learn more about how you can secure your device to cloud communication.
What are the options?
There are no one solution fits all, and requirements for a hobby project is very likely different from installation in a secure facility. The approaches we've tried out so far is:
- No security - for hobby projects where you're just reporting temperature values to the cloud, security might not seem to be a big deal. However, if you're sending that data to some cloud service, chances are that the cloud service want your device to present some credentials. Since sending unencrypted credentials over a public network is a bad idea, this option is only viable in cases where you control the entire network where no one can sniff traffic.
- Secured gateway to cloud - If you have a private network where you don't worry about someone sniffing out your credentials, you have the option of installing a local gateway or running a service on an existing gateway which forwards data over an encrypted link to a cloud service. This is useful if your device just cannot do encrypted traffic and you can be certain no one is sniffing credentials.
- Secure device to cloud - This requires your device to be capable of encrypting data, potentially supporting a standardized protocol like TLS (Transport Layer Security).
We'll explore the two latter options in this post.
Secured gateway to cloud
If you have a too little RAM to do TLS on the device, and you are on a secure network, then you can setup a gateway between the device and cloud. One way to do that is to use HAProxy, which is fairly simple to setup.
Here is an example configuration that you can append to the HAProxy config for proxying Device -http-> HAProxy -https -> Drogue Cloud
:
#---------------------------------------------------------------------
# main frontend which devices connect to
#---------------------------------------------------------------------
frontend main
bind *:80
default_backend drogue
#---------------------------------------------------------------------
# Drogue IoT backend
#---------------------------------------------------------------------
backend drogue
balance roundrobin
http-request set-header Host http.sandbox.drogue.cloud
server static http.sandbox.drogue.cloud:443 check sni str(http.sandbox.drogue.cloud) ssl
The frontend configuration configures the incoming port bound to where devices can connect. The backend configuration configures the route to the Drogue IoT sandbox service. For the requests to be routed correctly, the HTTP Host header and the TLS Server Name (SNI) must be set to the destination host.
Secured device to cloud
Doing TLS on devices is the preferred approach if you have the available RAM. Unfortunately, to use TLS in Rust embedded, is has been necessary to "pollute" your Rust project with an existing TLS library like mbed TLS, which is the old approach taken by drogue-tls.
We've started a new implementation of TLS 1.3 in pure Rust (nicknamed BobTLS after the initial author), and replaced the mbedTLS-based version. The goal is to implement the TLS 1.3 specification in pure Rust, with async support. The project does not have any dependencies on other Drogue IoT projects, and can be used with any TCP stack, by implementing two traits used for I/O. In fact, there are implementations of these traits for some common runtimes, and you can find examples of using Drogue TLS with tokio and embassy + smoltcp, with trait implementations for futures I/O traits as well.
The project is still under development, but implements basic functionality to get connected to Drogue IoT Cloud, such as Server Name Indication (SNI) extension. Critical features such as certificate validation and client certificate authentication is still on the TODO list.
Why TLS 1.3?
TLS 1.3 is simpler and easier to implement than TLS 1.2. This is partially because there are only a single cipher suite that needs implementing (TLS_AES_128_GCM_SHA256), which is also not too resource demanding for embedded devices, but also because there are fewer mandatory extensions.
Where does my RAM go?
We've tried to keep the memory footprint of drogue-tls
small. The overhead of a TLS connection is minimal, and correlates with the desired buffer size. To properly support any TLS compliant endpoint, the chosen buffer size should be at least the maximum TLS frame size, 16kB. However, if you know your application will not be sending much data, you can get away with less. Given the focus on embedded, the primary goal has been to make it small rather than fast.
Since Drogue TLS is built using async, the futures also require some stack allocation. However, since embassy uses static futures for the tasks, we know the memory usage at compile time (BSS).
As an example, memory usage of embassy + smoltcp binary, compile to the x86_64-unknown-linux-gnu
target, looks like this without TLS:
$ size target/release/ping-embassy-net
text data bss dec hex filename
1094295 42040 16880 1153215 1198bf target/release/ping-embassy-net
Whereas with TLS enabled using a 16 kB record buffer, the memory usage increases (difference shown in the last row).
$ size target/release/ping-embassy-net
text data bss dec hex filename
1175083 44392 37296 1256771 132d43 target/release/ping-embassy-net
8% 5% 2.2x 8%
Although the BSS increases to 2.2x, the 16kB is a large fraction of that increase. The BSS overhead of the library itself is about 4 kB when enabling TLS with a 16 kB record buffer. The same overhead can be observed when compiling to ARM architectures.
We have not yet look at flash usage, so there are probably improvements to be made there as well.
What about Drogue Device?
Drogue Device now supports TLS, and the example using micro:bit + ESP8266 WiFi is now using Drogue TLS to connect directly to the sandbox.
We want Drogue Device to provide a simple way to get started with IoT, so having a simple API to use networking is important. As an example, the following code joins the wifi network, creates a regular Socket
before wrapping it in a TlsSocket
which implements the same trait as a regular Socket
.
let mut wifi = device.wifi.mount((), spawner);
wifi.join(Join::Wpa {
ssid: WIFI_SSID,
password: WIFI_PSK,
})
.await
.expect("Error joining wifi");
log::info!("WiFi network joined");
static mut TLS_BUFFER: [u8; 16384] = [0; 16384];
let socket = Socket::new(wifi, wifi.open().await);
let socket = TlsSocket::wrap(socket,
TlsContext::new(rng,
unsafe { &mut TLS_BUFFER }) // We guarantee buffer to only be used by TLS
.with_server_name(HOST));
Because they implement the same trait, it's easy to write logic that does not have to care about the connection being over TCP or TLS.
Future work
As mentioned, Drogue TLS is still under development, with some critical features missing. However, we hope to see interest from the community and hope to collaborate on improving the TLS support for Rust embedded.
Another piece of security we have not yet discussed is the security of the device itself. If the device is installed in a public location, you also need to make sure someone cannot simply connect to it physically and extract the credentials from flash. This is a topic on its own that we'll try to cover in a later post, but newer generation microcontrollers allow you to define secure areas of flash to prevent extraction of credentials.
Summary
In this post we've gone through two options for securing device to cloud communication. One is by deploying a local gateway such as HAProxy to handle the secure communication. The other is to encrypt data on the device, using the new Drogue TLS library. Finally, we've seen how you can use Drogue TLS with Drogue Device.