It's hard to overemphasize the importance of considering security when developing firmware for Internet-connected devices. Many articles and papers have been written on the topic of IoT security. Because we at Very care a lot about IoT and our clients, we are also deeply concerned with developing secure firmware and applications.
We recently completed work on an Internet-connected project where we applied security best practices to the development of a smart fish tank.
The device had a pretty limited feature set:
- It will broadcast an adhoc wifi network.
- Once a user has connected to the device's adhoc wifi network, it will accept wifi credentials from a mobile app and connect to an existing, Internet-enabled network.
- It will register itself with a remote server.
- It can send state updates to the remote server.
- It can receive payloads from the remote server.
In this blog post, I'll share a few tips we identified during the development of this project that you can use when developing firmware for Internet-connected devices.
Reduce Your Attack Surface
The smaller the attack surface, the easier you can sleep at night. Even though our device needed to broadcast a wifi network and run an http server (for accepting credentials to the Internet enabled network), we did not need to do those things after the user has configured the device. By carefully and intentionally disabling the ad-hoc wifi network and http server, we ensured that neither of those features could be used for nefarious purposes during the regular usage of the device.
In addition, after the initial configuration step is complete, the mobile app never communicates directly with the device again. All communications are limited to a single WebSocket channel that is negotiated between the device and the remote server. The mobile app sends all commands and payloads through the remote server, and the server will forward that communication to the device. This way we don't have to support multiple communication paths on the device, and we can focus all of our concern on the single existing channel.
We also reduced the total amount of information stored on the device to ensure that if the device fell victim to an attack, the attacker would be very limited in the amount of information they could extract from the device. We architected the application so that no user data is stored locally. The only data that we do store locally are the wifi credentials, so that the device can automatically reconnect to wifi after being rebooted, and a token that will allow the device to send data to the remote server and the local wifi credentials. And of course, all of these are encrypted.
No Plain Text
This point cannot be overstated. Do not store sensitive information in plain text. Anywhere. Ever. Even though we store three potentially sensitive data points from the user on our devices, we make sure that those data points are encrypted before they go into the EEPROM.
As mentioned previously, we do not store any user info on the device. However we do allow the device to receive user information from a mobile app during device configuration. The device then forwards that data on to the remote server and does not retain a local copy. We do this to ensure that the device, when it is registered with the remote server, is coming from a valid user. However this data is not sent in plain text either, it is an encrypted JWT that is encrypted before it is sent to the device and only decrypted after it has been passed to the remote server. This way we can ensure that sensitive user data is never exposed on the device itself.
This also applies to communications between devices. When possible, always communicate over secure channels: WSS or HTTPS for web traffic and MQTT over TLS rather than simple TCP. Depending on hardware design restrictions, it can be difficult to implement secured communication protocols. In this case good secure design is just as important as secure implementation. Insecure channels are not inherently bad (and can be a positive from a cost perspective) as long as the data being sent is not sensitive.
Test for Potential Vulnerabilities
Another way to avoid vulnerabilities is to test for them. When an area of code may be problematic, such as accepting user input, it is possible to write tests that can check for obvious vulnerabilities and ensure that your code will not regress to a point where those vulnerabilities can be exploited in that code.
For instance, while we didn't care about sanitizing input that will never make its way to a user interface, we did care about how that data is stored. If we suspect that a particular part of our String handling may be vulnerable to a buffer overflow, we can write a test that will allow us to check how that code works with specific String length or content.
However, we obviously cannot reasonably expect to be able foresee all potential vulnerabilities which is why it is important to...
Allow for Updates
No matter how well thought-out a security practice may seem, there can be flaws. No system is perfect. It is important to be able to mitigate attacks before they can spread. Over-the-air updates are a great solution for addressing bugs or vulnerabilities that are discovered after a product has shipped. Arduino supports these OTA updates natively and they can be configured to allow the user to approve updates or to be applied automatically.
In conclusion, the IoT development process can sometimes feel like the wild wild west. But these devices can be developed for securely when developers and product owners are willing to put in time and effort to release the best product possible.