Sécuriser le processus de démarrage est la première étape afin de garantir qu’un système Linux embarqué est fiable. Cette technique, appelée Secure Boot, permet de s’assurer que seuls les logiciels authentifiés de manière cryptographique (bootloader, image noyau, etc.) pourront s’exécuter sur la cible, ceci afin de certifier par exemple qu’une mise à jour logicielle est sûre, qu’aucune faille de sécurité ne subsiste ni même qu’il existe une quelconque altération provenant d’une attaque externe malveillante.
Nous proposons ici de ne présenter qu’une partie de cette séquence de démarrage sécurisé. Pour ce faire, nous parlerons d’une des nombreuses fonctionnalités que propose le projet U-Boot, « Verified Boot », procédé permettant de garantir l’intégrité et la provenance des images chargées par ce dernier (noyau Linux). On parlera ici de mécanisme de signature numérique (fonction de hachage et clé asymétrique).
1. Présentation de la plateforme : SAMA5D27-SOM-EK1
Même si l’étude qui suit est entièrement valide sur la carte de développement utilisée habituellement par l’auteur, la fameuse WaRP7, qui rappelons-le embarque le System On Chip NXP i.MX7, nous utiliserons ici le kit d’évaluation Microchip (anciennement Atmel) de référence SAMA5D27-SOM-EK1 (voir figure 1).
Fig. 1 : SAMA5D27-SOM-EK1 devant l’objectif.
Son nombre important de périphériques (Ethernet, USB, Bus CAN, extension MikroBUS, GPIO, SD/uSD, EEPROM...), ainsi que la présence du System On Module SAMA5D27 embarquant le System On Chip de la famille SAMA5D2 en fait une plateforme très intéressante pour le prototypage rapide et qui plus est industriel (vs Raspberry-Pi/Odroid/...). De plus, cette famille de microprocesseurs intègre des fonctions avancées de sécurité qui nous seront, pour quelques-unes, utiles durant la suite de l’article (Secure Boot, True Random Number Generator, ARM Trust Zone, Tamper Detection (pour se prémunir des attaques physiques au système), chiffrement/déchiffrement à la volée des données en DDR, ...)). Pour les plus curieux, la figure 2 montre le diagramme représentant l’ensemble des fonctionnalités du microprocesseur.
Fig. 2 : Synoptique SAMA5D2.
Enfin, de par sa communauté et sa vocation à être utilisé dans un milieu plutôt industriel, cette plateforme possède un excellent support sur les deux « Build Systems » les plus connus dans le milieu de l’embarqué, à savoir Yocto/Openembedded et Buildroot, deux frameworks ayant déjà fait l’objet de divers articles dans les colonnes de GNU/Linux Magazine et Open Silicium. Pour plus d’informations sur ces systèmes de constructions de distribution/firmware, l’auteur ne peut que renvoyer le lecteur vers le très bon article de Pierre Ficheux en [1].
Concernant l’intégration avec le projet Yocto, une couche BSP (Board Support Package) spécifique (meta-atmel) est mise à disposition sur le dépôt GitHub dédié à la communauté Atmel/Microchip, comme le montre la figure 3.
Fig. 3 : Couche BSP « meta-atmel ».
Sur la figure 4, même combat pour le plus français des Build Systems, Buildroot (oui, rappelons ici que Thomas Petazzoni, CTO chez Bootlin est co-mainteneur du projet ! Sans oublier Romain Naour et Yann E. Morin contributeurs majeurs au projet).
Fig. 4 : Configuration de la plateforme au sein de Buildroot.
2. Parlons un peu sécurité
Sécuriser un système embarqué est dans beaucoup de projets industriels une étape primordiale dans le processus de développement. Protéger les logiciels est quelque chose de tout aussi important que la bonne gestion des mises à jour (Over The Air ou non), et c’est d’ailleurs, dans bien des cas, deux mondes qui se rejoignent. Cette partie sera ainsi l’occasion de rappeler quelques concepts liés à notre étude.
2.1 Quels composants protéger ?
En fait, l’ensemble des éléments est susceptible d’être mis à jour par une intervention externe (via Mender par exemple). Ainsi, on retrouvera les composants d’un système Linux embarqué classique, à savoir :
- le chargeur d’amorçage. Pour notre étude, nous parlerons ici du plus utilisé dans le monde de l’embarqué, U-Boot ;
Pour la petite histoire, attention à bien l’orthographier, sous peine d’irriter la communauté (voir figure 5).
Fig. 5 : U-Boot.
- on retrouvera l’image noyau ;
- le système de fichiers racine, ainsi que les différentes applications utilisateurs ;
- les différentes données liées à l’utilisateur (logs, fichiers de configuration, base de données…) ;
- enfin, dans certains cas, un applicatif tiers (firmware Cortex M4 pour une utilisation avec un microprocesseur i.MX7, firmware FPGA sur plateforme Xilinx Zynq, et bien d’autres encore).
2.2 Chaîne de confiance ?
Comme présenté en introduction, lors d’un démarrage sécurisé, chaque logiciel se verra être vérifié pour en assurer l’authenticité, ceci dans un seul et unique but, garantir l’intégrité et empêcher un logiciel non autorisé de fonctionner. Pour ce faire, on utilisera des mécanismes à clés publiques (ou cryptographie asymétrique) et plus particulièrement le mécanisme de signature numérique.
Signature numérique
Même si nous parlerons cryptographie durant la suite de l’article, l’idée n’est pas de présenter ici tous les concepts qui y sont liés, mais plutôt d’en faire une utilisation pour les besoins de notre étude. Mais il est quand même bon et intéressant de présenter le concept de signature numérique.
Imaginons ici, une entité Alice (la signataire) souhaitant envoyer des données signées à une entité Bob (vérifieur). Pour ce faire :
- Alice va utiliser ce qu’on appelle une fonction de hachage afin de générer une « empreinte » ou « hash » permettant d’identifier la donnée initiale. Pour les fonctions de hachage, on retrouve généralement des algorithmes comme MD5, SHA-1, SHA2 (qui est plus sûr) ;
- Alice va ensuite « chiffrer » ce « hash » par le biais de sa clé privée. C’est ce qu’on appellera la signature ;
- Alice peut maintenant envoyer les données à Bob ;
- Bob calcule le « hash » des données signées ;
- Bob déchiffre la signature pour en extraire le « hash » relatif aux données signées ;
- Bob compare les deux « hash ». S’ils sont identiques, Bob peut conclure que les données sont intègres et conformes à l’expéditeur.
Dans le principe, tout reposera sur ce qu’on appelle la chaîne de confiance (ou chain-of-trust pour les anglophones), le tout premier logiciel vérifiera la signature du second logiciel, le second logiciel vérifiera la signature du troisième et ainsi de suite. Ainsi, si une étape de vérification d’un des éléments de la chaîne échoue, la phase de démarrage se verra être interrompue de façon immédiate → démarrage impossible (violation du système) ! Dans un contexte embarqué, le schéma sera le suivant :
- Le ROM Code (appelé RomBOOT pour notre plateforme), qui est le code intégré dans le silicone de la puce en ROM (Read Only Memory ou mémoire morte), vérifiera la signature du Bootloader de second niveau. À savoir qu’il est le tout premier programme à être exécuté lors de la mise sous tension. Du fait de sa présence en zone mémoire protégée, cela nous apporte la garantie qu’il ne pourra jamais être modifié.
- Le Bootloader de second niveau ou chargeur de programme secondaire (SPL au sens générique) vérifiera la signature du Bootloader de troisième niveau (U-Boot, BareBox, etc.).
- Le Bootloader de troisième niveau vérifiera la signature de l’image noyau (et device-tree) ainsi que l’image du système de fichiers racine. C’est cette étape qui sera présentée durant l’ensemble de l’article.
La figure 6 permet de représenter le processus avec les différentes phases du démarrage.
Dans le cas de notre plateforme, le Bootloader de second niveau fait référence au logiciel at91bootstrap et le Bootloader de troisième niveau, U-Boot. Voici un exemple de la séquence de démarrage de notre plateforme (RomBOOT → At91Bootstrap → U-Boot) :
RomBOOT
AT91Bootstrap 3.8.10 (Wed Aug 29 11:08:21 UTC 2018)
SD/MMC: Image: Read file u-boot.bin to 0x23f00000
SD: Card Capacity: High or Extended
SD: Specification Version 3.0X
SD/MMC: Done to load image
<debug_uart>
U-Boot 2017.03-linux4sam_5.8-koncepto (Aug 29 2018 - 10:40:08 +0000)
CPU: SAMA5D27 1G bits DDR2 SDRAM
Crystal frequency: 24 MHz
CPU clock : 492 MHz
Master clock : 164 MHz
DRAM: 128 MiB
MMC: sdio-host@a0000000: 0, sdio-host@b0000000: 1
In: serial@f8020000
Out: serial@f8020000
Err: serial@f8020000
Net: eth0: ethernet@f8008000
Autre remarque : durant notre étude, nous n’aborderons pas l’aspect TEE (pour « Trusted Execution Environment ») et des différentes notions liées à l’utilisation d’un environnement sécurisé sur les System On Chip (SoC) offrant cette option (e.g TrustZone pour ARM). Même si en effet dans l’implémentation il faut l’avouer, c’est un environnement qui est chargé et vérifié durant la phase de démarrage. Mais rassurez-vous, cela fera sûrement l’objet d’un prochain article !
2.3 Racine de confiance
Vous l’aurez sans doute remarqué, tout repose sur la vérification et le chargement du tout premier logiciel, l’étape où le Rom Code authentifie le chargeur d’amorçage, car c’est lui qui permettra de garantir la cohérence sur l’ensemble de la chaîne. C’est donc ici que rentre en compte ce qu’on appellera la « Racine de confiance » (ou logiciel de confiance). En effet, de par l’utilisation d’une infrastructure à clés publiques pour la vérification des signatures, il est primordial de disposer d’un moyen de stocker celles-ci et de s’assurer de l’impossibilité d’une quelconque modification externe (changement des clés par un attaquant). Pour garantir cette « Racine de confiance » et implicitement s’assurer de l’authenticité du premier logiciel exécuté, plusieurs moyens sont à disposition :
- stocker le premier logiciel (e.g U-Boot) et la clé publique en lecture seul ;
- utiliser un composant intermédiaire, plus connu sous le nom de TPM (Trusted Platform Module), pour la gestion des clés publiques ;
- injecter les clés publiques (dans certains cas le « hash » (SHA256 par exemple) de celles-ci, car moins coûteux pour le stockage) dans une zone mémoire spécifique de la puce et accessible par le Rom Code pour la vérification de la signature du premier logiciel. C’est ce que l’on retrouve sous la dénomination de mémoire OTP (One-Time Programmable), qui, sans besoin de traduction, signifie qu’une fois les clés installées, il est par la suite impossible de venir les changer.
C’est sur ce dernier mécanisme que de nombreux fondeurs ont basé leur implémentation du « Secure Boot » au sein de leur SoC, pour citer quelques références :
- NXP, avec son implémentation HAB, pour High Assurance Boot ;
- Xilinx ;
- Microchip (voir figure 7) [2] ;
- Samsung, avec un exemple concernant la famille Artik en [3].
Fig. 7 : Mémoire OTP sur SoC Sama5.
L’authentification du premier logiciel est quelque chose de très spécifique en fonction des différents processeurs à disposition (outils pour injecter les clés, algorithmes, etc.). C’est d’ailleurs pour cette raison que nous aborderons uniquement l’aspect lié à U-Boot et à la vérification de l’image noyau (et device-tree). Mais si nous devions résumer l’étape de vérification du premier logiciel, elle serait la suivante :
- le ROM Code charge le premier logiciel depuis un espace sécurisé ;
- celui-ci vérifie que le « hash » de la clé publique contenue dans l’image du Bootloader, correspond au hash » contenu en mémoire OTP ;
- il déchiffre la signature du Bootloader par le biais de la clé publique préalablement vérifiée, afin d’en extraire le « hash » ;
- il finit par comparer le « hash » calculé avec le « hash » extrait, si les deux sont égaux, la source est donc authentifiée, et le logiciel peut être exécuté en toute confiance.
2.4 Private Keys
Très peu abordée jusqu’ici, la clé privée permettant de signer le logiciel est un élément critique et lui aussi un maillon essentiel dans le processus d’un démarrage sécurisé. Il n’est donc pas rare d’avoir à mettre en œuvre différentes solutions pour la gestion des clés privées dans un contexte de production. On pourra citer ici les smartcards, clés USB spécifiques ou encore les HSM (Hardware Security Module) qui sont des modules spécifiques pour ce type d’utilisation. Tous disposant d’une norme permettant les différents accès aux périphériques, la norme PKCS#11.
3. « Das U-Boot »
Après avoir parlé des différentes étapes du processus de démarrage sécurisé, il est maintenant temps d’explorer la partie relative à U-Boot afin d’aborder l’authentification de notre image noyau et du fichier dtb associé.
Nous commencerons par introduire les différents éléments et concepts mis à disposition par le projet, ensuite, il conviendra de récupérer les sources de notre chargeur d’amorçage pour par la suite compiler notre première image. Sur notre lancée, nous ferons la même opération, mais pour notre image noyau. Enfin, nous finirons sur différentes étapes liées à la cryptographie (création de clés, signature, etc.).
3.1 Préambule
L’utilisation du démarrage vérifié requiert l’emploi de diverses « configurations ». La principale repose sur l’utilisation du format FIT image, le successeur du format uImage classique.
3.1.1 FIT image : introduction
Comme nous l’avons présenté depuis le début de cet article, la question de la sécurité et de l’intégrité du logiciel est un élément clé dans le processus de développement. C’est pourquoi le format d’image FIT (pour Flattened Image Tree) a été défini afin de répondre aux différentes exigences liées à la sécurité lors du chargement des différents logiciels par le chargeur d’amorçage.
Contrairement à son prédécesseur, ce format se veut être comme une sorte de container pouvant contenir plusieurs types d’images :
- image Noyau ;
- fichiers device-tree ;
- firmwares ou image FPGA ;
- scripts U-Boot ;
- et bien d’autres encore ! L’ensemble étant défini dans les sources de U-Boot (common/image.c).
Ainsi, une FIT image permet de ne disposer que d’un seul fichier, qui lui-même encapsule un ensemble de binaires. Donc plus besoin de charger plusieurs fichiers ! Plutôt intéressant quand même.
Par définition, l’extension pour une FIT image non compilée sera sous la forme .its pour Image Tree Source.
À noter que la description du contenu de l’image est entièrement basée sur la syntaxe du device-tree. En effet, au lieu de faire une description de type matérielle, il est question ici de décrire sous forme de nœuds et propriétés, l’ensemble des logiciels nécessaires au démarrage (Linux, firmware, etc.).
Concernant le sujet principal qu’est la gestion de l’intégrité, dans ce « nouveau » type d’image il est possible d’y intégrer des fonctions de hachage de type SHA1, SHA2 et MD5, contrairement au format « legacy » (uImage) qui lui ne peut gérer que des sommes de contrôle de type CRC32.
Et qui dit fonction de hachage dit signature. En effet, de par son implémentation, il nous sera très facile d’intégrer le mécanisme de signature numérique avec l’utilisation du format d’image FIT qui nous permettra ainsi de garantir et protéger l’ensemble des éléments nécessaires lors de la phase de démarrage, éléments contenus dans un seul et unique fichier (itb, Image Tree Blob).
Nous découvrirons la syntaxe et son utilisation dans la suite de l’article, il est maintenant temps pour nous d’aller récupérer les sources du projet U-Boot et du noyau Linux !
3.2 Construction et mise en place...
3.2.1 U-Boot
Commençons par récupérer les sources de U-Boot, et pour être plus précis, le fork mis à disposition par la communauté Atmel/Microchip (linux4sam), qui est d’ailleurs le référentiel git utilisé par les deux Build Systems évoqués plus haut (l’auteur utilise Yocto/OE) :
$ git clone git://github.com/linux4sam/u-boot-at91.git
$ git checkout origin/u-boot-2017.03-at91 -b u-boot-2017.03-at91
$ cd u-boot-at91
Puis il conviendra de choisir la configuration relative à notre plateforme :
$ make sama5d27_som1_ek_mmc_defconfig
Ce qui aura pour effet de créer le fichier .config à la racine.
Il faudra aussi mettre à jour la variable CROSS_COMPILE, afin que celle-ci pointe vers notre chaîne de compilation croisée :
$ export CROSS_COMPILE=arm-linux-gnueabihf-
Enfin, avant de prétendre travailler avec une FIT image, il faudra s’assurer de son support au niveau de notre configuration. Pour ce faire, lançons l’interface ncurses :
$ make menuconfig
Dans Boot images > Support Flattened image tree, il conviendra de sélectionner l’option pour le support du format d’image FIT (voir figure 8).
Fig. 8 : Ajout du support pour le format d’image FIT.
Il est aussi intéressant de disposer de la commande iminfo qui permet d’afficher les informations concernant l’image chargée en mémoire (header). Pour ce faire, dans Command line interface > Boot commands, il faudra sélectionner celle-ci. Un petit exemple est visible en figure 9.
Fig. 9 : Sélection de la commande iminfo.
Nous voilà maintenant avec une configuration nous permettant de démarrer avec une FIT image, mais vous l’aurez sans doute remarqué, sans l’aspect signature à ce niveau-ci de l’article. Nous allons d’abord explorer le fonctionnement de ce format sur notre plateforme.
Avant de lancer la génération de notre image U-Boot, il est aussi nécessaire de compiler l’ensemble des outils essentiels pour la génération/manipulation d’une FIT image :
$ make tools
CHK include/config/uboot.release
CHK include/generated/version_autogenerated.h
CHK include/generated/timestamp_autogenerated.h
UPD include/generated/timestamp_autogenerated.h
CHK include/generated/generic-asm-offsets.h
CHK include/generated/asm-offsets.h
HOSTCC tools/mkenvimage.o
HOSTLD tools/mkenvimage
...
Il est maintenant temps de lancer la génération :
$ make
Et voilà !
3.2.2 Noyau Linux
Maintenant que nous disposons d’un Bootloader prêt à accueillir une FIT image, il nous faut dès à présent être en mesure de disposer des éléments qui seront à charger et vérifier par celui-ci pour être intégré au sein de la FIT image. Pour ce faire, il nous faudra générer image noyau et device-tree blob.
Sans plus attendre, téléchargeons les sources du noyau Linux sur le même référentiel que précédemment, configurons notre construction pour notre plateforme, puis lançons la construction :
$ git clone https://github.com/linux4sam/linux-at91
$ cd linux-at91/
$ git checkout origin/linux-4.9-at91 -b linux-4.9-at91
$ make ARCH=arm sama5_defconfig
$ make ARCH=arm menuconfig (pour effectuer des modifications)
$ make ARCH=arm
3.2.3 FIT image !
Nous voilà fin prêts à construire notre première FIT image à destination de notre cible sama5d27. Tout d’abord, commençons par rapatrier l’image noyau et le device-tree associé à la racine des sources de U-Boot :
$ ln -s <chemin vers sources du noyau linux>/arch/arm/boot/zImage
$ ln -s <chemin vers sources du noyau linux>/arch/arm/boot/dts/at91-sama5d27_som1_ek.dtb
Puis, éditons notre image en nous basant sur la syntaxe du device-tree comme évoquée un peu plus tôt. Appelons cette image fitImage.its :
/dts-v1/;
/ {
description = "Simple image with single Linux kernel and FDT blob for GLMF";
#address-cells = <1>;
images {
kernel {
description = "zImage";
data = /incbin/("./zImage");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0x22000000>;
entry = <0x22000000>;
hash-1 {
algo = "crc32";
};
hash-2 {
algo = "sha256";
};
};
fdt-1 {
description = "Flattened Device Tree blob";
data = /incbin/("./at91-sama5d27_som1_ek.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash-1 {
algo = "crc32";
};
hash-2 {
algo = "sha256";
};
};
};
configurations {
default = "conf-1";
conf-1 {
description = "configuration 1 (Kernel & dtb)";
kernel = "kernel";
fdt = "fdt-1";
};
};
};
Ceci mérite quelques explications :
- le champ description se suffit à lui-même, il est tout de même important de mettre un descriptif clair ;
- #address-cells = <1>, définit le nombre de cellules <u32> utilisées pour encoder le champ d’adresse dans la propriété load ou entry au sein du nœud enfant ;
- images est le nœud principal (node) qui contient un ensemble de nœuds enfants (child node), qui chacun représente une sous-image, dans notre cas :
- Le nœud enfant kernel, permet de décrire la configuration du composant sous-image, on retrouvera :
- un champ description permettant de définir brièvement le composant ;
- un champ data, qui permet de renseigner le chemin vers le fichier externe à prendre en compte pour le composant en question. Dans notre cas, c’est notre zImage ;
- un champ type, qui permet d’expliciter le type du composant de la sous-image. Ici, nous faisons référence au kernel. On retrouvera aussi des types comme firmware, ramdisk, script, etc. ;
- le champ arch pour le nom de l’architecture visée, qui est arm dans notre exemple ;
- le champ os quant à lui permet de spécifier le nom du système d’exploitation, qui fait logiquement référence à Linux dans notre cas ;
- un champ compression afin de renseigner si un algorithme doit être appliqué sur les données, ici aucune compression, donc propriété à none. Il est possible de spécifier ici : lzo, bzip2, gzip ;
- le champ load, spécifie l’adresse en RAM où l’image noyau doit être copiée ;
- le champ entry, spécifie l’emplacement de l’image noyau en mémoire afin de permettre à U-Boot de démarrer celle-ci ;
- un nœud enfant hash, permettant de spécifier le type de fonction de hachage ou checksum par le biais de la propriété algo. Dans notre exemple, nous avons positionné 2 nœuds enfants pour avoir une gestion par checksum (crc32) et l’autre pour une gestion par hachage (sha256).
- Le nœud enfant fdt-1, est quasiment identique au précédent, mais cette fois-ci, la propriété datacontiendra le fichier device-tree et la propriété type fera référence au device-tree (flat_dt) ;
- Le nœud configuration, qui dans notre cas permettra de définir la ou les configurations. Ici une seule configuration est de rigueur. Il faudra donc définir dans le sous-nœud de la configuration, les éléments qui la composent (kernel et fdt-1).
- Le nœud enfant kernel, permet de décrire la configuration du composant sous-image, on retrouvera :
Ensuite, il ne nous restera qu’à générer notre FIT image à partir du fichier source, le tout grâce à la commande mkimage :
$ tools/mkimage -f fitImage.its fitImage
Une fois généré, il nous est possible de vérifier le contenu de l’image en listant les informations par le biais de cette même commande :
$ tools/mkimage -l fitImage
FIT description: Simple image with single Linux kernel and FDT blob for GLMF
Created: Wed Aug 8 22:03:54 2018
Image 0 (kernel)
Description: zImage
Created: Wed Aug 8 22:03:54 2018
Type: Kernel Image
Compression: uncompressed
Data Size: 3652128 Bytes = 3566.53 KiB = 3.48 MiB
Architecture: ARM
OS: Linux
Load Address: 0x22000000
Entry Point: 0x22000000
Hash algo: crc32
Hash value: ecff0c84
Hash algo: sha256
Hash value: b6b3ff64c52370cd1f80af887058ef8d5e1bdf16a81beb640abf1b3631d6b593
Image 1 (fdt-1)
Description: Flattened Device Tree blob
Created: Wed Aug 8 22:03:54 2018
Type: Flat Device Tree
Compression: uncompressed
Data Size: 32255 Bytes = 31.50 KiB = 0.03 MiB
Architecture: ARM
Hash algo: crc32
Hash value: 61c42b20
Hash algo: sha256
Hash value: 76917d3e1697d11fa52cf5e0a5efa03061c669c3ba0e1d2b2cf10eda30420334
Default Configuration: 'conf-1'
Configuration 0 (conf-1)
Description: dummy description
Kernel: kernel
FDT: fdt-1
Le listing de l’image nous permet de constater que celle-ci contient bien une sous-image « Kernel » (kernel) et une sous-image « device-tree » (fdt-1) et que la configuration utilisée par défaut contient bien ces deux sous-images (Default Configuration: 'conf-1'). Il aurait été ici possible de mettre un deuxième fichier device-tree et ainsi créer une deuxième configuration. Mais ça, vous êtes capable de le faire !
Maintenant que l’ensemble nous donne entière satisfaction, il est temps pour nous d’essayer notre premier démarrage avec ce format d’image. Pour ce faire, il faudra mettre à jour l’image U-Boot (u-boot.bin) et positionner le fichier fitImage sur la première partition (juste à titre d’exemple ici, certains préfèreront positionner celui-ci sous/boot, l’auteur le premier). Une fois l’opération terminée, il suffira de stopper l’autoboot au niveau de notre chargeur d’amorçage afin d’avoir la main sur le shell mis à disposition par ce dernier.
Il nous faudra en premier lieu charger notre image (via la commande load qui est plus générique que fatload ou ext4load pour la gestion des accès au File-System) en spécifiant l’interface, le périphérique, la partition, l’adresse et le fichier à récupérer, ce qui nous donne :
=> load mmc 0:1 0x21000000 fitImage
reading fitImage
3686232 bytes read in 237 ms (14.8 MiB/s)
Une fois le fichier chargé, il nous est possible de démarrer nos images à l’adresse mémoire préalablement spécifiée. Ceci s’effectue par le biais de la commande bootm :
=> bootm 0x21000000
## Loading kernel from FIT Image at 21000000 ...
Using 'conf-1' configuration
Trying 'kernel' kernel subimage
Description: zImage
Type: Kernel Image
Compression: uncompressed
Data Start: 0x210000e0
Data Size: 3652128 Bytes = 3.5 MiB
Architecture: ARM
OS: Linux
Load Address: 0x22000000
Entry Point: 0x22000000
Hash algo: crc32
Hash value: ecff0c84
Hash algo: sha256
Hash value: b6b3ff64c52370cd1f80af887058ef8d5e1bdf16a81beb640abf1b3631d6b593
Verifying Hash Integrity ... crc32+ sha256+ OK
## Loading fdt from FIT Image at 21000000 ...
Using 'conf-1' configuration
Trying 'fdt-1' fdt subimage
Description: Flattened Device Tree blob
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x2137bc34
Data Size: 32255 Bytes = 31.5 KiB
Architecture: ARM
Hash algo: crc32
Hash value: 61c42b20
Hash algo: sha256
Hash value: 76917d3e1697d11fa52cf5e0a5efa03061c669c3ba0e1d2b2cf10eda30420334
Verifying Hash Integrity ... crc32+ sha256+ OK
Booting using the fdt blob at 0x2137bc34
Loading Kernel Image ... OK
Loading Device Tree to 27b55000, end 27b5fdfe ... OK
Comme nous le constatons sur les traces du shell précédent, le Bootloader charge bien les éléments contenus dans la FIT image (voir « Loading Kernel from FIT … ») tout en vérifiant l’intégrité de ceux-ci, qui dans notre cas n’ont pas été altérés (crc32+ sha256+ OK, le + signifiant la réussite lors de la vérification, à l’inverse, le signe – serait apparu).
Sans paramètre, la commande bootm prendra la configuration par défaut (voir using conf-1 configuration). Mais dans le cas où l’utilisateur souhaiterait par exemple spécifier les sous-images à charger, la commande deviendrait la suivante :
=> bootm 0x21000000:kernel – 0x21000000:fdt-1
La forme employée est bootm [<addr>]:<subimg1> - [<addr>]:<subimg2>.
Dans le cas d’une FIT image avec plusieurs configurations, l’exécution d’une configuration spécifique s’effectuerait avec la commande ci-après :
=> bootm 0x21000000#conf-2
Autre aspect intéressant, il est possible d’extraire une sous-partie de l’image via la commande imxtract, chose très utile pour les images de type firmware. Par exemple, sur un i.MX7, le chargement et le démarrage du cortex M4 se ferait comme suit :
=> imxtract $loadaddr firmware-1 $m4_addr
## Copying 'firmware-1' subimage from FIT image at 80800000 ...
crc32+ sha1+ Loading part 15 ... OK
=> bootaux $m4_addr
## Starting auxiliary core at 0x007F8000 …
Nous voilà maintenant prêts pour attaquer la partie fatidique de l’article, l’aspect vérification. Pour les plus curieux d’entre vous, la documentation relative au format d’image FIT se trouve sous doc/uImage.FIT/ dans les sources du projet U-Boot.
3.3 Verified (U-)Boot
Dans cette dernière partie de l’article, nous allons mettre en pratique les différentes techniques abordées tout au long de notre étude afin de réaliser notre premier démarrage vérifié. Soyez prêts !
3.3.1 Concept
L’étape de signature et vérification de la FIT image avec U-Boot repose sur les mêmes concepts qu’évoqués en début d’article. Dans notre contexte, les étapes seront :
- Pour la génération de la signature :
- génération du « hash » ou « empreinte » des images par le biais d’une fonction de hachage, sha1 ou sha256 ;
- chiffrement du « hash » avec la clé privée afin de générer la signature numérique ;
- stockage de la signature dans la FIT image (FIT image signée).
- Pour la vérification lors du démarrage :
- lecture de la FIT image ;
- récupération de la clé publique ;
- extraction de la signature depuis la FIT image ;
- calcul du « hash » de la configuration contenue dans la FIT image ;
- déchiffrement de la signature par le biais de la clé publique afin d’extraire le « hash » ;
- si les deux « hashs » sont identiques, alors U-Boot pourra charger les images et ainsi garantir l’authenticité de celles-ci.
3.3.2 Implémentation
Sans plus attendre, mettons à jour notre configuration afin d’y ajouter le support pour la vérification de signature d’une FIT image. Il faudra ainsi dans Boot images > Enable signature verification of FIT uImages, cocher l’option comme le montre la figure 10.
Fig. 10 : Ajout du support pour la vérification de la signature d’une FIT image, CONFIG_FIT_SIGNATURE.
Ce n’est pas tout, il estaussi primordial de rajouter le support de la partie cryptographique RSA nécessaire pour signer et vérifier. Il faut donc activer l’option Library routines > Use RSA Library.
Il est maintenant temps de construire notre FIT image avec la prise en compte de notre nouvelle configuration. Partons d’un nouveau fichier que l’on nommera fitImage_sign.its :
/dts-v1/;
/ {
description = "Simple signed image with single Linux kernel and FDT blob for GLMF";
#address-cells = <1>;
images {
kernel {
description = "zImage";
data = /incbin/("./zImage");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0x22000000>;
entry = <0x22000000>;
hash-1 {
algo = "sha256";
};
};
fdt-1 {
description = "Flattened Device Tree blob";
data = /incbin/("./at91-sama5d27_som1_ek.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash-1 {
algo = "sha256";
};
};
};
configurations {
default = "conf-1";
conf-1 {
description = "signed configuration 1 (Kernel & dtb)";
kernel = "kernel";
fdt = "fdt-1";
signature {
algo = "sha256,rsa4096";
key-name-hint = "glmf";
sign-images = "fdt", "kernel";
};
};
};
};
Explications :
- Le nœud images restera quasiment identique à notre première image. Il n’est en effet pas forcément nécessaire de garder plusieurs « hash » ici ;
- Le nœud configuration quant à lui se verra être mis à jour avec :
- le nœud enfant signature qui contient quelques propriétés essentielles à définir afin de garantir la signature de notre configuration :
- algo, qui permet de renseigner le type d’algorithme utilisé, ici sha256 et rsa4096 ;
- key-name-hint, pour spécifier le nom de la clé utilisée pour signer l’image. C’est le nom qu’il faudra donner à notre clé privée ainsi qu’au certificat lors de la génération avec openssl ;
- sign-images, la liste des images à signer. Dans notre cas, nous spécifions le fichier device-tree ainsi que l’image noyau. Mais nous aurions pu ajouter ici une image de type firmware par exemple.
Signature d’image VS signature configuration
Dans notre exemple, nous avons pris le parti de signer la configuration. Ceci a pour avantage de signer l’ensemble des « hashs » contenus dans la configuration. Ceci permet d’éviter les attaques de type mix-and-match lors d’une signature simple sur les images et non sur la configuration. En effet, il est simple dans le cas d’une configuration non signée de garder les images signées, mais d’établir une configuration différente.
Maintenant que nous avons notre fichier source prêt à être utilisé. Il va nous falloir disposer de tout le nécessaire afin de réaliser les différentes étapes de signature.
Mettons en place dans un premier temps un endroit afin de stocker le matériel relatif à la cryptographie :
$ mkdir keys
Puis générons notre clé privée de type RSA 4096 bits (ou 3072 selon votre humeur !) nécessaire pour les opérations cryptographiques, clé que nous appellerons ici glmf afin d’être cohérent avec notre fichier its :
$ openssl genpkey -algorithm RSA -out keys/glmf.key -pkeyopt rsa_keygen_bits:4096
Et notre certificat à clé publique de type x.509, ici glmf.crt :
$ openssl req -new -x509 -key keys/glmf.key -out keys/glmf.crt
Pour les curieux, l’extraction de la clé publique se fera de la manière suivante :
$ openssl rsa -pubout -in key.key -out pubkey.pem
3.3.3 Gestion de la clé publique
Comme déjà abordé lors du second chapitre, afin de vérifier une image signée par une clé publique lors de la phase de démarrage, il faut que celle-ci (la clé publique) soit dite de confiance.
Pour ce faire, U-Boot a fait le choix d’intégrer la clé publique dans le fichier device-tree de la plateforme (dtb au sens U-Boot et non Linux), qui en temps normal est utilisé pour du « run-time configuration » (versus CONFIG_* dans les fichiers de définition de la carte cible) tout comme le fait le noyau Linux par exemple. L’implémentation du support du device-tree dans U-Boot porte le nom de « Control FDT » (Flattened Device Tree), qu’il faudra sélectionner avec l’option CONFIG_OF_CONTROL. De plus, à l’inverse du noyau Linux où le fichier dtb est un fichier binaire à part entière dans l’étape de chargement (que nous avons ici encapsulé dans notre FIT image), il en est une tout autre histoire pour dtb utilisé par le Bootloader. En effet, lors de l’étape de compilation, celui-ci sera construit séparément, mais ajouté au binaire U-Boot par un simple appel à la commande cat, exemple :
$ cat u-boot.bin u-boot.dtb > image.bin
Avec ce type de processus, et de par le fait que notre Bootloader est lui-même vérifié par le logiciel précédent, nous pouvons ainsi faire confiance à notre clé publique.
À nous de jouer ! Nous allons dans un premier temps récupérer le fichier dtb de notre plateforme, fichier que nous avons compilé durant la première étape. Ainsi, nous le renommerons pubkey.dtb afin de le différencier durant notre étude, car c’est celui-ci qui contiendra notre clé publique pour la vérification de notre FIT image :
$ cp arch/arm/dts/at91-sama5d27_som1_ek.dtb pubkey.dtb
Puis, ayant rajouté le support pour la gestion de la signature, il nous faut maintenant compiler à nouveau les différents outils afin de pouvoir travailler avec notre nouvelle configuration :
$ make tools
Nous avons maintenant la commande mkimage à jour pour réaliser notre étape de signature. Respirez un grand coup et lancez la commande suivante :
$ tools/mkimage -f fitImage_sign.its -K pubkey.dtb -k <chemin vers le dossier contenant les clés> -r fitImage
Facile, non ?
Si on s’attarde un peu sur les différentes options et leurs paramètres :
- -f, pour spécifier le fichier source en entrée ;
- -K, permet de spécifier le fichier device-tree compilé afin d’y insérer notre clé publique. Nécessaire pour l’étape de vérification sur cible ;
- -k, pour renseigner le chemin contenant la clé privée (glmf.key) et le certificat contenant la clé publique (glmf.crt) pour la vérification ;
- -r, pour spécifier la propriété « required ». Ainsi, il sera donc impossible de démarrer tout logiciel n’ayant pas été vérifié avec cette clé. Dans notre cas, cela signifie que nous forçons la vérification de notre configuration avec notre clé glmf.
Pour de plus amples informations sur les différentes options de la commande mkimage, il sera conseillé de se référer à l’aide de celle-ci :
Usage: tools/mkimage -l image
-l ==> list image header information
tools/mkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image
-A ==> set architecture to 'arch'
-O ==> set operating system to 'os'
-T ==> set image type to 'type'
-C ==> set compression type 'comp'
-a ==> set load address to 'addr' (hex)
-e ==> set entry point to 'ep' (hex)
-n ==> set image name to 'name'
-d ==> use image data from 'datafile'
-x ==> set XIP (execute in place)
tools/mkimage [-D dtc_options] [-f fit-image.its|-f auto|-F] [-b <dtb> [-b <dtb>]] [-i <ramdisk.cpio.gz>] fit-image
<dtb> file is used with -f auto, it may occur multiple times.
-D => set all options for device tree compiler
-f => input filename for FIT source
-i => input filename for ramdisk file
Signing / verified boot options: [-E] [-k keydir] [-K dtb] [ -c <comment>] [-p addr] [-r] [-N engine]
-E => place data outside of the FIT structure
-k => set directory containing private keys
-K => write public keys to this .dtb file
-c => add comment in signature node
-F => re-sign existing FIT image
-p => place external data at a static position
-r => mark keys used as 'required' in dtb
-N => engine to use for signing (pkcs11)
tools/mkimage -V ==> print version information and exit
Use -T to see a list of available image types
Pour les (plus) curieux qui se demandent à quel endroit est stockée la signature au sein de note FIT image, mkimage a placé celle-ci au sein du nœud enfant signature dans la propriété value, contenu qu’il est facile de récupérer par la commande suivante :
$ fdtget -tx fitImage /configurations/conf-1/signature value
8694ec11 e8386c70 c57f43fd 1964dddd bd34e7c0 c91cf6f6 a03575e6 224e4125 2ff06e1e 99d4c92a 47d42fbf f63f39e5 9f5de96a 49c3bcc9 998a8f98 659f2299 417fe78d 2673b7d2 622201a2 8e66b91c 11238fa4 5552d67c 13ea54fc 7097d2a 4051e812 c7ab02d3 6c30bf9c 557377ff 99354e81 8fd724c4 aa79e01 1e97095c 67a8d550 ee9e32d1 c11c3ae2 a93e4953 803955f5 900d6481 3cef2d73 ...
Il est aussi possible de convertir notre fichier dtb en dts afin de porter attention au nœud signature contenant notre clé publique :
$ dtc -I dtb -O dts -o pubkey.dts pubkey.dtb
En voici le résultat :
/dts-v1/;
/ {
#address-cells = <0x1>;
#size-cells = <0x1>;
model = "Atmel SAMA5D27 SOM1 EK";
compatible = "atmel,sama5d27-som1-ek", "atmel,sama5d2", "atmel,sama5";
signature {
key-glmf {
required = "conf";
algo = [00];
rsa,r-squared = <0xd979a356 ...>;
rsa,modulus = <0xed12cc1d ...;
rsa,exponent = <0x0 0x10001>;
rsa,n0-inverse = <0xdeec77db>;
rsa,num-bits = <0x1000>;
key-name-hint = "glmf";
};
};
...
Nous voyons en effet qu’il existe maintenant l’ensemble de la définition relative à notre clé publique (child node key-glmf). On retrouvera par exemple le champ required précédemment configuré ou encore le champ rsa,num-bits qui spécifie le nombre de bits utilisés pour la clé RSA, ici 0x1000 pour 4096 en décimal. Pour de plus amples informations sur les différentes propriétés, la documentation est disponible en [4].
Afin de valider notre précédente opération, nul besoin pour l’instant de mettre à jour FIT image et binaire U-Boot. En effet, il est d’ores et déjà possible de vérifier notre image par le biais de l’utilitaire fit_check_sign que nous venons de compiler. Pour ce faire, nous allons passer en argument notre FIT image accompagnée du fichier dtb contenant la clé publique (pubkey.dtb) :
$ tools/fit_check_sign -f fitImage -k pubkey.dtb
Verifying Hash Integrity ... sha256,rsa4096:glmf+
## Loading kernel from FIT Image at 7fbd1975c000 ...
Using 'conf-1' configuration
Verifying Hash Integrity ...
sha256,rsa4096:glmf+
OK
Trying 'kernel' kernel subimage
Description: zImage
Created: Wed Aug 8 21:27:58 2018
Type: Kernel Image
Compression: uncompressed
Data Size: 3652128 Bytes = 3566.53 KiB = 3.48 MiB
Architecture: ARM
OS: Linux
Load Address: 0x22000000
Entry Point: 0x22000000
Hash algo: sha256
Hash value: b6b3ff64c52370cd1f80af887058ef8d5e1bdf16a81beb640abf1b3631d6b593
Verifying Hash Integrity ...
sha256+
OK
Loading Kernel Image ... OK
## Loading fdt from FIT Image at 7fbd1975c000 ...
Using 'conf-1' configuration
Trying 'fdt-1' fdt subimage
Description: Flattened Device Tree blob
Created: Wed Aug 8 21:27:58 2018
Type: Flat Device Tree
Compression: uncompressed
Data Size: 32255 Bytes = 31.50 KiB = 0.03 MiB
Architecture: ARM
Hash algo: sha256
Hash value: 76917d3e1697d11fa52cf5e0a5efa03061c669c3ba0e1d2b2cf10eda30420334
Verifying Hash Integrity ...
sha256+
OK
Loading Flat Device Tree ... OK
## Loading ramdisk from FIT Image at 7fbd1975c000 ...
Using 'conf-1' configuration
Could not find subimage node
Signature check OK
Comme vous pouvez le constater, la commande nous sort le résultat suivant : sha256,rsa4096:glmf+. Ceci signifie que pour la vérification, une clé de type RSA 4096 bits de nom glmf avec une fonction de hachage de type sha256 a été utilisée. Le + nous informe de la bonne réussite de l’opération (comme vu plus haut dans l’article). À l’inverse, le signe – aurait été présent. Nous pouvons faire le test en modifiant la signature de notre FIT image :
$ fdtput -tx fitImage /configurations/conf-1/signature value 8694ec12 e8386c70 c57f43fd 1964dddd bd34e7c0 c91cf6f6 a03575e6 224e4125 2ff06e1e 99d4c92a 47d42fbf f63f39e5 9f5de96a 49c3bcc9 998a8f98 659f2299 417fe78d 2673b7d2 622201a2 8e66b91c 11238fa4 5552d67c 13ea54fc 7097d2a 4051e812 c7ab02d3 6c30bf9c 557377ff 99354e81 8fd724c4 aa79e01 1e97095c 67a8d550 ee9e32d1 c11c3ae2 a93e4953 803955f5 900d6481 3cef2d73 ...
Et relançons l’étape de vérification :
$ tools/fit_check_sign -f fitImage -k pubkey.dtb
Verifying Hash Integrity ... sha256,rsa4096:glmf-
Failed to verify required signature 'key-glmf
Signature check Bad (error 1)
Nous observons bien un problème lors de la vérification de la signature de notre configuration (sha256,rsa4096:glmf-). Jusqu’ici tout va bien ...
L’ensemble de la chaîne étant maintenant validé et fonctionnel (signature + vérification), nous pouvons générer à nouveau notre binaire u-boot.bin. Notez tout de même qu’il faudra être vigilant et utiliser un argument à la commande make afin de spécifier le fichier dtb spécifique à utiliser (notre fameux fichier pubkey.dtb) et non celui généré en premier lieu par U-Boot. Sinon, la clé publique ne serait donc pas contenue dans notre binaire final. Afin de réaliser cette opération, l’argument EXT_DTB devra être utilisé lors de l’exécution de la commande :
$ make V=1 EXT_DTB=pubkey.dtb
...
cat pubkey.dtb > dts/dt.dtb
cat u-boot-nodtb.bin dts/dt.dtb > u-boot-dtb.bin
cp u-boot-dtb.bin u-boot.bin
./tools/mkimage -A arm -T firmware -C none -O u-boot -a 0x23f00000 -e 0 -n "U-Boot 2017.03-linux4sam_5.8 for sama5d27_som1_ek board" -d u-boot.bin u-boot.img
Image Name: U-Boot 2017.03-linux4sam_5.8 for
Created: Wed Aug 8 21:47:36 2018
Image Type: ARM U-Boot Firmware (uncompressed)
Data Size: 400515 Bytes = 391.13 KiB = 0.38 MiB
Load Address: 23f00000
Entry Point: 00000000
Grâce à l’option permettant d’activer le mode verbeux, il nous est possible de constater les différentes étapes relatives à l’ajout de notre fichier dtb contenant la clé publique au sein du binaire u-boot.bin. Ainsi, nous disposons de tout le nécessaire afin de prétendre réaliser un démarrage vérifié via notre bootloader !
3.3.4 Test sur cible
Vous l’aurez compris, il est temps de mettre à jour notre fichier u-boot.bin ainsi que notre fichier fitImage sur la première partition de notre SD Card (l’utilisation du support SD n’est utile que pour les phases de tests soulignons-le, car il n’y a rien d’industriel à utiliser un tel support !).
Comme nous l’avons fait lors du premier test, il nous faudra ici aussi charger le fichier fitImage afin de commencer nos différentes étapes de vérification :
=> load mmc 0:1 0x21000000 fitImage
Puis comme lors de notre premier démarrage, lançons l’exécution :
=> bootm 0x21000000
## Loading kernel from FIT Image at 21000000 ...
Using 'conf-1' configuration
Verifying Hash Integrity ... sha256,rsa4096:glmf+ OK
Trying 'kernel' kernel subimage
Description: zImage
Type: Kernel Image
Compression: uncompressed
Data Start: 0x210000e0
Data Size: 3652128 Bytes = 3.5 MiB
Architecture: ARM
OS: Linux
Load Address: 0x22000000
Entry Point: 0x22000000
Hash algo: sha256
Hash value: b6b3ff64c52370cd1f80af887058ef8d5e1bdf16a81beb640abf1b3631d6b593
Verifying Hash Integrity ... sha256+ OK
## Loading fdt from FIT Image at 21000000 ...
Using 'conf-1' configuration
Trying 'fdt-1' fdt subimage
Description: Flattened Device Tree blob
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x2137bc00
Data Size: 32255 Bytes = 31.5 KiB
Architecture: ARM
Hash algo: sha256
Hash value: 76917d3e1697d11fa52cf5e0a5efa03061c669c3ba0e1d2b2cf10eda30420334
Verifying Hash Integrity ... sha256+ OK
Booting using the fdt blob at 0x2137bc00
Loading Kernel Image ... OK
Loading Device Tree to 27b53000, end 27b5ddfe ... OK
Starting kernel ...
Booting Linux on physical CPU 0x0
Sans surprise, nous constatons que l’étape de vérification pour notre configuration (Kernel et device-tree) s’est effectuée sans problème pour notre nouveau test sur cible. Il serait maintenant judicieux d’améliorer notre process en intégrant à notre FIT une sous-image RAMDisk.
À titre d’exemple, voici la même séquence de démarrage sur la WaRP7 avec RAMDisk :
=> tftp 0x85000000 fitImage_ramdisk
...
TFTP from server 192.168.7.1; our IP address is 192.168.7.2
Filename 'fitImage_ramdisk'.
Load address: 0x85000000
Loading: #################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
##########################################################
629.9 KiB/s
done
Bytes transferred = 10392830 (9e94fe hex)
=> bootm 0x85000000
## Loading kernel from FIT Image at 85000000 ...
Using 'conf-1' configuration
Verifying Hash Integrity ... sha256,rsa4096:ynov+ OK
Trying 'kernel' kernel subimage
Description: zImage
Type: Kernel Image
Compression: uncompressed
Data Start: 0x850000b8
Data Size: 8581208 Bytes = 8.2 MiB
Architecture: ARM
OS: Linux
Load Address: 0x80800000
Entry Point: 0x80800000
Hash algo: sha256
Hash value: 2acbc1f07c1e576c7a73fd5ef4bd5fe44c87c32846c39ba4d1828e10ffa915eb
Verifying Hash Integrity ... sha256+ OK
## Loading ramdisk from FIT Image at 85000000 ...
Using 'conf-1' configuration
Trying 'ramdisk' ramdisk subimage
Description: initramfs
Type: RAMDisk Image
Compression: gzip compressed
Data Start: 0x858358a0
Data Size: 1753020 Bytes = 1.7 MiB
Architecture: ARM
OS: Linux
Load Address: 0x86000000
Entry Point: 0x86000000
Hash algo: sha256
Hash value: 8a46721ddd678c8e4bc1f440e97e5aacbff21794be7fd059b16dec8ae38330f8
Verifying Hash Integrity ... sha256+ OK
Loading ramdisk from 0x858358a0 to 0x86000000
## Loading fdt from FIT Image at 85000000 ...
Using 'conf-1' configuration
Trying 'fdt-1' fdt subimage
Description: Flattened Device Tree blob
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x8582f210
Data Size: 26067 Bytes = 25.5 KiB
Architecture: ARM
Hash algo: sha256
Hash value: 86f0938b586a01d06ecbfe6b49e3e68ac8e8b07e5e9bcb73648526b51394ee6d
Verifying Hash Integrity ... sha256+ OK
Booting using the fdt blob at 0x8582f210
Loading Kernel Image ... OK
Using Device Tree in place at 8582f210, end 858387e2
Starting kernel ...
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Linux version 4.17.4-fslc+g65375933 (oe-user@oe-host) (gcc version 7.3.0 (GCC)) #1 SMP Mon Sep 10 16:35:53 UTC 2018
...
[ 3.055701] #0: imx7-sgtl5000
[ 3.076914] Freeing unused kernel memory: 1024K
/ # ls /
bin dev etc init lib proc root run sbin sys usr var
/ # ...
/ # exec switch_root /newroot /sbin/init
...
[ 181.924093] systemd[1]: Detected architecture arm.
Welcome to Poky (Yocto Project Reference Distro) 2.5.1 (sumo)!
3.3.5 Pour résumer
Afin de récapituler un peu l’ensemble des éléments liés à la mise en place du démarrage vérifié avec U-Boot, voici en figure 11 un diagramme permettant de rappeler les différentes étapes.
Fig. 11 : U-Boot et démarrage vérifié.
Autre point : lors de la création de nos différentes FIT images, nous avons sur chaque partie de l’étude, élaboré celles-ci de façon manuelle ce qui, dans certains contextes (industriel pour n’en citer qu’un), peut s’avérer comme une tâche assez lourde et difficilement maintenable suivant les configurations. Mais c’est sans compter sur l’aide du projet Yocto ! En effet, il est possible via celui-ci de générer automatiquement durant la phase de construction, une FIT image, moyennant une configuration assez simple (fichiers*.conf) :
KERNEL_CLASSES ?= " kernel-fitimage "
KERNEL_IMAGETYPE ?= "fitImage"
Il est également possible de gérer les aspects signature de la FIT image depuis le build system :
UBOOT_SIGN_KEYDIR = "/keys/directory"
UBOOT_SIGN_KEYNAME = "glmf" # keys name in keydir (eg. "dev.crt", "dev.key")
UBOOT_SIGN_ENABLE = "1"
Conclusion
Dans cet article, nous avons pu naviguer dans les différentes étapes d’un démarrage vérifié, en partant du ROM code jusqu’au chargement de l’image noyau par le chargeur d’amorçage, mais sans aller jusqu’à l’étape de vérification du système de fichiers racine via des mécanismes comme dm-verity (pourtant un sujet tout aussi intéressant à explorer).
Tout ceci nous a permis de mettre en place sur une plateforme de type industrielle, un bout du processus du démarrage vérifié avec U-Boot [5]. Mécanisme qui est de plus en plus utilisé sur les périphériques grands publics (e.g smartphones, Chromebook, etc.).
Enfin, comme déjà évoqué un peu plus tôt, il serait à présent intéressant de continuer notre étude sur la partie TrustZone en explorant l’implémentation open source OP-TEE. La suite au prochain épisode…
Références
[1] P. FICHEUX, « Buildroot Vs Yocto Vs le reste du monde », Open Silicium n°20, octobre 2016 : https://connect.ed-diamond.com/Open-Silicium/OS-020/Buildroot-vs-Yocto-vs-le-reste-du-monde
[2] Secure Boot sur Sama5 : http://ww1.microchip.com/downloads/en/AppNotes/SAMA5D2-Linux-Secure-Boot-Application-Note-00002748A.pdf
[3] Secure Boot sur Artik : https://developer.artik.io/documentation/advanced-concepts/secure-os/secure-boot.html
[4] Documentation signature U-Boot : http://git.denx.de/?p=u-boot.git;a=blob;f=doc/uImage.FIT/signature.txt
[5] Documentation « verified boot » U-Boot : http://git.denx.de/?p=u-boot.git;a=blob;f=doc/uImage.FIT/verified-boot.txt