iiOTESP32 Brasil
Modo de contingencia: las páginas públicas y las descargas siguen disponibles; el inicio de sesión, la comunidad y los datos del servidor pueden no estar disponibles.
Volver al catálogo de tutoriales
Ilustração: placa ESP32 enviando dados por conexões cifradas com cadeados até uma nuvem MQTT e um servidor Mosquitto, com um escudo de segurança e um certificado digital
Imagem ilustrativa gerada por IA
AvanzadoMQTTLectura: 12 min

MQTT con TLS en el ESP32: valida el certificado del broker y deja de usar setInsecure

El ejemplo con setInsecure() cifra, pero acepta cualquier servidor. CA propia, Mosquitto en el puerto 8883, hora por NTP, heap y las pruebas negativas que demuestran la validación.

Tecnologías:#ESP32#MQTT#TLS#Mosquitto#WiFiClientSecure#OpenSSL

Requisitos de hardware:

  • Placa ESP32 ou ESP32-S3
  • Rede Wi-Fi de 2,4 GHz com acesso a um servidor NTP
  • Um broker Eclipse Mosquitto 2.x e o OpenSSL no computador que o administra

El cifrado que no protege de nada

Busca MQTT con TLS en el ESP32 y el primer ejemplo que aparece termina igual: el código crea un WiFiClientSecure, llama a setInsecure() y la conexión por el puerto 8883 pasa. El monitor serie dice "conectado", el tráfico sale cifrado y parece resuelto.

No lo está. setInsecure() activa el cifrado pero desactiva la verificación de quién está al otro lado. El ESP32 acepta el certificado de cualquier servidor que responda en esa dirección. Un router adulterado, un punto de acceso falso o un ordenador en medio de la red presenta su propio certificado, tu dispositivo lo acepta, y el usuario y la contraseña de MQTT llegan a quien montó la trampa. Cifrar sin validar es cerrar una puerta de cristal y dejar la llave colgada en ella.

TLS entrega tres cosas: confidencialidad (nadie lee), integridad (nadie altera por el camino) y autenticación del servidor (hablas de verdad con tu broker). La tercera es la que setInsecure() descarta, y es justamente la que impide el ataque de intermediario. Este artículo muestra cómo conseguir las tres, con un certificado propio y sin atajos.

Qué cambia respecto al puerto 1883

En el puerto 1883, el paquete CONNECT de MQTT lleva el usuario y la contraseña sin ninguna protección. Cualquier equipo en el camino entre el ESP32 y el broker lee ambos, y también todo el payload. En un banco de pruebas aislado eso pasa. En una red de fábrica compartida, en un Wi-Fi de visitas o en internet, no.

El puerto 8883 es MQTT sobre TLS. El ESP32 hace primero el handshake TLS, valida el certificado del broker contra una autoridad certificadora (CA) que ya conoce y solo entonces envía el CONNECT, ya dentro del canal cifrado.

Las cuatro trampas que hunden el primer intento

1. El reloj. Un certificado tiene fecha de inicio y de fin, y el ESP32 arranca creyendo que estamos en 1970. Con esa fecha, todo certificado es "todavía no válido" y el handshake falla. La salida es sincronizar la hora por NTP antes de intentar TLS. Sin red hacia el NTP no hay conexión segura, y el firmware debe aceptarlo en lugar de insistir.

2. El nombre del broker. El certificado se emite para un nombre, y la dirección que usa el firmware al conectar debe coincidir con él. Conectar por IP a un certificado emitido para un nombre falla en la validación. La forma segura es usar un nombre DNS (o una entrada en el DNS de tu router) y ponerlo en el certificado. También existe el certificado con la IP en los nombres alternativos, pero el soporte depende de la versión de mbedTLS de tu núcleo, así que pruébalo antes de depender de ello.

3. La memoria. El handshake TLS reserva buffers grandes, del orden de decenas de KB de heap. En un proyecto que ya carga servidor web, JSON y bibliotecas, la conexión puede fallar por falta de memoria, y el error no lo dice con claridad. Mide ESP.getFreeHeap() antes y después de conectar, y deja margen.

4. Qué grabar en el firmware. Graba la CA que firmó el certificado del broker, y no el certificado del propio broker. El certificado del servidor caduca y se cambia; la CA dura años. Si la CA es privada, tú decides su validez y debes planificar el cambio antes de que caduque, porque viaja dentro del firmware. El camino para cambiarla sin ir hasta el equipo está en el artículo de actualización OTA.

Paso 1: crear tu CA y el certificado del broker

Para una red interna, una CA privada resuelve y no cuesta nada. En el ordenador desde el que administras el broker, con OpenSSL instalado:

code.cpp
cpp
1openssl genrsa -out ca.key 4096
2openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -subj "/CN=IIoT Lab CA" -out ca.crt
3openssl genrsa -out broker.key 2048
4openssl req -new -key broker.key -subj "/CN=broker.lab.local" -out broker.csr
5printf "subjectAltName=DNS:broker.lab.local" > san.ext
6openssl x509 -req -in broker.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out broker.crt -days 825 -sha256 -extfile san.ext

Cambia broker.lab.local por el nombre que usará tu ESP32. Ahora tienes una pila de archivos nuevos. Dos importan fuera de aquí: ca.crt, que es público y va al firmware, y ca.key, que firma certificados y no puede quedarse en el broker ni en un repositorio. Guárdalo sin conexión, en una copia de seguridad a la que solo tú accedas.

Paso 2: configurar Mosquitto

Copia ca.crt, broker.crt y broker.key al servidor (por ejemplo en /etc/mosquitto/certs), con la clave legible solo por el usuario de Mosquitto. En el archivo de configuración:

code.cpp
cpp
1listener 8883
2cafile /etc/mosquitto/certs/ca.crt
3certfile /etc/mosquitto/certs/broker.crt
4keyfile /etc/mosquitto/certs/broker.key
5allow_anonymous false
6password_file /etc/mosquitto/passwd
7listener 1883 127.0.0.1

La última línea mantiene el puerto sin TLS, pero solo para quien está en el propio servidor. Reinicia el servicio y demuestra que el certificado es correcto antes de tocar el ESP32:

code.cpp
cpp
1openssl s_client -connect broker.lab.local:8883 -CAfile ca.crt

Al final de la salida debe aparecer Verify return code: 0 (ok). Si aparece otro código, el problema es del certificado o del nombre, y el ESP32 solo repetirá el mismo error con menos información. Una segunda prueba confirma la autenticación:

code.cpp
cpp
1mosquitto_pub -h broker.lab.local -p 8883 --cafile ca.crt -u esp32-lab -P SENHA_MQTT -t lab/teste -m ok

Si no hay mensaje de error, es señal de éxito.

Paso 3: el firmware

El patrón es el mismo del artículo de reconexión automática de MQTT: una función de guarda llamada en cada vuelta de loop(), sin bucle bloqueante. La diferencia es una guarda más, la de la hora válida.

code.cpp
cpp
1#include <WiFi.h>
2#include <WiFiClientSecure.h>
3#include <PubSubClient.h>
4#include <time.h>
5const char* WIFI_SSID = "REDE_WIFI";
6const char* WIFI_PASSWORD = "SENHA_WIFI";
7const char* MQTT_HOST = "broker.lab.local";
8const uint16_t MQTT_PORT = 8883;
9const char* MQTT_USER = "esp32-lab";
10const char* MQTT_PASSWORD = "SENHA_MQTT";
11const time_t MIN_VALID_EPOCH = 1700000000;
12const char CA_CERT[] = R"EOF(
13-----BEGIN CERTIFICATE-----
14PEGA AQUI EL CONTENIDO DEL ARCHIVO ca.crt
15-----END CERTIFICATE-----
16)EOF";
17WiFiClientSecure tlsClient;
18PubSubClient mqtt(tlsClient);
19uint32_t lastAttemptMs = 0;

MIN_VALID_EPOCH es un instante cualquiera posterior a la fecha en que escribiste el firmware. Cualquier hora menor que esa significa que el NTP aún no ha respondido. Ahora la función de conexión:

code.cpp
cpp
1void connectMqtt() {
2 if (mqtt.connected() || WiFi.status() != WL_CONNECTED) return;
3 if (time(nullptr) < MIN_VALID_EPOCH) return;
4 if (millis() - lastAttemptMs < 5000) return;
5 lastAttemptMs = millis();
6 char clientId[24];
7 snprintf(clientId, sizeof(clientId), "esp32-%06X", (uint32_t)(ESP.getEfuseMac() >> 24));
8 if (mqtt.connect(clientId, MQTT_USER, MQTT_PASSWORD, "lab/esp32/status", 1, true, "offline")) {
9 mqtt.publish("lab/esp32/status", "online", true);
10 } else {
11 char tlsErr[100] = "";
12 tlsClient.lastError(tlsErr, sizeof(tlsErr));
13 Serial.printf("MQTT rc=%d tls=[%s] heap=%u\n", mqtt.state(), tlsErr, ESP.getFreeHeap());
14 }
15}

El registro de fallos es la parte que ahorra horas. El rc=-2 de PubSubClient solo dice que la conexión de red falló; el lastError() del cliente seguro añade el motivo de mbedTLS (verificación del certificado, falta de memoria, tiempo agotado), y el heap restante muestra si la causa fue la memoria. El resto del programa:

code.cpp
cpp
1void setup() {
2 Serial.begin(115200);
3 tlsClient.setCACert(CA_CERT);
4 mqtt.setServer(MQTT_HOST, MQTT_PORT);
5 WiFi.mode(WIFI_STA);
6 WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
7 configTime(0, 0, "pool.ntp.org", "time.google.com");
8}
9void loop() {
10 connectMqtt();
11 mqtt.loop();
12}

Una salvedad honesta: el handshake TLS es bloqueante y puede tardar desde cientos de milisegundos hasta unos segundos, según la señal y el broker. Es un bloqueo corto y acotado, muy distinto del bucle infinito, pero si tu proyecto tiene una tarea crítica en tiempo, ejecuta MQTT en una tarea separada de FreeRTOS. Y con el watchdog activo, comprueba que tolera ese tiempo.

Paso 4: qué debes ver

En el monitor serie, con la hora sincronizada, la conexión ocurre sola y el tema lab/esp32/status pasa a valer "online". Para verlo, en otra terminal:

code.cpp
cpp
1mosquitto_sub -h broker.lab.local -p 8883 --cafile ca.crt -u esp32-lab -P SENHA_MQTT -t "lab/esp32/#" -v

Después haz la prueba que importa, la negativa. Si la validación real está activa, tiene que rechazar lo que no es tuyo:

  • Cambia el nombre del broker en el firmware por una dirección IP que no esté en el certificado: la conexión debe fallar con un error de verificación.
  • Altera una letra del contenido de ca.crt en el firmware: la conexión debe fallar.
  • Bloquea el NTP (puerto 123 en el router) y reinicia: el firmware debe quedarse esperando sin abrir conexión, y nunca conectar con la hora de 1970.

Si alguna de estas tres conecta, la validación no está activa. Eso vale más que ver aparecer el "online".

Cuando algo no funciona

rc=-2 y un error de verificación X.509 en lastError(). El certificado fue rechazado: la CA grabada no es la que firmó el del broker, el nombre no coincide o el certificado caducó. Repite el openssl s_client del paso 2.

rc=-2 sin texto en lastError() y heap bajo. Falta memoria para el handshake. Quita bibliotecas, reduce buffers o usa un módulo con más RAM.

Ningún intento aparece en el monitor serie. La guarda de la hora está bloqueando. Comprueba el acceso al NTP, o apunta a un servidor NTP de tu red.

Funcionó en el banco y falló en el cliente. El nombre DNS del broker no se resuelve en su red, o el puerto 8883 está bloqueado. El cortafuegos es el sospechoso más común.

Funcionó meses y se detuvo. Revisa la validez del certificado del broker y de la CA. El certificado del broker caduca antes y puede renovarse en el servidor sin tocar los dispositivos, siempre que la CA sea la misma.

Qué no cubre este artículo

  • Certificado de cliente (TLS mutuo). Da a cada ESP32 una identidad propia en lugar de una contraseña compartida. En Mosquitto son las opciones require_certificate y use_identity_as_username; en el ESP32, setCertificate() y setPrivateKey(). Aumenta el trabajo de emitir y revocar, y compensa cuando hay muchos dispositivos.
  • Permisos por tema. TLS prueba quién conecta, pero no limita lo que cada uno publica. Eso es la ACL del broker.
  • Protección de la clave en el dispositivo. La CA es pública, pero la contraseña de MQTT grabada en el firmware no. El cifrado de la flash y el arranque seguro del ESP32 entran aquí, y merecen un artículo propio.

Nota de honestidad

El código de este artículo sigue la API de WiFiClientSecure y PubSubClient del núcleo Arduino-ESP32, y los comandos de OpenSSL y Mosquitto son los estándar. Aun así, no fue compilado ni probado en una placa en este momento. Antes de llevarlo a campo, compílalo en tu entorno, ejecuta las tres pruebas negativas y comprueba el heap con tu aplicación completa.

Para publicar datos una vez tengas la conexión segura, el punto de partida es el artículo ESP32 publicando datos en Mosquitto. Si la red cae, el búfer local guarda las lecturas hasta que vuelva la conexión.

Ver más tutoriales técnicosIIoT ESP32 Brasil — Documentación práctica