W świecie Internetu Rzeczy (IoT) regularne aktualizacje firmware są kluczowe dla bezpieczeństwa, stabilności i dodawania nowych funkcji. Jednak w urządzeniach o ograniczonych zasobach, takich jak te działające na modemach 2G i mikrokontrolerach RP2350, proces aktualizacji Over-The-Air (OTA) staje się wyzwaniem. W tym wpisie podzielę się doświadczeniami z projektu, w którym wykorzystaliśmy protokół MQTT do bezpiecznego i efektywnego przesyłania aktualizacji firmware. Przyjrzymy się wybranemu rozwiązaniu, jego zaletom i problemom, z jakimi się zmierzyliśmy.
- Rozpoczęcie procesu aktualizacji. Aplikacja na tym etapie wie tylko jaki rozmiar ma aktualizacja.
- Aplikacja odbiera dane i zapisuje w pamięci flash w wyznaczonym miejscu.
- Poprzedni krok powtarza się dopóki aplikacja nie odbierze wszystkich danych.
- Aplikacja ustawia flagę informującą bootloader o tym, że jest aktualizacja do zainstalowania.
- Aplikacja restartuje procesor.
- Bootloader wykrywa flagę informującą o konieczności aktualizacji.
- Bootloader przepisuje dane z pamięci flash w miejsce gdzie znajduje się wykonywalny kod aplikacji.
- Bootloader czyści flagę i uruchamia aplikacje.
Dlaczego MQTT?
MQTT jest protokołem do przesyłania “wiadomości” (gdzie wiadomością jest zwykle tekst lub inne dane) na podstawie publikacji i subskrybcji. W protokole MQTT wyróżniamy broker (czyli serwer mqtt) oraz klientów (na przykład program komputerowy lub jakieś urządzenie IoT). Klienci łączą się do serwera i proszą o subskrybcję na danym kanale (lub kanałach). W takiej sytuacji, broker po otrzymaniu na tym kanale wiadomości, przesyła ją do wszystkich klientów, którzy ten kanał subskrybują. Klienci również mogą publikować wiadomości na kanałach. Dodatkowo, budowa protokołu MQTT zapewnia dobrą niezawodność w wypadku niestabilnych połączeń sieciowych.
Dodatkowo protokół MQTT można zabezpieczyć, wykorzystując między innymi szyfrowanie TLS lub mutual TLS oraz listy kontroli dostępu (ACL). Szyfrowanie oczywiście zapewnia nam poufność i autentyczność przesyłanych danych, natomiast listy kontroli dostępu mówią kto może subskrybować i publikować na danych kanałach. Pozwala to na przykład uniemożliwić komunikacje urządzeń IoT między sobą, ale zezwolić na komunikację urządzeń IoT z systemem zarządzającym.
Jak widać, protokół MQTT ma wiele zalet w zakresie projektów IoT. Dlaczego więc zdecydowaliśmy się na implementacje aktualizacji przez protokół MQTT?
- MQTT i tak musiałoby być ze względu na inne założenia projektu
- Użycie mutual TLS zapewnia dwustronną weryfikację – broker wie jakie urządzenie się do niego łączy, natomiast urządzenie ma pewność, że nikt pod serwer się nie podszywa.
- Implementacja innego protokołu (np HTTPS), który wykorzystywany byłby wyłącznie do aktualizacji, niepotrzebnie zwiększyłaby skomplikowanie całej infrastruktury. Wymagałoby to implementacji klienta HTTPS na urządzeniu oraz postawienia i utrzymywania serwera HTTPS.
Przebieg procesu aktualizacji OTA, który zrealizowałem
Po decyzji o wykorzystaniu MQTT, zaimplementowałem następujący proces aktualizacji firmware:
- System zarządzający przesyła na kanale
SecretDevice/SerialNumber/ota/startpayload zawierający długość (rozmiar, w bajtach) aktualizacji. - Urządzenie przygotowuje się do rozpoczęcia aktualizacji
- Urządzenie przesyła na kanale
SecretDevice/SerialNumber/ota/feedbackpayloadstart. - System zarządzający przesyła na kanale
SecretDevice/SerialNumber/ota/bytespierwszą paczkę danych (o długości 768 bajtów – wyjaśnienie rozmiaru za chwile). - Urządzenie zapisuje otrzymane dane do pamięci flash (jako plik w systemie plików)
- Urządzenie przesyła na kanale
SecretDevice/SerialNumber/ota/feedbackpayloadbytes. - Poprzednie 3 kroki powtarzane są, aż zostaną przesłane wszystkie dane.
- System zarządzający wysyła na kanale
SecretDevice/SerialNumber/ota/endpusty payload - Urządzenie kończy proces aktualizacji. W tym momencie zostaje ustawiona flaga, która mówi bootloaderowi, że przy następnym uruchomieniu trzeba wykonać aktualizację.
- Urządzenie przesyła na kanale
SecretDevice/SerialNumber/ota/feedbackpayloadend. - W tym momencie z punktu widzenia systemu zarządzającego, proces aktualizacji jest zakończony.
- Urządzenie ustawia timer na 2000 milisekund, po którym nastąpi restart do bootloadera
- Bootloader przepisuje nowy kod aplikacji z systemu plików do części pamięci flash, z której może być wykonywany kod i czyści ustawioną wcześniej flagę
- Bootloader uruchamia nowo załadowany kod z pamięci flash
W tym projekcie nie zostały wykorzystane mechanizmy zapewniające integralność danych takie jak weryfikacja podpisu cyfrowego przesłanego kodu oraz secureboot, ponieważ korzyści z tych konkretnie funkcji nie zostały uznane za warte wysiłku wymaganego do ich wdrożenia. MQTT wykorzystuje mutual TLS z pełną weryfikacją certyfikatów zarówno po stronie brokera jak i urządzenia, natomiast listy ACL brokera zapewniają, że tylko system zarządzający jest w stanie przesyłać aktualizacje. Nie uważam jednak, że podpisy cyfrowe aktualizacji oraz secureboot są niepotrzebne – zapewne wiele projektów będzie takich zabezpieczeń wymagać, tutaj jednak zostały uznane za zbędne.
Przesyłając aktualizację w ten sposób przez połączenie 2G, byłem w stanie uzyskać prędkości na poziomie 1.8kB/s. Nie jest to jakoś bardzo dużo, jednak to ograniczenie wynika przede wszystkim z przepustowości wykorzystanego modemu 2G. Nie stanowi to również dużego problemu, ponieważ skompilowany kod aplikacji zajmuje mniej niż 200kB. Dodatkowo, podczas przeprowadzania aktualizacji, aplikacja cały czas funkcjonuje.
Dane przesyłane są w paczkach po 768 bajtów, ponieważ wykorzystana biblioteka do MQTT statycznie alokuje bufory na przychodzące i wychodzące wiadomości. Rozmiar bufora ustawiony jest na 1024 bajty, z czego 768 to przesyłane dane, a reszta zostaje do wykorzystania przez nazwę kanału i inne informacje o przychodzącej wiadomości. Warto w tym momencie wspomnieć, że zgodnie ze specyfikacją protokołu MQTT, maksymalna długość wiadomości to 256 MiB, jednak tak duża wiadomość nie ma szans zmieścić się w pamięci RAM mikrokontrolera.
Inne problemy na które natknąłem się przy realizacji tego projektu, niekoniecznie związane z samymi aktualizacjami, to ograniczony rozmiar buforów wykorzystanej biblioteki do TLS. Zmniejszony rozmiar buforów pozwalał na komunikację w jedną stronę. Nie stanowiłoby to problemu w wypadku komunikacji HTTPS, gdzie w większości wypadków komunikacja faktycznie odbywa się tylko w jednym kierunku na raz. Jednak przy połączeniach MQTT, broker może przesłać do klienta dane w dowolnym momencie. Rozwiązanie tego problemu wymagało zmiany opcji w wykorzystanej bibliotece do połączeń TLS.
Podsumowując, mimo, że przesyłanie aktualizacji OTA przez protokół MQTT nie jest szczególnie często spotykaną opcją, w niektórych sytuacjach może okazać się najprostszą z punktu widzenia złożoności infrastruktury oraz ograniczenia wykorzystywanych zasobów. Dodatkowo, istniejące już zabezpieczenia infrastruktury zapewniają odpowiedni dla tego projektu poziom bezpieczeństwa przesyłanych aktualizacji.
Mam nadzieję, że ten wpis przybliżył Wam wyzwania związane z aktualizacją firmware urządzeń IoT o ograniczonych zasobach. Wykorzystanie protokołu MQTT okazało się skutecznym rozwiązaniem, ale wymagało uwzględnienia specyficznych ograniczeń i znalezienia kreatywnych rozwiązań.