ESP32 et Arduino

ESP32 MQTT SSL : sécuriser MQTT en TLS avec WiFiClientSecure

Connectez un ESP32 à un broker MQTT en TLS sur le port 8883 avec WiFiClientSecure et PubSubClient, vérifiez le serveur avec le bon certificat racine, synchronisez l'heure et maîtrisez la consommation de heap.

Carte ESP32 avec un cadenas se connectant sur le port 8883 dans un tutoriel ESP32 MQTT SSL
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

ESP32 MQTT SSL est l'étape qui transforme un sketch de bricoleur en un appareil que l'on peut laisser sur un vrai réseau : identifiants et payloads sont chiffrés, et l'ESP32 vérifie qu'il parle bien au broker authentique. Dans ce tutoriel, vous allez connecter un ESP32 à broker.connectmq.com sur le port 8883 avec WiFiClientSecure et PubSubClient, vérifier le serveur avec le bon certificat racine, synchroniser l'horloge via NTP et découvrir ce que le TLS coûte réellement en mémoire. Vous obtiendrez aussi une liste de contrôle pour les erreurs de handshake que presque tout le monde rencontre au moins une fois.

Ce qu'apporte le TLS, et ce qu'il n'apporte pas

Le TLS sur MQTT fait trois choses : il chiffre la connexion pour que personne sur le Wi-Fi ou sur le chemin vers le broker ne puisse lire votre nom d'utilisateur, votre mot de passe ou vos payloads ; il protège les messages contre toute altération ; et, s'il est correctement configuré, il authentifie le serveur, de sorte qu'un point d'accès malveillant ne puisse pas se faire passer pour votre broker.

Ce dernier point est celui que la plupart des tutoriels passent sous silence. Appeler setInsecure() vous donne le chiffrement sans l'authentification. C'est acceptable pour une expérience de cinq minutes, mais c'est une vraie faiblesse en production, car un attaquant capable d'intercepter le DNS ou le réseau peut présenter n'importe quel certificat et récupérer vos identifiants.

Le TLS ne remplace pas l'authentification. Côté broker, chaque appareil doit toujours disposer de son propre nom d'utilisateur, mot de passe et client ID. Pour une vue d'ensemble, consultez le guide des bonnes pratiques de sécurité MQTT.

Port Protocole Usage
1883 MQTT sur TCP Tests sur la zone publique, jamais avec des identifiants
8883 MQTT sur TLS ESP32 et autres appareils en production
8083 MQTT sur WebSocket Tests dans le navigateur
8084 MQTT sur WebSocket sécurisé (/mqtt) Applications navigateur en production

De quel certificat racine l'ESP32 a-t-il besoin ?

broker.connectmq.com utilise un certificat émis par Let's Encrypt, dont la chaîne remonte à la racine ISRG Root X1. Vous pouvez le vérifier vous-même depuis n'importe quel ordinateur avec OpenSSL :

openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null

La sortie affiche le certificat serveur de broker.connectmq.com, le certificat intermédiaire Let's Encrypt qui l'a émis et une chaîne qui se termine par ISRG Root X1, suivie de Verify return code: 0 (ok) lorsque votre système fait confiance à cette racine.

La règle importante : donnez à l'ESP32 la racine, pas le certificat serveur. Les certificats serveur Let's Encrypt ont une courte durée de vie et sont renouvelés automatiquement, et les intermédiaires changent eux aussi avec le temps. Un appareil qui épingle le certificat feuille ou son empreinte cessera de se connecter après le prochain renouvellement. Le certificat ISRG Root X1 est valable jusqu'en 2035 : en l'intégrant, votre firmware survit à tous les renouvellements de routine.

Prérequis

  • Une carte ESP32 avec le paquet de cartes esp32 d'Espressif installé dans Arduino IDE 2.x.
  • PubSubClient de Nick O'Leary, depuis le gestionnaire de bibliothèques.
  • Un broker ConnectMQ : inscrivez-vous, choisissez une offre, créez un broker et copiez depuis le portail le nom d'utilisateur, le mot de passe et le client ID générés. L'offre gratuite Serverless suffit. Le serveur MQTT public gratuit convient aux tests et aux prototypes sans compte, mais pas aux données de production : ce tutoriel utilise un broker privé avec identifiants.

S'il s'agit de votre premier sketch ESP32 MQTT, le tutoriel IoT ESP32 couvre d'abord la configuration de la carte et MQTT en clair ; le TLS est plus facile à déboguer une fois que vous savez que la version sans TLS fonctionne.

Le sketch ESP32 MQTT SSL complet

Le sketch ci-dessous connecte le Wi-Fi, synchronise l'heure, charge le certificat racine, se connecte au port 8883 avec des identifiants, s'abonne à un topic de commande et publie de la télémétrie toutes les dix secondes.

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

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 8883;
const char* MQTT_CLIENT_ID = "YOUR_CLIENT_ID";
const char* MQTT_USERNAME  = "YOUR_USERNAME";
const char* MQTT_PASSWORD  = "YOUR_PASSWORD";

const char* TOPIC_TELEMETRY = "devices/esp32-01/telemetry";
const char* TOPIC_COMMAND   = "devices/esp32-01/cmd";

// ISRG Root X1 (Let's Encrypt), valable jusqu'au 2035-06-04
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure net;
PubSubClient mqtt(net);
unsigned long lastAttempt = 0, lastPublish = 0;

void syncTime() {
  configTime(0, 0, "pool.ntp.org", "time.google.com");
  Serial.print("Waiting for NTP time");
  time_t now = time(nullptr);
  unsigned long start = millis();
  while (now < 1700000000 && millis() - start < 20000) {   // n'importe quelle date après 2023
    delay(250);
    Serial.print(".");
    now = time(nullptr);
  }
  Serial.printf("\nUTC time: %s", ctime(&now));
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  Serial.printf("Command on %s: %.*s\n", topic, (int)length, (const char*)payload);
}

bool connectMqtt() {
  Serial.printf("Free heap before TLS: %u bytes\n", ESP.getFreeHeap());
  if (!mqtt.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
    char err[128];
    int code = net.lastError(err, sizeof(err));
    Serial.printf("MQTT state=%d, TLS error %d: %s\n", mqtt.state(), code, err);
    return false;
  }
  Serial.printf("Connected over TLS. Free heap: %u bytes\n", ESP.getFreeHeap());
  mqtt.subscribe(TOPIC_COMMAND, 1);
  return true;
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
  Serial.printf("\nWi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());

  syncTime();
  net.setCACert(ROOT_CA);
  net.setHandshakeTimeout(15);              // secondes

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setKeepAlive(60);
  mqtt.setSocketTimeout(10);
  mqtt.setBufferSize(512);
}

void loop() {
  if (!mqtt.connected()) {
    if (millis() - lastAttempt > 10000) {
      lastAttempt = millis();
      connectMqtt();
    }
    return;
  }
  mqtt.loop();

  if (millis() - lastPublish > 10000) {
    lastPublish = millis();
    char json[96];
    snprintf(json, sizeof(json), "{\"uptime\":%lu,\"heap\":%u,\"rssi\":%d}",
             millis() / 1000, ESP.getFreeHeap(), WiFi.RSSI());
    mqtt.publish(TOPIC_TELEMETRY, json);
  }
}

Remplacez les trois valeurs d'identifiants par celles du portail. Comme ConnectMQ associe chaque client ID aux identifiants avec lesquels il a été émis, utilisez le client ID généré au lieu d'en inventer un, et ne faites pas tourner deux appareils avec le même jeu d'identifiants. Pour voir les messages arriver, ouvrez l'explorateur de topics en direct du portail ou son client MQTT intégré au navigateur, et abonnez-vous à devices/#.

Pourquoi la synchronisation de l'heure compte pour ESP32 MQTT SSL

Un certificat n'est valide qu'entre ses dates « not before » et « not after ». L'ESP32 ne possède pas d'horloge sauvegardée par pile : après chaque démarrage, il croit que nous sommes le 1er janvier 1970. Selon la configuration de mbedTLS dans votre version du core Arduino, les dates des certificats sont soit comparées à cette horloge, auquel cas chaque certificat paraît « pas encore valide » et le handshake échoue, soit pas vérifiées du tout, auquel cas un certificat expiré serait accepté sans bruit. Aucun de ces deux comportements n'est souhaitable.

Une synchronisation NTP avant la première connexion TLS règle les deux cas. Elle coûte une ou deux secondes au démarrage et nécessite le port UDP 123 en sortie. Sur les réseaux qui bloquent NTP, faites pointer configTime() vers le serveur NTP local autorisé par votre pare-feu. Une heure exacte est aussi utile en soi : vous pouvez ajouter de vrais horodatages à votre télémétrie, comme décrit dans horodatage NTP ESP32 pour les messages MQTT.

Mémoire : ce que coûte le TLS sur un ESP32

Le TLS est l'opération la plus gourmande en mémoire d'un sketch ESP32 typique. Pendant le handshake, mbedTLS alloue des tampons pour les enregistrements entrants et sortants, la chaîne de certificats en cours d'analyse et l'échange de clés. Le sketch affiche le heap libre avant et après la connexion, pour que vous puissiez constater l'effet sur votre propre carte et votre version du core.

Facteur Effet Que faire
Tampons d'enregistrements TLS Des dizaines de kilo-octets conservés pendant toute la durée de la connexion Prévoyez-les dans votre budget ; évitez un second client TLS simultané (par exemple un OTA en HTTPS pendant que MQTT est connecté) sur les cartes limitées
Analyse de la chaîne de certificats Pic temporaire pendant le handshake N'intégrez que la racine nécessaire plutôt qu'un gros bundle quand la RAM est juste
Fragmentation du heap Une longue durée de fonctionnement et de nombreuses allocations String peuvent ne plus laisser aucun grand bloc contigu Préférez des tampons char fixes, reconnectez-vous moins souvent
Cartes avec PSRAM (WROVER, nombreuses S3) Plus de marge Les gros tampons peuvent être placés en PSRAM par l'allocateur du core
Tampon PubSubClient setBufferSize(512) coûte 512 octets Dimensionnez-le selon votre plus gros message, voir taille du tampon PubSubClient

Un signal d'alerte concret : si les connexions commencent à échouer après des heures ou des jours avec des erreurs d'allocation mémoire dans lastError(), et qu'un redémarrage règle le problème, vous avez affaire à de la fragmentation. Journaliser ESP.getMaxAllocHeap() à côté du heap libre vous indique le plus grand bloc encore disponible.

Liste de contrôle pour le dépannage

  1. X509 - Certificate verification failed. Mauvais certificat racine, faute de frappe dans le PEM ou horloge non réglée. Recopiez le PEM, lignes BEGIN et END comprises, et vérifiez l'heure NTP affichée sur le moniteur série.
  2. Le handshake expire. Le réseau bloque le port 8883, ce qui est courant sur les Wi-Fi d'entreprise et invités. Testez avec la commande OpenSSL ci-dessus depuis un portable sur le même réseau.
  3. state=4 ou state=5 après un handshake réussi. Le TLS fonctionne ; ce sont les identifiants ou le client ID qui sont faux. Vérifiez l'absence d'espaces en fin de chaîne copiés depuis le portail.
  4. Échecs d'allocation mémoire dans lastError(). Pas assez de heap contigu. Libérez de la mémoire avant de vous connecter, évitez les sessions TLS simultanées, limitez les allocations de String.
  5. Fonctionne sur 1883 mais pas sur 8883. Vous utilisez encore WiFiClient au lieu de WiFiClientSecure, ou setServer() pointe vers le port 1883.
  6. Se connecte, puis décroche environ toutes les minutes. Il s'agit généralement d'un problème de keep-alive ou de client ID en double plutôt que de TLS ; les schémas de logique de reconnexion MQTT ESP32 vous aideront.

Des échecs d'authentification répétés peuvent déclencher un blocage d'IP sur le broker : corrigez donc les erreurs d'identifiants avant de laisser un appareil dans une boucle de tentatives rapide.

Alternatives à l'intégration d'une seule racine

Intégrer ISRG Root X1 est l'option robuste la plus simple pour un broker unique. Deux alternatives existent. Le core Arduino prend en charge un bundle de CA via setCACertBundle(), qui permet à un même firmware de faire confiance à de nombreuses racines publiques au prix de mémoire flash et d'un peu de configuration ; c'est utile lorsque le même appareil dialogue avec plusieurs services HTTPS et MQTTS. Vous pouvez aussi stocker le PEM dans une partition de système de fichiers pour pouvoir le mettre à jour sans reflasher le code. Évitez l'épinglage par empreinte de certificat pour tout broker dont les certificats sont renouvelés automatiquement.

Notes pour la production

Le chiffrement protège la liaison ; exploiter le broker de façon sûre en est l'autre moitié. Un broker MQTT managé gère pour vous le renouvellement des certificats, l'isolation multi-tenant et la protection contre la force brute, si bien que le firmware n'a besoin que du certificat racine et de ses identifiants. ConnectMQ sert le TLS sur le port 8883 et le WebSocket sécurisé sur le 8084 avec des certificats Let's Encrypt, et les mêmes identifiants fonctionnent depuis votre ESP32, un backend Python ou un tableau de bord dans le navigateur. Si vous hésitez entre l'auto-hébergement et un broker MQTT cloud, la gestion des certificats est l'une des plus grandes différences au quotidien, et le guide sur ce que fait un broker MQTT explique le reste des rouages.

Démarrez avec ConnectMQ

Créez gratuitement un broker Serverless sur app.connectmq.com, sans carte bancaire. Copiez les identifiants générés dans le sketch ci-dessus et votre ESP32 communique en TLS sur le port 8883 en quelques minutes.

Questions fréquentes

setInsecure() est-il acceptable pour ESP32 MQTT en TLS ?

Uniquement pour des tests rapides. Il chiffre le trafic mais saute la vérification du serveur : un attaquant capable de rediriger votre trafic peut donc capturer vos identifiants. Utilisez setCACert() avec le certificat racine du broker pour tout ce que vous déployez.

Quel certificat utiliser pour broker.connectmq.com ?

Utilisez le certificat racine ISRG Root X1, car le certificat du broker est émis par Let's Encrypt. N'intégrez pas le certificat serveur lui-même : il se renouvelle automatiquement et votre appareil cesserait de se connecter.

Faut-il NTP pour le TLS sur l'ESP32 ?

Vous devriez synchroniser l'heure avant de vous connecter. Sans cela, selon la configuration du core, les dates des certificats sont soit comparées à une horloge de 1970 et échouent, soit pas vérifiées du tout.

Combien de RAM le TLS nécessite-t-il sur un ESP32 ?

Comptez des dizaines de kilo-octets de heap tant qu'une connexion TLS est ouverte, avec un pic temporaire plus élevé pendant le handshake. Mesurez-le sur votre carte en affichant ESP.getFreeHeap() avant et après la connexion, comme le fait le sketch.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.