Skip to content

bootlock.pl

Xaeian edited this page Oct 26, 2026 · 1 revision

Bootloader i blokada

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

Jak działa

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.

Sloty za bootloaderem

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.

Włączenie

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-Link

Od 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 ON

boot 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.

Klucze

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.

Codzienna praca

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.

Klucze buildu i wydania

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.

Wydanie

make dist TAG=1.2.0

W 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.0

Wydanie wgrywasz na płytkę plikiem .hex:

opencplc --program projects/myapp/myapp-1.2.0.hex # bez blokady

Pł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.

Aktualizacja przez łącze

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, none gdy 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 pod plain dla bootloadera key
  • 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.

Łatka bezpieczeństwa

Wydanie, które łata lukę bezpieczeństwa, podnosi epokę:

# w main.h: #define PRO_SECURITY_EPOCH 1
make dist TAG=1.2.1

Urzą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.

Wyciek klucza

Gdy pierwszy klucz wycieknie albo zginie, podpisuj następnym:

# w main.h: #define PRO_SIGNING_KEY 1
make dist TAG=1.2.2

PRO_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.

Aplikacje jednego produktu

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 3

Na komputerze bez tych kluczy po prostu skopiuj linie BOOT_KEYS do boot.h kolejnej aplikacji.

Podtrzymanie zasilania

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.

Warianty płytki

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 2

Bootloader 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.

Kasowanie

make erase  # kasuje też bootloader
make stack  # STM32WB, stos radiowy, kasuje też bootloader
make flash  # bootloader i obraz z powrotem

Bez 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

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 podpisuje make dist, domyślnie 0
  • PRO_SECURITY_EPOCH: epoka obrazu, domyślnie 0
  • CMD_BOOT: ON dodaje do konsoli komendę boot, potrzebną do boot info i make update

boot.h czytają aplikacja i bootloader:

  • BOOT_KEYS: klucze publiczne, wpisuje je --keygen; bez nich bootloader działa w trybie plain
  • BOOT_BOARD_VARIANT: wariant płytki, domyślnie 0
  • 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ść.

🔒 Blokada

--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

Fabryka

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 pytania

W 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 --lock

Note

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.

Blokada na zawsze

opencplc --program projects/myapp/myapp-1.2.0.hex --lock 2

Warning

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.

Serwis

opencplc myapp
opencplc --lock 0  # zdejmuje blokadę i każdą ochronę zapisu, kasując cały flash
make flash         # bootloader i obraz z powrotem

Po 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.

🛡️ Bezpieczeństwo

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, ⚠️ częściowo.

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.