Isolation mit TrustZone und TF-M unter Zephyr
Dieser Beitrag knüpft an Firmware-Signierung unter Zephyr an, wo dasselbe Gerät lernt, nur noch signierte Firmware zu starten. Wer den ersten Teil nicht gelesen hat, kann hier trotzdem einsteigen.
Angenommen, ein Angreifer schafft es unter Ausnutzung einer Schwachstelle, eigenen Code auf dem Gerät auszuführen.
Die Signatur hat dabei ihre Aufgabe erfüllt: Sie stellt sicher, dass nur Firmware vom Hersteller ausgeführt wird, und sorgt dafür, dass der Angreifer keine persistente Änderung hinterlässt. Was sie nicht leistet: Sie sagt nichts über die Rechte, mit denen diese echte Firmware läuft. Und die sind auf einer MCU ohne Isolation unbeschränkt.
Der Cyber Resilience Act sieht das ähnlich. Anhang I nennt in Teil I Nummer 2 nicht nur den Integritätsschutz von Programmen und Konfiguration (Buchstabe f), auf dem der erste Teil beruhte, sondern auch die Vertraulichkeit gespeicherter Daten (Buchstabe e) und, direkt auf diesen Fall gemünzt, dass die Auswirkungen eines Sicherheitsvorfalls durch Mechanismen zur Minderung der möglichen Ausnutzung verringert werden (Buchstabe k). Die Formulierung setzt voraus, dass der Vorfall bereits eingetreten ist.
Der Aufbau ist derselbe wie im ersten Teil: ein ST Nucleo L552ZE-Q unter Zephyr 4.4.2.
Derselbe Lesezugriff, zwei Ergebnisse
Der erste Teil endete mit einem Gerät, das ausschliesslich signierte Firmware startet. Vier Zeilen Konfiguration, und ein einziges gekipptes Byte genügte, damit MCUboot den Start verweigert.
Dieselbe Firmware liest jetzt eine Adresse im sicheren Bereich des Flash:
uint32_t stolen = *(volatile uint32_t *)0x0C014000;
Im ersten Teil gelingt das. Die Firmware ist echt, korrekt signiert, vom Bootloader geprüft, und sie liest den sicheren Bereich so bereitwillig wie jede andere Adresse. Das ist die Unbeschränktheit von oben, an einer konkreten Zeile.
Mit TrustZone endet derselbe Zugriff so:
FATAL ERROR: HardFault
Gleiche Instruktion, gleiches Board, andere Welt. Der Unterschied ist TrustZone, und wie man es einschaltet, klären wir in diesem Artikel.
Es gibt keinen Schalter für TrustZone
Die naheliegende Erwartung ist ein Konfigurationsschalter, so wie CONFIG_BOOTLOADER_MCUBOOT im ersten Teil. Den gibt es nicht.
Der Schalter ist das Board-Target. Statt für nucleo_l552ze_q/stm32l552xx wird für nucleo_l552ze_q/stm32l552xx/ns gebaut. Das Suffix setzt CONFIG_TRUSTED_EXECUTION_NONSECURE, das wiederum BUILD_WITH_TFM auswählt: Der Build erzeugt zusätzlich Trusted Firmware-M für die sichere Welt und linkt die Anwendung so, dass sie in der nicht-sicheren läuft.
Das ist tatsächlich der ganze Mechanismus. Interessant ist deshalb nicht das Einschalten, sondern was es nach sich zieht.
Was das Board-Target macht
Die Bootreihenfolge bekommt eine Stufe. Im ersten Teil startete MCUboot die Anwendung. Jetzt startet BL2, das ist TF-Ms eigene MCUboot-Instanz, zuerst das sichere Image; TF-M richtet die Welten ein und übergibt an die nicht-sichere Anwendung. Drei Programme, drei Banner auf der Konsole.
Das Flash-Layout kommt vom Board. Der erste Teil brauchte ein eigenes Devicetree-Overlay, weil das einfache Target keine Partitionierung mitbringt. Das /ns-Target bringt seine eigene mit, und zwar folgende:
| Partition | Grösse |
|---|---|
boot_partition (BL2) |
80 KB |
slot0_partition (sicher) |
180 KB |
slot0_ns_partition (nicht-sicher) |
36 KB |
Dazu je ein zweiter Slot derselben Grösse (für Firmware-Updates). Auch der RAM wird aufgeteilt: Die letzten 64 KB von SRAM1 gehören TF-M, 128 KB bleiben der Anwendung.
Was auffällt: Der eigentlichen Anwendung bleiben 36 KB. Das Beispiel hier belegt davon 21, und darin steckt vor allem Zephyr selbst (Kernel, Treiber, Konsole), bevor eine einzige Zeile Produktlogik dazukommt. Wer TF-M einsetzen will, sollte den Flash-Bedarf deshalb schon bei der Controllerwahl berücksichtigen.
Zwei Unterschiede zum ersten Teil
In zwei Punkten funktioniert der zweite Aufbau anders als der erste.
Kein sysbuild
Naheliegend ist, den zweiten Aufbau als den ersten plus TF-M zu behandeln. Dann liefe der Build weiterhin mit --sysbuild, das MCUboot als eigenes Image dazubaut.
Das führt zu:
intelhex.AddressOverlapError: Data overlapped at address 0xC000000
TF-M bringt seinen Bootloader selbst mit. CONFIG_TFM_BL2 ist standardmässig gesetzt, BL2 ist bereits Teil des Builds. Sysbuilds MCUboot kommt als zweiter Bootloader hinzu, und beide beanspruchen denselben Flash-Bereich. Etwas früher im selben Build kollidieren sie schon über die Krypto-Bibliothek, mit der Meldung „One crypto library implementation allowed at a time.“ Zwei Symptome, eine Ursache.
Für die Anwendung heisst das: kein sysbuild.conf, kein Overlay, kein CONFIG_BOOTLOADER_MCUBOOT. Die Konfiguration des zweiten Teils ist kürzer als die des ersten.
Der eigene Schlüssel genügt nicht mehr
Der zweite Unterschied wiegt schwerer, weil er das Vorgehen aus dem ersten Teil nicht nur ergänzt, sondern ersetzt.
Im ersten Teil reichte eine Zeile, damit der Bootloader dem eigenen Schlüssel vertraut: SB_CONFIG_BOOT_SIGNATURE_KEY_FILE zeigt auf die Schlüsseldatei, und MCUboot kompiliert den öffentlichen Teil in sein Binary. Ein Build-Setting genügt.
Hier nicht. Bei EC-P256 setzt Zephyr fest verdrahtet MCUBOOT_BUILTIN_KEY=ON (in modules/trusted-firmware-m/CMakeLists.txt). Damit prüft BL2 überhaupt nicht mehr gegen eine Schlüsseldatei. Es vergleicht gegen einen Hash im OTP-Bereich des Geräts, den sogenannten ROTPK, und TFM_DUMMY_PROVISIONING hat dort den Hash von TF-Ms eigenem Demo-Schlüssel hinterlegt.
Wer also CONFIG_TFM_KEY_FILE_S und _NS auf das eigene Schlüsselpaar zeigen lässt, signiert die Images korrekt damit und bekommt:
[INF] PSA Crypto init done, sig_type: EC-P256, using builtin keys
[ERR] Image in the primary slot is not valid!
[ERR] Unable to find bootable image
Die Signatur ist gültig. Das Gerät wurde nur darauf vorbereitet, einem anderen Schlüssel zu vertrauen. Einem Gerät den eigenen Schlüssel beizubringen heisst, den Vertrauensanker in der Fertigung in den Chip zu schreiben. Das ist Provisionierung, keine Konfiguration.
Das ist nicht weiter schlimm, im Gegenteil. Ein Gerät, das nicht richtig provisioniert wurde, startet gar nicht erst. Der Fehler fällt damit am Ende der Fertigungslinie auf und nicht beim Kunden.
Die Demo
Das Beispiel tut zwei Dinge. Es fragt über die PSA-API Zufallsbytes aus der sicheren Welt an, und es liest anschliessend eine Adresse im sicheren Flash. Die Konsole, gekürzt:
[INF] Starting bootloader
[WRN] This device was provisioned with dummy keys. This device is NOT SECURE
[INF] PSA Crypto init done, sig_type: EC-P256, using builtin keys
[INF] Bootloader chainload address offset: 0x14000
[INF] Image version: v1.0.0
[INF] Jumping to the first image slot
Booting TF-M v2.2.2
[Sec Thread] Secure image initializing!
*** Booting Zephyr OS build v4.4.2 ***
*** Secure boot sample - part 2 (Non-Secure) ***
TF-M isolation level: 2
[ok] Secure service reachable: got random bytes a444269290d47603
(generated in the Secure world, not here)
[test] Now reading Secure flash at 0x0c014000 ...
FATAL ERROR: HardFault
Drei Stellen lohnen einen Blick.
Bootloader chainload address offset: 0x14000 zeigt, dass BL2 an das sichere Image springt, nicht an die Anwendung. Die drei Banner darunter sind die drei Programme in ihrer Reihenfolge.
got random bytes … ist die eine Hälfte der Aussage. Die nicht-sichere Anwendung hat einen Dienst der sicheren Welt genutzt. Die Entropiequelle und die Schlüssel liegen jenseits der Grenze; die Anwendung hat nach einem Ergebnis gefragt und eines bekommen, ohne selbst Zugriff auf die dahinterliegenden Ressourcen zu haben. Isolation ist keine Mauer, sondern ein Tor.
FATAL ERROR: HardFault ist die andere. Dieselbe Anwendung liest dieselbe Adresse wie im ersten Teil und wird gestoppt, und zwar nicht von einer Prüfung im Code und nicht von einer Berechtigung des Betriebssystems, sondern vom Speichercontroller. 0x0C000000 ist der sichere Alias des Flash, 0x14000 der Offset von slot0_partition.
Der Fault löst einen Reset aus, das Gerät bootet und faultet erneut, die Ausgabe wiederholt sich also.
Was Isolation kostet
Die ehrliche Zahl zuerst: rund 160 KB Flash und 64 KB RAM. Der erste Teil gab für MCUboot allein 32 KB aus.
| Image | Belegt | Bereich |
|---|---|---|
| BL2 | 40 856 B | 54 KB |
| TF-M (sicher) | 119 688 B | 171 KB |
| Anwendung (nicht-sicher) | 20 984 B | 36 KB |
Das ist der Preis, und er ist auf einem Teil dieser Grösse tragbar, aber nicht vernachlässigbar. Wer ihn einmal bezahlt hat, bekommt die nächste Stufe allerdings fast geschenkt.
TF-M kennt drei Isolationsstufen. Stufe 1 trennt die sichere von der nicht-sicheren Welt, also genau die Grenze, an der der HardFault oben entsteht. Stufe 2 trennt zusätzlich innerhalb der sicheren Welt: die PSA Root of Trust, wo Krypto und Speicherdienste die Schlüssel halten, von den Application-RoT-Partitionen. Ein Fehler in einem sicheren Dienst erreicht damit nicht mehr den Speicher eines anderen. Stufe 3 trennt auch die Application-RoT-Partitionen untereinander.
Der Unterschied zwischen Stufe 1 und Stufe 2, am sicheren Image gemessen:
| Flash | RAM | |
|---|---|---|
| Stufe 1 | 119 688 B | 54 784 B |
| Stufe 2 | 120 692 B | 54 828 B |
| Differenz | +1004 B | +44 B |
Ein Kilobyte. Die Anwendung selbst bleibt unverändert, was zu erwarten ist, denn die zusätzliche Grenze liegt vollständig in der sicheren Welt. Wer TF-M überhaupt einsetzt, hat kaum ein Argument, bei Stufe 1 zu bleiben.
Zwei Einschränkungen dazu. Stufe 2 und 3 setzen voraus, dass die sicheren Partitionen je einen eigenen Ausführungskontext bekommen und über Nachrichten miteinander reden, in TF-Ms Worten das IPC-Backend. Ohne CONFIG_TFM_IPC=y fällt die Konfiguration stillschweigend auf Stufe 1 zurück. Und die Konsolenausgabe oben belegt nicht, was Stufe 2 zusätzlich leistet: Die Logs beider Stufen unterscheiden sich in genau einer Zeile, weil die Demo die Grenze zwischen sicherer und nicht-sicherer Welt prüft, die beide Stufen gleich durchsetzen. Was Stufe 2 hinzufügt, läge erst offen, wenn eine sichere Partition versuchte, den Speicher einer anderen zu lesen. Gezeigt ist hier nur: Stufe 2 baut, läuft und kostet ein Kilobyte.
Was hier nicht bewiesen ist
Das Gerät sagt es bei jedem Start selbst:
[WRN] This device was provisioned with dummy keys. This device is NOT SECURE
Dieser Aufbau läuft auf TF-Ms veröffentlichten Demo-Schlüsseln und der Dummy-Provisionierung. Das ist bewusst so, weil, wie oben beschrieben, das eigene Schlüsselpaar hier ein Fertigungsschritt wäre und kein Build-Setting. Das Signieren mit einem selbst verwalteten Schlüssel zeigt der erste Teil.
Wie ein echter Ablauf aussieht, lässt sich in fünf Schritten umreissen, von denen keiner hier demonstriert ist: Der Signierschlüssel entsteht nicht exportierbar in einem HSM oder KMS. Nur der ROTPK, also der Hash des öffentlichen Teils, verlässt es und wird in der Fertigung an einer kontrollierten Station ins Gerät geschrieben. Danach wird das Gerät verschlossen, auf dem STM32L5 über RDP und die zugehörigen Optionsbytes. Signiert wird in einer abgesicherten CI-Stufe mit Freigabe und Protokoll. Und Rotation und Widerruf gehören von Anfang an geplant, denn ein fest verankerter Vertrauensanker macht den Schlüsseltausch zu einem Update-Problem, das auf gebrannten Fuses mitunter gar nicht mehr lösbar ist.
Ein Detail zu diesem Board: Sein OTP-Bereich ist emulierter Flash, kein echter Fuse-Speicher. Für ein Beispiel ist das bequem, weil wiederbeschreibbar. Auf einem Teil mit echten Fuses ist derselbe Schritt einmalig, und diese Erfahrung lässt sich von hier nicht übertragen.
Offen bleiben ausserdem der zweite Slot, der hier zwar reserviert, aber nie beschrieben wird, und damit Update-Transport, Rollback-Schutz und Image-Verschlüsselung.
Zurück zum Angreifer
Am Neustart scheitert der Angreifer weiterhin, dafür sorgt die Signatur.
Was sich geändert hat, sind die Rechte. Der Schlüssel, den er sucht, liegt in einer Welt, die er nicht lesen kann, und das entscheidet nicht der Code, sondern der Speichercontroller. Aus vollem Zugriff ist ein Tor geworden, durch das Ergebnisse kommen und sonst nichts.
Quellen
- Verordnung (EU) 2024/2847 (Cyber Resilience Act), EUR-Lex, massgeblich ist Anhang I Teil I Nummer 2
- Zephyr: TF-M-Integration, behandelt das
/ns-Board-Target undBUILD_WITH_TFM - TF-M: Isolationsstufen nach FF-M
- Zephyr: Getting Started Guide
Alle Konsolenausgaben und Grössenangaben stammen von einem Nucleo L552ZE-Q unter Zephyr 4.4.2, TF-M 2.2.2.
Entstanden unter Mitwirkung von KI. Die technischen Aussagen beruhen auf eigener Recherche; der gezeigte Code wurde auf echter Hardware verifiziert.