Cryptography for IoT Hackers - Part 4
Series Index
| Part | Title | Status |
|---|---|---|
| 1 | What TLS Actually Does (And Why Your IoT Device Needs It) | |
| 2 | Certificates From Scratch The Trust Chain Nobody Explains | |
| 3 | Setting Up Your Own CA and Issuing Certs with OpenSSL | |
| 4 - You are here | ESP32 + TLS: Your First Secure MQTT Connection | |
| 5 | What the TLS Handshake Looks Like on the Wire (Wireshark Lab) | |
| 6 | Mutual TLS (mTLS) Explained: When the Server Also Verifies the Client | |
| 7 | MITM Attack on a TLS IoT Device | |
| 8 | Cert Pinning The Fix, and How Attackers Bypass It | |
| 9 | Embedded Crypto Pitfalls: Hardcoded Keys and Weak RNG | |
| 10 | Breaking mTLS: Stolen Certs and Certificate Confusion | |
| 11 | Secure Provisioning How to Get Certs Onto Devices Safely | |
| 12 | Building a Hardened ESP32 TLS Client Checklist and Final Lab | |
| 13 | Secure OTA Updates Why Your Update Channel Is an Attack Surface |
Before You Read This
This is Part 4. If you haven’t done Parts 1, 2, and 3 yet go do them first. Seriously. This part picks up exactly where Part 3 ended. You should already have these files sitting in your ~/my-ca folder:
rootCA.key- your Root CA private keyrootCA.crt- your Root CA certificateserver.key- your server private keyserver.crt- your server certificate
If you don’t have those, go back to Part 3 and create them. This whole blog will make zero sense without them.
Also we are doing this on Kali Linux in VirtualBox with a Bridged Adapter. Not WSL. I tried WSL first and hit IP routing problems that were confusing and unnecessary. Kali on Bridged Adapter means it gets a real IP on your network, same subnet as your router, and your ESP32 on WiFi can reach it directly. That’s what we need.
What Are We Building
In Part 3, we created certificates. That was all terminal work no hardware involved.
Part 4 is where those certificate files finally go onto real hardware.
Here is the full picture of what we are setting up:
The ESP32 will connect to a Mosquitto MQTT broker running on your Kali machine. The connection will be encrypted using TLS. The ESP32 will publish a test message and we will see it arrive in the terminal.
That’s it. Simple goal. But getting there involves a bunch of steps and I’m not going to lie a bunch of errors. I hit almost every possible error during this setup and I’m going to document every single one of them so you don’t waste hours debugging what I already debugged.
What is MQTT and Why Are We Using It
MQTT is a lightweight messaging protocol used everywhere in IoT. The idea is simple — there is a broker (a server in the middle), there are publishers (devices that send messages), and there are subscribers (devices or programs that receive messages).
Think of it like a post office. The broker is the post office. The ESP32 is someone dropping off a letter. The subscriber is someone picking up mail from their box.
The problem with plain MQTT (port 1883) is that everything is in plaintext. Anyone who can intercept the traffic on the network can read every message. For IoT devices this is a real problem your sensor data, your commands, everything is readable.
MQTT over TLS (port 8883) puts all of that in a sealed envelope. The broker proves its identity with a certificate. The client verifies that certificate before connecting. After that, all communication is encrypted.
- Port 1883 = plain MQTT, no encryption, everything visible on the wire
- Port 8883 = MQTT over TLS, encrypted, broker identity verified
What You Need
- ESP32 DevKit (any variant)
- Kali Linux in VirtualBox with Bridged Adapter
- The cert files from Part 3 in
~/my-ca/ - Arduino IDE with ESP32 board support installed
- PubSubClient library by Nick O’Leary (install from Library Manager)
- Your WiFi network name and password
The WiFiClientSecure library comes built into the ESP32 board package no separate installation needed.
Step 1 - Check Your Kali Network Setup
Before anything else, confirm your Kali VM is on the same network as your ESP32.
In VirtualBox, your Kali adapter must be set to Bridged Adapter not NAT, not Host-Only. Bridged means Kali gets a real IP from your router, same subnet as everything else on your WiFi.
Run this on Kali:
ip route
You should see something like:
default via 192.168.28.1 dev eth0 proto dhcp src 192.168.28.63
192.168.28.0/24 dev eth0 proto kernel scope link src 192.168.28.63
Note down your Kali IP. In my case it was 192.168.28.63. Yours will be different use yours everywhere you see my IP in this blog.
Important: DHCP can change your Kali IP between reboots. We will deal with this but for now just note down the current IP.
Step 2 - Install Mosquitto on Kali
Mosquitto is an open source MQTT broker. Install it:
sudo apt install mosquitto mosquitto-clients -y
This installs two things:
mosquittothe broker itselfmosquitto-clientscommand line toolsmosquitto_pubandmosquitto_subfor testing
Step 3 - Fix Certificate Permissions
This is where most people get stuck and have no idea why. Let me explain before showing the commands.
When you created the cert files in Part 3, they were created as your user (iotsec in my case). But Mosquitto runs as a separate system user called mosquitto. By default, that user cannot read files in your home directory.
This is not a bug it is correct Linux behavior. A service running as its own user should not have access to other users’ files.
Here is what we need:
rootCA.crt- readable by everyone (it is a public certificate, that is fine)server.crt- readable by everyone (also public)server.key- readable ONLY by themosquittouser (this is the private key, never share it)
Run these commands:
# Make the directory accessible
sudo chmod 755 /home/iotsec/my-ca
# Public certs — readable by all
sudo chmod 644 /home/iotsec/my-ca/rootCA.crt
sudo chmod 644 /home/iotsec/my-ca/server.crt
# Private key — readable only by mosquitto
sudo chmod 600 /home/iotsec/my-ca/server.key
sudo chown mosquitto:mosquitto /home/iotsec/my-ca/server.key
Why chmod 755 on the directory? Because even if the file permissions are correct, if Mosquitto cannot enter the directory, it cannot read anything inside it. The directory must be executable (traversable) for other users.
After running these commands, verify with ls -la:
ls -la /home/iotsec/my-ca/
You should see:
-rw-r--r-- 1 iotsec iotsec 1472 rootCA.crt
-rw------- 1 iotsec iotsec 1704 rootCA.key
-rw-r--r-- 1 iotsec iotsec 1448 server.crt
-rw-rw-r-- 1 iotsec iotsec 1102 server.csr
-rw------- 1 mosquitto mosquitto 1704 server.key
Notice server.key has owner mosquitto and permissions 600 only the owner can read it.
Step 4 - The SAN Problem (Read This Before Creating Your Cert)
This is something I learned the hard way and I want to save you the headache.
When we created server.crt in Part 3, it had CN=iotsec.local. The CN (Common Name) is the hostname the certificate was issued for.
When mosquitto_sub connects using the hostname iotsec.local, it checks: does the hostname match the CN? Yes it works.
But the ESP32 connects using an IP address like 192.168.28.63. When it checks: does 192.168.28.63 match iotsec.local? No it rejects the certificate.
The fix is to add a SAN (Subject Alternative Name) to the certificate that includes the IP address. A SAN lets you say “this certificate is valid for BOTH the hostname iotsec.local AND the IP address 192.168.28.63”.
If you already created server.crt in Part 3 without a SAN, you need to regenerate it. Here is how.
First, temporarily give yourself back access to server.key to regenerate the CSR:
sudo chmod 644 /home/iotsec/my-ca/server.key
sudo chown iotsec:iotsec /home/iotsec/my-ca/server.key
Generate a new CSR with the SAN extension (replace the IP with your actual Kali IP):
openssl req -new -key /home/iotsec/my-ca/server.key \
-out /home/iotsec/my-ca/server.csr \
-subj "/C=NP/ST=Bagmati/L=Kathmandu/O=IoTSec/OU=IoTSec Server/CN=iotsec.local" \
-addext "subjectAltName=IP:192.168.28.63,DNS:iotsec.local"
Sign the new CSR with your Root CA:
openssl x509 -req -in /home/iotsec/my-ca/server.csr \
-CA /home/iotsec/my-ca/rootCA.crt \
-CAkey /home/iotsec/my-ca/rootCA.key \
-CAcreateserial \
-out /home/iotsec/my-ca/server.crt \
-days 365 -sha256 \
-extfile <(echo "subjectAltName=IP:192.168.28.63,DNS:iotsec.local")
Verify the SAN is in the new certificate:
openssl x509 -in /home/iotsec/my-ca/server.crt -noout -text | grep -A2 "Subject Alternative"
You should see:
X509v3 Subject Alternative Name:
IP Address:192.168.28.63, DNS:iotsec.local
Now fix the permissions back:
sudo chmod 600 /home/iotsec/my-ca/server.key
sudo chown mosquitto:mosquitto /home/iotsec/my-ca/server.key
Step 5 - Configure Mosquitto for TLS
Create a config file for Mosquitto:
sudo nano /etc/mosquitto/conf.d/tls.conf
Paste this inside:
allow_anonymous true
listener 8883
cafile /home/iotsec/my-ca/rootCA.crt
certfile /home/iotsec/my-ca/server.crt
keyfile /home/iotsec/my-ca/server.key
require_certificate false
What each line means:
allow_anonymous true- clients can connect without a username/password. Mosquitto 2.x requires this to be explicitly set, otherwise it rejects all connections.listener 8883- listen on port 8883 (TLS MQTT port)cafile- the Root CA certificate. Mosquitto uses this to verify client certificates if mTLS is enabled.certfile- the server certificate. This is what Mosquitto shows to clients to prove its identity.keyfile- the server private key. Used to prove Mosquitto actually owns the certificate.require_certificate false- clients do not need to present their own certificate. Only the broker proves its identity. (mTLS in Part 7 changes this.)
Save and exit (Ctrl+X, Y, Enter).
Step 6 - Start Mosquitto
sudo mosquitto -c /etc/mosquitto/conf.d/tls.conf -v
The -v flag is verbose mode it prints every connection, every message, every error. Keep this terminal open throughout the session. It is your best debugging tool.
You should see:
mosquitto version 2.0.22 starting
Config loaded from /etc/mosquitto/conf.d/tls.conf.
Opening ipv4 listen socket on port 8883.
Opening ipv6 listen socket on port 8883.
mosquitto version 2.0.22 running
If you see Permission denied for the cert files go back to Step 3 and fix permissions.
Step 7 - Test the Broker From Terminal First
Do not touch the ESP32 yet. Always verify the broker works from the command line first. If the broker is broken, adding ESP32 complexity just makes debugging harder.
Add iotsec.local to your hosts file so the hostname resolves locally:
echo "127.0.0.1 iotsec.local" | sudo tee -a /etc/hosts
Open a second terminal and subscribe to a topic:
mosquitto_sub -h iotsec.local -p 8883 --cafile /home/iotsec/my-ca/rootCA.crt -t iot/test
What each flag means:
-h iotsec.local- connect to this hostname (must match the CN/SAN in the certificate)-p 8883- port 8883 (TLS)--cafile- use this Root CA to verify the broker’s certificate-t iot/test- subscribe to this topic
This terminal will just sit and wait. That is correct — it is listening for messages.
Open a third terminal and publish a test message:
mosquitto_pub -h iotsec.local -p 8883 --cafile /home/iotsec/my-ca/rootCA.crt -t iot/test -m "hello from terminal"
You should see hello from terminal appear in the subscriber terminal.
If this works your broker is good. Move to the ESP32.
Common Errors During Broker Setup
Here are the errors I hit and what they mean. You will probably hit some of these too.
Permission denied on cert files
Error: Unable to load CA certificates. Check cafile "/home/iotsec/my-ca/rootCA.crt".
OpenSSL Error: error:8000000D:system library::Permission denied
Mosquitto cannot read your cert files. Go back to Step 3 and fix permissions. Also make sure the my-ca directory itself has 755 permissions.
tlsv1 alert internal error with mosquitto_sub
OpenSSL Error[0]: error:0A000438:SSL routines::tlsv1 alert internal error
Two possible causes. First you had tls_version tlsv1.2 in your config but your system negotiated TLS 1.3. Remove the tls_version line and let Mosquitto auto-negotiate. Second you are connecting using localhost or 127.0.0.1 instead of iotsec.local. The hostname must match the CN in the certificate.
Protocol error with mosquitto_sub
Usually means allow_anonymous true is missing from the config. Mosquitto 2.x will reject connections without this line.
Step 8 - The ESP32 Sketch
Open Arduino IDE. Install the PubSubClient library by Nick O’Leary from the Library Manager. The WiFiClientSecure library is already included with the ESP32 board package.
Here is the full sketch. I will explain each section after:
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
// WiFi credentials
const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
// Broker settings — use your actual Kali IP
const char* mqtt_server = "192.168.28.63";
const int mqtt_port = 8883;
// Root CA certificate — paste your rootCA.crt content here
const char* rootCA = "-----BEGIN CERTIFICATE-----\n"
"MIIEEzCCAvugAwIBAgIUHeY75SL6zKTMOEiLi/iMihtfar4wDQYJKoZIhvcNAQEL\n"
// ... your full certificate lines here ...
"-----END CERTIFICATE-----\n";
WiFiClientSecure espClient;
PubSubClient client(espClient);
void connectWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
Serial.print("ESP IP: ");
Serial.println(WiFi.localIP());
}
void reconnectMQTT() {
while (!client.connected()) {
Serial.print("Connecting to MQTT broker...");
if (client.connect("ESP32Client")) {
Serial.println("connected");
client.publish("iot/test", "ESP32 TLS Connected");
} else {
Serial.print("Failed, rc=");
Serial.print(client.state());
Serial.println(" retrying in 5 seconds");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
connectWiFi();
espClient.setCACert(rootCA); // Load Root CA for broker verification
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) {
reconnectMQTT();
}
client.loop();
static long lastMsg = 0;
if (millis() - lastMsg > 5000) {
lastMsg = millis();
client.publish("iot/test", "Hello from ESP32 TLS");
}
}
Section by section explanation:
#include <WiFiClientSecure.h> - this is what gives the ESP32 TLS capability. Without this, you can only do plain unencrypted connections.
#include <PubSubClient.h> - the MQTT library. Handles connecting to the broker, publishing messages, subscribing to topics.
const char* rootCA - this is your rootCA.crt file content pasted as a string. The ESP32 has no filesystem access in this sketch, so the certificate must be hardcoded. The \n at the end of every line is important it represents the line breaks in the original PEM file.
WiFiClientSecure espClient - a TLS-capable network client. Think of this as the secure channel.
PubSubClient client(espClient) - the MQTT client, built on top of the secure channel.
espClient.setCACert(rootCA) - this is the key line. You are telling the ESP32: “use this Root CA to verify whoever you connect to”. If the broker’s certificate was not signed by this CA, the connection will be rejected.
client.setServer(mqtt_server, mqtt_port) - point the MQTT client at your Kali IP on port 8883.
client.connect("ESP32Client") - connect to the broker with the client ID “ESP32Client”.
client.publish("iot/test", "Hello from ESP32 TLS") - publish a message to the topic iot/test every 5 seconds.
How to Get Your rootCA.crt Into the Sketch
Run this on Kali to get the cert formatted for the sketch:
awk 'NF {sub(/\r/, ""); printf "%s\\n", $0;}' /home/iotsec/my-ca/rootCA.crt
This outputs the certificate as a single line with \n markers. Copy the entire output.
In your sketch, format it like this:
const char* rootCA = "-----BEGIN CERTIFICATE-----\n"
"LINE2HERE\n"
"LINE3HERE\n"
// ... all lines ...
"-----END CERTIFICATE-----\n";
Each line of the certificate becomes a separate string ending with \n". C++ automatically concatenates adjacent string literals so this is one big string at compile time.
Common Errors During ESP32 Connection
rc=-2 Failed to connect
Connecting to MQTT broker...Failed, rc=-2
The ESP32 cannot reach the broker at all. Check:
- Is Mosquitto actually running? Look at the broker terminal.
- Did your Kali IP change? VirtualBox DHCP can reassign IPs. Run
ip routeon Kali and check. - Is the IP in your sketch correct?
ssl/tls alert bad certificate
OpenSSL Error[0]: error:0A000412:SSL routines::ssl/tls alert bad certificate
The ESP32 is rejecting the broker’s certificate. Most common cause — the server.crt does not have a SAN with your Kali IP. The ESP32 connects using IP, finds no matching SAN, rejects the cert. Go back to Step 4 and regenerate server.crt with the SAN.
The Insecure Shortcut (And Why You Must Not Use It)
During debugging I tried this:
espClient.setInsecure(); // NEVER use this in production
This tells the ESP32 to skip certificate verification completely. It connected immediately. Problem solved right?
No. This is dangerous and defeats the entire purpose of TLS.
When you skip certificate verification, any attacker on the same WiFi network can set up their own fake broker, point your ESP32 at it, and your device will connect without questioning. The attacker receives all your data and can send fake commands to your device.
This is called a Man in the Middle attack we cover it in Part 8.
setCACert(rootCA) is what prevents this. The fake broker cannot produce a certificate signed by your Root CA. So the ESP32 rejects it. setInsecure() throws that protection away completely.
Use setInsecure() only for temporary debugging to isolate whether the issue is network connectivity or certificate verification. Remove it before anything goes to production.
What Just Happened - The TLS Handshake in Plain Language
When the ESP32 connected successfully, here is what happened behind the scenes:
This is exactly the same thing openssl verify did in Part 3 it is just happening in code now, on a microcontroller, in real time.
What You Should See
Serial Monitor:
Connecting to WiFi.....
WiFi connected
ESP IP: 192.168.28.64
Connecting to MQTT broker...connected
Mosquitto broker terminal:
New connection from 192.168.28.64:64177 on port 8883.
New client connected from 192.168.28.64:64177 as ESP32Client
Sending CONNACK to ESP32Client (0, 0)
Received PUBLISH from ESP32Client (d0, q0, r0, m0, 'iot/test', ... (20 bytes))
mosquitto_sub terminal:
Hello from ESP32 TLS
Hello from ESP32 TLS
Hello from ESP32 TLS
If you see all three of these you have done it. Your ESP32 is publishing encrypted, authenticated MQTT messages to a TLS broker running on your own CA infrastructure that you built from scratch in Part 3.
Quick Knowledge Check
Before you move on, answer these three questions. They will come up again in later parts:
1. The ESP32 has rootCA.crt hardcoded. Why does it NOT need server.crt or server.key?
Because the broker sends server.crt during the handshake the ESP32 does not need to store it. And server.key is the broker’s private key it never leaves the broker. The ESP32 only needs rootCA.crt to verify that whatever certificate the broker sends was signed by a trusted authority.
2. What is the difference between port 1883 and port 8883?
1883 is plain MQTT everything in plaintext, no encryption, no authentication. 8883 is MQTT over TLS encrypted channel, broker identity verified.
3. If an attacker intercepts the traffic on port 8883 what do they see?
Encrypted data. They can see that a connection is happening between two IPs on port 8883, but the actual message content is unreadable without the session keys.
What is NOT Covered Here (Coming in Later Parts)
- Part 5 - Deep dive into Mosquitto TLS setup on Kali, all the errors, all the fixes, permissions explained in full detail
- Part 6 - Wireshark capture of the actual TLS handshake so you can see what happens on the wire
- Part 7 - mTLS where the broker also verifies the ESP32’s identity
- Part 8 - MITM attack demonstration against a TLS IoT device
What’s Next
In Part 5 we go deeper into the Mosquitto setup itself all the permission issues, the SAN certificate problem, why 127.0.0.1 does not work but iotsec.local does, and what each error message actually means. This part covered the happy path. Part 5 covers the reality.

