Remote access to an ESP32 without TLS

The same firmware needed 23.6 KB of RAM to stay connected over TLS and 9.4 KB over the Noise protocol. This is what was measured, what had to be written, and what the choice costs.

The problem

A device that has left your desk is on a network you cannot reach. To open its web page, read its log or send it new firmware, it has to keep a connection open to a server you can reach. On an ESP32 that connection is nearly always TLS, and TLS is the most expensive thing most firmware does with memory.

What TLS costs

The first version of this component used a WebSocket over TLS with mbedTLS, as nearly everything on the ESP32 does. On an ESP32-S2 with ESP-IDF 5.5.3, measured as the free heap of the same application built with and without the component:

Over TLS
RAM while connected and idle23,632 bytes
Further dip under trafficabout 6 KB
Flash added to the applicationabout 155 KB more than without TLS

That was with CONFIG_MBEDTLS_DYNAMIC_BUFFER, which frees the handshake buffers once the session is up. Without it the figure is higher: Espressif's own estimate for one TLS connection is about 35 KB. On a chip where the application already has perhaps 100 KB of heap to live in, a quarter of it goes to staying reachable.

What the connection actually has to do

TLS solves a general problem: any client, any server, trust decided by a chain of certificates. A device that only ever talks to one server, whose address is compiled into it, has a much smaller problem:

None of that needs certificates, a negotiation of cipher suites or a 16 KB record buffer. It needs one public key that the device already knows.

Noise, in two messages

The Noise protocol framework is what WireGuard's handshake is built on. The pattern used here is Noise_NK_25519_ChaChaPoly_SHA256: the server has a static key that the device knows in advance, the device has none.

device -> server   "PSL1" | e (32 bytes) | tag (16)     -> e, es
server -> device          e (32 bytes) | tag (16)     <- e, ee

After those two messages each direction has its own ChaCha20-Poly1305 key and a counter. Every message from then on is a two-byte length, the encrypted frame and a 16-byte tag. Only the holder of the server's private key can finish the handshake, so the device knows who it is talking to. The keys exist only for this connection, so recording the traffic and obtaining the server's key later reveals nothing. The device's project key and credential go in the first encrypted frame.

The whole exchange is 100 bytes on the wire and about 0.3 s of CPU on the ESP32-S2.

What had to be written

Dropping TLS means dropping the library that came with it, so the component carries its own X25519, ChaCha20-Poly1305 and SHA-256, written for size and checked against a reference implementation with the same inputs. A few other things turned out to cost more than expected:

The result

Bytes
Frame buffers (1 KiB out, 512 B in) and the queue for logs and metrics (2 KiB)3,625
Rest of the state, and code placed in IRAM1,343
Task stack3,072
Task, lock and the socket1,544
Total while connected9,584

The component is 18 KB of flash. Through the tunnel a 64 KB download runs at about 85 KB/s, against 77 KB/s over TLS on the same board, and a small request takes 25 to 40 ms. Nothing is configurable: every buffer has a fixed size, so the figure is the same in every project.

What it costs

This is not a free win, and it is not the right choice for everything.

The protocol is written down in full in the repository, and the documentation has the same memory breakdown.

Try it

Three devices are free with every feature, and no card is needed. Create an account, or read the five-minute guide first.

More