Перейти к содержимому

HTTPS, SSL/TLS соединения на Arduino и ESP8266

Метки:

Добрый день, уважаемый читатель! Продолжаем тему HTTP-запросов, но сегодня поговорим об защищенных соединениях. В наше время HTTPS обязателен для каждого веб-сайта, браузеры уже давно помечают сайты без TLS как небезопасные.

Впрочем, все сказанное в данной статье относится не только к HTTP(S) протоколу, но и к шифрованным соединениям посредством других “прикладных” протоколов – MQTT, FTP, OTA и так далее.

В этой статье я расскажу как работать с защищенными интернет-запросами применительно только к ESP8266 или ESP32 под управлением фреймворка Arduino, так как на ESP-IDF библиотека mbedTLS довольно сложная и требует отдельного обсуждения.

 


Немножко теории

Для начала определимся с терминами.

HTTPS, SSL, TLS – а в чём, собственно разница?

HTTPS не является отдельным протоколом. HTTPS – это самый обычный HTTP, но работающий через шифрованные транспортные механизмы SSL и TLS. То есть заменили “открытое” соединение на “защищенное” и всё – добавили буковку S в конце названия, сам протокол при этом никак не изменился. Изменился только транспортный уровень – то есть как передаются через сеть биты и байты.

Точно так же протокол MQTT может работать как поверх TCP/IP, так и поверх TLS – ему без разницы, в каком виде дойдут байты до сервера – в открытом или зашифрованном. Но вот MQTTS его при этом почему-то никто не называет, хотя термин SFTP есть.

А в чем разница между SSL и TLS? По большому счёту это одно и то же, просто старая и новая версии одного и того же действа (конечно, “внутри” они отличаются друг от друга, но дня нас с вами никакой разницы нет). Термины SSL (secure sockets layer — слой защищённых сокетов) и TLS (transport layer security — протокол защиты транспортного уровня) часто используются как взаимозаменяемые, поскольку TLS пришел на место SSL. На текущий момент SSL считается устаревшим и на большинстве сайтов уже не используется. Но во множестве документов и инструкций по прежнему можно встретить этот термин.

Источник: Википедия

Для установки защищенных соединений на ESP8266 используется библиотека BearSSL, которая интегрирована в библиотеку WiFi для ESP8266. Поэтому всё нижеописанное справедливо именно для BearSSL.

 

Установка защищенного соединения между клиентом и сервером

Я не специалист в области криптографии, думаю и вы тоже. Но в самых общих чертах понимать как работает SSL / TLS нужно. Основные шаги процедуры создания защищённого сеанса связи (выдержка из вики) – так называемое “рукопожатие”:

  • клиент подключается к серверу, поддерживающему TLS, и запрашивает защищённое соединение;
  • клиент предоставляет список поддерживаемых алгоритмов шифрования и хеш-функций;
  • сервер выбирает из списка, предоставленного клиентом, наиболее надёжные алгоритмы среди тех, которые поддерживаются сервером, и сообщает о своём выборе клиенту;
  • сервер отправляет клиенту цифровой сертификат для собственной аутентификации. Цифровой сертификат содержит имя сервера, имя удостоверяющего центра сертификации и открытый ключ сервера;
  • клиент, до начала передачи данных, проверяет полученный сертификат сервера относительно имеющихся у клиента корневых сертификатов удостоверяющих центров (центров сертификации). Клиент также может проверить, не отозван ли серверный сертификат, связавшись с сервисом доверенного удостоверяющего центра;
  • для шифрования сессии используется сеансовый ключ. Получение общего секретного сеансового ключа клиентом и сервером проводится по протоколу Диффи-Хеллмана.

На этом заканчивается процедура подтверждения связи. Между клиентом и сервером установлено безопасное соединение, и все данные, передаваемые по нему, шифруются и расшифровываются с использованием сеансового ключа до тех пор, пока соединение не будет завершено.

Я не зря выделил жирным шаги, связанные с проверкой сертификата сервера – это основное, что потребуется от вас как программиста для установки защищенного соединения с “умного” устройства.
Все остальные шаги скрыты в недрах библиотек и нам не особо важны.

Из всего этого следует, что для TLS и HTTPS потребуется сертификат. Но сертификат самого сервера нам вообще не интересен, так как его сервер и так вышлет клиенту при “рукопожатии”, нас интересует корневой сертификат, которому мы безусловно доверяем.

Справедливости ради стоит отметить, что для BearSSL можно просто выбрать режим setInsecure() и устанавливать SSL-соединения без проверки сертификата. Если вы предпочитаете такое себе решение, то далее читать статью просто не имеет никакого смысла.

 

Все сертификаты сайтов выданы и подписаны какими-либо центрами сертификации, которые так же имеют свой сертификат. Центр сертификации не обязательно должен быть один, их может быть несколько в цепочке, подписанных один за другим. Например: сертификат для сайта wqtt.ru был выдан ЦС R3, а тому, в свою очередь, выдал сертификат ISRG Root X1.

Первый сертификат в списке называется “корневым”. Если корневому сертификату мы доверяем целиком и полностью, то остальные “вложенные” сертификаты мы сможем проверить просто “по цепочке”. Поэтому для проверки сертификата любого сайта мы должны иметь “всего лишь” список доверенных корневых сертификатов, которым мы доверяем абсолютно. Его и потребуется указать библиотеке (любой – что для esp8266 / arduino, что для esp32 / esp-idf) перед началом защищенного соединения.

У вас может возникнуть вопрос – “а почему на компьютерах с windows или linux, например, мы не указываем никаких корневых сертификатов, а “замочек” в браузере есть”? Да просто потому что на взрослых компьютерах “в тихую” ведется специальный реестр актуальных корневых сертификатов, исходя из которого браузеры и остальные программы проверяют сертификаты сайтов.

Хранилище сертификатов в windows – certmgr.msc

Но список корневых сертификатов (на компьютере) достаточно большой и занимает довольно много памяти, что для относительно “маленьких” микроконтроллеров абсолютно не приемлемо. Приходится себя ограничивать, и указывать только те, что требуются только для конкретной программы.

Впрочем, у ESP32 памяти побольше, и если вам её не жалко, то можно “закачать” в прошивку и весь список корневых сертификатов – ESP-IDF начиная с версии 4.3 (если не ошибаюсь) это позволяет. Но об этом будет рассказано в следующей статье.

 


Как получить файл корневого сертификата

Итак, нам нужен файл корневого сертификата. Самое простое в windows – использовать обычный браузер, например хром. Если у вас установлен антивирус, придется его отключить на время, так как антивирусные программы подменяют сертификаты ЦС своими собственными и это ни к чему хорошему не приведет. Затем откройте в браузере нужный вам сайт и кликните на замочек в адресной строке:

Затем кликните на строку “Безопасное подключение”, а затем на “Действительный сертификат”:

Откроется окошечко просмотра сертификата, где мы должны перейти на вкладку “Подробнее”, выделить в иерархии верхний сертификат и нажать “Экспорт”:

Затем просто указываем, где бы мы хотели сохранить файл и нажимаем ОК. В итоге у вас в выбранной папке должен появится новый файлик с вот таким примерно содержанием:

Это и есть то, что нам нужно – корневой сертификат! Как его “прописать” в прошивку, я расскажу чуть ниже. Можно устанавливать TLS-соединение? Не совсем!

 

Срок действия сертификата

У всех сертификатов (и корневых и не только) имеется встроенная головная боль для embedded-программистов, и называется она “срок действия сертификата”. После заранее определенного в сертификате срока он считается “испорченным”. Кроме того, сертификат может быть отозван досрочно, если есть подозрения на его компрометацию. И тогда таким “порченным” корневым сертификатом ничего и никого проверить уже будет нельзя.

Для сертификата сервера обычно это не проблема – владельцы сервера запросят новый сертификат у ЦС и все будет работать как раньше. Ни вы в браузере, ни ваше устройство замены сертификата сервера (не корневого) даже не заметите.

Но с корневым сертификатом для embedded устройств это не прокатывает и нам потребуется оперативно заменить его в прошивке. Обычно для корневых доверенных сертификатов срок действия достаточно велик, чтобы не очень беспокоиться по этому поводу, но событие в календарик добавить всё-таки стоит. Поэтому для критичных устройств стоит предусмотреть альтернативный вариант подключения, дабы не потерять контроль над устройством.

Кроме того, для проверки сертификата, вам обязательно потребуется правильные системные дату и время. Казалось бы – зачем устанавливать правильные дату и время? Только для логов? А вот и нет – как минимум чтобы проверить сроки действия сертификатов.

После сброса микроконтроллера системное время в нём – 01 января 1970 00:00:00 GMT, и оно абсолютно не годится для проверки сертификатов. А это значит, что даже имея “на руках” все козыри и туз бубновый, мы всё равно не сможем установить защищенное соединение.

Что же делать, как же быть? Варианта как минимум два – использовать внешние или встроенные часы RTC или получить актуальное время через SNTP. Как получить время через сеть интернет, я уже рассказывал ранее.

Работа с датой и временем и SNTP-синхронизация на ESP32 и ESP8266

И только после того, как системные “часики” вашего микроконтроллера тикают правильно, можно приступать к активным действиям. Поехали!

 


Использование SSL / TLS на ESP8266

Как я уже писал в прошлой статье цикла, класс WiFiClient обеспечивает только транспортный уровень, то есть тот самый пресловутый стек TCP/IP.

Впрочем, все сказанное в данной статье относится не только к HTTPS протоколу, но и к шифрованным соединениям посредством MQTT, FTP, OTA и так далее.

 

Шаг 1 – изменяем переменные WiFiClient на WiFiClientSecure и добавляем новую – X509List

Для использования защищенных соединений нам потребуется использовать другой аналогичный класс – WiFiClientSecure. Это тот же самый WiFiClient, но с поддержкой SSL и TLS. Достаточно заменить переменную в вашей программе, и всё – поддержка TLS уже имеется. За шифрование соединений в ESP8266 при этом отвечает встроенная библиотека BearSSL.

Для хранения корневого сертификата в программе потребуется создать дополнительную глобальную или статическую переменную, в библиотеке BearSSL для этого используется класс X509List:

BearSSL::WiFiClientSecure client;
const char x509CA PROGMEM = R"EOF(....)EOF";
void setup() {
    BearSSL::X509List x509(x509CA);
    client.setTrustAnchor(&x509);
}
void loop() {
    client.connect("192.168.1.1", 443);
}

Если сертификат X509 в вашей программе один, то хранилище X509List проще и удобнее объявить статически в глобальной переменной, например так:

// Корневой сертификат
BearSSL::X509List certISRG(ISRG_Root_x1);
BearSSL::WiFiClientSecure wifiClient;

Важное замечание! X509List может хранить только один единственный сертификат. Если Вам потребуется установить подключение к нескольким серверам, имеющим разные корневые сертификаты, то потребуются дополнительные танцы с бубном. Основной сертификат (например для MQTT) можно объявить как в примере, а дополнительные – через локальные переменные или размещение в куче (heap).

Шаг 2 – синхронизируем время

Для проверки срока действия сертификата необходимо знать точное время. Поэтому после подключения к точке доступа WiFi рекомендуется сразу же получить правильное системное время с SNTP, сделать это можно примерно так:

// Подключение успешно установлено
Serial.println("");
Serial.print("WiFi connected, obtained IP address: ");
Serial.println(WiFi.localIP());

// Для работы TLS-соединения нужны корректные дата и время, получаем их с NTP серверов
configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov");
// Ждем, пока локальное время синхронизируется
Serial.print("Waiting for NTP time sync: ");
i = 0;
time_t now = time(nullptr);
while (now < 1000000000) {
  now = time(nullptr);
  i++;
  if (i > 60) {
    // Если в течение этого времени не удалось подключиться - выходим с false
    // Бескорнечно ждать подключения опасно - если подключение было разорвано во время работы
    // нужно всё равно "обслуживать" реле и датчики, иначе может случиться беда
    Serial.println("");
    Serial.println("Time sync failed!");
    return false;
  };
  Serial.print(".");
  delay(500);
}

// Время успешно синхронизировано, выводим его в монитор порта
// Только для целей отладки, этот блок можно исключить
Serial.println("");
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.print("Current time: ");
Serial.print(asctime(&timeinfo));

Шаг 3 – добавляем содержимое корневого сертификата в текст программы

Добавим к нашему проекту строковую константу, например так:

static const char ISRG_Root_x1[] PROGMEM = R"EOF()EOF";

а затем скопируйте содержимое из файла сертификата и вставьте между круглых скобок. У вас должно получиться нечто вроде этого:

// Корневой сертификат ISRG Root x1, действителен до 4 июня 2035 года
static const char ISRG_Root_x1[] 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";

Только не забудьте вставить в Ваш код новый сертификат, когда истечёт срок действия текущего

А что если нам потребуется несколько подключений с разными корневыми сертификатами?

Повторите шаг 3 для других корневых сертификатов, если они вам потребуются (например если у вас используется несколько защищенных соединений к разным серверам, которые получили свои сертификаты в разных центрах сертификации). Но потребуется использовать несколько разных локальных экземпляров X509List и WiFiClientSecure. И следует учитывать, что ESP8266 “не потянет” больше одного TLS-подключения одновременно – только попеременно. Но об это ниже.

 

Шаг 4 – добавляем сертификат в список корневых доверенных сертификатов

После этого уже можно смело добавить корневой сертификат в список доверенных:

// Время успешно синхронизировано, выводим его в монитор порта
// Только для целей отладки, этот блок можно исключить
Serial.println("");
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.print("Current time: ");
Serial.print(asctime(&timeinfo));

// Теперь можно привязать корневой сертификат к клиенту WiFi
wifiClient.setTrustAnchors(&certISRG);

Вот теперь всё готово к TLS-соединению!

Шаг 5 – укажите другой порт на сервере, который соответствует защищенному протоколу

Как правило, для открытых и защищенных соединений сервер использует разные порты. Для протокола HTTP стандартным незащищенным является порт 80, а для защищенных соединений – уже другой – 443. У MQTT это могут быть 1883 и 8883, но могут быть использованы и любые другие. Поэтому для корректной работы программы придется изменить и номер порта тоже.

Если взять пример из предыдущей статьи, то изменений будет не так уж и много:

httpS (SSL/TLS) соединения на Arduino / ESP8266

Как то так, на мой взгляд не так уж и сложно.

Как сделать то же самое, но для MQTT-клиента – я уже рассказывал в другой статье, отличий там очень не много:

Телеметрия на Arduino и ESP8266. Создание проекта с MQTT управлением в среде PlatformIO

 


Проблема одновременного использования нескольких TLS-соединений на ESP8266

Допустим нам нужно использовать одновременно несколько защищенных соединений, да еще с разными корневыми сертификатами. Как это сделать? Ну например можно так:

void telegramSendMessage(const char* chatId, char* message)
{
  Serial.print("TG :: Send message: ");
  Serial.print(message);
  Serial.println("...");

  // Настраиваем безопасное подключение к API
  WiFiClientSecure wifiTg;
  X509List certTg(TG_API_Root);
  wifiTg.setTrustAnchors(&certTg);

  // Пробуем подключиться к Telegram API: обратите внимание - API принимает входящие запросы только по HTTPS на 443 порту
  if (wifiTg.connect("api.telegram.org", 443)) {
    // Формируем и отправляем GET-запрос для отправки сообщения
    wifiTg.printf("GET https://api.telegram.org/bot%s/sendMessage?chat_id=%s&parse_mode=HTML&text=%s HTTP/1.1\r\n", tgToken, chatId, message);
    // Отправляем служебные HTTP-заголовки
    wifiTg.println(F("Host: api.telegram.org"));
    wifiTg.println(F("User-Agent: ESP8266"));
    // Отправляем пустую строку, которая указывает серверу, что запрос полностью отправлен
    wifiTg.println();
    
    // Читаем ответ сервера (только первую строку)
    Serial.print(F("TG :: API responce: "));
    Serial.println(wifiTg.readStringUntil('\n'));
    // Закрываем соединение
    wifiTg.stop();
  } else {
    Serial.printf("#ERROR# :: Failed to connect to Telegram API: %d\r\n", wifiTg.getLastSSLError());
  };
}

Но тут есть одна проблема…

Для установки и поддержания защищенного соединения требуется достаточно много памяти. Поскольку TLS был разработан для систем с большим объемом памяти, им по умолчанию требуется буфер размером 16 КБ для приема и передачи. А всего на одно TLS-соединение библиотекой BearSSL может расходоваться до 22 КБ оперативной памяти (пруфы здесь). Поэтому для ESP8266, у которого общая доступная куча составляет всего около 40 КБ, одновременно возможно использовать только одно защищенное соединение.

Поэтому в некоторых случаях приходится чем-то жертвовать или “разносить” соединения по времени с удалением всех буферов и локальных переменых.

Например: Telegram API принципиально не принимает и не обрабатывает незащищенные запросы. И единственный выход использовать уведомления в Telegram на ESP8266 – это использовать обычное (не защищенное) подключение к MQTT-брокеру (либо отключаться от MQTT-сервера на время отправки запроса к API, что не удобно). То же самое касается и OTA-обновлений – либо использовать их без TLS, либо отключаться от MQTT-сервера на время обновления прошивки.

Как об этом узнать? Если WiFiClientSecure.connect(server, 443) возвращает 0, проверьте код ошибки с помощью WiFiClientSecure.getLastSSLError() – если результат будет -1000, то это как раз и означает, что вашему ESP не хватает памяти.

if (wifiClient->connect("server.ru", 443)) {
  ...
} else {
  Serial.printf("#ERROR# :: Failed to connect to server: %d\r\n", wifiClient->getLastSSLError());
};

Если в результате вы видите текст:

...
#ERROR# :: Failed to connect to server: -1000

То это означает “Unable to allocate memory for SSL structures and buffers.” или по-русски “Невозможно выделить память для структур и буферов SSL.”

А дальше думайте – как оптимизировать и от чего отказаться… Как вариант можно перейти на более мощную ESP32 – памяти у неё гораздо больше, в том числе и RAM / HEAP.


На этом можно закончить, ну а в следующий раз поговорим о том же самом, но для ESP-IDF.

На этом пока всё, до встречи на сайтe и на dzen-канале!

💠 Полный архив статей вы найдете здесь

 


Пожалуйста, оцените статью:
[ 5 из 5, всего 1 оценок ]

2 комментария для “HTTPS, SSL/TLS соединения на Arduino и ESP8266”

  1. Дмитрий

    Может я что то не допонял. Но, как обновлять сертификат в процессе эксплуатации? не каждый же раз прошивку править.

    1. Нет, вы все правильно поняли – каждый раз обновлять прошивку. Можно через OTA-обновления. Иного способа нет – ведь сертификат у вас в коде.
      Можно попробовать загружать сертификат с FLASH-памяти или SD-карты, но это совсем другой уровень.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *