Certificate pinning no ESP32, embutir uma CA raiz e usar o bundle de certificados do ESP-IDF são três formas diferentes de garantir que seu dispositivo converse com o broker MQTT verdadeiro, e não com um impostor. Este tutorial as compara com honestidade, explica qual delas sobrevive às renovações de certificado e oferece código Arduino e ESP-IDF funcional que se conecta a um broker MQTT na porta 8883.
Por que a validação importa em microcontroladores
Muitos tutoriais de ESP32 chamam client.setInsecure() para "fazer o TLS funcionar". Isso criptografa o tráfego, mas pula totalmente a autenticação do servidor. Um atacante na rede, por exemplo um ponto de acesso falso com o mesmo SSID, pode apresentar qualquer certificado, receber o usuário e a senha MQTT do dispositivo e retransmitir ou alterar cada mensagem. Criptografia sem autenticação protege você apenas contra espiões passivos.
Ao contrário de um notebook, o ESP32 não tem um repositório de CAs de sistema operacional. Você precisa decidir em que o dispositivo confia e colocar isso no firmware. Essa decisão também determina como o dispositivo vai se comportar anos depois, quando os certificados do broker mudarem, e é por isso que ela merece mais reflexão do que copiar o primeiro trecho de código que você encontrar.
Três estratégias de validação comparadas
| Estratégia | Em que o dispositivo confia | Sobrevive à renovação do certificado folha | Sobrevive à troca de CA | Custo de flash | Esforço |
|---|---|---|---|---|---|
| CA raiz embutida | Um ou dois certificados raiz | Sim | Só se a raiz continuar a mesma | Pequeno (cerca de 2 KB por PEM) | Baixo |
| Bundle de certificados do ESP-IDF | Muitas raízes públicas comuns | Sim | Geralmente sim | Maior (dezenas de KB) | Baixo |
| Pinning do certificado folha ou da chave | Um certificado ou chave de servidor exato | Muitas vezes não | Não | Mínimo | Alto (exige disciplina de OTA) |
CA raiz embutida (padrão recomendado)
Você embute o certificado raiz que ancora a cadeia do broker. Qualquer certificado válido emitido sob essa raiz para o hostname correto é aceito, então as renovações de rotina passam despercebidas para o dispositivo. Essa é a escolha certa para a maioria dos projetos. A rigor, também é uma forma de pinning: você fixa a confiança em uma única CA, em vez das centenas em que um navegador confia.
Bundle de certificados do ESP-IDF
O ESP-IDF inclui um bundle de certificados raiz comuns (derivado da lista da Mozilla) que pode ser compilado no firmware. O dispositivo confia nas mesmas CAs públicas que um navegador. Você abre mão de parte do rigor, mas, se o broker um dia migrar para outra CA pública, os dispositivos continuam funcionando.
Pinning do certificado folha ou da chave pública
Você fixa no código a impressão digital do certificado exato do servidor ou da sua chave pública. Isso oferece a confiança mais restrita, mas quebra sempre que o certificado do servidor é substituído. Com certificados Let's Encrypt, que têm vida curta e são renovados automaticamente, o certificado folha é substituído a cada dois meses ou antes, e a chave também pode mudar. Fazer pinning do certificado folha de um broker assim é uma forma certeira de derrubar uma frota inteira. Só faça pinning de chaves de certificado folha quando você controlar o servidor, mantiver a chave estável de propósito e puder enviar atualizações OTA antes de qualquer mudança.
Certificate pinning no ESP32 para uma CA raiz com Arduino
O broker da ConnectMQ apresenta um certificado Let's Encrypt. As cadeias atuais da Let's Encrypt levam à ISRG Root X1 (RSA), e ela também opera a ISRG Root X2 (ECDSA). Embutir as duas dá a você um conjunto de confiança restrito, com margem para uma futura mudança de cadeia. Baixe os arquivos PEM na página de certificados da Let's Encrypt e cole o conteúdo completo de cada um, um após o outro, na string abaixo. O mbedTLS interpreta vários certificados PEM concatenados em um único 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"; // use YOUR_CLIENT_ID em um broker privado
const char* TOPIC = "connectmq/demo/5b21/status";
// Âncoras de confiança fixadas: cole os PEMs completos da ISRG Root X1 e da 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);
}
}
Vale a pena manter a chamada lastError() nas builds de produção: ela transforma um vago state=-2 em uma mensagem legível do mbedTLS, como um certificado expirado ou uma CA desconhecida. A verificação do hostname acontece automaticamente porque o WiFiClientSecure recebe o hostname por meio de setServer(), então nunca substitua o hostname por um endereço IP.
Usando o bundle de certificados do ESP-IDF
Se você compila diretamente com o ESP-IDF, o componente esp-mqtt pode validar contra o bundle embutido. Habilite-o no menuconfig em Component config, mbedTLS, Certificate Bundle (a opção padrão de CAs comuns serve), e então:
#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);
}
Chame mqtt_start() depois que o Wi-Fi estiver conectado e o SNTP tiver ajustado o horário. Para fixar uma única raiz no ESP-IDF, defina .broker.verification.certificate com sua string PEM embutida e não defina crt_bundle_attach. A documentação do ESP-MQTT da Espressif lista todas as opções de verificação.
Evitando uma frota inutilizada
Seja qual for a estratégia escolhida, projete para o dia em que as âncoras de confiança mudarem:
- Fixe raízes, não certificados folha, a menos que você controle o ciclo de vida da chave do servidor.
- Embuta uma âncora reserva, como uma segunda raiz da mesma CA, para que uma troca de cadeia não quebre a conectividade.
- Mantenha as atualizações OTA independentes da confiança do MQTT, ou ao menos teste se o caminho de OTA continua funcionando quando o TLS do MQTT falha. Nosso guia de atualização OTA do ESP32 via MQTT mostra uma abordagem.
- Sincronize o horário antes de se conectar. Dependendo da configuração do core, o mbedTLS verifica as datas de validade dos certificados, e um relógio parado em 1970 faz todo certificado parecer ainda não válido.
- Registre erros de TLS remotamente quando uma conexão finalmente tiver sucesso, para perceber cedo problemas de validação em campo.
- Acompanhe os anúncios das CAs. As CAs públicas anunciam mudanças de raízes e intermediárias com bastante antecedência; coloque um lembrete no seu calendário de manutenção.
Solução de problemas
state=-2com "X509 - Certificate verification failed": PEM errado ou truncado, ou a raiz que você embutiu não ancora a cadeia do broker. Verifique comopenssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts.- "The certificate validity starts in the future": o NTP não terminou.
- Falta de memória durante o handshake: o TLS precisa de algumas dezenas de kilobytes de heap; libere buffers, reduza outras alocações e evite abrir vários sockets TLS ao mesmo tempo.
- Funciona na 1883, falha na 8883: um firewall pode estar bloqueando a 8883. Tente WebSocket seguro na 8084 se a sua biblioteca oferecer suporte.
Para mais sintomas, veja como corrigir falhas no handshake TLS do MQTT e nosso tutorial de MQTT sobre SSL no ESP32.
Escolhendo uma estratégia para o seu projeto
Para um projeto hobby ou uma frota pequena que você pode regravar manualmente, a abordagem da CA raiz embutida é a vencedora clara: poucas linhas de código, baixo custo de flash e nenhuma surpresa na renovação de certificados. Para um produto comercial que ficará anos em campo, pense primeiro no cenário de troca de CA. Embuta duas raízes da CA do broker ou use o bundle do ESP-IDF para que os dispositivos continuem confiando em qualquer CA pública popular, e garanta que seu caminho de OTA continue funcionando se o TLS do MQTT falhar algum dia. Pinning de certificado folha só cabe em sistemas em que você opera o servidor, mantém a chave estável de propósito e tem um processo testado para distribuir um novo pin antes que a chave mude. Seja qual for a escolha, registre a decisão junto à versão do firmware, para que quem mantiver a frota depois saiba quais âncoras de confiança estão implantadas em cada lugar.
Por que um broker MQTT gerenciado simplifica o pinning
O pinning só é tão estável quanto a gestão de certificados do servidor. Com um broker auto-hospedado, renovações, escolhas de cadeia e trocas de chave ficam por sua conta, e qualquer erro aparece como dispositivos offline, muitas vezes espalhados por fazendas ou subestações de difícil acesso. Um broker MQTT gerenciado como a ConnectMQ renova automaticamente seu certificado Let's Encrypt publicamente confiável, de modo que dispositivos que fixam as raízes da Let's Encrypt continuam se conectando ao longo das renovações de rotina. Você pode prototipar com o código acima no servidor MQTT público (gratuito), feito para testes e não para dados de produção, e depois mudar para credenciais privadas sem tocar no código TLS. Para o fluxo completo do lado do dispositivo, comece pelo nosso tutorial de IoT com ESP32.
Comece a usar a ConnectMQ
Cadastre-se em app.connectmq.com, escolha o plano Serverless (Free) sem cartão de crédito e crie um broker. Copie o usuário, a senha e o client ID gerados para o sketch acima, mantenha as raízes fixadas, e seu ESP32 vai se conectar na porta 8883 com validação completa de certificado.
Perguntas frequentes
Usar setInsecure() é aceitável para testes?
Só na bancada, com credenciais descartáveis. Ele desativa a autenticação do servidor, então nunca entregue um firmware que o utilize.
Devo fazer pinning da impressão digital do certificado de broker.connectmq.com?
Não. O certificado do servidor é renovado automaticamente e sua impressão digital muda. Faça pinning dos certificados raiz da Let's Encrypt.
Quanto de flash o bundle de certificados do ESP-IDF ocupa?
Depende da lista de CAs selecionada no menuconfig; a lista completa de CAs comuns custa dezenas de kilobytes, enquanto um único PEM de raiz embutido custa cerca de dois kilobytes.
O certificate pinning substitui usuário e senha?
Não. O pinning autentica o broker perante o dispositivo. O dispositivo ainda precisa de suas próprias credenciais para que o broker possa autenticá-lo.



