Jak działa Secure Boot?


Bezpieczeństwo w Embedded: Trusted Firmware i TrustZone

W projektowaniu urządzeń wbudowanych mamy poważny problem. Musimy założyć, że każdy chce zhakować nasze urządzenie – wykraść dane, klucze, ominąć zabezpieczenia. Im bardziej skomplikowana technologia i więcej komponentów, tym więcej potencjalnych luk i wektorów ataku. Kluczowe jest skuteczne chronienie sekretów, takich jak klucze kryptograficzne.

Przykładem świetnego rozwiązania jest Secure Boot, który daje pewność, że wszystkie komponenty biorące udział w starcie systemu pochodzą z zaufanego źródła. Do tego dochodzą też wymagania prawne. Już za niedługo Cyber Resilience Act (CRA) narzuci na producentów elektroniki i oprogramowania konkretne standardy bezpieczeństwa. Secure Boot przestanie być „nice to have”, a stanie się obowiązkowym elementem większości systemów.

Pytanie: jak to wszystko bezpiecznie zaimplementować? Rozwiązania czysto softwarowe, kontenery czy biblioteki kryptograficzne to za mało. Jak mówi stary żart: istnieją trzy sposoby na w pełni bezpieczne programowanie i żaden nie działa. Potrzebujemy rozwiązania łączącego hardware i software.

I tu właśnie wkraczają Trusted Firmware oraz TrustZone. To o nich opowiem w tym materiale.

Na poniższym diagramie widzisz architekturę Trusted Firmware. Jest tu sporo nowych skrótów: EL0, EL1, BL3, Trusted Boot Firmware i wiele innych. Na początku może to przytłaczać, ale po przeczytaniu tego artykułu wszystko będzie jasne. Przeczytaj do końca, bo pokażę, co się dzieje, gdy zignorujemy Secure Boota…

Secure Boot – Podstawy

Najpierw przypomnijmy, jak może wyglądać proces uruchamiania systemu na procesorze Cortex-A (na którym możemy uruchomić Linuxa):

  1. ROM Code: Kod wgrany przez producenta procesora. Wykonuje bardzo podstawową inicjalizację sprzętu (np. zegary). Jego zadaniem jest załadowanie FSBL.
  2. FSBL (First Stage Bootloader): Inicjalizuje resztę zegarów i uruchamia zewnętrzną pamięć DDR RAM, aby móc załadować coś większego i bardziej skomplikowanego. Podobnie jak ROM Code, jego zadaniem jest załadowanie SSBL.
  3. SSBL (Second Stage Bootloader): Tu może być np. U-Boot. Odpowiada za załadowanie systemu plików (z pamięci masowej lub przez TFTP) i przekazywanie informacji userowi (np. splash screen). SSBL ładuje jądro (kernel) wraz z Device Tree Blobem (strukturą opisującą podłączony sprzęt).
  4. Kernel: Inicjalizuje resztę sprzętu, uruchamia sterowniki (na podstawie Device Tree), montuje rootfs (system plików) i przygotowuje środowisko.
  5. Userspace: Po uruchomieniu wszystkich procesów jądra, system jest gotowy. Tutaj działają nasze normalne aplikacje, np. GUI czy serwisy zbierające dane z sensorów.

To oczywiście przypadek ogólny. Na każdej platformie może to wyglądać nieco inaczej.

A teraz spójrzmy, jak wygląda proces Secure Boota na prawdziwym sprzęcie, np. na platformie z serii STM32MP2.

Ten diagram pochodzi bezpośrednio z dokumentacji STM32MP2. Jest znacznie bardziej skomplikowany, ale widzisz tu znane już elementy: ROM Code, FSBL, SSBL. Szybko jednak pojawiają się nowe symbole: TFA (Trusted Firmware A – dla Cortex-A), BL2, BL31, OP-TEE. Przede wszystkim, wprowadza on podział na dwa światy: Non-Secure oraz Secure (TZ – TrustZone). Za chwilę wyjaśnię, o co w tym wszystkim chodzi.

TrustZone: Dwa Światy

W naszym procesorze istnieje podział na dwa światy: Secure i Non-Secure. Wiąże się to z koncepcją izolacji sprzętowej – mamy dla nich oddzielną pamięć i oddzielny stan procesora.

Na tym diagramie widzisz abstrakcyjne przedstawienie pamięci i stanów procesora w architekturze Cortex-A.

  • Non-Secure World (po lewej stronie): To „normalna”, ogólnodostępna pamięć i domyślny stan procesora (Normal State). Tutaj działają nasze aplikacje w userspace’ie (Netflix, przeglądarka) oraz Linux Kernel, zarządzający sprzętem i zasobami.
  • Secure World (po prawej stronie, zwany też TrustZone): Dysponuje oddzielną pamięcią, do której aplikacje z Non-Secure świata (nawet kernel) nie mają bezpośredniego dostępu. Gdy CPU działa w Secure World, wchodzi w Secure State i ma dostęp do instrukcji przeznaczonych wyłącznie dla tego bezpiecznego środowiska. Nie ma tu „normalnych” aplikacji ani kernela. Zazwyczaj działa tu Secure Operating System, np. OPTEE (często jest to RTOS, prostszy niż Linux Kernel), który zarządza Trusted Applications (TA) i sprzętem im potrzebnym. Mamy też zazwyczaj specjalną, bardziej ograniczoną implementację kernela – Secure Kernel.

Właśnie tu widać potęgę TrustZone’a: nie ufamy tylko rozwiązaniom softwarowym, ale mamy prawdziwą separację sprzętową. Korzystamy z oddzielnej pamięci, a CPU przechodzi w inny stan, gdy przemieszczamy się między światami.

Exception Levels (ELx): Poziomy Uprawnień

Wiemy już, że istnieją dwa światy. Powstaje pytanie, jak się między nimi przełączać? Jak np. przejść ze świata Non-Secure do Secure, by wykonać bezpieczne operacje kryptograficzne? To wszystko dzieje się za pomocą wywołań programowych wyjątków, czyli przez exception handler.

Kluczowe pojęcie to Exception Levels (ELx). Choć nazwa sugeruje wyjątki, tak naprawdę są to poziomy uprawnień (privilege levels). Im wyższy poziom ELx, tym program ma więcej możliwości i uprawnień. Architektura ARM definiuje poziomy od EL0 do EL3:

  • EL0 (Exception Level 0): Najniższe uprawnienia. Aplikacje nie mogą ustawiać przerwań, nie mają dostępu do konfiguracji MMU (Memory Management Unit), pamięci podręcznej ani rejestrów systemowych. To poziom, na którym działają aplikacje w userspace’ie.
  • EL1 (Exception Level 1): Może zarządzać sprzętem, konfigurować MMU. Standardowym przykładem jest Linux Kernel, który odpowiada za scheduling, zarządzanie pamięcią i zasobami sprzętowymi.
  • EL2 (Exception Level 2): Głównie dla Hypervisorów, które zarządzają gościnnymi systemami operacyjnymi (np. wirtualizacja, popularna w branży Automotive).
  • EL3 (Exception Level 3): Najwyższy poziom uprawnień. Może zarządzać zasobami na wszystkich poprzednich poziomach. Aplikacja działająca na tym poziomie to Secure Monitor. To on odpowiada za przechodzenie ze świata Normalnego do Secure.

Warto zaznaczyć, że podczas bootowania, początkowy exception level to EL3. Na tym poziomie wykonuje się Secure Boot i inicjalizacja systemu. Po skonfigurowaniu najważniejszych komponentów i weryfikacji obrazu U-Boota, kontrola jest przenoszona na EL1 (do kernela), a stamtąd prosto do EL0 (userspace).

Na EL0 w Non-Secure mamy aplikacje userspace’a. Na EL1 działa Linux Kernel. Na EL2 Hypervisor. A na EL3 Secure Monitor, który umożliwia przejścia między światami.

Po stronie Secure World, na EL2 mamy Trusted Partition Managera (nie będę się na nim skupiał). Na EL1 mamy Trusted OS (najprawdopodobniej RTOS, często OPTEE), który zarządza Trusted Applications (TA) działającymi na EL0 w Secure World. To TA odpowiadają za najbardziej krytyczne operacje, np. szyfrowanie/odszyfrowywanie, i komunikują się ze światem Non-Secure przez Secure Monitora.

Przejście Między Światami: Instrukcja SMC

Wiemy już, jak wygląda model poziomów uprawnień (Exception Levels). Teraz zobaczmy, jak za pomocą tych „wyjątków” przechodzić ze świata Non-Secure do Secure.

Zaczynamy od aplikacji na poziomie EL0. Załóżmy, że chce ona wykonać operację wymagającą przejścia do świata Secure (np. uwierzytelnianie, szyfrowanie). Zazwyczaj wywołujemy prostą funkcję z biblioteki. To wszystko dzieje się na EL0.

Żeby to zrobić, musimy przejść na wyższy poziom uprawnień, czyli odwołać się do Kernela (EL1). Robimy to za pomocą instrukcji asemblerowej SVC (Service Call), co jest odpowiednikiem wykonania syscalla – prosimy kernela o dostęp do zasobów.

Kernel przekazuje nasze żądanie do odpowiedniego sterownika, który potrafi komunikować się z TrustZone. W tym momencie musimy odwołać się do Secure Monitora, który działa na EL3 (najwyższe uprawnienia). Służy do tego inna, kluczowa dla nas instrukcja asemblerowa: SMC (Secure Monitor Call).

Wywołanie SMC powoduje przejście do Secure Monitora. Procesor, po sprawdzeniu uprawnień, przechodzi do Secure Monitora, który przełącza nas do Secure World. Jesteśmy teraz na Secure Memory, a nasz procesor wchodzi w Secure State.

Nasze żądanie jest przetwarzane przez Trusted Kernel (lub OPTEE), a następnie trafia do Trusted Application, jest tam wykonywane, a wynik wraca z powrotem, również przez Secure Monitora, do naszej aplikacji w Non-Secure World.

Tak z grubsza wygląda proces przechodzenia między światem Normalnym a Secure. Serce tego mechanizmu to instrukcja SMC, wywołująca Secure Monitora – element EL3 odpowiedzialny za te przejścia.

BLx – Komponenty Trusted Firmware

Wróćmy teraz do diagramu Secure Boota, od którego zaczęliśmy. Sporo już rozszyfrowaliśmy: wiemy, czym są światy Non-Secure i Secure, TrustZone, OPTEE. Co jednak z oznaczeniami BL31, BL2?

Muszę przyznać, że oznaczenia tych komponentów softwarowych są dość niefortunne i ciężkie do zapamiętania. Pochodzą z oficjalnej specyfikacji Trusted Firmware dla procesorów Cortex-A i po prostu tak zostały nazwane poszczególne części. Ciężko mi wymyślić tu jakieś mnemotechniki, więc po prostu przedstawiam słowniczek:

  • BL1: Pierwszy, niezmienialny kod z wbudowanej pamięci ROM (ROM Code). Wykonuje minimalną inicjalizację i ładuje BL2.
  • BL2: Drugi etap bootowania. Inicjalizuje pamięć DRAM, weryfikuje i ładuje wszystkie kolejne obrazy (BL31, BL32, BL33).
  • BL31: Instancja Secure Monitora. To, o czym mówiliśmy wcześniej – zarządza przechodzeniem między światami, działa na EL3. Ładowany przez BL2.
  • BL32: Bezpieczny system operacyjny (Secure OS) działający po stronie Secure. Widoczny na diagramie, również ładowany przez BL2. Zazwyczaj będzie to OPTEE.
  • BL33: Program rozruchowy (bootloader) dla świata normalnego (Non-Secure). Ładuje i uruchamia główny system operacyjny, np. U-Boot.

Te nazwy nie są moim zdaniem najlepsze, łatwo się tu pogubić. Możesz sobie zapisać ten słowniczek i z niego korzystać. Po prostu zaakceptuj, że sformułowania BLx pochodzą ze specyfikacji Trusted Firmware, ponieważ istniała potrzeba nazwania tych wszystkich osobnych komponentów softwarowych w Secure TrustZone, które nie są takie same jak w Non-Secure (gdzie np. mamy U-Boota).

Dokończę jeszcze o OPTEE. Jest to open-source’owy Trusted Execution Environment (TEE), czyli właśnie ta warstwa umożliwiająca działanie Trusted Applications (TA) po stronie Secure. Jest odpowiednikiem niezabezpieczonego Linux Kernela. OPTEE to projekt open-source’owy działający pod egidą Trusted Firmware, obecny na wielu urządzeniach (np. STM32MP2, niektóre Raspberry Pi).

W kolejnym artykule o TrustZone pokażę Ci, jak samodzielnie możesz używać OPTEE i pisać Trusted Applications! Strona niezabezpieczona często jest określana jako Rich Execution Environment (REE).

Podsumowanie

Wracamy do naszego diagramu Secure Boota dla platformy MP2. Teraz wszystko powinno być jasne:

Zaczynamy od ROM Code (BL1), który jest uruchamiany po stronie Secure (na EL3), a następnie ładuje First Stage Bootloader (BL2). BL2 ładuje Secure Monitor (BL31) – ten element odpowiada za przejścia między światem Secure a Non-Secure. Dzięki temu, że monitor działa, możemy przejść do świata Non-Secure i uruchomić Second Stage Bootloader (BL33), czyli np. U-Boota.

W tym samym momencie, po stronie Secure, ładowany jest OPTEE (BL32) – mini system operacyjny działający w zabezpieczonej pamięci TrustZone. W międzyczasie U-Boot załaduje Linuxa wraz z Device Tree, a następnie kernel uruchomi nasz userspace.

Oczywiście, po uruchomieniu Secure Monitora, za pomocą OPTEE i Trusted Applications, możemy weryfikować kolejne obrazy: czy obraz U-Boota się zgadza, czy pochodzi z bezpiecznego źródła, czy obraz kernela jest prawidłowy itd. Możemy w ten sposób w bezpieczny sposób kontrolować cały łańcuch bootowania i uchronić się przed wieloma atakami. Na przykład, jeśli ktoś chciałby podmienić kernela na taki z lukami, nie będzie mógł tego zrobić, bo sprawdzimy sygnaturę naszego kernela i pozwolimy na załadowanie wyłącznie tego z naszego własnego źródła.

Źródła

Dokumentacja STM32MP2: https://wiki.st.com/stm32mpu/wiki/Boo…

Dokumentacja ARM Trustzone: https://developer.arm.com/documentati…


Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *