A demo with native C implementation of X3DH Key Agreement Protocol for ESP32.
This project demonstrates how to securely communicate ESP32 microcontrollers using the X3DH Key Agreement Protocol over MQTTS, leveraging a VPN connection established via WireGuard. The architecture consists of an X3DH Server, a VPN Server, a RabbitMQ message broker, and a PostgreSQL database, all orchestrated using minikube to create a local Kubernetes cluster.
Host's specifications:
- OS: macOS 26.3
- Architecture: arm64
- CPU : Apple M2 (8) @ 3.50 GHz
- RAM : 8 GB
- Command Line Tools for Xcode: 26.3
- Python: 3.12.10 (at least 3.9)
- clang: 17.0.0
- cmake: 4.2.3 (at least 3.20)
- ninja: 1.13.2
- ccache: 4.12.3
- git: 2.53.0
- dfu-util: 0.11
- OpenSSL: 3.6.1
- Docker Desktop: 4.61.0
- minikube: 1.38.0
- Kubernetes: 1.35.0
- Docker: 29.2.0
- containerd 2.2.1
X3DH Server's specifications:
- OS: Alpine Linux 3.23.2
- Python: 3.13.11
- OpenSSL: 3.5.4
VPN Server's specifications:
- OS: Alpine Linux 3.23.2
- WireGuard: 1.0.20250521
RabbitMQ's specifications:
- OS: Alpine Linux 3.23.2
- RabbitMQ: 4.2.3 (at least 4.0.4)
- Erlang: 27.3.4.6
- OpenSSL: 3.5.4
PostgreSQL's specifications:
- OS: Debian GNU/Linux 13.3
- postgres: 18.1
ESP32's specifications:
- MCU module: ESP32-WROOM-32E
- Chip: ESP32-D0WD-V3 (revision v3.0)
- ESP-IDF: 5.5.1 (at least 5.3.0)
- gcc: 14.2.0
- cjson: 1.7.19
- libsodium: 1.0.20
- libxeddsa: 2.0.1
- MbedTLS: 3.6.4
- esp_wireguard: 0.9.0
- ESP-MQTT: 1.0.0
Raspberry Pi 4 Model B's specifications:
- OS: Raspbian GNU/Linux 12
- Architecture: arm64
- CPU: BCM2711 (4) @ 1.80 GHz
- RAM: 8 GB
If you are using macOS:
- make sure to update the Python certificates. To make it easier, I strongly suggest to download Python from the official website, instead of using HomeBrew/MacPorts, and then run the following commands in your terminal:
cd /Applications/Python\ 3.x/
./Install\ Certificates.command-
if you cannot see the ESP32 serial port after connecting it via USB, you might need to install the appropriate drivers. You could try to install the WCH34 driver and follow the installation guide from the official repository, or you could try to install the Silicon Labs CP210x driver.
-
if you are using
minikubewithdockeras driver, and you are facing this error ERROR KubeletVersion, you might need to execute this script:minikube_down.sh.
git clone https://github.com/mastronardo/ESP32-X3DH-Demo.git
cd ESP32-X3DH-Demo && chmod +x create_cluster.sh gen_certs.shThe first step is to set up the ESP-IDF environment. You can follow the official guide here.
Now you can add the need components via ESP Component Registry. You can simply run the following commands to add the required dependencies for this project:
pip install -U pip # be sure to have the latest pip version
pip install -U idf-component-manager # be sure to keep the component Manager updated
idf.py add-dependency "espressif/libsodium^1.0.20~3"
idf.py add-dependency "espressif/cjson^1.7.19~1"
idf.py add-dependency "trombik/esp_wireguard^0.9.0"
idf.py add-dependency "espressif/mqtt^1.0.0"Since libxeddsa library is not available in the Component Registry, it was necessary to manually build it for ESP-IDF.
The first step was to clone the repository inside the components directory.
mkdir -p client/components && cd client/components
git clone https://github.com/Syndace/libxeddsa.gitAfter that, the following files inside the libxeddsa directory were been modified to make the library compatible with ESP-IDF: CMakeLists.txt, ref10/CMakeLists.txt, ref10/include/cross_platform.h. In addition, some files that were not needed for this project (for example tests and docs), were deleted to free some space in the flash memory.
sdkconfig.defaults was created to automatically generate the ready-to-use sdkconfig file when you open the menuconfig or set the target.
- To make sure that the client binary executable file will fit inside the flash memory of the ESP32, and to avoid stack overflow issue when running the project, these parameters were set:
(Top)βPartition TableβPartition TableβTwo large size OTA partitions(Top)βSerial Flasher ConfigβFlash sizeβ4MB(Top)βComponent configβESP System SettingsβMain task stack sizeβ12288(Top)βComponent configβLWIPβTCP/IP Task Stack Sizeβ4096
- To enable the HKDF algorithm required by the X3DH Key Agreement Protocol and TLSv1.3:
(Top)βComponent configβmbedTLSβHKDF algorithm (RFC 5869)
- Since "IPv6 support is alpha and probably broken" in esp_wireguard component, it is recommended to disable it:
(Top)βComponent configβLWIPβEnable IPv6(disable)
- To enable the PPP support needed by the esp_wireguard component:
(Top)βComponent configβLWIPβEnable PPP support
- To enable MQTT 5.0 and disable MQTT 3.1.1:
(Top)βComponent configβESP-MQTT ConfigurationsβEnable MQTT protocol 5.0(Top)βComponent configβESP-MQTT ConfigurationsβEnable MQTT protocol 3.1.1(disable)
- To avoid connection issues when running the project, it is recommended to increase the MQTT buffer and stack sizes:
(Top)βComponent configβESP-MQTT ConfigurationsβMQTT Using custom configurationsβDefault MQTT Buffer Sizeβ2048(Top)βComponent configβESP-MQTT ConfigurationsβMQTT Using custom configurationsβMQTT task stack sizeβ8192
- To force using TLSv1.3:
(Top)βComponent configβmbedTLSβmbedTLS v3.x relatedβSupport TLS 1.3 protocol(Top)βComponent configβmbedTLSβSupport TLS 1.2 protocol(disable)
Since we are going to store the keys in the NVS memory, it is recommended to erase the flash:
- before flashing the client for the first time,
- or, if you want to execute the project from a clean state.
idf.py -p PORT erase-flashWith ESP-IDF VS Code extension, you can easily check the size of the generated binary file after building the project. The following table shows the size of the different memory sections for the generated binary file:
| Flash Code | Flash Data | IRAM | DRAM | RTC SLOW |
|---|---|---|---|---|
| 868KB | 164KB | 96KB/128KB | 65KB/177KB | 0KB/8KB |
Make sure to have minikube installed on your machine. You can follow the official guide here. The choice to use minikube is influenced by the fact that is a lightweight K8s instance that can be easily set up on a local machine, making it ideal for development and testing purposes. It allows us to create a local Kubernetes cluster without the need for complex infrastructure, which is perfect for this demo.
The suggested Web UI is Headlamp, thanks to its user-friendly interface and powerful features for managing Kubernetes clusters. It provides an intuitive way to visualize and interact with the cluster resources, making it easier to monitor the status of the deployed components.
The X3DH server is implemented in Python and runs inside a lean Docker container. To build the Docker image, you need to pull the base image and then build the custom image using the provided Dockerfile. The server interacts with a PostgreSQL database to store user info and uses TLSv1.3 for secure communication with broker. The database password can be retrieved from the Kubernetes secret created during the cluster setup.
# do not use sudo if your user has permissions to run docker commands
sudo docker pull python:3.13.11-alpine3.23
sudo docker build -t x3dh-server:1.1 ./serverThe VPN server was integrated into the demo to provide a secure communication channel between the ESP32 clients and the RabbitMQ broker. By using WireGuard, we can ensure that all data transmitted between the clients and the broker is encrypted and protected from potential eavesdropping or tampering. The VPN server runs inside a lightweight Docker container based on Alpine Linux, which keeps the resource usage low. The WireGuard configuration is set up to allow multiple clients to connect securely, and the necessary keys are generated during the cluster setup.
Since the esp_wireguard repository is no longer maintained, if you try to use the component as it is, you are going to face issues with the newest versions of ESP-IDF. Mainly thanks to issues opened during 2025, and Kerem Erkan's post about MTU fixes, it was possible to make the component work again.
-
Overview: the MCU performs NTP time synchronization and initializes the WireGuard tunnel. All runtime parameters used by the client are provided via the generated header
keys.h. -
Generate
keys.h:generate_keys.pyextracts the required information from WireGuard and RabbitMQ containers, and writes them toclient/main/keys.h. You need to provide two arguments: the host's local IP address where the Minikube cluster is running, and the peer number assigned in the WireGuard server configuration (starting from 1). -
Preshared Key (PSK) compatibility: the
esp_wireguardclient used does not supportPresharedKey. For that reason the helper scriptupdate_wg_config.sh(embedded ink8s-deployment.yaml) comments outPresharedKeylines in the server configuration and replacesPostUp/PostDownrules to ensure proper forwarding/NAT and add an MSS clamp to avoid MTU issues. The script writes a flag file so it runs only once. -
Runtime network notes:
app_main.csets the WireGuard interface address fromWG_LOCAL_IP_ADDRand, in the current code, forces a class-A netmask (255.0.0.0) and a gateway of10.13.13.1. The interface MTU is reduced to1280to prevent packet fragmentation/loss. If your network topology requires different netmask/gateway/MTU, updatekeys.hor modifystart_wireguard()inapp_main.caccordingly.
β οΈ Known Issue: Due to CGNAT (Carrier-Grade NAT) used by mobile hotspot, the client may not be able to reach the WireGuard server. If you experience connectivity issues, please try to connect the MCU to a different network. If you are using a different client device (such as a PC), yet got the same issue, please check this Kerem Erkan's post.
β Known Limitation: esp_wireguard does not support Ethernet interface.
The RabbitMQ message broker it is used to reduce the load on requests that were previously sent directly to the server, and to use a lighter protocol that is better suited to the IoT ecosystem (MQTT). The broker is configured to use TLSv1.3 for secure communication with both the server and the clients. The necessary certificates are generated during the cluster setup and mounted as Kubernetes secrets. Three replicas of RabbitMQ are deployed to ensure high availability, and a ClusterIP service is created to allow internal communication within the cluster.
The PostgreSQL database is used to store user information for the X3DH server. It is managed using CloudNativePG Operator, which simplifies the deployment and management of PostgreSQL clusters (1 primary instance + 2 replicas) on Kubernetes. The choice of using PostgreSQL is due to its robustness and reliability, making it suitable for production environments.
A pgAdmin instance is also deployed to provide a web-based interface for managing the database. It is accessible via localhost:30080 and can be used to verify that the database is correctly set up and to inspect the stored data.
First of all, make sure to:
- set the target for your ESP32 device (e.g.
esp32oresp32s3), - use a valide WiFi credentials inside
client/main/app_main.cfile, - set the number of peers for VPN Server inside
k8s-deployment.yaml, - update the Postgres env varibles inside
server/start.shandserver/server.py, - choose an email and a password for pgAdmin inside
pgadmin.yaml, - retrieve the password to access the
x3dh_dbfrom pgAdmin with:
kubectl get secret x3dh-db-cluster-app -n x3dh-project -o jsonpath='{.data.password}' | base64 -d- Start the Minikube cluster:
# Run this only the first time to create the cluster
./create_service.sh # Run this to start the cluster if you stopped it earlier
minikube start- Generate the
keys.hfile for the client:
python3 generate_keys.py <HOST_LOCAL_IP> <PEER_NUMBER>- Build, flash and monitor the client:
cd client && get_idf
idf.py build
idf.py -p PORT flash monitorβ Known Issue: If you get these errors, you should check your network connection and VPN configuration, and make sure that the client can reach the broker through the VPN tunnel. If the issue persists, try to restart the host machine and the client, or to increase timeout values and stack size for the client.
esp-tls: [sock=54] select() timeoutesp-tls: Failed to open new connectiontransport_base: Failed to open a new connectionmqtt_client: Error transport connect
The ESP32 client will firstly connect to WiFi, then it will perform NTP time synchronization. After that, the WireGuard tunnel will be initialized to connect to the VPN Server, to securely communicate with RabbitMQ broker, ensuring end-to-end encryption and secure key exchange. The following menu will be displayed:
The following schema illustrates the architecture of the demo, showing how the different components interact with each other:
To measure the energy consumption needed by the ESP32 to perform the X3DH protocol (under the above-mentioned architecture), the following electrical schema was used.
Acting as the primary control unit, a Raspberry Pi 4 interfaced with the INA219 sensor via the I2C (Inter-Integrated Circuit) protocol. This standard enables synchronous, serial communication within a multi-master multi-slave architecture, relying entirely on just two dedicated lines: Serial Data (SDA) and Serial Clock (SCL). The sensor is able to measure the main physical quantities consumed by the ESP because they are connected together through a USB powered switch. The MCU sends signals through GPIO to Raspberry Pi 4 to start/stop saving the energy measurement data. In addition, the MCU sends a signal to a Relay to power on the green led when the secret key is generated. Lastly, the GND is in common among all devices.
To provide a clear overview of the deviceβs energy profile, the energy detection was performed during both steady and execution modes. In its steady state, the device operates at 3.28V and draws approximately 330mA, establishing a baseline power consumption of roughly 1080mW. During the execution of the protocol, the voltage is the same, whereas the average current draw increases to 466mA, resulting in an average power consumption of 1528.55mW.
Given the mathematical complexity of the Extended Triple Diffie-Hellman protocol, the energy overhead of approximately 500mW is notably modest. The ability to execute advanced, secure cryptographic handshakes locally without inducing prohibitive energy spikes demonstrates that the ESP32 is not merely a passive IoT node, but a highly capable Edge device.
| Physical Quantity | Average | Std Dev | Lower CI 95% | Upper CI 95% |
|---|---|---|---|---|
| Voltage (V) | 3.28 | 0.0 | 3.28 | 3.28 |
| Current (mA) | 466.01 | 17.35 | 453.60 | 478.42 |
| Power (mW) | 1528.55 | 56.94 | 1487.82 | 1569.28 |


