Погодное API, на котором висело моё табло, однажды ответило кодом 301 — «переехали», и новая прописка была на https://. В уроке про HTTP-запросы мы остановились на том, что ESP32 со своими 520 КБ RAM тянет TLS, а сертификатами «разберёмся отдельно» — обещание пора выполнять. Тогда я поступил как миллионы читателей StackOverflow: вставил волшебную строку client.setInsecure(), запрос пошёл, табло ожило. А месяц спустя я стоял в кафе, где WiFi слушал чужой трафик, и впервые всерьёз задумался, что именно я отключил.
В этом уроке — HTTPS на ESP32 по-взрослому: что происходит при TLS-рукопожатии, куда вставлять CA-сертификат и почему без него плата верит любому, кто представился сервером, сколько стоит криптография в секундах и килобайтах, и когда setInsecure() всё же допустим.
Что TLS добавляет к HTTP
TLS — слой шифрования между HTTP и сетью. Схема такая: клиент соединяется с сервером на порт 443 и просит предъявить сертификат; сервер отдаёт цепочку — свой сертификат, подписанный промежуточным, а тот подписан корневым. У клиента в прошивке лежит корневой сертификат (CA) — эталон, которым он проверяет эту цепочку, как кассир проверяет купюру по образцу. Совпало — стороны договариваются о ключе сессии, и дальше весь обмен шифруется.
Проверка — не одна, а сразу три: цепочка подписей восходит к доверенному корню; сертификат ещё не истёк — даты действия проверяются строго; имя внутри сертификата совпадает с адресом, к которому вы обращались. Провалена хоть одна — WiFiClientSecure рвёт соединение, а GET возвращает -1, тот же код, что и «сервер недоступен». Поэтому поиск причин при https начинается с сертификата, а не с сети.
Корневой сертификат — текстовая PEM-строка: начинается с -----BEGIN CERTIFICATE----- и тянется килобайт-полтора. Берут его у провайдера сервера: в браузере раскрывают замок и смотрят цепочку, либо командой openssl s_client -connect api.example.com:443 -showcerts — и вставляют в скетч самый последний сертификат цепочки, корневой.
Что именно лежит в сертификате, стоит один раз увидеть глазами: домен владельца, срок действия — «с ... по ...», издатель, открытый ключ и подпись издателя. Всё это открытый текст, браузер показывает его без всякой магии, а защищает не сам сертификат, а подпись корня: подделать данные несложно, а вот подпись без приватного ключа издателя не подделаешь. Поэтому кража публичного сертификата сервера мало что даёт злоумышленнику, а вот подмена цепочки на лету сработала бы, если бы клиент не держал свой корень, — ровно то, что делает setInsecure().
#include <WiFi.h>#include <HTTPClient.h>#include <WiFiClientSecure.h> const char* ssid = "DachaNet";const char* password = "superParol123";String url = "https://api.example.com/v1/weather"; // Корневой сертификат CA провайдера (PEM): браузер -> замок -> цепочка,// или openssl s_client -connect api.example.com:443 -showcertsconst char ROOT_CA[] = R"EOF(-----BEGIN CERTIFICATE-----MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw... (тело сертификата, ещё 15-20 строк)T0Z3dE9UQXhNVEF5TURJeE5qQTBNVEl3TnpFMk1EVXhOREV4TURFeE1ESXhNVEEx-----END CERTIFICATE-----)EOF"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); WiFiClientSecure client; client.setCACert(ROOT_CA); // настоящая проверка сервера HTTPClient https; https.begin(client, url); // перегрузка: HTTPClient поверх secure-клиента int httpCode = https.GET(); if (httpCode == 200) { Serial.println(https.getString()); } else { Serial.print("code="); Serial.println(httpCode); // -1 — чаще всего провалена проверка сертификата } https.end();} void loop() { delay(10000); }Строка, которую все копируют: client.setInsecure() — «прими любой сертификат, даже самоподписанный и даже подменённый». Рукопожатие проходит всегда, и вот цена: владелец точки доступа — роутер в кафе, сосед, корпоративный шлюз — может сесть между платой и сервером, увидеть и подправить оба направления. Для лампочки в кладовке это терпимо, для термостата, открывающего дверь, — нет. Моё правило: setInsecure — только пока отлаживаешь; на постоянке — CA-сертификат, даже если провайдер API о нём ни словом не упоминает.
Цена честной проверки — секунды и килобайты. TLS-рукопожатие на ESP32 занимает порядка одной-двух секунд, и одна защищённая сессия держит за собой порядка 40-50 КБ свободной памяти — примерно десятая часть всей RAM платы. Отсюда два практических вывода: опрос по HTTPS чаще раза в десять секунд я не ставлю никогда — каждый GET с новым соединением означает новое рукопожатие, поэтому держите соединение вызовом https.setReuse(true) или опрашивайте реже. И не заводите рядом тяжёлых потребителей: большой ответ сервера плюс TLS-буферы легко съедают остаток heap.
И история, которая показывает, зачем всё это: 30 сентября 2021 года истёк корневой сертификат DST Root CA X3, а Let's Encrypt годами выпускала сертификаты с кросс-подписью на него. Тысячи устройств с зашитым старым корнем разом перестали ходить на свои API — код был верный, сеть была, не хватало одной строки с новым корнем ISRG Root X1, который действует до 2035 года. Мораль: CA в прошивке — расходник со сроком годности; закладывайте путь обновления, хотя бы перепрошивку.
| Вариант | Стоит в прошивке | Что проверяется | Кому годится |
|---|---|---|---|
| http без TLS | Ничего | Ничего | Демо и домашняя лаборатория |
| setInsecure() | Ничего | Ничего: принимается любой сертификат | Отладка; устройства без тайн в данных |
| client.setCACert(корень) | 1-2 КБ PEM-строки | Подпись, срок, имя сервера | Боевые проекты — мой выбор |
| Набор корней (crt-bundle) | Порядка 30 КБ во флеше | Как у браузера: сотни корней | Клиент на десятки разных API |
Между «любой сертификат» и «проверка корнем» есть третий путь — самоподписанный сервер. Свой API на VPS я иногда закрываю сертификатом, который подписал сам себе, и тогда в скетч клиента идёт не корневой CA, а сам сертификат сервера: проверка остаётся настоящей, просто цепочка из одного звена. Это дешевле покупки и надёжнее setInsecure, но капризно при переезде сервера: сертификат в прошивке придётся обновлять так же, как корень от Let's Encrypt.
Где HTTPS обязателен, а где избыточен, я разделяю по содержимому. Данные с паролями, ключами, дверными замками и кошельками — только TLS и только с проверкой CA, без компромиссов. Публичная телеметрия вроде температуры за окном или статуса кормушки — по мне, так честный http внутри домашней сети допустим: шифровать погоду — тратить секунды рукопожатия впустую. А вот тот же датчик, вынесенный за пределы дома и стучащийся на облачный сервер, снова попадает в категорию «только TLS»: в чужих каналах он не хозяин.
Симулятор: тот же разбор ответа, сеть без шифрования
Читать симулятор просто: code=200 — сервер ответил, dt= — длительность запроса в миллисекундах, temp= и desc= — значения, доставленные по ключам из JSON-ответа (сам разбор мы подробно проходили в уроке про ArduinoJson). На реальной плате тот же скетч с CA-сертификатом печатает те же пары — плюс первые одна-две секунды на рукопожатие, которые хорошо видно в dt= первого запроса.
Диагностика по -1: если GET по https возвращает -1, порядок проверки у меня такой — сначала сертификат (истёк? тот ли корень? совпадает ли имя сервера с адресом?), потом сам PEM (скопирован ли целиком, с переносами строк?), и только потом сеть. Отдельный класс граблей — PEM, склеенный в одну линию без переносов: парсер отказывается от него молча, и поиск длится полвечера. Проверено на себе дважды.
- Замер цены. На плате оберните https.GET() замерами millis() до и после: первый запрос покажет рукопожатие 1-2 секунды, повторный по setReuse(true) — сотни миллисекунд.
- Сломанный CA. Подмените один символ в ROOT_CA — GET начнёт возвращать -1 при живой сети: так выглядит провал проверки со стороны.
- Свободная память. Печатайте ESP.getFreeHeap() до соединения и после — увидите цену TLS-сессии в килобайтах на своей плате.
- Адрес и порт. Проверьте, что адрес начинается с https:// и порт 443 не занят ручным подключением — https.begin(client, url) разбирает адрес сама.
- 1. Что именно делает client.setInsecure()?
- 2. GET по https с CA-сертификатом возвращает -1, WiFi подключён. Что проверить первым?
- 3. Сколько свободной памяти обычно держит одна TLS-сессия на ESP32?
- 4. Почему в сентябре 2021 года тысячи устройств разом перестали ходить на свои API?
- 5. Что делает вызов https.begin(client, url) с WiFiClientSecure?
- HTTPS = HTTP внутри TLS: сервер показывает цепочку сертификатов, клиент проверяет её зашитым в прошивку корневым CA
- client.setCACert(ROOT_CA) — настоящая проверка; client.setInsecure() принимает любой сертификат: удобно в отладке, опасно на чужом WiFi
- Корневой CA — PEM-строка на 1-2 КБ; берут в браузере или через openssl s_client -showcerts; это расходник: DST Root CA X3 истёк 30.09.2021
- Цена TLS: 1-2 секунды рукопожатия и 40-50 КБ heap на сессию — опрашивайте реже или держите соединение setReuse(true)
- GET по https с кодом -1 при живой сети — в девяти случаях из десяти провалена проверка сертификата, а не сеть
Трио закрыто: у платы есть имя из урока про mDNS, честное время из NTP-урока и защищённая дорога к настоящим API. На этой базе собирается то, ради чего ESP32 и берут: устройства, которые сами ходят в сеть, держат расписание и открываются из браузера по человеческому имени. Мой тест готовности прост: если скетч выживает после ночной перезагрузки роутера, печатает правильное время утром и не пускает чужой роутер между собой и сервером — его можно оставлять без присмотра.
Если дальше — то в сторону постоянного канала: в разделе про MQTT плата не спрашивает, а публикует и слушает, причём брокеры часто живут за TLS-портом 8883 — сертификаты из этого урока едут с нами и туда.
Частые вопросы по теме урока
- Что именно делает client.setInsecure()?
- Шифрование остаётся, проверка сервера — нет: любой сертификат принимается. Владелец точки доступа может подсмотреть и подправить обе стороны обмена. Принимает любой сертификат сервера без проверки — защита от подмены исчезает
- GET по https с CA-сертификатом возвращает -1, WiFi подключён. Что проверить первым?
- Провал проверки сертификата рвёт соединение с тем же кодом -1, что и недоступная сеть. Поэтому сертификат проверяют первым: срок, корень, имя сервера. Корректность и срок действия CA-сертификата
- Сколько свободной памяти обычно держит одна TLS-сессия на ESP32?
- Буферы TLS — порядка 40-50 КБ heap на сессию, десятая часть RAM платы. Проверяется вызовом ESP.getFreeHeap() до соединения и после — цифры видно своими глазами. 40-50 КБ
- Почему в сентябре 2021 года тысячи устройств разом перестали ходить на свои API?
- Истёк корень DST Root CA X3, на котором держалась кросс-подпись Let's Encrypt. Лечится перепрошивкой с новым корнем ISRG Root X1. Истёк корневой сертификат DST Root CA X3, зашитый в старые прошивки
- Что делает вызов https.begin(client, url) с WiFiClientSecure?
- Это перегрузка begin: HTTPClient шлёт запросы через переданный WiFiClientSecure, и тот применяет CA при рукопожатии. HTTP-клиент поверх secure-клиента: запрос уйдёт с проверкой сертификата