Repository navigation
bootlock.pl
Dzięki bootloaderowi płytka przyjmuje nowy firmware bez programatora i uruchamia tylko kompletny obraz przeznaczony dla niej. Blokada zamyka płytkę przed programatorem, więc nikt nie odczyta ani nie nadpisze jej flasha. Każde z nich działa samodzielnie, a o tym, jak razem chronią produkt, mówi część Bezpieczeństwo. Podstawy Forge są w readme. Budowę bootloadera od środka opisuje strona Bootloader i aktualizacja firmware w wiki Core.
Do prototypu wystarczy Włączenie. Przy produkcie przejdź kolejno przez Klucze, Codzienną pracę i Wydanie, a na koniec przez Fabrykę.
Bootloader to mały program na początku flasha, który po każdym resecie uruchamia się przed Twoją aplikacją.
Forge kompiluje go z kodu Core razem z aplikacją, według ustawień z boot.h Twojego projektu.
Resztę PRO_FLASH_kB Forge dzieli na dwa równe sloty: slot aplikacji, w którym działa Twój firmware, i slot tymczasowy, do którego najpierw trafia aktualizacja.
Ostatnia strona obszaru bootloadera to mailbox: aplikacja zgłasza w nim aktualizację, a bootloader zapisuje jej wynik.
Obraz trafia do slotu aplikacji dopiero wtedy, gdy jest kompletny i sprawdzony.
Dlatego przerwany transfer albo zanik zasilania podczas kopiowania niczego nie psuje: działa stara wersja albo kopiowanie powtarza się przy następnym starcie.
Bootloader działa w jednym z dwóch trybów:
plain |
key |
|
|---|---|---|
| Uruchamia | obraz z poprawnym CRC | obraz podpisany Twoim kluczem |
| Obszar bootloadera | 12kB na G0, 16kB na WB | 32kB |
| Koszt | brak | mniejsze sloty, dłuższy start, klucze do pilnowania |
| Dla kogo | prototypy, nauka | produkt przyjmujący aktualizacje |
plain chroni tylko przed wypadkami: przerwaną albo uszkodzoną aktualizacją, zanikiem zasilania, plikiem dla innego urządzenia.
Nie nadaje się do zabezpieczenia produktu, bo każdy, kto dotrze do łącza aktualizacji Twojej aplikacji, zainstaluje dowolny obraz zbudowany na tę płytkę.
key dodatkowo sprawdza przy każdym starcie podpis Ed25519, więc uruchamiają się tylko obrazy podpisane Twoimi kluczami.
Ustawienia bootloadera leżą w pliku boot.h obok main.h, a sam bootloader włącza jedna linia w main.h:
#include "boot.h"Nowy projekt założony poleceniem opencplc -n myapp -b uno -B ma oba od razu.
W istniejącym dopisz tę linię i utwórz obok main.h pusty plik boot.h.
make run # bootloader i aplikacja przez ST-LinkOd teraz przed Twoją aplikacją uruchamia się bootloader, w trybie plain, dopóki nie dodasz kluczy.
Żeby sprawdzić, co zrobił, włącz w konsoli komendę boot:
#define CMD_BOOT ONboot info pokazuje mode:plain, w result wynik tego startu, a w last wynik ostatniej aktualizacji.
Żeby wyłączyć bootloader, usuń #include "boot.h" i znowu uruchom make run: zwykły obraz nadpisze bootloader.
Do podpisanych aktualizacji potrzebne są klucze, które tworzy się raz na produkt:
opencplc --keygen acme 3 # klucz produktu i dwa zapasowe--keygen tworzy klucze acme, acme-1 i acme-2 w katalogu kluczy tego komputera, %LOCALAPPDATA%/OpenCPLC/keys, a ich części publiczne wpisuje do boot.h:
#define BOOT_KEYS \
"bffaf48c944589091627837a361bda7ab914aa87bf0b396d868788d305cc8d80" \
"74f466497f944fe491df8c5653c28041b71dfe815f8ed291397b0825b0f598e6" \
"c9191a33dda22d8ca8b1784ca20f8d3e3c34d4256b8fc7362ee08a0b356daed8"Gdy w boot.h jest BOOT_KEYS, bootloader działa w trybie key i uruchamia tylko obrazy podpisane jednym z tych kluczy.
Projekt bez bootloadera dostaje razem z pierwszymi kluczami plik boot.h i jego #include w main.h.
Bez liczby --keygen tworzy jeden klucz, a acme to tylko przykład: daj kluczom nazwę swojego produktu.
Klucze zapasowe są na wypadek wycieku. Bootloader zostaje w urządzeniu na zawsze i ufa tylko kluczom, które dostał w fabryce, więc klucz dopisany później nie trafi do urządzeń, które są już u klientów. Gdy pierwszy klucz wycieknie, podpisujesz drugim, a urządzenia unieważniają pierwszy, patrz Wyciek klucza.
Klucze publiczne mogą trafić do gita, bo i tak są zapisane w każdym urządzeniu.
Tajne są tylko pliki .key, a obok każdego leży jego część publiczna w pliku .pub; na Linuksie katalog kluczy to ~/.local/share/OpenCPLC/keys, a zmienna OPENCPLC_KEYS wskazuje inny.
Hasło, o które pyta --keygen, jest opcjonalne: sam Enter zapisuje klucze bez hasła.
Warning
Wyciek klucza pozwala każdemu podpisać oprogramowanie, które przyjmą Twoje urządzenia, a utrata wszystkich kluczy kończy ich aktualizacje.
Trzymaj pliki .key w dwóch kopiach offline, zapasowe osobno od pierwszego, z hasłem albo na zaszyfrowanym nośniku.
make run # build i wgranie
make update # obraz z tego buildu przez konsolęW trybie key do codziennej pracy nie potrzebujesz klucza produktu.
Przy pierwszym buildzie Forge tworzy klucz deweloperski tego komputera, podpisuje nim buildy i wstawia go do bootloadera zamiast Twoich kluczy.
Płytka na biurku przyjmuje więc wszystko, co zbudujesz na tym komputerze.
Nic nie zapisuje się na niej na stałe: bootloader bierze progi epoki i kluczy z obrazu w slocie, a nie z OTP, więc make run zawsze przywraca płytkę do stanu wyjściowego.
make update wysyła obraz przez konsolę, tak jak urządzenie dostaje aktualizację u klienta, dlatego w main.h musi być #define CMD_BOOT ON.
Płytka się resetuje, bootloader instaluje obraz, a na koniec make update pokazuje boot info z result:installed.
make update FILE=<plik> wysyła inny obraz, np. plik .bin z wydania.
Pod F5 debugger zna symbole bootloadera i aplikacji, więc krok po kroku przejdziesz z jednego do drugiego.
Na komputerze make test ma model flasha i OTP w RAM, więc epokę, klucze i zaniki zasilania można sprawdzać bez ograniczeń, patrz 🧪 Testy.
make dist TAG=1.2.0W projects/myapp/ pojawiają się dwa pliki:
-
myapp-1.2.0.hex: bootloader z Twoimi kluczami i obraz, do programatora -
myapp-1.2.0.bin: sam obraz, do aktualizacji
W trybie key oba są podpisane pierwszym kluczem z BOOT_KEYS, a make dist pyta o jego hasło, jeśli je ma.
Bootloader z tego pliku .hex zapisuje progi epoki i kluczy na stałe w OTP.
Płytka na biurku odrzuci ten .bin z wynikiem signature, bo jej bootloader ma klucz deweloperski.
Gdy w BOOT_KEYS jest tylko jeden klucz, make dist ostrzega, że jego wyciek zakończyłby aktualizacje.
Gdy klucza nie ma na komputerze, make dist kończy się błędem no private key for ..., a przy złym haśle błędem wrong password for key acme, i w obu przypadkach nie zostawia żadnego pliku.
W CI klucz i hasło podaje się w zmiennych środowiskowych:
export OPENCPLC_KEYS=/secure/keys # acme.key i acme.pub
export OPENCPLC_KEY_PASSWORD=... # tylko dla klucza z hasłem
opencplc myapp
make dist TAG=1.2.0Wydanie wgrywasz na płytkę plikiem .hex:
opencplc --program projects/myapp/myapp-1.2.0.hex # bez blokadyPłytka ma teraz Twoje klucze: przyjmuje tylko pliki z make dist, a build z make odrzuca z wynikiem signature.
boot info pokazuje mode:key i rdp:0, a make run przywraca na płytce klucz deweloperski.
Sztukę dla klienta wgrywasz tym samym plikiem, tylko z --lock, patrz Fabryka.
Aplikacja odbiera plik myapp-1.2.0.bin swoim łączem, np. BLE, RS albo USB, i przekazuje bajty do BLD_Begin, BLD_Write i BLD_End z hal/stm32/sys/bld.h, a podpis do BLD_Signature.
BLD_End zgłasza obraz w mailboxie, płytka się resetuje, a bootloader sprawdza obraz, kopiuje go i uruchamia.
Aplikacja nie sprawdza obrazu, decyduje bootloader.
Kto może rozpocząć aktualizację i czy łącze jest szyfrowane, decyduje aplikacja.
Stos radiowy STM32WB też aktualizuje aplikacja: przyjmuje binarkę ST swoim łączem i zleca jej instalację FUS, a bootloader stosu nie dotyka.
Aplikacja powinna uruchamiać się także bez stosu i dalej przyjmować aktualizacje przez USB, bo zanik zasilania podczas instalacji zostawia CPU2 bez stosu: WPAN_Start zwraca wtedy ERR, a kopia w slocie tymczasowym pozwala dokończyć instalację.
Po odrzuconej aktualizacji działa dalej poprzednia wersja, a boot info podaje powód w result przy tym starcie i w last aż do następnej aktualizacji:
-
installed: nowy obraz zainstalowany,nonegdy start był bez aktualizacji -
crc: obraz uszkodzony albo inny niż zgłoszony -
size: obraz większy niż slot -
origin: obraz pod inny slot, np. zbudowany podplaindla bootloaderakey -
chip: obraz na inny układ -
board: obraz na inny wariant płytki -
signature: brak podpisu albo podpis innym lub unieważnionym kluczem -
epoch: epoka niższa niż próg
Poza tym boot info pokazuje mode, epoch i revoked z progami, loader z odciskiem bootloadera i rdp z poziomem blokady.
Wydanie, które łata lukę bezpieczeństwa, podnosi epokę:
# w main.h: #define PRO_SECURITY_EPOCH 1
make dist TAG=1.2.1Urządzenie, które je zainstaluje albo uruchomi zaraz po wgraniu programatorem, zapisuje nowy próg w OTP i nie przyjmie już obrazu z epoką 0, więc nikt nie przywróci wersji z luką. Epokę podnoś tylko przy łatce bezpieczeństwa, bo ma blokować powrót do wersji z luką, a nie każdy powrót do starszej wersji, np. po nieudanym wydaniu.
Gdy pierwszy klucz wycieknie albo zginie, podpisuj następnym:
# w main.h: #define PRO_SIGNING_KEY 1
make dist TAG=1.2.2PRO_SIGNING_KEY to pozycja klucza w BOOT_KEYS, liczona od 0.
Urządzenie, które zainstaluje ten obraz, zapisuje w OTP, że klucze poniżej 1 są unieważnione, i od tej chwili nie przyjmie ani nie uruchomi obrazu podpisanego kluczem, który wyciekł.
Chronione są urządzenia, które zainstalują to wydanie przed obrazem atakującego, więc wypuść je od razu.
Wszystkie aplikacje jednego produktu muszą mieć w BOOT_KEYS te same klucze w tej samej kolejności, bo numer klucza jest zapisany w każdym obrazie.
W kolejnej aplikacji wywołaj to samo polecenie, a --keygen weźmie istniejące klucze z katalogu kluczy i wpisze tę samą listę:
opencplc panel --keygen acme 3Na komputerze bez tych kluczy po prostu skopiuj linie BOOT_KEYS do boot.h kolejnej aplikacji.
Gdy zasilanie płytki podtrzymuje sam procesor, np. na baterii włączanej przyciskiem, musi on włączyć podtrzymanie zaraz po resecie, bo inaczej straci zasilanie.
Robi to bootloader: po każdym resecie najpierw ustawia wyjście podtrzymania, jeszcze przed zegarem i sprawdzaniem podpisu, i nigdy go nie zwalnia, także gdy nie ma czego uruchomić.
Wyjście wskazuje się w boot.h:
#define BOOT_HOLD { .port = GPIOB, .pin = 5, .reverse = true }Pin opisuje się jak wszędzie w Core, przez GPIO_t.
.reverse = true oznacza podtrzymanie stanem niskim, a bez tego pola stanem wysokim.
Aplikacja zastaje pin w stanie aktywnym i nie może go zwolnić.
W jej GPIO_t tego pinu ustaw .mode = GPIO_Mode_Output i .set = true, bo GPIO_Init ustawia stan pinu z pola set.
Inna wersja sprzętu na tym samym układzie, która potrzebuje innej binarki, dostaje w boot.h własny wariant:
#define BOOT_BOARD_VARIANT 2Bootloader tej wersji uruchamia tylko obrazy wariantu 2, a bootloader wersji z wariantem 0 tylko obrazy wariantu 0.
Obie wersje wspierasz więc równolegle: każda dostaje swoje wydania, a plik jednej nie zainstaluje się na drugiej.
Tym wariant różni się od epoki, która tylko rośnie i zamyka drogę starszym obrazom.
Kod aplikacji też może z niego korzystać, np. #if BOOT_BOARD_VARIANT == 2.
make erase # kasuje też bootloader
make stack # STM32WB, stos radiowy, kasuje też bootloader
make flash # bootloader i obraz z powrotemBez bootloadera albo z pustym slotem płytka nie uruchamia aplikacji, a wyjścia zostają w stanie po resecie: czy to bezpieczne dla maszyny, zależy od projektu płytki.
make erase nie zdejmuje ochrony zapisu, więc nie skasuje stron, które firmware chroni przez FLASH_Protect; zdejmuje ją --lock 0.
Ustawienia z tego przewodnika leżą w dwóch plikach projektu.
main.h opisuje aplikację i to, jak Forge ją buduje i podpisuje:
-
#include "boot.h": włącza bootloader -
PRO_SIGNING_KEY: numer klucza, którym podpisujemake dist, domyślnie0 -
PRO_SECURITY_EPOCH: epoka obrazu, domyślnie0 -
CMD_BOOT:ONdodaje do konsoli komendęboot, potrzebną doboot infoimake update
boot.h czytają aplikacja i bootloader:
-
BOOT_KEYS: klucze publiczne, wpisuje je--keygen; bez nich bootloader działa w trybieplain -
BOOT_BOARD_VARIANT: wariant płytki, domyślnie0 -
BOOT_HOLD: wyjście podtrzymania zasilania, które bootloader ustawia zaraz po resecie i nigdy nie zwalnia
Forge pilnuje tego podziału: gdy ustawienie trafi do złego pliku, zatrzymuje się i podpowiada, dokąd je przenieść.
--lock zmienia przez programator ustawienia układu (option bytes):
- RDP1: debugger i programator tracą dostęp do flasha, a zdjęcie blokady kasuje cały flash
- start z bootloadera ST w ROM jest wyłączony, niezależnie od stanu pinu
BOOT0 - przy bootloaderze dochodzi ochrona zapisu jego kodu w obszarze A, bez mailboxa, a obszar B zostaje dla danych, które firmware chroni sam przez
FLASH_Protect
Blokada działa w trybie plain, w trybie key i bez bootloadera.
--lock sprawdza tylko, czy programator widzi układ z projektu, a nie zawartość flasha: tę ustala --program, dlatego w fabryce oba idą jednym poleceniem.
--lock, RDP1 |
--lock 2, RDP2 |
|
|---|---|---|
| Debugger | odcięty, --lock 0 kasuje flash i go oddaje |
odcięty na zawsze |
| Ochrona zapisu bootloadera | zdejmie ją luka w aplikacji, przestawiając option bytes | zamrożona |
| Analiza uszkodzonej sztuki w ST | możliwa | niemożliwa |
| Stos radiowy na WB |
make stack przed blokadą |
tylko FUS z aplikacji |
opencplc myapp
opencplc --program projects/myapp/myapp-1.2.0.hex --lock # pełny obraz z dist, potem RDP1
opencplc --program projects/myapp/myapp-1.2.0.hex --lock -y # linia produkcyjna, bez pytaniaW trybie key wgrywaj zawsze plik z make dist: build z make run ma klucz deweloperski, więc taka sztuka przyjmowałaby aktualizacje tylko z komputera programisty.
Po zablokowaniu make flash, F5 i --program nie mają już dostępu do flasha, aktualizacje nadal przechodzą łączem aplikacji, a boot info pokazuje rdp:1.
Przy kilku podłączonych ST-Linkach opencplc myapp -s <serial> przypisuje do projektu właściwy, inaczej --lock trafi na obcy układ i odmówi.
Na STM32WB najpierw wgrywa się stos radiowy, a dopiero potem obraz, bo make stack używa SWD i kasuje bootloader:
make stack
opencplc --program projects/myapp/myapp-1.2.0.hex --lockNote
Po zablokowaniu wyłącz zasilanie całkowicie i włącz je ponownie, bo układ zablokowany przy podłączonym debuggerze nie uruchomi aplikacji.
Adapter USB-UART na pinach konsoli też potrafi zasilać układ, więc odłącz również jego, a na Nucleo przełóż zworkę zasilania na CHG.
opencplc --program projects/myapp/myapp-1.2.0.hex --lock 2Warning
RDP2 nieodwracalnie wyłącza port programatora, także dla producenta. Sztuki nie da się już odblokować, przeprogramować ani zbadać debuggerem, zostają tylko aktualizacje łączem aplikacji.
Forge pyta wtedy drugi raz, nawet z -y.
RDP2 stosuj tylko w produkcie, w którym klient albo norma wymaga niezmiennego bootloadera.
opencplc myapp
opencplc --lock 0 # zdejmuje blokadę i każdą ochronę zapisu, kasując cały flash
make flash # bootloader i obraz z powrotemPo make flash wyłącz zasilanie całkowicie, bo STM32G0 po skasowaniu flasha startuje z ROM aż do pełnego restartu.
Na STM32WB blokadę zdejmuje STM32CubeProgrammer, ten sam, którego wymaga make stack, a kasowanie trwa kilka sekund.
Razem z blokadą znika cały flash, więc przy analizie sztuki zwróconej przez klienta zostają tylko logi zebrane wcześniej.
Bootloader odpowiada za to, co się uruchamia: podpis, CRC i epokę, sprawdzane przy każdym starcie. Blokada odpowiada za to, czy da się ominąć bootloader: port programatora, start z ROM i ochronę zapisu bootloadera. Aplikacja odpowiada za to, kto i jak przesyła aktualizację: dostęp do łącza, uwierzytelnienie, np. parowanie, i szyfrowanie.
Szyfrowanie zostaje w aplikacji, bo bootloadera się nie zmienia, więc nie dałoby się w nim wymienić ani klucza, ani algorytmu. Podpis liczony jest z jawnej treści, więc bootloader i tak sprawdzi wynik rozszyfrowania, a slot tymczasowy leży w wewnętrznej pamięci pod blokadą. Jeden klucz szyfrowania na produkt, odczytany z jednej rozebranej sztuki, otwiera jednak wszystkie pliki aktualizacji, więc czy i jak szyfrować, decyduje producent.
✅ chroni, ❌ nie chroni,
| Zagrożenie | plain |
plain + --lock
|
key |
key + --lock
|
|---|---|---|---|---|
| Przerwana albo uszkodzona aktualizacja | ✅ | ✅ | ✅ | ✅ |
| Zanik zasilania w trakcie instalacji | ✅ | ✅ | ✅ | ✅ |
| Aktualizacja dla innego urządzenia | ✅ | ✅ | ✅ | ✅ |
| Obce oprogramowanie przez łącze aktualizacji | ❌ | ❌ | ✅ | ✅ |
| Wersja deweloperska przez aktualizację na sztuce produkcyjnej | ❌ | ❌ | ✅ | ✅ |
| Powrót do starej wersji ze znaną luką | ❌ | ❌ | ✅¹ | ✅¹ |
| Obraz podpisany kluczem, który wyciekł | ❌ | ❌ | ||
| Obce oprogramowanie wgrane programatorem | ❌ | ✅ | ❌ | ✅ |
| Skopiowanie oprogramowania z urządzenia | ❌ | ✅ | ❌ | ✅ |
Start z bootloadera ST w ROM (pin BOOT0) |
❌ | ✅ | ❌ | ✅ |
| Nadpisanie bootloadera przez błąd w aplikacji | ❌ | ✅ | ❌ | ✅ |
| Trwałe przejęcie przez lukę w działającej aplikacji | ❌ | ❌ | ❌ | |
| Poznanie treści oprogramowania z pliku aktualizacji⁴ | ❌ | ❌ | ❌ | ❌ |
| Ataki sprzętowe: zakłócanie zasilania, pomiar poboru prądu | ❌ | ❌ | ❌ | ❌ |
¹ Gdy lukę zamknęło wydanie z podniesioną epoką. ² Od wydania podpisanego kluczem zapasowym, na urządzeniach, które zainstalują je przed obrazem atakującego, i na sztukach wgranych nim w fabryce. ³ Luka, która wykonuje obcy kod, może przestawić option bytes i zdjąć ochronę zapisu bootloadera, a zamraża je dopiero RDP2. ⁴ Poufność to zadanie aplikacji.
Projekt bez bootloadera, ale z blokadą, chroni przed programatorem, skopiowaniem i startem z ROM, tylko nie ma aktualizacji.
Celowo nie ma:
- aktualizacji bootloadera w terenie, bo przerwana podmiana zostawiłaby martwe urządzenie, więc błąd w bootloaderze naprawia serwis,
- dopisywania kluczy w terenie, z tego samego powodu, więc klucze zapasowe trafiają do bootloadera od pierwszej sztuki,
- sprzętowej strefy ochronnej, bo STM32G0 ją ma, a STM32WB nie, a jedną drogę dla obu rodzin łatwiej sprawdzić,
- automatycznej blokady przy pierwszym starcie, bo każdy test wydania na płytce kończyłby się kasowaniem flasha,
- watchdoga w bootloaderze, bo koliduje z trybami uśpienia, więc aplikacja włącza go sama.
Unijny akt o cyberodporności (CRA), norma IEC 62443-4-2 dla komponentów automatyki, a dla urządzeń radiowych dyrektywa RED z normami EN 18031 oczekują między innymi autentycznego oprogramowania, bezpiecznych aktualizacji i zamkniętych interfejsów serwisowych:
| Oczekiwanie | Gotowe | Po stronie producenta |
|---|---|---|
| Tylko autentyczne oprogramowanie | podpis sprawdzany przy każdym starcie | ochrona kluczy produktu |
| Bezpieczne aktualizacje | podpis, odporność na zanik zasilania, raport wyniku | kto może rozpocząć aktualizację, wydawanie poprawek |
| Ochrona przed cofnięciem wersji | epoka | podniesienie epoki z każdą łatką |
| Odcięcie klucza, który wyciekł | klucze zapasowe i unieważnienie w OTP | klucze zapasowe offline od pierwszej sztuki |
| Zamknięte interfejsy serwisowe |
--lock: port programatora i BOOT0
|
blokada każdej sztuki przed wysyłką |
| Bezpieczna konfiguracja fabryczna |
key i --lock, każde jedną komendą |
key + --lock dla produktu |
| Poufność oprogramowania | brak w bootloaderze | szyfrowanie w aplikacji, jeśli wymagane |
| Obsługa podatności | poza bootloaderem | zgłoszenia, poprawki, informowanie klientów |
Note
Ten dokument nie jest deklaracją zgodności. Pokazuje, w czym mechanizmy pomagają, a które regulacje obejmują produkt, ustala producent.