Seguridad MQTT

Certificate pinning en ESP32 y validación de CA para MQTT

Tres formas de validar el certificado de un broker en un ESP32: incrustar una CA raíz, usar el bundle de certificados de ESP-IDF o hacer pinning. Conoce sus ventajas e inconvenientes y obtén código MQTT funcional para cada una.

Ilustración de certificate pinning en ESP32 con un microcontrolador que guarda un certificado raíz fijado mientras se conecta a un broker MQTT por TLS
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

El certificate pinning en ESP32, incrustar una CA raíz y usar el bundle de certificados de ESP-IDF son tres formas distintas de asegurarte de que tu dispositivo habla con el broker MQTT real y no con un impostor. Este tutorial las compara con honestidad, explica cuál sobrevive a las renovaciones de certificados y te da código Arduino y ESP-IDF funcional que se conecta a un broker MQTT en el puerto 8883.

Por qué importa la validación en los microcontroladores

Muchos tutoriales de ESP32 llaman a client.setInsecure() para "hacer que TLS funcione". Eso cifra el tráfico, pero se salta por completo la autenticación del servidor. Un atacante en la red, por ejemplo un punto de acceso falso con el mismo SSID, puede presentar cualquier certificado, recibir el usuario y la contraseña MQTT del dispositivo y retransmitir o alterar cada mensaje. El cifrado sin autenticación solo te protege de quienes escuchan de forma pasiva.

A diferencia de un portátil, un ESP32 no tiene almacén de CA del sistema operativo. Tienes que decidir en qué confía el dispositivo y ponerlo en el firmware. Esa decisión también determina cómo se comportará el dispositivo años después, cuando cambien los certificados del broker, y por eso merece más reflexión que copiar el primer fragmento que encuentres.

Comparativa de tres estrategias de validación

Estrategia En qué confía el dispositivo Sobrevive a la renovación del certificado hoja Sobrevive a un cambio de CA Coste en flash Esfuerzo
CA raíz incrustada Uno o dos certificados raíz Sí Solo si la raíz no cambia Pequeño (unos 2 KB por PEM) Bajo
Bundle de certificados de ESP-IDF Muchas raíces públicas habituales Sí Normalmente sí Mayor (decenas de KB) Bajo
Pinning del certificado hoja o de la clave Un certificado o clave de servidor concretos A menudo no No Mínimo Alto (exige disciplina con OTA)

CA raíz incrustada (opción por defecto recomendada)

Incrustas el certificado raíz que ancla la cadena del broker. Se acepta cualquier certificado válido emitido bajo esa raíz para el hostname correcto, así que las renovaciones rutinarias son invisibles para el dispositivo. Es la opción adecuada para la mayoría de los proyectos. En sentido estricto también es una forma de pinning: fijas la confianza en una sola CA en lugar de en los cientos en los que confía un navegador.

Bundle de certificados de ESP-IDF

ESP-IDF incluye un bundle de certificados raíz habituales (derivado de la lista de Mozilla) que puede compilarse en el firmware. El dispositivo confía en las mismas CA públicas que un navegador. Renuncias a algo de rigor, pero si el broker se pasa alguna vez a otra CA pública, los dispositivos siguen funcionando.

Pinning del certificado hoja o de la clave pública

Fijas en el código la huella del certificado exacto o de la clave pública del servidor. Ofrece la confianza más estrecha, pero se rompe cada vez que se sustituye el certificado del servidor. Con los certificados de Let's Encrypt, que son de vida corta y se renuevan automáticamente, el certificado hoja se sustituye cada par de meses o antes, y la clave también puede cambiar. Fijar el certificado hoja de un broker así es una forma segura de dejar una flota offline. Haz pinning de claves hoja solo cuando controles el servidor, mantengas la clave estable de forma deliberada y puedas distribuir actualizaciones OTA antes de cualquier cambio.

Certificate pinning en ESP32 sobre una CA raíz con Arduino

El broker de ConnectMQ presenta un certificado de Let's Encrypt. Las cadenas actuales de Let's Encrypt llevan a ISRG Root X1 (RSA), y también opera ISRG Root X2 (ECDSA). Incrustar ambas te da un conjunto de confianza estrecho con margen para un futuro cambio de cadena. Descarga los archivos PEM desde la página de certificados de Let's Encrypt y pega su contenido completo, uno detrás de otro, en la cadena de abajo. mbedTLS analiza varios certificados PEM concatenados desde un mismo buffer.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>

const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-password";

const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 8883;
const char* CLIENT_ID = "esp32-pin-5b21";   // usa YOUR_CLIENT_ID en un broker privado
const char* TOPIC = "connectmq/demo/5b21/status";

// Anclas de confianza fijadas: pega los PEM completos de ISRG Root X1 e ISRG Root X2
static const char PINNED_ROOTS[] = R"PEM(
-----BEGIN CERTIFICATE-----
...ISRG Root X1...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...ISRG Root X2...
-----END CERTIFICATE-----
)PEM";

WiFiClientSecure tlsClient;
PubSubClient mqtt(tlsClient);

void syncClock() {
  configTime(0, 0, "pool.ntp.org", "time.google.com");
  Serial.print("Waiting for NTP");
  while (time(nullptr) < 1700000000) {
    delay(300);
    Serial.print(".");
  }
  Serial.println(" done");
}

void connectMqtt() {
  while (!mqtt.connected()) {
    Serial.print("Connecting to MQTT over TLS... ");
    // Broker privado: mqtt.connect(CLIENT_ID, "YOUR_USERNAME", "YOUR_PASSWORD")
    if (mqtt.connect(CLIENT_ID)) {
      Serial.println("connected");
      mqtt.publish(TOPIC, "online", true);
    } else {
      char err[100];
      tlsClient.lastError(err, sizeof(err));
      Serial.printf("failed, state=%d, tls=%s\n", mqtt.state(), err);
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);
  Serial.println("Wi-Fi connected");

  syncClock();
  tlsClient.setCACert(PINNED_ROOTS);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  static unsigned long last = 0;
  if (millis() - last > 10000) {
    last = millis();
    char msg[48];
    snprintf(msg, sizeof(msg), "{\"uptime\":%lu}", millis() / 1000);
    mqtt.publish(TOPIC, msg);
  }
}

Merece la pena mantener la llamada a lastError() en las compilaciones de producción: convierte un vago state=-2 en un mensaje legible de mbedTLS, como un certificado caducado o una CA desconocida. La verificación del hostname se realiza automáticamente porque WiFiClientSecure recibe el hostname a través de setServer(), así que nunca sustituyas el hostname por una dirección IP.

Uso del bundle de certificados de ESP-IDF

Si compilas directamente con ESP-IDF, el componente esp-mqtt puede validar contra el bundle integrado. Actívalo en menuconfig en Component config, mbedTLS, Certificate Bundle (la opción por defecto de CA habituales sirve) y después:

#include "esp_crt_bundle.h"
#include "esp_log.h"
#include "mqtt_client.h"

static const char *TAG = "mqtt_bundle";

static void mqtt_event_handler(void *args, esp_event_base_t base,
                               int32_t event_id, void *event_data)
{
    esp_mqtt_event_handle_t event = event_data;
    if (event_id == MQTT_EVENT_CONNECTED) {
        ESP_LOGI(TAG, "Connected over TLS");
        esp_mqtt_client_publish(event->client,
                                "connectmq/demo/5b21/idf", "hello", 0, 1, 0);
    } else if (event_id == MQTT_EVENT_ERROR) {
        ESP_LOGE(TAG, "MQTT error, check TLS and credentials");
    }
}

void mqtt_start(void)
{
    const esp_mqtt_client_config_t cfg = {
        .broker.address.uri = "mqtts://broker.connectmq.com:8883",
        .broker.verification.crt_bundle_attach = esp_crt_bundle_attach,
        .credentials.client_id = "esp32-idf-5b21",
        // .credentials.username = "YOUR_USERNAME",
        // .credentials.authentication.password = "YOUR_PASSWORD",
    };
    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&cfg);
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID,
                                   mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
}

Llama a mqtt_start() después de que el Wi-Fi esté conectado y SNTP haya fijado la hora. Para fijar en su lugar una sola raíz en ESP-IDF, asigna tu cadena PEM incrustada a .broker.verification.certificate y deja sin definir crt_bundle_attach. La documentación de ESP-MQTT de Espressif enumera todas las opciones de verificación.

Cómo evitar inutilizar la flota

Elijas la estrategia que elijas, diseña pensando en el día en que cambien las anclas de confianza:

  1. Fija raíces, no certificados hoja, salvo que gestiones tú el ciclo de vida de la clave del servidor.
  2. Incrusta un ancla de respaldo, como una segunda raíz de la misma CA, para que un cambio de cadena no rompa la conectividad.
  3. Mantén las actualizaciones OTA independientes de la confianza MQTT, o al menos comprueba que la vía OTA sigue funcionando cuando falla TLS en MQTT. Nuestra guía de actualización OTA del ESP32 por MQTT muestra un enfoque.
  4. Sincroniza la hora antes de conectar. Según la configuración del core, mbedTLS comprueba las fechas de validez de los certificados, y un reloj atascado en 1970 hace que todos los certificados parezcan todavía no válidos.
  5. Registra los errores TLS en remoto cuando la conexión acabe teniendo éxito, para detectar pronto los problemas de validación en campo.
  6. Sigue los anuncios de las CA. Las CA públicas anuncian con mucha antelación los cambios de raíces e intermedios; pon un recordatorio en tu calendario de mantenimiento.

Solución de problemas

  • state=-2 con "X509 - Certificate verification failed": PEM incorrecto o truncado, o la raíz incrustada no ancla la cadena del broker. Compruébalo con openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts.
  • "The certificate validity starts in the future": NTP no ha terminado.
  • Memoria insuficiente durante el handshake: TLS necesita del orden de decenas de kilobytes de heap; libera buffers, reduce otras reservas de memoria y evita abrir varios sockets TLS a la vez.
  • Funciona en el 1883 y falla en el 8883: puede que un firewall bloquee el 8883. Prueba WebSocket seguro en el 8084 si tu librería lo admite.

Para más síntomas, consulta cómo solucionar fallos del handshake TLS en MQTT y nuestro tutorial de MQTT sobre SSL con ESP32.

Cómo elegir una estrategia para tu proyecto

Para un proyecto aficionado o una flota pequeña que puedes reprogramar a mano, la CA raíz incrustada es la clara ganadora: unas pocas líneas de código, poco coste en flash y ninguna sorpresa al renovar el certificado. Para un producto comercial que pasará años en campo, piensa primero en el escenario de cambio de CA. Incrusta dos raíces de la CA del broker o usa el bundle de ESP-IDF para que los dispositivos sigan confiando en cualquier CA pública habitual, y asegúrate de que la vía OTA sigue funcionando si alguna vez falla TLS en MQTT. El pinning del certificado hoja solo tiene cabida en sistemas en los que operas el servidor, mantienes su clave estable de forma deliberada y tienes un proceso probado para desplegar un nuevo pin antes de que cambie la clave. Elijas lo que elijas, anota la decisión junto a la versión del firmware para que quien mantenga la flota más adelante sepa qué anclas de confianza hay desplegadas y dónde.

Por qué un broker MQTT gestionado simplifica el pinning

El pinning solo es tan estable como la gestión de certificados del servidor. Con un broker autoalojado, tú eres responsable de las renovaciones, la elección de cadenas y los cambios de clave, y cualquier error se traduce en dispositivos offline. Un broker MQTT gestionado como ConnectMQ renueva automáticamente su certificado de Let's Encrypt de confianza pública, así que los dispositivos que fijan las raíces de Let's Encrypt siguen conectándose tras las renovaciones rutinarias. Puedes hacer un prototipo con el código anterior en el servidor MQTT público gratuito, pensado solo para pruebas y prototipos y no para datos de producción, y después pasar a credenciales privadas sin tocar el código TLS. Para el flujo completo en el dispositivo, empieza por nuestro tutorial de IoT con ESP32.

Empieza con ConnectMQ

Regístrate en app.connectmq.com, elige el plan gratuito Serverless sin tarjeta de crédito y crea un broker. Copia el usuario, la contraseña y el client ID generados en el sketch anterior, mantén las raíces fijadas y tu ESP32 se conectará por el puerto 8883 con validación completa de certificados.

Preguntas frecuentes

¿Es aceptable setInsecure() para hacer pruebas?

Solo en el banco de pruebas y con credenciales desechables. Desactiva la autenticación del servidor, así que nunca distribuyas firmware que lo use.

¿Debo fijar la huella del certificado de broker.connectmq.com?

No. El certificado del servidor se renueva automáticamente y su huella cambia. Fija en su lugar los certificados raíz de Let's Encrypt.

¿Cuánta flash ocupa el bundle de certificados de ESP-IDF?

Depende de la lista de CA que selecciones en menuconfig; la lista completa de CA habituales ocupa decenas de kilobytes, mientras que un único PEM raíz incrustado ocupa unos dos kilobytes.

¿El certificate pinning sustituye al usuario y la contraseña?

No. El pinning autentica al broker ante el dispositivo. El dispositivo sigue necesitando sus propias credenciales para que el broker lo autentique.

Artículos relacionados

Cómo rotar credenciales MQTT sin tiempo de inactividad

Una estrategia de doble credencial paso a paso para rotar usuarios y contraseñas MQTT en toda una flota: prepara la nueva credencial, cambia con respaldo, verifica y solo entonces revoca la antigua.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.