iiOTESP32 Brasil
Modo de contingência: páginas públicas e downloads seguem disponíveis; login, comunidade e dados do servidor podem ficar indisponíveis.
Voltar para catálogo de tutoriais
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
AvançadoMQTTLeitura: 12 min

MQTT com TLS no ESP32: valide o certificado do broker e pare de usar setInsecure

O exemplo com setInsecure() cifra, mas aceita qualquer servidor. CA própria, Mosquitto na porta 8883, hora por NTP, heap e os testes negativos que provam a validação.

Tecnologias:#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

A criptografia que não protege de nada

Procure por MQTT com TLS no ESP32 e o primeiro exemplo que aparece termina assim: o código cria um WiFiClientSecure, chama setInsecure() e a conexão na porta 8883 passa. O Serial mostra "conectado", o tráfego sai cifrado e parece resolvido.

Não está. O setInsecure() liga a cifra, mas desliga a verificação de quem está do outro lado. O ESP32 aceita o certificado de qualquer servidor que responda naquele endereço. Um roteador adulterado, um ponto de acesso falso ou um computador no meio da rede apresenta um certificado próprio, o seu dispositivo aceita, e usuário e senha do MQTT chegam em texto claro a quem montou a armadilha. Cifrar sem validar é fechar a porta de vidro e deixar a chave pendurada nela.

TLS entrega três coisas: sigilo (ninguém lê), integridade (ninguém altera no caminho) e autenticação do servidor (você fala mesmo com o seu broker). A terceira é a que o setInsecure() descarta, e é justamente a que impede o ataque de homem no meio. Este artigo mostra como fazer as três, com um certificado seu e sem atalho.

O que muda em relação à porta 1883

Na porta 1883, o pacote CONNECT do MQTT leva o usuário e a senha sem nenhuma proteção. Qualquer equipamento no caminho entre o ESP32 e o broker lê os dois, e também todo o payload. Em bancada isolada isso passa. Numa rede de fábrica compartilhada, num Wi-Fi de visitantes ou na internet, não passa.

A porta 8883 é o MQTT sobre TLS. O ESP32 faz primeiro o aperto de mãos TLS, valida o certificado do broker contra uma autoridade certificadora (CA) que ele já conhece e só então envia o CONNECT, já por dentro do canal cifrado.

As quatro armadilhas que derrubam a primeira tentativa

1. O relógio. Um certificado tem data de início e de fim, e o ESP32 liga achando que estamos em 1970. Com essa data, todo certificado é "ainda não válido" e o aperto de mãos falha. A saída é sincronizar a hora por NTP antes de tentar o TLS. Sem rede para o NTP, não há conexão segura, e o firmware precisa aceitar isso em vez de insistir.

2. O nome do broker. O certificado é emitido para um nome, e o endereço que o firmware usa na conexão precisa bater com esse nome. Conectar por IP num certificado emitido para um nome falha na validação. A forma segura é usar um nome DNS (ou uma entrada no DNS do seu roteador) e colocá-lo no certificado. Certificado com o IP no campo de nomes alternativos também existe, mas o suporte depende da versão do mbedTLS que seu núcleo traz, então teste antes de depender disso.

3. A memória. O aperto de mãos TLS aloca buffers grandes, na casa das dezenas de KB de heap. Num projeto que já carrega servidor web, JSON e bibliotecas, a conexão pode falhar por falta de memória, e o erro não diz isso com clareza. Meça ESP.getFreeHeap() antes e depois de conectar, e deixe margem.

4. O que gravar no firmware. Grave a CA que assinou o certificado do broker, e não o certificado do próprio broker. O certificado do servidor vence e é trocado; a CA dura anos. Se a CA for privada, você decide a validade, e precisa planejar a troca antes do vencimento, porque ela viaja dentro do firmware. O caminho para trocar sem ir até o equipamento está no artigo de atualização OTA.

Passo 1: criar a sua CA e o certificado do broker

Para uma rede interna, uma CA privada resolve e não custa nada. No computador onde você administra o broker, com o 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

Troque broker.lab.local pelo nome que o seu ESP32 vai usar. Você deve ter agora uma pilha de arquivos novos. Dois importam fora daqui: ca.crt, que é público e vai para o firmware, e ca.key, que assina certificados e não pode ficar no broker nem em repositório. Guarde-o offline, num lugar de backup que só você acessa.

Passo 2: configurar o Mosquitto

Copie ca.crt, broker.crt e broker.key para o servidor (por exemplo, em /etc/mosquitto/certs), com a chave legível apenas pelo usuário do Mosquitto. No arquivo de configuração:

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

A última linha mantém a porta sem TLS, mas só para quem está no próprio servidor. Reinicie o serviço e prove que o certificado está certo antes de tocar no ESP32:

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

No fim da saída deve aparecer Verify return code: 0 (ok). Se aparecer outro código, o problema é do certificado ou do nome, e o ESP32 só vai repetir o mesmo erro com menos informação. Um segundo teste confirma a autenticação:

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

Sem mensagem de erro é sinal de sucesso.

Passo 3: o firmware

O padrão é o mesmo do artigo de reconexão automática do MQTT: uma função de guarda chamada a cada volta do loop(), sem laço bloqueante. A diferença é uma guarda a mais, a da 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-----
14COLE AQUI O CONTEUDO DO ARQUIVO ca.crt
15-----END CERTIFICATE-----
16)EOF";
17WiFiClientSecure tlsClient;
18PubSubClient mqtt(tlsClient);
19uint32_t lastAttemptMs = 0;

O valor MIN_VALID_EPOCH é um instante qualquer posterior à data em que você escreveu o firmware. Qualquer hora menor que isso significa que o NTP ainda não respondeu. Agora a função de conexão:

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}

O registro de falha é a parte que economiza horas. O código rc=-2 do PubSubClient só diz que a conexão de rede falhou; o lastError() do cliente seguro acrescenta o motivo do mbedTLS (verificação do certificado, falta de memória, tempo esgotado), e o heap restante mostra se a causa foi memória. O restante do 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}

Uma ressalva honesta: o aperto de mãos TLS é bloqueante, e pode levar de centenas de milissegundos a alguns segundos, conforme o sinal e o broker. É um bloqueio curto e limitado, bem diferente do laço infinito, mas se o seu projeto tem uma tarefa crítica de tempo, rode o MQTT numa tarefa separada do FreeRTOS. E com o watchdog ativo, confira que ele tolera esse tempo.

Passo 4: o que você deve ver

No monitor serial, com a hora sincronizada, a conexão acontece sozinha e o tópico lab/esp32/status passa a valer "online". Para assistir, em outro 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

Depois faça o teste que importa, o negativo. Se a validação de verdade está ligada, ela tem que reprovar o que não é seu:

  • Troque o nome do broker no firmware por um endereço IP que não esteja no certificado: a conexão deve falhar com erro de verificação.
  • Altere uma letra do conteúdo de ca.crt no firmware: a conexão deve falhar.
  • Bloqueie o NTP (porta 123 no roteador) e reinicie: o firmware deve ficar tentando sem abrir conexão, e nunca conectar com a hora de 1970.

Se alguma dessas três coisas conectar, a validação não está ativa. Isso vale mais que ver o "online" aparecer.

Quando algo não funciona

rc=-2 e um erro de verificação do X.509 no lastError(). O certificado foi recusado: a CA gravada não é a que assinou o do broker, o nome não bate ou o certificado venceu. Repita o openssl s_client do passo 2.

rc=-2 sem texto no lastError() e o heap baixo. Falta memória para o aperto de mãos. Tire bibliotecas, reduza buffers ou use um módulo com mais RAM.

Nenhuma tentativa aparece no Serial. A guarda da hora está barrando. Confira o acesso ao NTP, ou aponte para um servidor NTP da sua rede.

Funcionou na bancada e falhou no cliente. O nome DNS do broker não resolve na rede dele, ou a porta 8883 está bloqueada. O firewall é o suspeito mais comum.

Passou meses funcionando e parou. Confira a validade do certificado do broker e da CA. O certificado do broker vence antes e pode ser renovado no servidor sem tocar nos dispositivos, desde que a CA seja a mesma.

O que este artigo não cobre

  • Certificado de cliente (TLS mútuo). Dá a cada ESP32 uma identidade própria, em vez de uma senha compartilhada. No Mosquitto são as opções require_certificate e use_identity_as_username; no ESP32, setCertificate() e setPrivateKey(). Aumenta o trabalho de emitir e revogar, e vale o custo quando houver muitos dispositivos.
  • Permissões por tópico. TLS prova quem conecta, mas não limita o que cada um publica. Isso é ACL do broker.
  • Proteção da chave no dispositivo. A CA é pública, mas a senha do MQTT gravada no firmware não é. Criptografia da flash e boot seguro do ESP32 entram nesse ponto, e merecem um artigo próprio.

Nota de honestidade

O código deste artigo segue a API do WiFiClientSecure e do PubSubClient do núcleo Arduino-ESP32, e os comandos do OpenSSL e do Mosquitto são os padrões. Mesmo assim, ele não foi compilado nem testado numa placa neste momento. Antes de levar a campo, compile no seu ambiente, rode os três testes negativos e confira o heap com a sua aplicação completa.

Para publicar dados depois de ter a conexão segura, o ponto de partida é o artigo de ESP32 publicando dados no Mosquitto. Se a rede cair, o buffer local guarda as leituras até a conexão voltar.

Ver mais tutoriais técnicosIIoT ESP32 Brasil — Documentação Prática